Skip to content

[composition-api] Migrate modals, cells, layouts and admin pages#2004

Open
frankrousseau wants to merge 28 commits intomainfrom
composition-api-migration
Open

[composition-api] Migrate modals, cells, layouts and admin pages#2004
frankrousseau wants to merge 28 commits intomainfrom
composition-api-migration

Conversation

@frankrousseau
Copy link
Copy Markdown
Contributor

Problems

  • 40+ modals, 11 cells, several admin pages + lists, the page layouts and the budget salary-scale page were still on Options API.
  • formatListMixin (27+ consumers) and descriptorMixin had no Composition API equivalent, so new <script setup> components had to either keep a legacy <script> block alongside or duplicate the helpers.
  • A few latent bugs surfaced during migration: prop defaults () => {} returning undefined, EditPlaylistModal's active prop typo (value: instead of default:), SetFrames…Modal.reset() clearing the wrong field, locale-dependent arrays initialised in data() that froze at first render.
  • Body metadata taglist cells use inline z-index up to 1000 (so an open combo dropdown can spill over the next row) but the datatable headers were at z-index 2/4 — metadata cells covered the headers on vertical scroll, and sticky-left body columns (name, episode, sticky metadata) were at z-index 1 so non-sticky cells slid over them on horizontal scroll.
  • Number inputs in metadata cells rendered browser up/down spinner buttons.
  • After migrating the `Add*Modal` files to `<script setup>`, `mixins/task.js` and `widgets/AddComment.vue` started throwing because `reset()` / `addFiles()` weren't exposed via `defineExpose`.

Solutions

  • Migrate all 40+ modals, 11 cells (incl. `ValidationCell`), `PageLayout` / `PageLeftSideLayout`, the `HardwareItems` / `SoftwareLicenses` / `Studios` / `Departments` pages and their lists, and `pages/budget/SalaryScale` to `<script setup>`.
  • Add `src/composables/format.js` (`useFormat()` + pure named exports) and export `getDescriptorChecklistValues` from `descriptorMixin` so new `<script setup>` components have a clean Composition API path; the legacy mixins stay in place for their existing consumers.
  • Apply CLAUDE.md cleanup conventions across migrated files (import order, section comments, functional simplifications, dead-CSS removal, no-mutation confirm pattern, computed locale-reactive `tabs`).
  • Align responsive behaviour with the admin-page convention (768px breakpoint) on the migrated pages and `BuildFilterModal`, plus list column collapse on the matching lists.
  • Fix the datatable z-index hierarchy in `App.vue`: body taglist ≤1000 < body sticky-left 1001 < header non-sticky 1002 < header sticky-left 1003; drop the now-redundant inline `z-index: 1001` from `` in `AssetList` / `ShotList`.
  • Add WebKit / Firefox spinner-hide rule on `.input-editor[type='number']` in `MetadataInput`.
  • Add the missing `defineExpose` entries on `AddPreviewModal` (`reset`) and `AddAttachmentModal` (`addFiles`).

frankrousseau and others added 28 commits April 29, 2026 20:41
Convert HardwareItems.vue to <script setup> following Studios.vue
pattern: store getters via useStore, async/await for confirm handlers,
useHead for the title, and computed tabs so labels react to locale
changes. Drop the unused choices state and fix the dead
.software-license-list selector to .hardware-item-list.

Align responsive with other admin pages by adding the 768px page
padding rule, and on HardwareItemList give the name column a
min-width with collapsing paddings on mobile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert SoftwareLicenses.vue to <script setup> mirroring the
HardwareItems migration: useStore for getters and dispatches,
async/await for confirm handlers, useHead for the title, computed
tabs for locale reactivity, and drop the unused choices state.

Align responsive with other admin pages by adding the 768px page
padding rule, and on SoftwareLicenseList give the name column a
min-width with extension/version/cells collapsing paddings on mobile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert SalaryScale.vue to <script setup>: store getter/dispatches
through useStore, refs for isLoading and salaryScale state.

Dedupe the table body — the nine hand-rolled <tr> blocks (3 positions
× 3 seniorities) collapse into a single <tr v-for> driven by static
positions and seniorities arrays, with v-if guards on the department
and position cells to preserve their rowspan layout. The salary
lookup uses chained optional access so it stays safe while the scale
is loading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert HardwareItemList.vue to <script setup> with defineProps /
defineEmits. Fix the remainingHardwareItems prop default — it was
() => {} (a function with an empty block, returning undefined)
instead of the intended () => ({}). Replace the deprecated $tc with
$t for the entry-count pluralization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert SoftwareLicenseList.vue to <script setup> with defineProps /
defineEmits. Replace the deprecated $tc with $t for the entry-count
pluralization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring Studios.vue and StudioList.vue (already on <script setup>) in
line with the other admin pages: imports alphabetical by source path
with @unhead/vue first; section comments; refs and reactive object
keys alphabetized; Vuex getters alphabetized; on*Clicked handlers
grouped together; confirmEditStudio no longer mutates the form arg
and uses { ...form, id } spread; [headers].concat(rows) replaced by
[headers, ...rows]; try/catch param renamed to err.

On StudioList, alphabetize defineProps and switch to single-line
form, and add class="color" to the color column header so it gets
the same width:60px / text-align:center as the body cells.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert Departments.vue to <script setup>: store getters/dispatches
through useStore, ref/reactive state, computed tabs (so labels react
to locale switches), watchers and onMounted at the bottom, useHead
for the title.

Apply the project cleanup conventions: imports alphabetical by source
path with @unhead/vue first and components grouped lists / modals /
pages / widgets, section comments, refs and reactive keys
alphabetized, Vuex getters alphabetized, [headers, ...rows] spread,
confirmEditDepartment async/await with { ...form, id } spread (no
form mutation), confirmDeleteDepartment flattened so loading.del is
reset once after the try/catch, onLinkItem/onUnlinkItem destructure
{ itemId, departmentId } at the param list, and onDeleteClicked
regrouped with the other on*Clicked handlers.

DepartmentList is also imported with an eslint-disable comment for
no-unused-vars: vue-eslint-parser can't disambiguate <department-list>
from <department-links> (same kebab prefix).

Add the 768px page-padding rule to align with the other admin pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert DepartmentList.vue to <script setup> with defineProps /
defineEmits. Switch each prop from required: true to typed defaults
matching the other lists. Replace deprecated $tc with $t and rename
the nb-asset-types class (copy-paste leftover) to nb-departments.

Align CSS with the other admin lists: move the first-row border-top
reset to the top, switch .name from width:300px to min-width:300px
so the column can grow on wide screens, drop the misleading
height:20px on the .color td (overridden by sibling cells anyway),
widen .color to 60px so the header text fits, and add the 768px
breakpoint that collapses cell padding and resets .name / .items
min-widths.

Add class="color" to the color column header so it shares the body
cell's width and centering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both layouts are slot-only wrappers — convert them to <script setup>.
PageLayout keeps its single side prop via defineProps with a typed
default; PageLeftSideLayout has no script content so the block stays
empty (the original only declared the component name, which is now
auto-derived from the filename).

Drop the ref="page" template ref on both — no consumer reads
$refs.page anywhere in the codebase, so it was vestigial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert 10 cells to <script setup>: BooleanCell, DepartmentNamesCell,
DescriptionCell, LastCommentCell, PeopleNameCell, StatsCell,
TaskStatusCell, TaskTypeCell, TimeSliderCell, ValidationHeader.

Standard transforms: defineProps with typed defaults (single-line
form, alphabetized), defineEmits, computed for Vuex getters via
useStore, top-level arrow functions for methods, refs for state and
template refs (DescriptionCell), watch / nextTick imports from vue.

A few cleanups picked up along the way:
- TaskStatusCell entry default was () => {} (returns undefined) →
  () => ({}); same fix as the recent HardwareItemList one.
- LastCommentCell: drop unused timeout state and the
  methods: { renderMarkdown } pass-through (just import it).
- StatsCell: simplify the percent helper (early return on zero
  total) and the reduce accumulator.
- TimeSliderCell: extract the repeated label-style object and inline
  the static stepStyle / marks tables (they're not reactive).

Skip ValidationCell — it depends on formatListMixin which is shared
with 27+ other files; converting that mixin to a composable is a
separate workstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the formatting helpers from formatListMixin into a Composition
API composable at src/composables/format.js. useFormat() returns
organisation, isDurationInHours, formatBoolean, formatDuration and
formatPriority (which need vue-i18n + the store) plus the pure
helpers, while formatDate / formatFullDate / formatSimpleDate /
formatPrioritySymbol / sanitizeInteger / sanitizeIntegerLight are also
exposed as plain named exports for callers that don't need to
instantiate the composable.

The legacy mixin in src/components/mixins/format.js stays in place —
its 27+ Options API consumers can migrate one at a time.

Use the new composable to migrate ValidationCell.vue to <script setup>
(it was the last cell still on Options API). Replace the dynamic
emit('select' or 'unselect') with explicit branches so vue's
no-unused-emit-declarations rule can see both events being used.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert the five modals that already wrap their content in
<base-modal> but still mixed in modalMixin: EditHardwareItemModal,
EditSoftwareLicenseModal, EditBudgetModal, EditBudgetEntryModal,
NotifyClientModal.

Drop modalMixin from all five — BaseModal already drives Escape via
useModal, so the mixin was actually adding a second window-level
keydown listener that emitted 'cancel' a second time on each Escape.
Convert each component to <script setup> with section comments,
defineProps with typed defaults, store getters via useStore, and the
existing watchers / lifecycle / template refs preserved.

Two small cleanups picked up along the way:
- EditBudgetEntryModal had a refreshKeys: { endMonth: 0 } reactivity
  hack (read once inside the endMonth computed but never written
  anywhere) — pure dead code, dropped.
- NotifyClientModal had ref="studioField" / ref="departmentField"
  declared on combobox fields but never accessed in the script —
  vestigial, dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert four small modals from Options API: ConfirmModal,
EditDepartmentsModal, EditLabelModal, EditMilestoneModal. Drop
modalMixin from each.

ConfirmModal keeps its custom modal markup (no title bar, just text +
buttons) and uses useModal(toRef(props, 'active'), emit) directly.
The other three switch to wrapping their content in <base-modal>,
collapsing the duplicated v-if/v-else <h1> blocks into a single
:title computed.

CSS cleanup: drop the dead .error rule on EditLabelModal (the class
isn't used anywhere in the template), and the entire scoped style
block on EditDepartmentsModal (.modal-content .box p.text and
.is-danger never matched anything in the original markup either, and
scoped styles can't reach into <base-modal> now anyway).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit the scoped styles on the five modals migrated to BaseModal in
the previous batch. Three of them carried selectors that no longer
match anything: scoped styles can't reach into <base-modal>'s
.modal-content / .box, and the .is-danger / .task-types / p.text
markup hooks weren't actually used in the original templates either.

- EditHardwareItemModal: drop the entire <style> block
  (.modal-content .box p.text and .is-danger were both dead).
- EditSoftwareLicenseModal: drop the entire <style> block (same
  two rules plus an unused .task-types).
- EditBudgetModal: drop only .modal-content .box p.text; keep the
  live .revision-number rule that styles the v{n} number.

EditBudgetEntryModal and NotifyClientModal had no dead CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert four edit modals from Options API: EditAssetTypeModal,
EditBackgroundModal, EditEpisodeModal, EditSequenceModal. Each
switches to wrapping its content in <base-modal> with a single
:title computed (collapsing the duplicated v-if/v-else <h1>) and
drops modalMixin.

Cleanups picked up along the way:
- EditAssetTypeModal: drop the unused loadTaskTypes mapped action,
  the unused assetTypes / assetTypeStatusOptions getters, and the
  ref="shortNameField" that wasn't accessed; pull the inline
  @update:model-value handler into a named addTaskType helper.
- EditBackgroundModal: bind ref="fileUpload" properly so resetForm
  can call .reset() on it; eslint-disable no-unused-vars on the
  FileUpload import to work around the vue-eslint-parser false
  positive (same as the earlier DepartmentList case).
- EditEpisodeModal / EditSequenceModal: drop the duplicated form
  initialization that was happening in both data() and resetForm —
  keep only the schema and let resetForm populate it.
- EditEpisodeModal: drop unused episodeSuccessText state and three
  unused template refs (descriptionField, resolutionField,
  nameField — v-focus already auto-focuses on mount).
- EditSequenceModal: same — drop sequenceSuccessText and three
  unused refs; flatten the runConfirmation → confirmClicked
  indirection.
- All four: drop dead CSS rules (.modal-content .box p.text,
  .is-danger, .info-message — none matched anything in the
  templates and scoped styles can't reach into <base-modal>).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…modals

Convert four more modals from Options API: EditTaskStatusModal,
EditTaskTypeModal, DayOffModal, ChangePasswordModal. Each switches
to wrapping its content in <base-modal> with a computed title and
drops modalMixin.

Notable details:
- EditTaskStatusModal: the 25-color array becomes a top-level
  constant (not reactive). nameField / shortNameField refs preserved
  — confirmClicked uses them to focus the empty field.
- EditTaskTypeModal: bug fix — dedicatedToOptions was using this.$t
  inside data(), so the labels were frozen at first render and
  didn't react to locale switches; now a computed. Dropped two
  unused mapped getters (taskTypeStatusOptions, departments) and
  two unused template refs.
- DayOffModal: original made the <form> itself the .box — now the
  form sits inside BaseModal's slot and inherits BaseModal's box.
  Kept the .ml2 override (live, used on the start-date column).
- ChangePasswordModal: internal isLoading / isError / isValid state
  preserved as refs. The kebab-case ref="first-password" is renamed
  to firstPassword (Vue 3 expects a valid identifier when binding
  template refs in <script setup>). Promise chains converted to
  async/await.

CSS: dropped dead .modal-content .box p.text / .is-danger rules
across all four. The new .ml2 in DayOffModal is the only override
that survived.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert four more modals from Options API: EditEditModal,
EditHistoryModal, EditSearchFilterModal, EditSearchFilterGroupModal.
Each switches to wrapping its content in <base-modal> with a
computed or static title and drops modalMixin.

Notable details:
- EditEditModal: drop unused editSuccessText state and the
  editCreated watcher that wrote to it (its only consumer); drop
  two unused template refs (descriptionField, resolutionField).
  Lose the .title { border-bottom: 2px } rule — scoped styles can't
  reach BaseModal's title, and the underline isn't used by the
  other admin modals anyway.
- EditHistoryModal: drop the unused headerWrapper template ref.
  Convert the loadEditHistory promise chain to async/await. Use the
  imported formatDate directly (drop the methods passthrough).
- EditSearchFilterModal: preserve the keyCode-guarded confirm
  (only emit on Enter or programmatic call).
- EditSearchFilterGroupModal: computed title interpolates the group
  name with embedded quotes.

CSS: dropped dead .modal-content .box p.text / .is-danger /
.info-message rules across all four. EditHistoryModal keeps its
table styling (still applies to elements rendered in this
component's scope, not in BaseModal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert four more modals from Options API: ImportEdlModal,
ImportModal, ShotHistoryModal, ShortcutModal. Each switches to
wrapping its content in <base-modal> with a static title and drops
modalMixin.

Notable details:
- ImportEdlModal: pull the duplicated TVShow-naming-convention logic
  in mounted() and the currentProduction watcher into a single
  updateNamingConvention helper.
- ImportModal: bug fix — tabs array was using $t inside data() so the
  tab labels were frozen at first render and didn't react to locale
  switches; now a computed.
- ShotHistoryModal: same pattern as EditHistoryModal (just migrated).
  Promise chain → async/await; drop the unused headerWrapper template
  ref; use the imported formatDate directly.
- ShortcutModal: bug fix — the large shortcutGroups array was using
  $t inside data() (locale-frozen); now a computed. Object literals
  compressed to single-line.

ImportEdlModal and ImportModal both need eslint-disable no-unused-vars
on the FileUpload import — vue-eslint-parser's same false positive
hit before on DepartmentList / EditBackgroundModal.

CSS: dropped dead .modal-content .box p.text on ImportEdlModal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert four more modals from Options API: ChangeAvatarModal,
EditAvatarModal, PreviewModal, SelectTaskTypeModal. Drop modalMixin
from each.

Three switch to <base-modal>; PreviewModal stays on its custom shell
(full-screen image overlay with absolute-positioned action buttons —
no title, no .box, BaseModal doesn't fit) and just swaps the mixin
for useModal(toRef(props, 'active'), emit).

Notable details:
- ChangeAvatarModal: BaseModal with the title passed as a prop.
- EditAvatarModal: BaseModal + custom button row (no ModalFooter).
  Rename ref="input-file" (kebab) to ref="inputFile" so script setup
  can bind it. Wire <base-modal @cancel="close"> so Escape and
  background-click both go through the close-while-busy guard.
- SelectTaskTypeModal: bug fix — taskTypeList had default: () => {}
  (returns undefined, would crash on taskTypeList[0] if no value
  passed); now () => []. Added ?.id || '' guard in the active watcher
  so it won't throw on an empty list either.

CSS: dropped dead rules (.modal-content .box p.text, .description,
.is-danger) across the four. Kept .error and .right where they're
live.

ChangeAvatarModal needs eslint-disable no-unused-vars on the
FileUpload import — same vue-eslint-parser false positive as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert four more modals from Options API:
SetFramesFromTaskTypePreviewsModal, NewTokenModal, CreateTasksModal,
EditPlaylistModal. Drop modalMixin from each.

Two switch to <base-modal> (SetFrames…, EditPlaylistModal); two stay
on a custom shell with useModal directly:

- NewTokenModal — the original's <div class="modal-background"></div>
  intentionally has no @click handler so a stray click outside can't
  dismiss the modal mid-copy and lose an unrecoverable token.
  BaseModal would silently re-enable background-click cancel; keep
  the custom shell and add a code comment explaining why.
- CreateTasksModal — uses <page-title> as its title instead of
  <h1 class="title">, so BaseModal's own <h1> would render alongside.

Bug fixes:
- SetFramesFromTaskTypePreviewsModal: reset() set this.taskType = null
  but the data field is taskTypeId — the original was creating a new
  reactive property on every active toggle instead of resetting the
  combobox value. Now reset() correctly clears taskTypeId.
- EditPlaylistModal: the active prop had value: false (typo) instead
  of default: false — corrected.

NewTokenModal also swaps timeMixin (only used `today`) for the
existing useTime() composable from @/composables/time.

Other cleanups: drop dead .is-danger, dropping unused refs, simplify
forEntityOptions, factor the duplicated emit payload in
CreateTasksModal into a buildPayload helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert four medium modals from Options API: AddAttachmentModal,
AddThumbnailsModal, EditShotModal, EditAssetModal.

Three switch to <base-modal>; AddAttachmentModal stays on a custom
shell with useModal — its <h2 class="subtitle"> sits ABOVE the
<h1 class="title">, and BaseModal would force the subtitle into the
slot (after the h1), changing the visual order.

EditShotModal pulls in sanitizeInteger from @/composables/format
instead of formatListMixin (it was only using that one helper).

Notable cleanups:
- AddAttachmentModal: kebab ref="file-field" → "fileField" for Vue 3
  binding; expose showAnnotationLoading / hideAnnotationLoading via
  defineExpose (parent components call them on the modal ref); drop
  the empty onDrag() method.
- AddThumbnailsModal: bug fix — entityMap was only declared
  implicitly via this.entityMap = {} inside a method, dynamically
  added properties on `this` aren't reactive and are a code smell;
  now a proper ref({}). Expose markLoading / markUploaded.
- EditShotModal: drop seven unused template refs (only nameField is
  read in script); drop unused shotSuccessText state and the
  shotCreated watcher that wrote to it; inline the five single-use
  frame*/fps/etc. computeds into resetForm; pull the duplicated
  frameIn/frameOut watcher logic into one updateNbFramesFromRange.
- EditAssetModal: extract buildPayload to dedupe the two confirm
  emits; collapse the source_id reset into a [...].includes(...)
  check.

CSS: drop dead .modal-content .box p.text / .is-danger / .info-message
across all four where they no longer matched anything; the
.title { border-bottom } rule on EditShotModal is dropped (scoped
styles can't reach BaseModal's title).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert three more modals from Options API: BuildPeopleFilterModal,
ViewPlaylistModal, AddPreviewModal.

BuildPeopleFilterModal switches to <base-modal>; the other two stay
on a custom shell with useModal:
- ViewPlaylistModal has no title <h1> (its content is the
  <playlist-player> directly inside the box), and the root <div>
  carries an id="temp-playlist-modal" plus the dark theme class.
- AddPreviewModal puts the <h2 class="subtitle"> ABOVE the
  <h1 class="title"> (same pattern as AddAttachmentModal — switching
  to BaseModal would force the subtitle into the slot, after the h1).

Notable cleanups:
- BuildPeopleFilterModal: drop a duplicate 'peopleSearchText' entry
  in mapGetters; drop the unused unionOptions array (its combobox
  was already commented out in the template); drop two helper methods
  (addDepartmentFilter, removeDepartmentFilter) that weren't called
  from the template or anywhere else.
- ViewPlaylistModal: maps stay as plain `let` variables — they're
  used as lookups in event handlers, not rendered, so reactivity is
  unnecessary; eslint-disable on the PlaylistPlayer import (the same
  vue-eslint-parser false positive we hit before).
- AddPreviewModal: refactor the dynamic `:ref="`video-${index}`"`
  pattern into a direct @loadedmetadata handler — script setup
  doesn't have a clean equivalent of iterating $refs, and the event
  handler is simpler anyway. Drop three unused template refs (modal,
  background, dropMask) and the empty onDrag method. Expose setFiles
  via defineExpose for parent-component callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert ManageShotsModal and ImportRenderModal from Options API.

- ManageShotsModal: useModal directly. Uses <page-title> instead of
  <h1 class="title">, same pattern as CreateTasksModal — BaseModal
  would render its own h1 alongside. Drop the unused
  ref="shot-padding". The reactive blocks for `names` and `loading`
  use `reactive()`; the rest is plain refs. The three input refs
  (addEpisodeInput / addSequenceInput / addShotInput) are accessed
  in the active watcher and methods, so they stay. focusAddSequence
  and focusAddShot exposed via defineExpose for parents.
- ImportRenderModal: switch to <base-modal> with a static title.
  Inline the small onReupload / onConfirmClicked handlers directly
  in the template (one-liner emits). The columnSelect computed
  intentionally returns parsedCsv[0] so v-model writes back into
  the parsed CSV — added a code comment to make the deliberate
  mutation pattern explicit. Drop the mounted hook that just reset
  formData to null (it starts as null via ref(null) already).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert EditCommentModal and EditPersonModal from Options API.

EditCommentModal stays on a custom shell with useModal — its title
is conditional (`v-if="commentToEdit?.id"` with no `else` branch),
so only the editing case shows an h1; BaseModal would render an
empty <h1> for new comments and push the form down. Also: complex
template with at-mentions textarea, attachments, file-upload.

EditPersonModal switches to <base-modal> with a computed title that
covers both create/edit and bot/person variants. timeMixin (only
`today` was used) → useTime() composable. Custom button row with
multiple actions stays as-is.

Notable details:
- EditCommentModal: bug fix — attachmentFilesToDelete was added
  implicitly via this.attachmentFilesToDelete = [] inside reset();
  now declared as a proper ref([]). Drop ref="file-field" (kebab,
  never accessed); rename ref="input-link" → "inputLink"; eslint-
  disable on the TextField import (vue-eslint-parser false positive).
- EditPersonModal: rename ref="name-field" → "nameField". emitForm
  dispatches one of 'confirm' / 'confirm-invite' / 'invite'
  dynamically, so vue/no-unused-emit-declarations can't trace it
  statically — disabled rule for the whole defineEmits block with
  a comment explaining why.

CSS: drop dead .modal-content .box p.text and .is-danger rules from
both files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert BuildFilterModal.vue (1035 lines) — the last and largest
modal still on Options API — to <script setup>.

The modal mixed in descriptorMixin only to call one helper —
getDescriptorChecklistValues. Rather than pull the whole mixin (used
by 27+ files) through a composable extraction, expose the helper as
a named export from src/components/mixins/descriptors.js and call it
directly. The mixin's method becomes a one-liner pointing to the
same export, so the existing 27+ Options API consumers still work
unchanged.

Standard transforms: mapGetters → store.getters via computed, methods
→ top-level arrow functions, watch.active → watch with toRef, mounted
→ onMounted, mixins: [modalMixin] → useModal(toRef(props, 'active'),
emit). Reactive blocks (assignation, hasThumbnail, priority…) stay as
reactive(); union becomes a ref. Replace dynamic computed lookups
(this[`${entityType}MetadataDescriptors`]) with explicit lookup
tables (validationColumnsByEntity, metadataDescriptorsByEntity,
searchTextByEntity).

Cleanups per CLAUDE.md:
- Pull static option arrays out of the `general` reactive object
  (operatorOptions, taskTypeOperatorOptions, booleanOptions,
  checklistOptions, unionOptions) up to module scope as plain
  constants — they never change. Update the template to reference
  them directly. taskTypeOperatorOptions aliases operatorOptions
  (they were identical).
- Replace the long if/else chain in setFiltersFromCurrentQuery with
  a filterDispatchByType lookup map.
- Add // Static options / // State / // Computed / // Functions /
  // Watchers / // Lifecycle section comments.

CSS:
- Drop dead .field { margin-top: 0; margin-bottom: 0 } rule (no
  class="field" element at this component's scope).
- Drop dead .descriptor-text-value nested rule (class never used in
  the template).
- Add a 768px responsive block — the modal keeps its custom shell
  rather than wrapping in BaseModal, so it doesn't inherit
  BaseModal's mobile defaults. Mirror them locally (margin/max-height,
  box padding, title font) and let the multi-combobox filter rows
  flex-wrap on narrow screens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the WebKit/Firefox appearance overrides on .input-editor[type='number']
so the small up/down spinner buttons don't render in the metadata cell.
Same pattern as the SalaryScale page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regression from the script-setup migration: parents call methods on
the modal via `$refs[...].method()`, but defineExpose has to opt in
explicitly (the Options API auto-exposed everything).

- AddPreviewModal: expose reset (called from mixins/task.js
  resetModals which would throw "reset is not a function")
- AddAttachmentModal: expose addFiles (called from
  widgets/AddComment.vue's getAttachmentModal().addFiles(files))

Other Add* modals already had the right exposes — verified by
grepping for `$refs['add-*-modal']` and `addThumbnailsModal()`
callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Body metadata cells with data_type === 'taglist' set their inline
z-index to up to 1000 so an open combo dropdown can spill over the
next row. The header z-indexes were sized for a much lower body
range and ended up below 1000 when scrolling, so the metadata cells
visibly overlapped the headers — and sticky-left body columns (name,
episode, sticky metadata cells) sat at z-index 1 which let
non-sticky body cells slide over them on horizontal scroll.

Move every layer above 1000 with a documented hierarchy:

  body taglist               ≤ 1000  (inline)
  body sticky-left            1001   .datatable-row-header
  header non-sticky           1002   .datatable-head th
  header sticky-left          1003   .datatable-head .datatable-row-header

The +1 between body sticky-left and header non-sticky breaks the
document-order tie at the vertical-scroll seam (otherwise the body,
which comes after the header in the DOM, wins and re-covers it).

Drop the now-redundant inline `z-index: 1001` on
`<metadata-header is-stick>` in AssetList and ShotList — that was
originally there to beat the body taglist range, but with the new
non-sticky header at 1002 the inline value would actually slide the
sticky metadata header *under* non-sticky cells during horizontal
scroll. The CSS rule (1003) gives it the right stacking now.

Inline comment on the .datatable-head th rule documents the whole
hierarchy so future contributors don't lower a layer back below 1000.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant