feat: UI migration to Vuetify 4 (phase 1 — all form components)#15
Merged
Conversation
Spike validating the materialize -> Vuetify 4 migration approach. Outcome: GO. Foundation: - vuetify@4.1.1 installed; shared instance in src/common/vuetifyPlugin.js, registered in both the main and admin apps - theme 'scriptServer' mirroring the existing palette (teal #26a69a primary, #00796B darken-1) from src/assets/css/shared.css - icons: Vuetify 'md' iconset reusing the Material Icons font the app already ships — zero new icon dependency (default MDI font isn't installed; without this, checkbox/control glyphs render blank) - vitest: vuetify added to server.deps.inline (its lib code imports .css, which Node's ESM loader rejects) + ResizeObserver stub + global plugin in the VTU config Pilot — checkbox.vue rewritten on v-checkbox: - identical external API (modelValue/config/disabled, update:modelValue), indeterminate while modelValue is null, boolean normalisation on mount - defaults: density compact, hideDetails, color primary (teal checkmarks) Validation: - 821 unit tests green (incl. the 91 ParameterConfigForm tests exercising Checkbox), e2e 8/8, build green - visual coexistence check on the admin config form: 17 v-checkbox rendered among 34 materialize inputs, no style bleed either way (Vuetify CSS layers lose to materialize's unlayered globals only on shared bare-element selectors, none hit here); checked/unchecked/indeterminate all correct Known notes for later phases: - bundle: full-components import for now (~3.2M assets) — switch to vite-plugin-vuetify treeshaking once more components are migrated - combobox_test "focus search field on open" is flaky under full-suite load (pre-existing; that suite gets rewritten in Phase 2 anyway) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second component on Vuetify (after the Phase 0 checkbox pilot). Only the rendering layer changed: v-text-field, or v-combobox when the parameter type is editable_list (replacing the materialize input + M.Autocomplete). The validation engine and the external contract (modelValue/config/disabled props, update:modelValue + error emits, data-error attribute, focus()) are untouched. Tests: - textfield autocomplete tests rewritten for the v-combobox dropdown: the menu is a teleported v-overlay on document.body, so options are queried via .v-overlay .v-list-item instead of an in-component <ul> - deliberate behaviour change: reopening the menu with a pre-set value shows all options (Vuetify standard; materialize filtered on the current value). Filtering while typing is unchanged and still covered. - ScriptField_test: .input-field input (materialize wrapper) -> .textfield input, the class the migrated component keeps jsdom setup fixes required by Vuetify overlays: - visualViewport stub (VOverlay's connected location strategy subscribes to its resize/scroll events) - offsetParent stub now returns null for <body>/<html> like real browsers; the old `parentElement || document.body` fallback made Vuetify's isFixedPosition offsetParent walk bounce between body and html forever (synchronous infinite loop, hung test workers at 95% CPU) README: added a "Vuetify 4 migration (in progress)" entry. Validation: 821 unit tests green, e2e 8/8, build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third component on Vuetify. Only the rendering layer changed: v-textarea with auto-grow replaces the materialize textarea + M.textareaAutoResize / M.updateTextFields. The validation logic (required, max_length) and the external contract (modelValue/config props, update:modelValue + error emits, data-error attribute) are untouched; setCustomValidity is still applied to the native textarea. Test note: Vuetify forwards non class/style/id/data-* attrs to the inner textarea, so the config.description tooltip now lives on the <textarea> instead of the root element (same as the migrated Textfield); the assertion was updated accordingly. Validation: 821 unit tests green, e2e 8/8, build green. Visual check on the admin script form (port 5099 fixture): v-textarea renders with the floating label and auto-grows, value round-trips through v-model, no console errors, no materialize textarea left, no style bleed on the surrounding materialize inputs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dialog Fourth component on Vuetify. v-radio-group/v-radio replace the materialize radio markup; the custom per-option icon (changed-mode warning) keeps its own <i class="option-icon"> in the label slot since Vuetify's md iconset also renders i.material-icons for the radio glyphs. New RadioGroup unit tests (the component had none). The migration surfaced two latent Vue 3 bugs around its only consumer, the script-edit dialog — both fixed: - ScriptField declared the dialog with the Vue 2 async-component syntax (`() => import(...)`), which Vue 3 renders as the literal text "[object Promise]": the dialog could never open in the built app. The module was already statically imported for its mode constants, so the lazy wrapper is simply dropped. - RadioGroup still used the Vue 2 v-model contract (value prop + input event), so the dialog's v-model never received mode changes. Moved to modelValue/update:modelValue. Fallout of the earlier textfield migration, fixed here: the old Textfield imported materialize input-fields globally, which defined M.updateTextFields for everyone. ScriptEditDialog's call (its fields are all Vuetify now) is removed; SchedulePanel and TimePicker, still materialize, now import input-fields themselves. Validation: 826 unit tests green (incl. 5 new RadioGroup), e2e 8/8, build green. Browser check on the fixture admin: dialog opens, the three modes (path/code/upload) switch correctly via the Vuetify radios, no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fifth and biggest component on Vuetify. v-select renders the dropdown; when the option list is long enough to need filtering (>10 options, as before), the component switches to v-autocomplete with type-to-filter in the field — replacing the materialize in-dropdown ComboboxSearch, which is deleted. All the FormSelect plumbing (rebuildCombobox, manual DOM sync, onchange subscription, Firefox two-phase disable) is gone; the value/validation business logic (_fixValueByAllowedValues, _validate, forceValue with disabled obsolete options, multiselect emit filtering) is untouched. External contract preserved: config/modelValue/disabled/ forceValue/showHeader props, update:modelValue + error emits, data-error. dropdownContainer is accepted but unused (Vuetify menus are teleported overlays). The "Choose your option(s)" header becomes a persistent placeholder; loading uses the built-in progress bar instead of the CircleSpinner overlay. Tests: - combobox_test.js rewritten against the Vuetify markup (40 tests, all active): options are read from the teleported overlay, search drives the v-autocomplete input. 8 scenarios that were skipped under jsdom with materialize (multiselect by clicks, forced initial values, click on disabled values, loading=true) now actually run. - the shared admin-form helper setValueByUser drives Combobox through its update handler (no native <select> anymore); findFieldInputElement returns the v-select <input>. - e2e "changes a combobox value" updated to click the v-field and the overlay list item. Side fix: materialize Collapsible relied on the anime.js global loaded transitively via the old combobox (select -> dropdown -> anime); the collapsible import now loads it explicitly. Validation: 827 unit tests green, e2e 8/8, build green. Browser check on the fixture main app: v-select renders with label/selection, menu opens, selecting beta updates the field, no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three more components on Vuetify:
- ChipsList: v-combobox (multiple + chips + closable-chips) replaces
M.Chips. Enter-to-add and chip removal come with the component; the CSV
behaviours are preserved as business logic: typing an unescaped comma
commits the finished segments live (keeping the trailing fragment in
the input), focus loss commits the pending text (via VCombobox's own
blur commit, normalized in the update handler), and `\,` escapes a
comma inside a value. One subtlety: when the committed search returns
to the same prop value (''), Vue won't rewrite the user-typed DOM input,
so the component syncs the native input explicitly.
- PromisableButton: v-btn with the built-in :loading spinner replaces the
materialize flat button + embedded preloader markup. Promise handling
(click -> inProgress -> error with userMessage) untouched; the
preloaderStyle prop is accepted but unused now. The dead spinner CSS in
ScriptConfig's footer is removed.
- CircleSpinner: deleted — its last consumer was the pre-migration
combobox.
Tests: ChipList suite rewritten against the v-combobox markup (9 tests,
incl. 2 new: external value change, chip removal via the close button).
The shared setChipListValue helper now emits through the component's
v-model instead of driving the M.Chips instance.
Validation: 829 unit tests green, e2e 8/8, build green. Browser check on
the fixture admin: CSV typing 'user1,user2\,with-comma,partial' produces
the user1 + "user2,with-comma" chips with 'partial' left in the input;
Delete/Save footer buttons render as v-btn; no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pickers on Vuetify: - DatePicker: v-date-input (text field + calendar menu) replaces the M.Datepicker modal. Same constraints: min date today, weeks start on Monday, value emitted only when the picked date changed. The showHeaderInModal prop is kept for compatibility (the Vuetify calendar has no modal header at all, matching the old headless rendering). - TimePicker: v-text-field replaces the materialize input + M.updateTextFields / M.validate_field plumbing. The HH:MM validation is untouched; the displayed text is deliberately decoupled from the model so an invalid typed time stays visible with its error instead of snapping back to the last valid value (the model only receives valid times, as before). The 10 existing TimePicker tests pass unchanged. - SchedulePanel drops its materialize datepicker import. Third latent Vue 3 bug found and fixed (browser check, not by tests): SchedulePanel.checkErrors walked this.$children — an API removed in Vue 3 — and threw on every field error, so the Schedule button was never disabled on invalid input. Field errors now arrive through keyed @error events into a fieldErrors map, filtered by the fields rendered in the current mode (one-time vs repeat, end option), which mirrors the old walk over mounted children. e2e fixture: scheduling enabled on E2E Echo so the schedule panel is reachable in e2e and manual checks. Validation: 829 unit tests green, e2e 8/8, build green. Browser check on the fixture main app: schedule panel opens with both Vuetify pickers prefilled, calendar opens with past days disabled, picking a date updates the field, typing 99:99 shows 'Format HH:MM' and disables the Schedule button, fixing the time re-enables it; no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The server-file picker field is on Vuetify: a readonly v-text-field with the folder_open icon inside the field (append-inner) opens a v-dialog hosting the existing FileDialog browser — file_dialog.vue is untouched, it never depended on materialize JS (only on shared CSS classes). Preserved behaviours: open on click/Enter/Space, focus the browser on open and the field back on close, required validation, value emitted only when the chosen path changed, left-side path truncation. e2e fixture: a ConfFile server_file (recursive) parameter was added to E2E Echo so the field actually renders in e2e and manual checks (non-recursive server_file params are flattened to plain list values by the server and render as a Combobox). Parameter-count assertions updated to 3. Validation: 829 unit tests green, e2e 8/8, build green. Browser check on the fixture main app: field renders with the folder icon, the dialog opens with the file listing, double-clicking a file closes the dialog and fills the field; no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
materialize-css is unmaintained (and the subject of open Dependabot alerts on this repo). This branch migrates the UI to Vuetify 4 to keep the frontend stack secure and maintainable. The migration is incremental: Vuetify and materialize coexist until every piece is ported.
Draft PR: opened to run CI on the branch. Phase 2 (views: script list sidebar, schedule panel layout, admin dialogs/tabs, login, then materialize removal + Vuetify treeshaking) will land on this same branch.
What's in phase 1
vuetifyPlugin.js),scriptServertheme mirroring the existing palette,mdiconset reusing the Material Icons font already shipped (no new icon dependency), vitest/jsdom setup for Vuetify overlays.v-checkboxv-text-field/v-combobox(editable_list)v-textarea(auto-grow)v-radio-groupv-select/v-autocomplete(>10 options)v-comboboxchips (CSV typing kept)v-btn(built-in loading)v-date-inputv-text-field(HH:MM validation kept)v-text-field+v-dialog[object Promise](Vue 2 async-component syntax) — the dialog could never open.$childrenAPI — the Schedule button was never disabled on invalid input.Validation
Known follow-ups (phase 2)
vite-plugin-vuetifytreeshaking (bundle currently ~3.2 MB with full-component import)🤖 Generated with Claude Code