Skip to content

Feat/list editors - #214

Open
volarname wants to merge 323 commits into
betafrom
feat/list-editors
Open

volarname wants to merge 323 commits into
betafrom
feat/list-editors

Conversation

@volarname

Copy link
Copy Markdown
Contributor

No description provided.

@volarname
volarname temporarily deployed to npmjs-publish May 5, 2026 09:14 — with GitHub Actions Inactive
@volarname
volarname force-pushed the feat/list-editors branch from 8a777ff to de3e563 Compare June 10, 2026 09:44
…temp ids, managed add/delete (item-factory/manage-delete), unsaved-section-label, renumberPositions util, row-key warnings
volar added 30 commits September 23, 2026 17:22
…ers, collab takes io from the admin

Docs
- the vitepress site is gone (docs/, its scripts and devDependencies, i18nDocs.ts, the docs-props
  regions); what was still true lives as plain markdown: doc/guide (installation, i18n, styleguide),
  doc/editor (anzutap nodes, marks, extensions, scraper types), README-DEV.md for local development
- the pages were checked against admin-cms and the other admins: pluralization, the admin i18n and
  vuetify setup, plugin options, global components, the eslint plugin, per-editor node availability,
  shortcuts, paste extensions and the embedExternal <-> scraper flow
- README describes the versioning: Semantic Versioning with a few edge cases
- CHANGELOG.md is an index; 2.0.0 is the first release with a changelog, its draft describes what
  the package contains instead of listing changes, and the per-topic unreleased notes are dropped

Dependencies
- every peer is required; unplugin is dropped (nothing imports it), sortablejs added (useSortable)
- @sentry/vue, dayjs and @vueuse/integrations/useSortable were bundled into dist because the external
  list matched plain names only; they now resolve from the consumer
- socket.io-client is no longer a peer: collab gets `io` through the plugin options
  (`collab.io`), so only an admin with collaboration installs it; CommonAdminCollabOptions is exported
Vite lib mode inlines assets, so the JPEG sat in dist/common-admin.js as 77 KB of base64.
The SVG is drawn from the JPEG (same shapes, sampled colours; renders within antialiasing
of the original at 1600x900, 320x180 and 160x90) and keeps width/height 1600x900, which
VImg needs for the aspect ratio in ImageWidgetInner and ImageMediaWidgetInner.
…tup and the library's source maps

The six admins carried the same sentryVitePlugin block, and Sentry showed frames from the library
minified: Vite ignores sourceMappingURL in node_modules, so the admin maps ended in
dist/common-admin.js.

- commonAdminSourcemaps() loads the library dist files together with their maps, so the admin build
  chains them and frames resolve to the library's .vue and .ts sources; only when the build emits
  maps, the JS is unchanged
- anzuSentry({ project }) returns it plus the Sentry plugin: on when APP_DEPLOY_ENV and SENTRY_URL
  are set, hidden maps, uploaded and deleted from the resolved build.outDir; a missing token, org or
  project, or a failed upload, fails the build instead of a warning; other options override the
  sentryVitePlugin defaults (e.g. a test release with finalize: false)
- @sentry/vite-plugin and vite are optional peers; without the plugin installed anzuSentry works
  while Sentry is off and says what is missing once it is on
- node tests in src/test/vite run real Vite builds of a fixture (no network: upload skipped or
  rejected by a local server)
- copy.sh fails fast, copies src/vite too and takes the target admins as arguments
…ts and sort its imports

scripts/codemods/explicit-imports, run from here against an admin with the admin's own TypeScript,
Vue compiler, unimport, oxlint and oxfmt:

- run.mjs adds the imports: the TypeScript checker, without lib, module resolution or the
  generated auto-imports.d.ts, finds the names a file uses without binding them (value and type
  meaning apart), templates through _ctx.X of the compiled render function, template types through
  its TypeScript. Before and after it records what unimport injects, where every template reference
  is taken from, the runtime props and the import graph, and fails on anything left unexplained:
  residual injections, names unimport never injected, a template reference changing its target
  other than _ctx -> $setup for a name imported now, a prop gaining Boolean or Function, a new edge
  inside a cycle, a prop shadowed by an import. Exceptions from an allowlist in the admin; the
  manifest records what was added, the tool versions and this commit
- sort.mjs runs oxlint --fix until it changes nothing (at most five passes), then oxfmt, then both
  in check mode
- css-order.mjs compares the CSS of two builds when only the import order changed and lists the
  swapped rules that could decide the cascade
- fixtures for the cases the plan lists (shadowing, the value and type namespaces, the readonly
  prop, template scopes, template casts, macros, the empty <script setup>, cycles, …); oxlint and
  eslint skip them
… library's declaration bundle

A .d.ts with `declare global` anywhere in the repository reached the bundled common-admin.d.ts and
renamed its types (Ref_2, IntegerId_2, useI18n_2). The fixtures keep it as
auto-imports.d.ts.fixture and the test runner renames it in its copy.
…, and only swaps on the same box

admin-cms showed what a per-file comparison misses: after its imports were sorted, rolldown packed
the modules it loads at startup into fewer chunks (the same 2278 modules, 111 chunks instead of
143), so rules moved between the stylesheets index.html links while the page gets the same ones.

- the stylesheets index.html links are one unit, in link order; lazily loaded ones stay per file
- one entry per selector: the minifier merges neighbouring rules with equal declarations, and
  which rules neighbour depends on the order being compared
- a scoped style's id is ignored in its keyframe names too
- a swap only counts when both rules style the same box: not ::before against ::after, not img
  against div
…eleted

The cleanup the labs merge prepared (commented out in src/lib.ts, the admins built and tested
against that state): with the admins now on explicit imports, their six builds and test suites
confirm nothing they use is gone.

- src/lib.ts: the commented-out imports, exports, aliases and their JSDoc deleted (the section
  headings stay); SortableNested and SortableNestedItem are declared in listEditorTypes, the only
  link from live code to the old sortable
- 62 files deleted: the old lib pairs of the labs components (filters, datatable pagination and
  ordering, remote autocompletes, subject select, sortable), the old api helpers, filter helpers and
  query builder, ACropperjs (cropper.js v1), the dead list editor composables, and files nothing
  used any more (KeywordCreateButton, useDirtyBaseline, locales/xx.ts, Image, AssetSlot)
- dead symbols deleted where their file stays (CurrentUserType, the purge-room event bus,
  CollabRouteMeta, UseListEditorOptions, GrantDefault, LogLevelDefault, ApiResponseList,
  EnableDisable, the *Default constants of AssetFile and DamConfig, ObjectPaths, Prettify,
  arrayToString, timestampCurrent, yearNow, objectGetValues, stringTrimLength,
  stringNormalizeForSlotName, stringUrlTemplateReplaceVueRouter) and the imports they left unused
- the eslint rule anzu/no-fatal-error-axios-check goes with the old helpers whose error shape it
  checked; recommended() ignores an unknown option
- tests: the cropper suites drive ACropper only, the query builder contract keeps the labs half,
  the add-after cases run on useListEditorController, the tests of deleted functions are deleted;
  cropperjs (v1) leaves the devDependencies
- playground: the cropper view shows v2 only, the sortable view is gone (the list editor views
  replace it)
- the browser vitest no longer collects scripts/ (the codemod fixtures hold a *.test.ts)
- doc/2.0.0-UPRATOVANIE.md, the working note, is deleted now that it is done

yarn ci, yarn build, yarn test:run (browser 1463, node 47).
… the public entry

Preparing per-module output with `sideEffects`, where a consumer's bundler keeps only what is used:

- ACropper calls `$define()` for the seven cropper.js elements it builds. cropper.js registers them as a
  side effect of loading its entry; once the library ships per module and says it has no side
  effects, that module is dropped whenever only the element classes are used, and the cropper
  would build unregistered elements. `$define` is a no-op for a registered element
- jobPriority and AssetImagePreview imported `objectGetValueByPath` and `isUndefined` from `@/lib`, a
  cycle through the entry that would put every export into the graph of those modules; they import
  from `@/utils/object` and `@/utils/common` now, and eslint forbids `@/lib` (or a relative path to
  it) inside `src`

yarn ci; the cropper suites twice (139).
…dules, the bookmark dialog loads its editor on demand

Two edges that pulled heavy modules into every admin's startup once the library ships per module:

- AnzuSystemsCommonAdmin imported initCommonAdmin{Image,CoreDam,Collab}Options from the DAM, image
  and collab composables, and with them the image API and their other imports. The option refs and
  the init functions live in src/plugins/pluginOptions.ts now; the area modules read the refs from
  there and still re-export their init function. Same behaviour: one ref per option, the image and
  coreDam composables throw until configured, collab starts disabled -- pinned by
  pluginOptions.test.ts, written against the old wiring first and green on both
- FilterBookmarkDialog imported ASortableListEditor (and sortablejs under it) statically, so every list
  page with the bookmark button carried it. The manage tab loads it on its first visit
  (listEditorLoader, with a 15 s timeout), on its own track next to the bookmark fetch: a spinner
  while it comes, an error with a retry button when it does not, the save disabled until it is
  there. The template ref reaches the editor once it is mounted; everything that talks to it already
  tolerated its absence. New cases: loaded on the first visit and not with the dialog, a failed load
  and the retry, the fetched list kept across a failed load, a second dialog after the first closed

yarn ci, yarn test:run (browser 1477, node 47).
…ts and exports its entry points explicitly

- vite.config.lib.mts: `preserveModules` with `src` as the root, so dist/ mirrors src/; bundled
  dependencies go to dist/vendor/ (yarn pack leaves out every node_modules directory, a nested one
  too), their names built by hand while the library's own modules keep `[name]`, which rolldown
  sanitizes (no `?`, `&` or `=` in any path); `rollupOptions` becomes `rolldownOptions`
- a pre plugin resolves the locale files up front, sorted: @intlify/unplugin-vue-i18n numbers its
  virtual modules in resolve order, and per module that number is a file name, so two builds of one
  commit differed in 96 files. Two builds, one of them on a single CPU, are now identical (1593 files)
- package.json: `"sideEffects": ["**/*.css"]`. It applies to this build too: compared with a build
  without it, it drops 12 bare imports kept only for their order, each of a module with no top-level
  effect (metadataToImageMap, useNestedListEditor, logFilter, logApi twice, types/coreDam/Asset
  twice, ImageWidgetInner, damAssetApi, useNestedListEditor again, utils/common). The load-time
  effects left in the library are module state (theme, language, pinia stores, caches), HMR guards
  and dayjs.extend in the three modules that use the plugins themselves
- exports: `.`, `./styles`, `./eslint`, `./oxlint`, `./vite`, `./package.json`, types first; the `./*`
  wildcard goes. The admins use `.`, `./styles`, `./eslint` and `./vite`
- common-admin.d.ts is byte for byte the one the single-file build emits; the packed tarball holds
  the same 1593 dist files as the local build
- changelog: the packaging of 2.0.0

yarn ci, yarn test:run (after the previous commit, unchanged sources), yarn pack.
The region editor (DamAssetImageRoiSelect with cropper.js) loads when a detail dialog of the upload queue
first shows it, through DamAssetImageRoiSelectLazy and damAssetImageRoiSelectLoader (15 s timeout, a
retry). rusha, which hashes an upload, loads when a file starts uploading and before its asset is created,
also with a 15 s timeout. A page that only shows the upload queue no longer carries either.

The item is marked uploading before that first await, as it was before: the queue restarts every item it
still sees waiting, and it looks again after each file it adds. A stop while rusha loads or while the
asset is created sends no chunk.

Tests: DamAssetImageRoiSelectLazy, uploadStopWhileHashLoads (each guard fails the test without it) and
uploadQueueStartsEachFileOnce (five starts instead of two without the fix).
Step 3 criterion: admin-ugc's DAM median route -10.7 % (was -7.7 %); all six admins pass.
… build info

copy.sh refuses to copy when dist/ lacks common-admin.js, .d.ts or .css (a build that did not finish)
before it deletes the admins' copies. It also removes node_modules/.cache/tsc in each admin: tsc --build
does not look at node_modules, so a warm vue-tsc would call the admin up to date against the old
declarations. README-DEV describes both.
…slint and stylelint without a cache

tsBuildInfoFile moves to node_modules/.cache/tsc (all tsconfigs are noEmit). eslint and stylelint lose
--cache: eslint reuses a file's result without tracking the files it depends on, and stylelint hashes only
the first configuration it resolves (.vue and .scss resolve differently), so a warm run could pass what a
cold one reports.

The Lint job restores node_modules/.cache after yarn install (actions/cache/restore@v6) and saves it after
a passing yarn ci, before the audit (actions/cache/save@v6). The key covers runner, Node version, yarn.lock
and tsconfig*.json (vue-tsc's build info does not notice a vueCompilerOptions change) plus the run.

Verified: yarn ci; only tsc in node_modules/.cache. The workflow is YAML-checked, it has not run yet.
release.config.json, the release workflow as the stub of the tool (its action builds, checks and publishes a tag the tool created), the release:* scripts, and the changelog in the format of the tool: doc/changelog/2.0.0.md and doc/changelog/template.md with the header "{version} — unreleased". The README says how releases are made.
…d tests

Fixes found by the 2.0.0 review (upload queue, image widgets, filters and lists,
list editors, forms, auth and collaboration, utils, styles, fonts), and what the
six admins carried as copies, now in the library:

- token refresh, start-up (startWithEnvConfig), reload on a new version
- api clients (defineApiClient), app shell (ALayoutSwitch, ALayoutLoader,
  AActionbar, AActionbarTarget), createAnzuVuetify
- createRemoteAutocomplete / createFilterRemoteAutocomplete with props,
  filterFromProps and scope
- ./testing: the admins' guard tests (routes, close buttons, route history,
  sortable lists and their unsaved registration, alert host, CSS layer order)
- ./eslint admin preset, ./vite dev watch into an admin
- the stylesheet in the anzu-common cascade layer, Vuetify's sub-layer order
- noUncheckedIndexedAccess on for the library

Breaking changes and the migration: doc/changelog/2.0.0.md.
AuthUnavailableError, SessionExpiredError and isInCauseChain move out of refreshSession into
model/error, next to the library's other errors: alerts and apiErrors imported the refresh only for
them, and that closed an import cycle through eight parts of the library. SortOrder and SortOrderType
move out of datatableColumns into labs/api/SortOrder: the list helpers are query primitives and used
nothing else of the datatable. Exports, behaviour and the classes themselves are unchanged.

Groundwork for the domain layout (E1), which moves files only.
src/ by domain, as in the admins: src/domains/<domain>/[<entity>/]<kind>/, src/shared/, src/plugins/;
labs/ is gone. Tests move into __tests__ next to the code they test; src/test keeps the tests of the
library as a whole and those of src/eslint and src/vite, which ship as source.

Renames and rewritten paths only, plus the tsconfig and eslint globs for nested __tests__, two test
globs, the api lint rules' path of useApiRequest, the sort order of one listed import cycle and doc
prose. The import graph is the graph before, renamed edge by edge; the build exports the same names,
modules and types.

A branch from before: git merge -X find-renames=30% (three short files that are mostly imports are
below git's default 50 % similarity).
The Pinia stores are commonAdmin… (the admins' own common… stores would share state with them), the
browser keys of the users and permission groups lists common…: both lists start once with the default
filter and columns. Comments and test names without labs; the api lint rules have a test that they see
the library's own useApiRequest. doc/API-HELPERY-MIGRACIA.md, the analysis of the labs api migration,
leaves the repository.
…fter an immediate delete

- useFormatAndSizeCheck compares `accept` and `maxSizes` keys lowercased and trimmed (`.JPG`,
  `'image/png, .jpg'`); an uppercase key never matched.
- useLoginStatus keeps each call's verdict: a later navigation reset it before the start-up read it
  after its awaits.
- List editors: a row deleted in `immediate` mode leaves the saved state too (in the nested editor with
  its saved subtree), so `reset()` no longer brings it back.
- The nested editor's children toggle is named Expand/Collapse, with `aria-expanded`.
- The plugin warns when it is installed without `i18n`.
- ./testing: describeDeclaredDependencies fails on an import of a package the admin's package.json does
  not declare, which only a hoisting node_modules resolves. The library checks itself with it:
  `@typescript-eslint/parser`, which the eslint rule tests import, is now a devDependency.
…says what a misread costs

The scan took `export default /…/` for a division. Its comment said a division misread as a regex drops
nothing; it copies to the next `/` on that line and reads on from there. Test with a regex after
`default` that, misread, hides the import behind it.
…lopment warnings that reach the admins

- createRefreshSession watches the JWT cookie for up to a second after a 400 before the session counts as
  gone: the other tab's answer, with the rotated cookie, can arrive after this tab's.
- The plugin registers AChipNoLink, the component; `commonAliases` no longer maps it to a plain VChip.
- <Acl> warns in development, once per permission, when its check throws. isDevelopment() reads
  process.env.NODE_ENV, which the admin's build replaces; import.meta.env.DEV is fixed by the library's
  own build, so ALogDatatable's warning never showed in an admin. A test keeps import.meta.env out of src.
- AFormRemoteSwitch and AFormRemoteCheckbox with `hideLabel` take their accessible name from the label.
- The test setup builds Vuetify with the library's aliases, so ABtn* are real buttons in the tests.
…er restarts that keep the old one

- AImageMediaWidget releases its collab field with the image and the media (ImageMediaCollabValue), and
  with what it just assigned: under a v-model parent an assigned model shows only after the parent renders,
  so a release that read it back sent the previous image.
- A readonly ADatetimePicker / AFormDatetimePicker offers neither the calendar nor clearing. The text fields
  and AFormSwitch declare `readonly`, left to a readonly VForm when not set; AFormTextField declares and
  forwards `modelModifiers`. AFormValueObjectOptionsSelect shows `help` and applies its `hideDetails`.
- useRouteHistory's blacklist takes route names (RouteRecordName).
- createRemoteAutocomplete types the autocomplete's events on its result.
- commonAdminDevWatch hands a restart a copy of the server's environments and empties it once the new
  server stands: Vite 8.3 keeps the previous server's environments in the new one's closure
  (vitejs/vite#23493, fix in PR #23499). TODO to drop it once Vite ships the fix.
- describeDeclaredDependencies reads module augmentations (`declare module '…'`), and the guard reads
  `.d.ts` files.
- The asset picker tile's title has a data-cy for the admins' e2e.
- Tests for each.
…odel with permissions only

- ADamUserRemoteAutocomplete, the form field to ADamUserFilterRemoteAutocomplete: picks a DAM user in an
  admin that is not DAM, through the DAM client of the `coreDam` plugin options (`configName`) or `client`.
- AUserCopyPermissionsDialog copies into any model with roles, permission groups and permissions, not only a
  whole AnzuUser: a wizard holds a system's permissions without the rest of an account.
- The image widget reloads the media it just assigned, not the model it was assigned to.
- Changelog: the widget's collab value is a change an admin that applies room data itself has to follow.
- Tests for each.
…op while another editor holds it

- One release per lock taken, in both collab image widgets: the explicit release after a remove or a confirm and
  the menu/dialog-close watcher each released, the server refused the second, and the refusal read as the lock
  still held. In an inactive room every release still writes the value for the editors who join. The image
  confirm releases before its reload, so what goes out is what was just assigned.
- A new lock request resets the "really locked" state, so a drop waits for the answer to its own request.
- A drop is refused, as a click on the dropzone is, while another editor holds the field.
- The DAM region editor test waits until picture and selection hold still, not a fixed pause: it flaked once
  in the full suite.
- Tests for each.
… ask again for a refused one

- A pick from DAM counts as an open dialog while the asset is fetched or copied into the licence: the picker
  closes first, and the close gave the lock up with the old value, so the pick never reached the other editors.
  A pick that ends without a dialog still releases.
- A lock not yet confirmed is asked for again by the next drop and when the menu opens: a refused request left
  every later drop waiting 5 s for a lock never granted. Never while a request is out, and never from the click
  that also closes the menu, which raced its release on the server. A failed answer keeps the lock counted: a
  timed-out request answers the same, and the server may have granted it, so it is still released.
- Tests for each.
…em bar's retries, the DAM socket teardown

- The upload queue stops an item it removes as a stopped upload is: one still sending aborts its chunk instead of
  finishing unseen, being polled afterwards and uncounted against the parallel limit; one whose status fallback
  waits for the asset polls no more (it went on for the tries left, holding the file).
- An upload stopped during its finish request stays stopped and is not polled.
- ASystemBar clears the pending retry before it schedules one: a return to the window while a retry was pending
  started a second chain, and the unmount cleared only the last timer.
- destroyDamNotifications is exported again (in 1.49.0, dropped from 2.0 with the commented-out exports), and a
  handle disposed directly is forgotten, so the next initDamNotifications() returns one that opens.
- Changelog: a stop between or during chunks keeps the upload from finishing, it never kept the asset from being
  created.
- Tests for each.
…image widgets ask again after a lost connection

- A lock held when the other editor leaves is released on the server too, for every collab field and the
  useCollabAnyDataChange locks, also one whose request timed out (the server may grant it late and tells only the
  others): it stayed, and whoever came back found the field locked. The value still goes to the buffer.
- The image widgets release a lock taken in an active room after the room turned inactive. After a lost
  connection they ask for it again once the room is back, and a menu or dialog opened while alone asks for it
  when another editor arrives.
- A drop whose wait for the lock ran out releases a lock the server may have granted late, when no other drop,
  menu or dialog waits for or holds it. While a request is out, the answer to an earlier release leaves the lock
  state alone: a refused one read as the lock held let a drop upload before its own answer.
- A DAM pick that ends early leaves the metadata dialog not loading.
- AFileInputDialog.activate() opens the dialog and the file choice: it reached for a ref that was never set.
- Tests for each.
…irmed, the image widgets ask again for a refused lock on arrival

- A lock request that timed out stays counted as possibly held until a release of it is confirmed: a retry can be
  refused while the slow request still holds the field on the server, which grants it after, and a release that
  timed out may not have reached it. Cleared on any acquire answer and before every release, the claim was lost and
  a release in an inactive room skipped the lock the server still held.
- When another editor arrives, an image widget with its menu or a dialog open asks again for a lock that was refused
  or went unanswered, not only for one never asked for.
- Tests for both composables, a dialog open on arrival and while a drop waits, a refused lock asked for again; the
  widget tests reset the shared upload queue dialog, so a failed try no longer lets its retry pass.

This branch was successfully deployed

1 active (outdated) deployment
npmjs-publish — e4c49b96 Deployed Sep 23, 2026 by volarname via Build and push package on node 24 #482
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