Skip to content

feat: UI migration to Vuetify 4 (phase 1 — all form components)#15

Merged
knep merged 10 commits into
masterfrom
feat/vuetify
Jun 11, 2026
Merged

feat: UI migration to Vuetify 4 (phase 1 — all form components)#15
knep merged 10 commits into
masterfrom
feat/vuetify

Conversation

@knep

@knep knep commented Jun 11, 2026

Copy link
Copy Markdown
Owner

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

  • Foundation: shared Vuetify instance (vuetifyPlugin.js), scriptServer theme mirroring the existing palette, md iconset reusing the Material Icons font already shipped (no new icon dependency), vitest/jsdom setup for Vuetify overlays.
  • All form components migrated (external APIs and validation logic unchanged):
    Component Now
    checkbox v-checkbox
    textfield v-text-field / v-combobox (editable_list)
    TextArea v-textarea (auto-grow)
    RadioGroup v-radio-group
    Combobox v-select / v-autocomplete (>10 options)
    ChipsList v-combobox chips (CSV typing kept)
    PromisableButton v-btn (built-in loading)
    DatePicker v-date-input
    TimePicker v-text-field (HH:MM validation kept)
    server_file_field v-text-field + v-dialog
  • Deleted dead code: ComboboxSearch (replaced by v-autocomplete filtering), CircleSpinner.
  • Three latent Vue 3 bugs found and fixed (none were covered by jsdom tests; all found via real-browser checks):
    1. Script-edit dialog rendered as the literal text [object Promise] (Vue 2 async-component syntax) — the dialog could never open.
    2. RadioGroup still used the Vue 2 v-model contract — mode switching never reached the dialog.
    3. SchedulePanel collected field errors via the removed $children API — the Schedule button was never disabled on invalid input.
  • One deliberate behaviour change: reopening an autocomplete with a value set shows all options (Vuetify standard) instead of filtering on the current value; filtering while typing is unchanged.

Validation

  • 829 unit tests green (Combobox suite rewritten: 40 tests all active, 8 previously jsdom-skipped scenarios now run; new RadioGroup + ChipsList coverage)
  • Playwright e2e 8/8 (combobox test rewritten for Vuetify; fixture gained scheduling + a recursive server_file param to exercise the new components)
  • Manual browser verification of every migrated component on the e2e fixture app

Known follow-ups (phase 2)

  • Views: ScriptListItem spinner, schedule panel layout, admin dialogs/tabs/cards, login
  • Remove materialize-css and jquery-free leftovers, then enable vite-plugin-vuetify treeshaking (bundle currently ~3.2 MB with full-component import)

🤖 Generated with Claude Code

Thomas Kpenou and others added 10 commits June 10, 2026 11:14
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>
@knep knep marked this pull request as ready for review June 11, 2026 20:46
@knep knep merged commit db60aa9 into master Jun 11, 2026
13 of 14 checks passed
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