Skip to content

refactor(ccenter): migrate queues to pinia + ui-datalist [WTEL-10140] - #1429

Draft
dlohvinov wants to merge 17 commits into
mainfrom
refactor/WTEL-10140/queues-datalist
Draft

refactor(ccenter): migrate queues to pinia + ui-datalist [WTEL-10140]#1429
dlohvinov wants to merge 17 commits into
mainfrom
refactor/WTEL-10140/queues-datalist

Conversation

@dlohvinov

@dlohvinov dlohvinov commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

Migrates the queues section — the registry, the card, its seven nested tabs and
the members sub-entity — from Vuex + mixins onto Pinia + @webitel/ui-datalist,
and deletes the Vuex module at the end.

165 files, −2,500 lines net. Eleven commits, each of which typechecks,
lints, tests and builds on its own; the legacy module is removed only in the
last one.

Important

Blocked on webitel/webitel-ui-sdk#1665. This branch imports
getQueueDefaults, queueSchema, queueTypeRules, seven api modules and
QueuesAPI.getPermissionsList, none of which are in the published
@webitel/api-services yet. CI will be red until that lands and publishes.
Left as a draft for that reason.

Shape

commit what
1 groundwork — shared defaults and api, the pinia stores, the type-branching configs
2 the queues table
3 the queue card and its five form tabs
4–8 the nested tabs: hooks, buckets + resource groups, skills, agents, logs
9 members
10 delete the legacy vuex module
11 tests

Worth knowing when reviewing

Type-dependent queues. A new queue's fields depend on ?type=, which
createCardStore knows nothing about, so the card uses manualSetup and seeds
getQueueDefaults(type) into the draft before anything reads it. That is what
gives Regle a complete key set to build $fields from — it derives them from
state, not from the schema, so a missing key costs a field its required marker
and error text with no error anywhere. The 250-line per-type Vuelidate switch
is gone; the asterisk on a type-dependent field now comes from the shared rule
table rather than the old :required="v.itemInstance.calendar" trick.

Create-on-demand is kept. Adding a hook or skill to an unsaved queue still
saves the queue first, but through the card's own validated save, provided to
the tabs. The mixin dispatched ADD_ITEM directly and could persist a
half-filled queue; now an invalid one blocks the add and shows its errors.

The logs filters move into the logs tab. The card used to render them in
its actions panel via a currentTab.filters component name, so the page had to
know one tab's internals and no other tab could ever have filters.

Persistence is off for the five in-card tabs. Route persistence writes
un-namespaced filters/page/size/sort, and the tabs share one url. Logs
and members keep it — they have their own routes, and a filtered log is worth
sharing.

Behaviour changes

  • Queue filters now survive leaving and returning, like every migrated
    registry. The WTEL-9657 ?type=-preserving reset goes with its cause: the
    old panel reset by rewriting the whole route query, on unmount.
  • The agents tab loses its edit and delete row actions — the api has no
    delete, and editItem pushed a param no route declares, so neither worked.
  • Members' mislabelled settings icon becomes a real column selector.
  • The queue log's duration filter starts working. The store held duration
    as {from, to} while the api read durationFrom/durationTo, so the bounds
    were never sent.
  • Buckets and resource groups still delete without confirmation, and hooks
    still asks. Preserved as-is — unifying them is a UX call, not a refactor.
  • Validation messages are English-only until a Regle i18n layer exists. That
    is already true of every migrated entity.

Tests

The five component specs here were shallowMount + exists() smoke tests
bound to mixin mocks, two of them overriding a computed, which <script setup> does not allow. They are replaced by 86 tests over the logic that can
fail silently: the CSV normaliser, the member communications collection, the
per-type control map, tab-to-route mapping, table headers across all eight
tables, and filter names matching the params each api reads.

Writing them found a real bug in this branch: useMemberCommunications.remove
compared raw objects against the draft's reactive proxies, so Set.has never
matched. It worked from the tab, which passes proxies back, but any caller
holding the underlying object would have deleted nothing.

Verification

typecheck at the repo's pre-existing error count (this branch adds none) · lint
clean · 136 unit tests · production build succeeds.

Not verified: the app was never exercised against a backend. The per-type
$fields seeding is the one thing no automated check covers — please open a
queue of each of the ten types, create and edit, and confirm required markers
and error text appear on Params and Processing.

Related Tasks

Related PR's / Issues

dlohvinov and others added 3 commits August 12, 2026 09:47
…EL-10140](https://webitel.atlassian.net/browse/WTEL-10140)

Groundwork for the datalist migration, with the page still on vuex.

The ten per-type queue schemas and the queues api module both existed here and
in api-services. The app now uses the api-services copies, so the two cannot
drift while the migration is in flight: `queueStateMap` becomes
`getQueueDefaults(type)`, and the four other modules that reached into
`queues/api/queues` for `getLookup` import `QueuesAPI` from the package.

Adds the pinia stores — datalist, card, permissions — which nothing renders
yet, plus the pieces the card page will need: a `Queue` type widening
`EngineQueue` with the `payload` shape the generated models omit, the per-type
tab table lifted out of `opened-queue.vue`, and `useQueueTypeControls`, which
replaces the identical `specificControls` computed in each of the three form
tabs. Control ids are untouched, so the templates' `v-if`s carry over as-is.

`QueueTypeProperties` becomes TypeScript; it was previously invisible to
`vue-tsc`, since the app sets `allowJs: false`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…EL-10140](https://webitel.atlassian.net/browse/WTEL-10140)

The registry now runs on `useQueuesDatalistStore`. Sorting, paging, column
state and the filter set come from the store, and all of it persists to the URL
and local storage the way every other migrated registry does.

Search moves from a bare `wt-search-bar` to `dynamic-filter-search`, and the
four filters become a `TableFiltersPanel`. `queueType` and `team` come from the
shared filter set — the queue-type filter applies the same `VITE_STAGING_ENV`
gate the app's own options list did, so that list goes away, as does the team
lookup api. Tags stays app-side: `FilterOption.Tag` is the call-history tag
enum, and its name is not the param `searchQueue` expects. `FilterConfig` keeps
only the keys it knows, so a `searchRecords` passed through `createFilterConfig`
would be dropped — the field component calls the api directly instead.

The global state switcher moves into its own component. Its debounce works now:
the page defined `debouncedFetchGlobalState()` as a method that reassigned
itself to a debounced function on first call, so the first invocation only ever
built the debouncer and never fetched.

Two behaviour changes worth knowing. Filters now survive leaving and returning
to the page, like every other migrated registry. And the WTEL-9657 workaround
that preserved `?type=` on reset is gone with its cause: the old panel reset by
rewriting the whole route query, on unmount, wiping the param the create-queue
popup had just set.

Also adds the two composables the card page needs next — the vuex bridge that
keeps the un-migrated tabs fed, and `useEnsureQueueSaved`, which routes their
"add before the queue exists" flow through a validated save instead of the
mixin's unvalidated one.

The five component specs are deleted rather than ported: all were
`shallowMount` + `exists()` smoke tests bound to mixin mocks, and two overrode
a `computed`, which `<script setup>` does not allow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-10140](https://webitel.atlassian.net/browse/WTEL-10140)

The card page and its five form tabs now run on `useQueuesCardStore`. Tabs
render through `router-view` rather than a local component registry, the
permissions tab comes from the package, and every field binds with `v-model`
against the draft instead of dispatching a mutation per keystroke — the
`_dirty` bookkeeping that went with those goes away.

`manualSetup` on `useCardComponent`, because a new queue's shape depends on
`?type=`, which the card store knows nothing about. Owning `initialize` here
lets `getQueueDefaults(type)` merge into the draft before anything reads it,
which is what gives Regle a complete field set to build `$fields` from. The
cost is re-implementing three small things: `initialize`, `$reset` on unmount,
and the "replace `new` in the url once the server assigns an id" watcher —
`useCardRouting` is not exported. The local watcher also keeps the route query,
which `useCardTabs`' own `changeTab` drops; without that, `?type=` was lost on
the first tab switch of an unsaved queue.

Validation is no longer assembled per type in the component. The 250-line
Vuelidate switch is gone; fields read `validationFields`, and the asterisk on a
type-dependent field comes from the shared rule table rather than the old
`:required="v.itemInstance.calendar"` trick, which was truthy only because that
type's branch had declared a rule.

The WTEL-8174 rule — first enabling prolongation also enables the timeout retry
— moves from a vuex action to a watcher on the Processing tab. The WTEL-3268
default alert tone and the quirk where the `renewalSec` control is namespaced
under `prolongationOptions` while its value sits at the top of `taskProcessing`
are both preserved as they were.

The six nested tabs still run on vuex and read their parent id from that store,
so `useLegacyQueueVuexBridge` mirrors the pinia card into it. The mirror is
established before `router-view` renders — a legacy tab mounting first would
read id 0 and load the wrong list.

Queue strategy, resource strategy and the two lookups move out of `store/`,
which the sweep commit deletes; strategy and time-base-score now come from
api-services rather than being declared twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c761c6fe-66d3-4078-a9db-cb2eeebfd3e5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

dlohvinov and others added 8 commits August 12, 2026 13:23
…10140](https://webitel.atlassian.net/browse/WTEL-10140)

First of the nested tabs, and the template the rest follow: a datalist store
scoped under the queue's namespace, a popup driven by `useNestedCardComponent`
off the `:hookId?` route param, and validation from the shared Zod schema.

Persistence is off for this table. Route persistence writes un-namespaced
`filters`/`page`/`size`/`sort` keys and every tab on the card shares one url, so
leaving it on would have the tabs overwrite each other's state.

Adding a hook to a queue that does not exist yet still works: the tab asks the
card to save first, through the `ensureQueueSaved` it provides. That path
validates, unlike the mixin's, which dispatched the parent's `ADD_ITEM`
directly and could persist a half-filled queue.

Also adds `useDeleteConfirmation`, a thin wrapper over ui-sdk's composable that
re-creates its refs locally. The app and the symlinked sdk resolve different
copies of vue, so refs crossing that boundary are not the app's `Ref` type and
vue-tsc will not unwrap them in a template — every `:delete-count` binding
reported an error. It is a local-linking artifact that CI would not see, but
without the wrapper each migrated table repeats the same two errors, and the
typecheck stops being a useful signal. Applying it to the queues table too
brings the app back to its pre-existing error count, so this branch adds none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…0140](https://webitel.atlassian.net/browse/WTEL-10140)

Both follow the hooks template: a datalist store with persistence off, a popup
on the existing `:bucketId?` / `:resourceId?` route param, and the shared Zod
schema for validation.

Two behaviours preserved rather than tidied. The buckets state column reads as
"enabled" while the stored field is `disabled`, so the switcher stays inverted
on both the binding and the patch. And neither tab asks for confirmation before
deleting — unlike hooks — which is how they behave today; changing that is a UX
decision, not part of a refactor.

Resource groups has no `patch` on its service, so the table has no inline
column to toggle and none is invented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nto a computed [WTEL-10140](https://webitel.atlassian.net/browse/WTEL-10140)

Same shape as the other nested tabs, plus the one piece of extra behaviour:
the popup listing a skill's buckets.

That popup was backed by store state and a `GET_ITEM_BUCKETS` action, but the
action only searched `dataList` for the row it was given — every row already
carries its own `buckets`. It is a computed now, and the shared
`object-list-popup` renders it, so `bucketsPopupMixin`, the bespoke popup
component and the store's `buckets` state all go away.

Two fixes fall out of that. The lookup compares ids as strings, where the old
code did `parseInt` against ids that can be strings; and closing the popup
strips the query param instead of `$router.go(-1)`, which went somewhere else
entirely when the url had been opened directly.

Capacity keeps its existing arrangement: the column renders "min - max" while
sorting on `max_capacity`, and the cross-field rule that keeps min below max
now comes from the shared Zod schema rather than a pair of Vuelidate
validators.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…L-10140](https://webitel.atlassian.net/browse/WTEL-10140)

Membership is edited from the agent side, so this tab only lists. There is no
card store and no popup — `createTableStore` needs `getList` alone.

Its edit and delete row actions are gone rather than ported: the api module has
no `delete`, and `editItem` pushed an `agentId` param no route declares, so
neither did anything.

The supervisors and skills popups keep their deep-linkable query params but
read the collection straight off the row, which already carries it. That drops
the store getter and the `dataList` watcher that re-derived it, and closing now
strips the param instead of `$router.go(-1)`.

`agentStatusMixin` becomes `useAgentStatusIndicator`. The mixin file stays —
three other agent tables still use it.

Search moves to `dynamic-filter-search`, so the tab picks up the same filter
handling as every other migrated table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…https://webitel.atlassian.net/browse/WTEL-10140)

Last of the nested tabs, and the only one that changes the card page.

The card used to render this tab's filters in its own actions panel, picking
the component out of a `currentTab.filters` string and handing it a vuex
namespace — so the page had to know about one tab's internals, and no other tab
could ever have filters. The panel now lives in the tab, behind its own filters
icon, and the card's actions panel is gone.

Filters become a `TableFiltersPanel`. `agent` comes from the shared set; the
other three are app-side configs, since a queue log's result codes and its two
ranges are not shared vocabulary. `joinedAt` and `duration` each collapse from
two flat filters into one `{ from, to }` value, which is what makes `duration`
work at all — the api read `durationFrom`/`durationTo` while the store only ever
set `duration`, so those bounds were never sent.

The log still opens on today, and reset returns to today rather than clearing
the range: an unbounded attempt history is not a useful default.

This tab keeps persistence, unlike its siblings. It is a report, a filtered view
of it is worth sharing as a url, and no other tab writes those keys.

With this the last nested table leaves vuex; only members and permissions are
still registered as child modules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…10140](https://webitel.atlassian.net/browse/WTEL-10140)

Members is a second entity rather than a tab — its own page, its own card, CSV
import, a reset action and an in-memory sub-collection — so it lands last.

The card uses `useNestedCardComponent`: every members request needs the queue
id, and only that composable threads a `parentId` through. It also has no
`$reset` on unmount, because the route param it watches never clears while the
page is open, so the card adds one — otherwise the next member opened would
start on the previous one's draft.

Communications stop being a store module. They have no endpoint of their own
and save with the member, so they are plain array editing on the draft, in a
composable that can be tested. The popup keeps its route param for
deep-linking but is deliberately hand-rolled: `useNestedCardComponent` needs an
entity with an id, and a communication is addressed by its index in an unsaved
draft. It validates against the same Zod schema, which retires the bespoke dtmf
validator.

Deleting a communication now matches by identity. Matching on destination and
type meant two identical rows deleted whichever came first.

Bulk delete, reset and the affected-row count read `filtersManager` directly.
"Delete selected" stays one request; `deleteEls` would fire one per row, which
is right for the row icon and wrong here.

`from`/`to` become one `createdAt` range, and `cause`/`priority` are renamed to
`stopCause`/`memberPriority` to avoid the shared set's `HangupCause` and
`CasePriority`. The api accepts both spellings.

The mislabelled `settings` icon becomes a real column selector, which is what
it was pretending to be now that the headers store is real.

`import-csv-upload-action` moves across in the same commit — it imported both
the members api and the csv mixin, and was the only thing outside this module
reaching into it.

With this the queue's last child module leaves vuex; only permissions remains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…//webitel.atlassian.net/browse/WTEL-10140)

Nothing reads it any more, so the module, its headers, the permissions child
and the registration in the contact-center aggregator all go.

With it goes `useLegacyQueueVuexBridge`, which existed only to keep the
un-migrated tabs fed while the card ran on pinia, and the `inheritAttrs: false`
lines that absorbed the `namespace` and vuelidate props the card used to pass
every tab.

`TypesResourceStrategy` is deleted rather than converted — general reads it from
api-services now. `AmdAiLabels` becomes TypeScript; with `allowJs: false` it was
invisible to the typechecker where it sat.

The members dummy artwork goes too: empty states come from `useTableEmpty`.

Sweep is clean — no `ccenter/queues` namespace left anywhere, no table or card
mixin under the module, and no `.js` files. The seven shared mixins this branch
stopped using all still have other consumers and are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
https://webitel.atlassian.net/browse/WTEL-10140)

The five component specs this branch deleted were `shallowMount` + `exists()`
smoke tests bound to mixin mocks, and nothing replaced them. These cover the
places where a mistake produces no error at all.

- `useNormalizeCsvMembers` — the riskiest pure logic in the module and
  previously untested: zipping the parallel destination/code columns, stopping
  at the shorter one, skipping unknown codes, rejecting bad dtmf and rows left
  with no communications, and keying variables by their mapped csv tags.
- `useMemberCommunications` — including the duplicate-row case, which is the
  only one that can tell identity-based deletion from the value-based matching
  it replaced.
- `useQueueTypeControls` — the control map per type, dotted ids surviving
  intact, an unknown type yielding `{}` rather than throwing as the old
  computed did, and `isFieldRequired` agreeing with the shared rule table.
- `queueTabs` — every tab id resolves to a declared route name. A typo there
  only shows when someone clicks the tab.
- `headers` across all eight tables — `show: true` everywhere, since a header
  without it loses its column *and* stops its field being requested, plus the
  field names that deliberately disagree with their column: the queue's
  snake_case `resource_groups`, the bucket's inverted `disabled`, the log
  duration sorting on `joined_at`.
- `filtersOptions` — filter names match the params each api reads. That
  invariant is invisible from either side alone, and breaking it is what left
  the queue log's duration filter doing nothing for as long as it existed.

Writing them turned up a real bug: `useMemberCommunications.remove` compared
raw objects against the draft's reactive proxies, so `Set.has` never matched.
It happened to work from the tab, which passes proxies straight back, but any
caller holding the underlying object would have deleted nothing. Both sides go
through `toRaw` now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dlohvinov dlohvinov changed the title Refactor/WTEL 10140/queues datalist refactor(ccenter): migrate queues to pinia + ui-datalist [WTEL-10140] Aug 12, 2026
dlohvinov and others added 6 commits August 13, 2026 16:51
…://webitel.atlassian.net/browse/WTEL-10140)

Drop custom stopCause/memberPriority widgets. Locales link to ui-datalist.

Co-authored-by: Cursor <cursoragent@cursor.com>
…webitel.atlassian.net/browse/WTEL-10140)

OfferingAt, attempts, name, destination. Filter badge shows when any
panel filter is set.

Co-authored-by: Cursor <cursoragent@cursor.com>
…40](https://webitel.atlassian.net/browse/WTEL-10140)

Drop app-local widgets; panels now use ui-datalist FilterOptions
whose names still match the request params.

Co-authored-by: Cursor <cursoragent@cursor.com>
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