Skip to content

Migrate to Vue 3 Composition API and TypeScript#3751

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

Migrate to Vue 3 Composition API and TypeScript#3751
Vondry wants to merge 38 commits into
bolt:6.2from
Vondry:refactor/vue3-composition-api

Conversation

@Vondry

@Vondry Vondry commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

This PR modernizes the Bolt admin frontend assets for Vue 3, TypeScript, stricter quality checks, and stronger UI test coverage.

Main Changes

Vue 3 Migration

  • Migrated admin Vue components under assets/js/app/ to Vue 3 Composition API with <script setup lang="ts">.
  • Replaced Vuex stores with typed Pinia stores for listing, sidebar, and toolbar state.
  • Removed legacy mixins and replaced shared field-value behavior with useFieldValue.ts.
  • Replaced Vue 2 global filters with direct typed helper imports.
  • Added a typed mitt event bus for editor title/slug synchronization.

TypeScript

  • Converted core frontend entry points, filters, stores, API helpers, and shared utilities from .js to .ts where useful.
  • Added strict TypeScript configuration for Vue SFCs, TypeScript files, declaration files, and unit tests.
  • Enabled stricter compiler checks including:
    • strict
    • noImplicitReturns
    • noFallthroughCasesInSwitch
    • allowUnreachableCode: false
    • allowUnusedLabels: false
    • noImplicitOverride
    • isolatedModules
  • Added vue-tsc --noEmit type checking.

Build Tooling

  • Updated webpack config to ESM via webpack.config.mjs.
  • Added Vue 3 loader setup.
  • Added TypeScript loader setup for .ts and Vue SFC files.
  • Updated frontend dependencies for the Vue 3 toolchain.

Testing

  • Migrated unit testing from Jest to Vitest and Vue Test Utils for Vue 3.
  • Added broad unit coverage for editor fields, listing components, sidebar, toolbar, and shared composables.
  • Added Vitest coverage thresholds:
    • Statements: 95%
    • Branches: 85%
    • Functions: 95%
    • Lines: 95%

CI

  • Updated the assets CI workflow to run UI unit tests with coverage.
  • Added TypeScript checks to CI via npm run typecheck.
  • CI now validates:
    • ESLint
    • Stylelint
    • Vitest coverage
    • Vue/TypeScript type checking

Linting And Code Quality

  • Replaced the legacy ESLint config with the modern flat config format.
  • Added TypeScript-aware ESLint rules.
  • Made @typescript-eslint/no-explicit-any an error.
  • Added SonarJS linting to catch maintainability issues and suspicious code patterns.
  • Added eslint-plugin-vuejs-accessibility to enforce accessibility checks in Vue components.
  • Added Prettier integration through ESLint for consistent formatting.
  • Kept targeted accessibility rule exceptions only where existing UI behavior requires them.

Accessibility

  • Added or improved labels, aria-labels, screen-reader text, and keyboard roles across Vue components.
  • Improved editor field accessibility for inputs, buttons, file/image controls, selects, and date fields.
  • Improved listing and toolbar accessibility for action controls, search, dropdowns, and status interactions.

Shared Utilities

  • Extracted shared field value handling into useFieldValue.ts.
  • Extracted shared server file/image browser modal flow into serverFileBrowser.ts.
  • Centralized upload error-message extraction so server-provided errors are surfaced properly.
  • Added typed shared debounce utility.
  • Added typed event bus for editor cross-field communication.

Performance And Bug Fixes

Editor Fields

  • Added reactive collection item removal so collection state is updated through Vue instead of direct DOM deletion.
  • Fixed select handling for numeric 0 values.
  • Fixed single-select tagging so it keeps a single selected value instead of accumulating tags.
  • Fixed image extra-field handling so base image record keys do not leak into submitted extra values.
  • Avoided flatpickr invalid-locale errors for the default locale.
  • Surfaced useful server error messages on file/image upload and file-browser failures.
  • Kept media crop values numeric.

Listing

  • Hid listing status action links when a status URL is unavailable.
  • Improved typed handling of listing records, localized slugs, row actions, row metadata, and selection state.

@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
dependabot Bot and others added 23 commits July 7, 2026 18:07
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.9 to 2.0.10.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.10/CHANGELOG.md)
- [Commits](chimurai/http-proxy-middleware@v2.0.9...v2.0.10)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
# Conflicts:
#	package-lock.json
Bumps [undici](https://github.com/nodejs/undici) from 6.26.0 to 6.27.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](nodejs/undici@v6.26.0...v6.27.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.27.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [launch-editor](https://github.com/vitejs/launch-editor) from 2.14.0 to 2.14.1.
- [Commits](vitejs/launch-editor@v2.14.0...v2.14.1)

---
updated-dependencies:
- dependency-name: launch-editor
  dependency-version: 2.14.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
The Relation API resource is readable with `api:get` (PUBLIC_ACCESS) and
embeds both `fromContent` and `toContent`, but ContentExtension only
filtered the Content and Field resources. Anonymous users could
therefore enumerate relations involving unpublished, draft or viewless
content, leaking its existence, IDs and the relationship graph.

Apply the same published/non-viewless filter to Relation queries,
requiring both ends of the relation to be publicly visible. Add tests
asserting the collection and item queries constrain both ends; they fail
if the filter is removed.
Bind content-type slugs as query parameters instead of interpolating
them into raw SQL strings. The split() helper now returns an array of
slugs bound via ArrayParameterType::STRING rather than a pre-quoted SQL
fragment.

Add regression tests proving the slugs are passed as bound parameters
and never reach the SQL string.
The /embed endpoint fetched any user-supplied URL server-side via the
embed library after only a CSRF check, with the same SSRF exposure as
the upload-from-URL endpoint. Guard it with UrlSafetyChecker before the
fetch.

Add controller tests for both /embed and /upload-url asserting that
internal/private/metadata hosts and disallowed schemes are rejected with
a 400. These tests fail if the UrlSafetyChecker guard is removed.
The /upload-url endpoint fetched any user-supplied URL server-side via
Filesystem::copy() after only a format/scheme check, allowing requests
to internal services and cloud metadata endpoints (e.g. 169.254.169.254).

Add UrlSafetyChecker, which rejects non-http(s) schemes and any URL whose
host resolves to a private, reserved, loopback or link-local address
(including IPv4-mapped IPv6), and call it before fetching the URL.

Add unit tests covering allowed public URLs and blocked schemes, private
ranges, loopback, link-local/metadata and malformed input.
Enable strict TypeScript and replace loose Record<string, any> typings with
explicit interfaces and shared type modules across the Vue editor, listing,
sidebar and toolbar components. Includes hardened async/error handling
(BrowserError class, shared upload-error helper, safe DOM lookups), tightened
lint/tsconfig, and the accompanying unit-test type-safety updates.
Image.vue built its reactive extra-field map from every key of the extra-data
payload. In an imagelist the parent passes the whole ImageField record, so base
keys (filename, media, thumbnail, ...) were coerced into the map. Key the map off
the declared extra fields instead, normalizing the object/array/whole-record
inputs to a single lookup.
getStatusLink() returns null for new content or when the user lacks CONTENT_VIEW
permission, so the status-change links rendered a dead "&status=..." relative
href. Guard each on extras.statusLink so they hide entirely, which is also the
correct authorization behavior.
normalizeError checked `instanceof Error` and returned Axios's generic "Request
failed with status code N" before extracting the server-provided message. Since
AxiosErrors are Error instances, the response.data message was unreachable.
Extract the response message first, then fall back to error.message.
fixSelectedItems used a truthiness check (`if (props.value)`) that discarded a
valid numeric key of 0. Use an explicit nullish check. Also, addTag wrapped the
new tag in an array in the single-select branch; bind the object directly so the
non-multiple v-model stays a single object.
getLocale() returns undefined for English/unresolved locales, and passing
locale: undefined makes flatpickr throw "invalid locale undefined". Only include
the locale key when one is resolved; flatpickr then uses its English default.
The test emitted the kebab-case "update:model-value", which vue-multiselect does
not declare, producing a Vue emit warning. Emit the declared camelCase event.
fixSelectedItems always assigned an array, so single-select mode left
addTag pushing into it and accumulating multiple tags. Bind the shape
vue-multiselect expects: a single object (or null) when multiple is false.
Align each component's props with its real usage (Twig templates as the
source of truth) so passed attributes bind as props instead of falling
through onto the root element:

- Image: title (imagelist/simple_image)
- File: thumbnail, alt (file.html.twig, Filelist)
- Textarea: label
- Password: label, label_class (kept underscore to match the emitted attribute)
- listing Row _Actions: size
Vondry added 11 commits July 7, 2026 18:10
- Fix theme/skeleton/assets/favicon/manifest.json 404 by explicitly defining 'bolt' package in favicon twig partials.
- Fix Vue warnings on cropX/cropY/cropZoom props by coercing them to strings in twig and allowing number in Text.vue.
- Fix Vibrant.js getSwatches/getHex TypeError for node-vibrant v4 compatibility.
- Fix bolt_logo_dashboard.svg 404 by converting absolute path to relative in Toolbar.vue to enable Webpack processing.
- Add test coverage for Toolbar logo src.
- Integrate eslint-plugin-vuejs-accessibility
- Map orphaned labels to inputs via id
- Replace non-semantic empty anchors with buttons
- Remove redundant click handlers on labels
- Convert static text labels to spans to prevent screen reader confusion
- Adjust eslint rules to allow valid Bootstrap label-input sibling structures
- Revert removal of autofocus from Text.vue and suppress a11y rule
- Remove fragment-creating a11y suppression comments from root nodes of File.vue and Image.vue
- Suppress a11y rules for File.vue and Image.vue directly in eslint.config.mjs
fixRequired() ran on mount and called input.reportValidity(), which
popped the native 'please fill in this field' bubble on page load for
every empty required date field, before any user interaction. Only
report validity on subsequent updates/value changes; still apply the
required attribute on mount.
Investigate the two remaining review candidates with executable tests:

- Bug 2 (Slug/Text eventBus scoping): add integration tests proving the
  supported top-level title->slug flow works end-to-end, and that a field
  nested in a collection (fields[blocks][hash][title]) does not drive the
  page's top-level slug. The latter fails if fieldKey is changed to extract
  the innermost segment, confirming the current first-segment scoping is the
  correct design and nested auto-slug was never a supported behaviour.

- Bug 3 (Collection move/reorder): add tests proving the DOM submission
  order (hash-named inputs) survives a manual move followed by an add or a
  remove. The claimed 'reorder snaps back' failure does not occur, because
  Vue's keyed diff never relocates the jQuery-moved nodes.

Both candidates pass on current code; these lock in the correct behaviour.
@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
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.

2 participants