Skip to content

Migrate to Vue 3 Composition API and TypeScript - #3751

Open
Vondry wants to merge 1 commit into
bolt:6.2from
Vondry:refactor/vue3-composition-api
Open

Vondry wants to merge 1 commit into
bolt:6.2from
Vondry:refactor/vue3-composition-api

Conversation

@Vondry

@Vondry Vondry commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

This PR moves the Bolt admin frontend to Vue 3 (Composition API, <script setup lang="ts">), Pinia and TypeScript. It builds on the groundwork already in 6.2: the TypeScript setup, the ESLint flat config and the Vitest suite.

Main changes

Vue 3 migration

  • Admin components under assets/js/app/ are rewritten with <script setup lang="ts">.
  • Typed Pinia stores replace Vuex for the listing, sidebar and toolbar.
  • The mixins/value field mixin is replaced by the useFieldValue composable.
  • A typed mitt event bus replaces $root.$emit for keeping titles and slugs in sync. The payloads are scoped, so a Text field only drives the Slug fields generated from it.
  • Third-party components move to their Vue 3 versions: vue-multiselect 3, vuedraggable 4, vue-trumbowyg 4 and vue-flatpickr-component 12. The Markdown field now drives easymde directly instead of going through vue-easymde.
  • The entry points (bolt, editor, listing, sidebar, toolbar) are now TypeScript and boot with createApp().

Shared utilities

  • serverFileBrowser.ts holds the server file/image browser modal flow shared by the File and Image fields.
  • upload.ts pulls the error message out of a failed upload, preferring the server's message over Axios's generic one.
  • Also added: a typed debounce helper, and shared types for the editor, listing and sidebar.

Build tooling

  • Encore 7 with vue-loader 17 and the ts-loader for .ts files and SFCs. The webpack config is now ESM (webpack.config.mjs).
  • Babel 8, webpack-cli 7, sass-loader 17 and postcss-preset-env 11.
  • Other dependency upgrades: Luxon 3, hotkeys-js 4, Dropzone 6 (beta), flagpack-dart-sass 2.
  • Removed: vuex, vue-easymde, vue-template-compiler and @vitejs/plugin-vue2.

Type checking and linting

  • npm run typecheck now runs vue-tsc --noEmit, so the SFCs are type-checked along with the .ts files, under 6.2's strict compiler settings.
  • The ESLint config moves from the Vue 2 preset to flat/recommended. typescript-eslint now covers the SFCs too, and @typescript-eslint/no-explicit-any is an error.
  • New ESLint plugins: eslint-plugin-sonarjs and eslint-plugin-vuejs-accessibility. Rule exceptions are kept only where existing UI behaviour needs them.
  • The temporary exemptions in 6.2's config are removed, because the components no longer do what they excused: vue/no-mutating-props for File/Image/Select, and no-useless-assignment.

Accessibility

  • Labels, aria-labels and screen-reader text are added or fixed across the editor fields, listing, toolbar and sidebar.
  • <label> elements that labelled no input are now plain text. The Checkbox label now points at the input's id.

Backend

  • ClearCacheController and ExtensionsController now extend TwigAwareController.
  • The bolt, public and files asset packages are now declared in config/packages/assets.yaml.
  • Twig: the date field passes labels as a bound prop, and the markdown field binds model-value.

Unit tests

6.2's Vitest suite was written against the Vue 2 components as a baseline for this migration. It is ported here rather than replaced:

  • Mount API: specs use @vue/test-utils 2 (props, global: { plugins, stubs }, unmount()).
  • Helpers: the store helpers build a real Pinia and record state writes and action calls. The bus helpers drive the mitt bus.
  • Stubs: third-party stubs follow the Vue 3 modelValue contracts.
  • Tests for this PR's fixes: every it.skip/it.todo that was waiting on this branch is now a real test. Tests that pinned Vue 2 defects fixed here now assert the fixed behaviour.
  • New specs: serverFileBrowser, useFieldValue, and an end-to-end Slug/Text test.
  • Coverage gate: 95% statements, functions and lines, and 90% branches.
  • Status: 537 tests in 48 files, run shuffled in CI.

Bug fixes

Editor fields

  • Removing a collection item now goes through Vue state rather than deleting DOM nodes. The item's slot is only freed once the removal is confirmed, so cancelling can no longer push past the limit.
  • The collection's delegated jQuery handlers are removed on unmount.
  • Select:
    • An option whose key is the number 0 stays selected.
    • Single-select tagging replaces the selection instead of accumulating values.
  • Image:
    • Declared extra fields bind correctly when the parent passes the whole image record.
    • Base record keys no longer leak into the extra values.
  • File/Image upload messages: failed uploads and file-browser errors show the server's message.
  • Date:
    • The field no longer shows the browser's validation popup on page load.
    • The default locale no longer triggers flatpickr's invalid-locale error.
  • Numeric media crop values stay numeric.
  • The Filelist, ThemeSelect and Slug fields no longer throw when the expected data or page elements are missing.
  • An unreadable slim-sidebar value in localStorage now falls back to a wide sidebar. Without that, a Vue 3 setup error would have removed the sidebar entirely.

Listing

  • The status action links are hidden when a record has no status URL.
  • Typing was tightened for listing records, localized slugs, row actions, row metadata and selection state.

Other

  • Dead code that no template used was removed: Toolbar.createMenu, Filter.enableSorting, SelectBox.order and Row.sorting.

Known issue, unchanged

  • Clearing a required date field still doesn't mark it as required (the cleared value is null, not ''). A test pins this.

Checks

  • Frontend: npm run lint, npm run stylelint, npm run typecheck, npm run test:unit:coverage and npm run build.
  • Backend: vendor/bin/phpstan analyse, vendor/bin/ecs check src, bin/console lint:yaml config --parse-tags and bin/console lint:twig templates.

@Vondry
Vondry force-pushed the refactor/vue3-composition-api branch 3 times, most recently from 5304b92 to 36f5bda Compare July 6, 2026 17:52
@Vondry
Vondry marked this pull request as ready for review July 7, 2026 11:49
This was referenced Jul 7, 2026
@Vondry
Vondry changed the base branch from 6.1 to 6.2 July 7, 2026 16:04
@Vondry
Vondry force-pushed the refactor/vue3-composition-api branch from 4f7e067 to 3778f43 Compare July 7, 2026 19:11
@Vondry
Vondry force-pushed the refactor/vue3-composition-api branch from b420363 to ccafced Compare July 9, 2026 09:29
@bobvandevijver bobvandevijver added the needs splitting Issue is too big: Needs to be split up into several smaller issues label Aug 3, 2026
@bobvandevijver

Copy link
Copy Markdown
Member

While I do appreciate the work done, this is simply not reviewable in it's current form. I need to ask you to split the work done in separate PRs to be able to do something with this. My suggestion for PRs would be:

  1. Update test cases, so that those can be used to validate the migrated components in the future.
  2. Migrate to Vue 3 only.
  3. Migrate from webpack to vite.
  4. Accessibility stuff. Possibly multiple PRs based on whatever needed to be updated.

And possibly even more. Keep it simple, keep it reviewable.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs splitting Issue is too big: Needs to be split up into several smaller issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants