Skip to content

WIP: Publication admin / manage area#2271

Draft
wreality wants to merge 157 commits into
masterfrom
feature/publication-admin
Draft

WIP: Publication admin / manage area#2271
wreality wants to merge 157 commits into
masterfrom
feature/publication-admin

Conversation

@wreality

@wreality wreality commented May 1, 2026

Copy link
Copy Markdown
Contributor

WIP reference PR for the publication-admin work stacked on feature/admin-tables.

Scope: 119 files, ~17.7k insertions, 129 commits.

Intended split

This branch will be broken into smaller PRs roughly in this order:

  1. Backend assignment builder + publication policyPublicationAssignmentBuilder, PublicationBuilder additions, PublicationPolicy, Publication model.
  2. Search hardening / FULLTEXT — fulltext migrations + PublicationBuilder search scope.
  3. User Settings (preferences / dismissals / lab features) — settings migration, UserSettings mutation, SettingsPage, LabFeaturesPage, userPreferences/applyUserPreferences composables, lab preview images, admin user settings tab.
  4. Tables / atoms / molecules infra — common cells, EmptyState, NeedsActionPublicationsTable, SectionHeader, RoleBadge.
  5. Status taxonomy + dashboard polishstatusCategories, DashboardStatusFilter, StatusBadgeCell, submissionStatusTransitions, SubmissionStatusCountsField resolver.
  6. User detail componentsSubmissionStatusBar, UserProfileCard.
  7. Submission card refactorSubmissionCard, PublicationHomePage, breadcrumb/setup polish.
  8. Manage backendPublicationUser DTO, paginators (PaginatePublicationSubmissions, PaginatePublicationUsers, PaginateSubmissions, PublicationUser, PublicationUserSubmissions), AddReviewers mutation, SubmissionPaginator, schema, tests.
  9. Manage shell + landingroutes/manage/index.vue, publication shell + (tabs), ManagePanel, ManageInfoCallout, ManageTabHeader.
  10. Manage tabs (team / invited / dashboard list)(tabs)/team, (tabs)/invited, dashboard.vue, dashboard/{index,submissions,submitters}.
  11. Manage submission detailsubmissions/[submissionId].vue, AssignReviewTeamDialog, SubmissionAssigneeList, ManageSubmissionAudit.
  12. Manage per-user pagessubmitters/[userId], team/[userId].
  13. Seeders / test configDashboardDemoSeeder, UserSeeder, phpunit.xml, .env.testing.
  14. Misc — gitignore, codegen, eslint, cypress shim, residual i18n/schema regen.

Cross-cutting risks

  • client/src/graphql/schema.graphql (+312) regenerates each PR — expect churn.
  • i18n/en-US.json (+319) — split keys per feature.
  • typed-router.d.ts regenerates from new route files — land alongside producers.
  • Models/{Publication,User,Submission}.php accumulate across PRs 1/3/5/8 — strict order.

Test plan

  • Reference only — do not merge as-is.
  • Each split PR will carry its own backend + vitest coverage.

wreality added 30 commits March 20, 2026 12:24
- Migrate from typescript/typescript-operations plugins to client-preset
- Enable colocated GraphQL fragments and queries in Vue SFCs
- Add document scanning for .vue files in pages, layouts, and components
- Update schema snapshot
- Define avatarImage fragment on User in AvatarImage.vue
- Define avatarBlock fragment on User in AvatarBlock.vue
- Use generated fragment types for component props
- Add PublicationAssignment and SubmissionAssignment models with builders
- Add PublicationBuilder and SubmissionBuilder with search/filter scopes
- Add Paginator interface listener for GraphQL pagination
- Update User, Publication, and Submission models with new relationships
- Add GraphQL types for publication/submission assignments with ordering
- Add backend tests for user assignments, publications, and paginator
- Update publication seeders with visibility/accepting fields
- Add QueryTable component wrapping Quasar q-table with GraphQL pagination
- Add usePaginatedQuery composable for paginated GraphQL queries
- Add useQueryCapabilities for introspecting query search/sort support
- Add useUrlPaginationSync for syncing table state with URL params
- Add DateTimeCell, NameAvatarCell, and WithAsideCell table cell components
- Add FieldDisplay molecule for labeled field rendering
- Add tests for useQueryCapabilities and useUrlPaginationSync
- Update client schema with new admin table types and pagination
- Update codegen config and GraphQL queries
- Update vue-apollo boot configuration
- Update yarn.lock with new dependencies
- Add UsersIndex page with searchable, sortable QueryTable
- Add UserDetailLayout with user info card, breadcrumbs, and tabs
- Add UserDetails page showing user's publication assignments
- Add row-click navigation and no-data empty states
- Add tests for UsersIndex and UserDetails pages
- Add UserDetailsSubmissions page with sortable submissions table
- Add filter panels for status, role, and publication filtering
- Add row-click navigation to submission details
- Add updated_at column with backend sorting support
- Add no-data empty state message
- Add search, sorting, and filter support to publications table
- Add PublicationsFilterPanel for visibility and accepting status filters
- Add row-click navigation and router-link on publication names
- Replace icon-only actions with configure button
- Restyle create publication dialog with accent header
- Move submit button from inline form to dialog actions
- Expose CreateForm submit method via defineExpose
- Update PublicationIndexPage and CreateForm tests
- Add admin dashboard page with card links to users and publications
- Add /admin route and breadcrumbs on all admin pages
- Simplify app header admin menu to single dashboard link
- Add admin routes for dashboard, users, and user details
- Update i18n with admin table headers, labels, and empty states
- Add AdminDashboard test covering mount and card navigation
Change admin index "All Users" label to "Users" and delete unused
UserListBasic and UserListBasicItem components.
…me mutator

Remove responsive grid check and unused useQuasar import from
UserDetailsSubmissions. Guard against null in Publication setNameAttribute
to fix backend test failures.
…ma types

Update PublicationTest to use `public` instead of removed
`is_publicly_visible` query argument. Update SubmissionTest to use
SubmissionAssignment type for user submissions. Add defaultCount to
user submissions and publications paginators.
# Conflicts:
#	backend/app/Models/Publication.php
#	backend/app/Models/Submission.php
#	client/yarn.lock
Add yarn.lock entries for @graphql-codegen/client-preset and
@graphql-codegen/fragment-matcher. Fix AdminDashboard test to use
the local installQuasarPlugin helper instead of the removed
@quasar/quasar-app-extension-testing-unit-vitest package.
Add use statement for Submission model in SubmissionAssignmentBuilder
and fix arrow function formatting in Publication model.
Backport improvements from feature/publication-admin to keep QueryTable
in sync across branches:

- Toolbar: always-flat refresh button, inset separator before top-after
  slot content, and aligned search-hint caption
- Row-click: forward via v-on object binding so Quasar only attaches a
  pointer cursor when the parent actually listens for @row-click
- New column-level options (linkTo for router-link cells; cell/grid
  refinements) and additional QueryTable props (binary-state-sort,
  visibleColumns, grid, searchHint)
- Expose pagination and result to parents via defineExpose

NameAvatarCell now reads the user from scope.value (aligned with the
dashboard cell convention), handles a null user with an em-dash
placeholder, and shows username as a caption under the display name.
UsersIndex passes the full row as field and sets hideUsername on the
name column since username has its own dedicated column.
Add a publication-level dashboard page for editors and publication
admins to view and filter submissions by status. Includes summary
cards grouped by workflow category (needs action, in progress,
awaiting author, completed) with status counts from a global
publication query, and a paginated submissions table powered by a
custom SubmissionPaginator that supports filter-aware statusCounts.

Backend:
- Add custom PaginatePublicationSubmissions resolver for publication-
  scoped paginated submissions (no user-visibility filter)
- Add custom PaginateSubmissions resolver for top-level submissions
  query with statusCounts on the SubmissionPaginator type
- Add SubmissionStatusCount type and submission_status_counts field
  on Publication for global (unfiltered) counts
- Replace Publication.submissions @hasmany with paginated @paginate
- Add SubmissionPaginator tests covering status counts, filters,
  pagination, ordering, and visibility scoping

Frontend:
- Add PublicationDashboard page at /publication/:id/dashboard
- Add StatusBadgeCell with category-matched colors, icons, and
  color-blind accessible patterns (diagonal, zigzag, dots, crosshatch)
- Add NameAvatarListCell for compact multi-user avatar display
- Refactor NameAvatarCell to read from scope.value for flexibility
- Add accessibility patterns gated behind .a11y-patterns CSS class,
  auto-enabled via prefers-contrast: more media query
- Add dashboard button on PublicationHomePage for admins and editors
- NameAvatarListCell: add toggle to expand compact avatars into a list
  of full avatar+name rows; announce count to screen readers via
  Quasar's sr-only class and semantic ul/li markup
- NameAvatarCell: show em-dash with aria-label for empty/null users
- PublicationDashboard: select id on nested publication query so Apollo
  can normalize it with the cache entity from the global query,
  resolving the "Cache data may be lost" warning on navigation
- Use q-checkbox with label for the Select all/Deselect all row,
  leveraging native tri-state support via null model value
- Bump category name to 1rem (16px) weight-medium to match the table's
  column header size
- Add vertical separator between icon and category name, styled to
  inherit current text color so it reads correctly on both dark-text
  (warning/info) and white-text (secondary/completed) cards
- Restructure individual status rows to mirror the select-all row:
  q-checkbox with label (col) + fixed-width count aligned with the
  three-dot menu in the header
…avatars

- QueryTable: add binary-state-sort so header clicks cycle only between
  ascending and descending, never back to an unsorted state
- NameAvatarListCell: single-user cells render the full avatar+name row
  (matching NameAvatarCell) instead of a lone collapsed avatar
- NameAvatarListCell: expose NameAvatarListExpandAllKey injection key;
  each cell watches a parent-provided ref to sync expand/collapse state
- NameAvatarCell/NameAvatarListCell: use size=md consistently so the
  avatar is the same size across single cell, collapsed, and expanded
  states
- NameAvatarListCell: flush the expand/collapse button to the right
  with a q-space spacer; drop ul/li wrapper in favor of rendering
  q-item directly inside q-td to match NameAvatarCell's structure
- PublicationDashboard: provide expandAllReviewers ref and add an
  expand/collapse-all button in the table's top-after slot; top-align
  all body cells so multi-row reviewer lists don't push siblings down
Backend:
- Add search argument to Publication.submissions field
- PaginatePublicationSubmissions resolver: apply search before the
  statusCounts snapshot so counts reflect the search
- Parse prefixed search syntax in the resolver:
    submitter:term        matches submitter name/email/username
    reviewer:term         matches any reviewer
    coordinator:term      matches review coordinator
    (plain term)          matches submission title (default)

Frontend:
- Pass $search variable through the dashboard's GraphQL query so
  QueryTable auto-detects the searchable capability and renders its
  built-in search input
- Add searchHint prop to QueryTable: renders the hint in its own div
  below the toolbar row so the input field's height stays constant
  and buttons can stretch to match the input, not the hint; wires the
  input to the hint via aria-describedby with a Vue useId() for
  multi-instance uniqueness
- Dashboard: pass a translated search_hint describing the syntax
Simple text cells (title) sat at the top of the row while avatar cells
sat lower because q-item vertically centers its content within a 48px
min-height. Wrap plain content in the same q-item structure so every
column uses the same vertical rhythm.

- Add a generic TextCell that wraps scope.value in a q-item label
- Use TextCell for the Title column
- Restructure StatusBadgeCell to wrap the badge in a matching q-item
- Drop the padding-top:16px override on body td; no longer needed now
  that all cells use q-item
…in length, fulltext indexes

Search behavior:
- Default search (no prefix) now matches submission title OR any
  assigned user (submitter, reviewer, coordinator), not just title
- Add user: prefix to search all roles at once without matching title
- title:, submitter:, reviewer:, coordinator: remain for single-field
  narrowing
- Escape %, _, and \ in LIKE values so users can't accidentally (or
  intentionally) inject SQL LIKE wildcards
- Short-circuit searches shorter than 3 characters on the server to
  keep high-frequency single-character queries from scanning

Migration:
- Add MySQL FULLTEXT indexes on submissions.title and users.name,
  users.email, users.username. LIKE doesn't use them yet, but they're
  in place for a future switch to MATCH...AGAINST when search volume
  justifies it.

Client hint:
- Update the search hint to document the 3-character minimum and the
  narrowing prefixes
Add a status-change dropdown directly on the status badge in the
submissions table, so publication admins and editors can transition a
submission's status from the dashboard without navigating into it.

- Add a useStatusTransitions composable with a declarative state
  machine (statusTransitions map) listing the legal next states and
  their action names; includes a reviewer-role check
- Refactor StatusBadgeCell: the badge is keyboard-focusable and opens
  a q-menu rendered via v-for over the transitions returned by the
  composable (no nested v-if conditionals); row click is stopped from
  propagating so clicking the badge doesn't navigate
- Reuse the existing ConfirmStatusChangeDialog to perform the mutation
  and show feedback
EXPIRED submissions still need a decision from the editor (accept,
request resubmission, or reject), so semantically they belong in the
Needs Action category rather than Completed. With EXPIRED out of
Completed, that category is now purely closed/terminal states:
ACCEPTED_AS_FINAL, REJECTED, ARCHIVED, DELETED.
Drafts are the author's private work-in-progress and should not be
visible to publication admins/editors until the author submits.

- PaginatePublicationSubmissions: exclude DRAFT from the publication
  submissions query before the base-query snapshot
- Publication::getSubmissionStatusCounts: exclude DRAFT from the
  global status counts
- Remove DRAFT from the Awaiting Author dashboard category
- Add test: testDraftsAreHiddenFromPublicationSubmissions verifying
  both the paginated data and status counts omit DRAFT
- Update existing tests for the new DRAFT exclusion
wreality and others added 29 commits April 23, 2026 12:19
Adds a dismissible banner to the top of /dashboard that links to
/manage for users who can actually use it — anyone with a
publication_admin or editor role on at least one publication, plus
application administrators.

A small GraphQL probe (first: 1) on the publications query with
`my_role: [publication_admin, editor]` gates visibility; app admins
skip the probe and always see the banner. Dismissal persists in
Quasar localStorage for a week, matching the AppBanner pattern.
Replaces q-table's default "No data" strip with a centered empty
panel: large icon, primary message, and a one-line hint. Distinguishes
between "you don't manage any publications yet" (no rows overall) and
"no publications match your search" (active filter).
`PublicationBuilder::visible()` returned `public()->orWhereHas(...)`
without grouping, so when the scope was composed with other filters
(search, my_role, etc.) SQL's AND-before-OR precedence let public
publications leak through regardless of the downstream filter.
Wrap both branches in a grouped `where(fn)` so later filters AND
against the whole disjunction.

Adds a regression test pinning that a search term narrows the
result set across both public and assigned publications.
The previous override baked `csrf_token()` into a meta tag at page
render and passed it as a static header to the GraphiQL fetcher.
Laravel rotates the session token on login, so after running any
login mutation in GraphiQL the in-memory header was stale and
every subsequent request returned 419 Page Expired.

Replace the static header with a custom `fetch` that reads the
XSRF-TOKEN cookie per-request. Laravel rewrites that cookie on
every response via AddQueuedCookiesToResponse, so token rotations
are picked up automatically.
Drop the `my_role: [PublicationRole!]` arg from the global
publications query and move /manage to `currentUser.publications`.
Authorization is now implicit (you only ever see your own
assignments) and the visibility policy no longer has to juggle
three different consumer shapes inside `visible()`.

Backend
- Extend `User.publications` with `search: String @scope` and
  `orderBy: [PublicationAssignmentOrderBy!]`, mirroring
  `User.submissions`. New `PublicationAssignmentOrderBy` input
  type + enum sortable over NAME / CREATED_AT / UPDATED_AT.
- Add `search()` and `orderByPublication()` methods on
  `PublicationAssignmentBuilder` that traverse through the
  related publication.
- Remove `PublicationBuilder::myRole()` — no callers after the
  schema change.
- Drop `my_role` from `Query.publications`.

Client
- `QueryTable` column `field` strings now accept dot-paths
  (e.g. "publication.name") so callers don't have to hand-roll
  accessor functions for nested shapes.
- `PublicationNameCell` reads the assignment shape first and
  falls back to the flat Publication shape, so it works with
  both consumers (/manage + admin user-detail).
- `/manage` page and the dashboard's "managed-publications"
  probe query now hit `currentUser.publications(...)`.
The auto-generated typed-router.d.ts only tracks file-based routes
(src/routes/**). The submission:export:html route is still declared
in routes.ts, so it was missing from the RouteNamedMap — router-link
targets with that name failed TS checking.

Add it to the existing manual-routes.d.ts augmentation alongside
its sibling submission:export, and annotate the two computed route
objects with `as const` on the name so the literal type propagates
through the computed's inferred return.

vue-tsc --noEmit now passes clean.
Adds a new filter to `User.publications` that restricts assignments
to publications currently holding at least one non-draft submission
in any of the given statuses. Composes naturally with the existing
`roles`, `search`, and `orderBy` filters.

Matches the SubmissionAssignmentBuilder pattern: the scope traverses
`whereHas('publication.submissions', …)` with a nested status check
and a draft exclusion, so the filter respects the same "drafts
never count" rule as the dashboard's status-count roll-ups.

Enables /manage stage-heading deep linking and the dashboard's
needs-attention publications table.

Pinned by a new PublicationTest covering the three-case matrix
(matching public, non-matching public, non-matching
draft-only assigned) and confirming the filter returns only the
matching row.
/manage — URL-synced stage filter
- Stage group headers in the table (Screening / Reviewing /
  Decision / Closed) are now clickable buttons that toggle a
  `?statuses=[…]` URL parameter, driving the new `with_statuses`
  arg on User.publications. The row set narrows to publications
  with activity in the selected stage.
- Active stage reads with an underline + bold weight; a
  removable q-chip above the table reflects the filter and clears
  it on dismiss. Empty state branches three ways (no data, search
  mismatch, filter mismatch).
- Fix grid-mode stage counts: `countStatuses` was unwrapping the
  assignment row via `pub(row)`, but `stageGroups` passes a
  Publication directly. Move the unwrap to the call sites so both
  shapes compose correctly.

/dashboard — needs-attention publications table
- New `NeedsActionPublicationsTable` molecule shows up to `limit`
  (default 3 on the dashboard) publications the user manages that
  currently have needs-action submissions, sorted by total count
  desc. Per-stage columns break down the activity. `+N` overflow
  chip + "Manage my publications" CTA appear when the total
  exceeds the limit.
- When the user has zero publications needing action, the card
  mirrors the same structure with a positive-colored header
  ("All caught up") and a link to the manage index — keeps the
  "needs your attention" zone visible as a persistent surface
  that flips between states.
- Hides during the initial query load to avoid flashing the
  all-clear state before data resolves.
- Structured so a future user-preference gate can toggle the
  whole component with a single v-if wrap.

Shared — RoleBadge atom + dark-mode link contrast
- Extract `RoleBadge` (src/components/atoms/) as the single
  source of truth for publication role chips. Filled, not
  outlined, with explicit text-color so contrast clears WCAG AA
  in both light and dark modes. PublicationNameCell, the manage
  grid card, and the needs-attention table all use it.
- Lighten $dark-primary from #537FCA to #8AB4F8. `.text-primary`
  already routes through this in dark mode, so every themed link
  and CTA across the app (dashboard banner, publication title
  links, admin hint, etc.) picks up comfortable contrast against
  the dark surface (~10:1).
# Conflicts:
#	backend/database/seeders/PublicationSeeder.php
PublicationBuilder::public() and ::acceptingSubmissions() each hardcoded
`where(..., true)` and ignored the boolean argument Lighthouse's @scope
directive passes in. The admin publications filter could only return
accepting=true / public=true rows regardless of the user's selection.

Both methods now take `bool $value = true` and apply it. The existing
internal caller (PublicationBuilder::visible's `->public()`) keeps
working via the default.
Align the admin filter panels with the inline-filter-in-header pattern
used by feature/publication-admin's manage dashboard:

- Filter button: flat/dense/no-caps with `filter_list` icon, sitting in
  the table's `#top-after` slot alongside the other header controls
  instead of occupying its own row above the table.
- Active-count label: "Filter · N active" when any dimension is
  off-default, plain "Filter" otherwise, so the toolbar signals how
  much the user's view diverges from the default set.
- Single top-level All/None/Invert btn-group at the top of the menu.
  Removes the per-sub-panel Select: All/None controls inside Status /
  Roles, and the redundant outer Reset Filters / Reset filters to
  default buttons.

Internal checkbox rendering stays on q-option-group; the alternatives
(q-list/q-item, q-btn-dropdown, @click.stop wrapper, :auto-close=false)
did not resolve the menu-closes-on-checkbox-click behaviour, so none
was worth keeping. That behaviour is pre-existing and parked for a
separate investigation.
Port the conventions from feature/publication-admin so pages can be
colocated under src/routes/ and declare their own breadcrumb crumbs
via `definePage`:

Routing
- Add the VueRouter Vite plugin (`vue-router/vite`, already in deps
  via vue-router@5) with `src/routes` as the routesFolder and a
  generated typed-router.d.ts for IDE/type-check support.
- src/router/routes.ts now spreads `...autoRoutes` into the
  MainLayout children so file-based pages inherit `requiresAuth`.
- Add router/manual-routes.d.ts to augment RouteNamedMap with the
  entries still declared manually in routes.ts (publication, setup,
  submission/*), so `<router-link :to="{ name }">` stays typed.
- Scope eslint's vue/multi-word-component-names rule off for
  src/routes/**/*.vue (file names tie to URL segments).
- Add src/routes/**/*.vue to the graphql-codegen document glob so
  colocated graphql() calls register.

Breadcrumbs
- src/use/breadcrumbs.ts: composable that walks route.matched,
  builds crumbs from `meta.crumb` (single or array), and supports
  dynamic label overrides via setCrumbLabel for async-loaded names.
- src/components/BreadCrumbs.vue: q-breadcrumbs renderer.
- MainLayout mounts <bread-crumbs> so all auth'd pages get the
  same surface for free.

Supporting fixes
- Stub `definePage` as a global no-op in the vitest setup file so
  SFCs under src/routes/ can mount under test without pulling the
  VueRouter plugin into the vitest build.
- Narrow route-name literals (`as const`) / route.params accesses
  (`as Record<string, string>`) in pre-existing pages that the
  generated typed-router now flags (AcceptInvite, VerifyEmail,
  ResetPassword, SubmissionExport, Publication/SetupLayout).
Move the manually-configured admin pages under src/pages/Admin and
layouts/Admin into the file-based routing convention that landed
with the previous commit. Each route file uses `definePage(...)` to
declare its name, `requiresAppAdmin` meta (or inherits from parent),
and its breadcrumb crumb, replacing inline q-breadcrumbs blocks.

Route mapping (all route names preserved so call-sites don't change):
- /admin              → routes/admin/index.vue              (admin:dashboard)
- /admin/users        → routes/admin/users.vue              (admin:users)
- /admin/publications → routes/admin/publications.vue       (admin:publication:index)
- /admin/user/:id     → routes/admin/user/[id].vue          (layout, admin:user:id)
                      + routes/admin/user/[id]/index.vue    (user_details — publications tab)
                      + routes/admin/user/[id]/submissions.vue (user_details:submissions)

- routes/admin.vue is an intermediate layout that holds the shared
  `requiresAppAdmin` gate and the root "Administration" crumb, so
  children only declare their own leaf crumb.
- The [id].vue layout owns the user-card header, the tab bar, and
  the user-detail query. It uses setCrumbLabel to swap a placeholder
  "User" crumb for the resolved user's name once the query resolves.
- Children read `id` from the typed useRoute(name) instead of a
  defineProps prop, matching the convention.
- Filter-panel components at pages/Admin/components/ stay put; the
  migrated pages import them via their absolute src/pages/Admin/...
  path.
- Tests colocated with each route file; updated mocks to supply
  `route.params` for the [id] children.
Adds MIN_SEARCH_LENGTH short-circuit and LIKE-wildcard escaping to
PublicationBuilder::search, delegates the assignment builder to it so
the guards live in one place, and lands a FULLTEXT index on
publications.name to match the submissions/users treatment and leave
the door open for a MATCH...AGAINST switch. Test pins the min-length
and wildcard-escape behavior.
…e empties

- Gate the pattern overlay on both the manage-index grid chip and the
  table-view CategoryCountCell behind the global .a11y-patterns /
  prefers-contrast:more rules, and wrap the number in a
  pattern-text-mask span so the digit keeps a solid halo when the
  pattern does kick in.
- Shrink empty-state count columns on the manage table so the
  Publication column absorbs the remaining width, keeping the layout
  close to the populated view.
- Surface a search-hint on the manage table ("Enter at least 3
  characters...") to match the backend min-length guard.
- Constrain NeedsActionPublicationsTable width via the dashboard
  section grid instead of a hard max-width on the card.
…re/publication-admin

# Conflicts:
#	client/codegen.ts
#	client/eslint.config.js
#	client/src/router/manual-routes.d.ts
#	client/src/typed-router.d.ts
Add a new tabbed submission detail page at /manage/publication/:id/submissions/:submissionId and reorganize review-team assignment around a popup picker so admins see workload context, recency, and pending invitees up front.

Page (routes/manage/publication/[id]/submissions/[submissionId].vue):
- Co-located GetManagedSubmission query (trimmed to fields actually rendered, with a co-located ManageSubmissionAuditFields fragment)
- Header card with id, inline-editable title, status badge, action button
- Tabs for Details, Review Team, Activity; active tab synced to ?tab= for deep linking
- Activity tab uses q-timeline with role-aware icons and dual absolute/relative timestamps
- "Action needed" flag on the Review Team tab when the coordinator slot or reviewers list is empty, with a tooltip naming the gap

New components (pages/Publication/components/):
- SubmissionAssigneeList: linked names + assigned-X-ago caption + per-row remove; staged users get a folded-corner triangle with pending icon and "Invited" badge, both with tooltips
- AssignReviewTeamDialog: popup picker over publication.users(roles: [<role>]), top-10 sorted by LAST_ASSIGNED_AT, role-aware active counts, free-email invite, two-column layout in multi mode (search/list left, selected list + shared message right)
- ManageSubmissionAudit: q-timeline-entry wrapper around the existing description rendering
- ManageTabHeader: tab content with optional action-needed flag + reason tooltip; exports ACTION_NEEDED_TAB_CLASS so the tab itself adopts the warning treatment
- ManageInfoCallout: dismissable explainer panel (pale-blue, localStorage-persisted) used on the team-tab and invited-tab pages

Backend:
- last_assigned_at scalar field on PublicationUser (max submission_user.created_at, scoped to the same roles filter as the parent users() query)
- LAST_ASSIGNED_AT added to QueryPublicationUsersOrderByColumn
- addReviewers mutation: bundled connect + invite_emails + shared message in one transaction so a partial failure can't leave a half-saved assignment set
- Carbon parsing on the recency selectSub so Lighthouse's DateTimeUtc scalar serializes correctly

Other:
- Shared .section-heading SASS class to standardize subsection headings across the manage interface
- BreadCrumbs.vue truncates long crumb labels with a tooltip fallback
- Existing dashboard/team/submitter linkTo's now route to the new manage submission page; SubmissionCard derives the publication id from the route to do the same in grid view
- Explainer callouts on the Review Team and Invited tabs make the "no add-here button" workflow explicit
- New i18n keys under publication.manage.{assign_team,review_team} plus submission.{tabs,team_alert,assignee_list}
Polish pass over the tabbed submission detail page introduced in
a60f53e, plus defensive fixes for nullable content and a demo-data
prereq.

UI consolidation:
- New SectionHeader atom and ManagePanel component capture the
  "title + count + missing flag + action" header row and the
  "bordered card with header + separator + body" panel pattern
  that had drifted across the three tabs
- All three submission tabs (Details, Review Team, Activity) wrap
  their card content in ManagePanel; outer Review Team header sits
  at h2 above the role sub-section h3s for natural hierarchy
- SubmissionAssigneeList uses SectionHeader internally and exposes
  a `headerless` prop so a wrapping ManagePanel can own the heading;
  drops per-row separators and aligns rows flush with the section
  heading instead of stepping in another notch

New Overview panel (Details tab):
- Surfaces submitted_at, updated_at, and created_by (avatar + linked
  name) in a definition-list layout matching the user-detail stat
  strips' typography
- Query gains created_at, updated_at, submitted_at, created_by

Submission header redesign:
- ID number sits as a tucked-corner chip flush with the card border
- Status row becomes a colored footer with the status icon/label
  and a transition dropdown when permitted on the left, plus an
  Actions menu (review/preview + export) on the right; replaces
  the separated badge + button row

Defensive fixes:
- SubmissionContent.vue optional-chains submission.value?.content?.data
  so the editor doesn't throw before content has loaded or when a
  submission has no content yet
- DashboardDemoSeeder seeds content history on demo submissions and
  points submission.content at the latest entry so the legacy preview
  page renders against demo data
- Submitter and team-member detail pages migrate from q-banner to
  the shared ManageInfoCallout for staged-user explainer consistency

i18n: new submission.overview.{heading, submitted, not_submitted,
last_updated, created_by} keys; submission.actions_menu added.
…saves

Submission::getSubmittedAt() chained Collection::where('old_values',
'like', '%"status":0%') against the eager-loaded audits relation,
which silently never matched: Illuminate\Support\Collection has no
LIKE operator (unknown operators fall through to a no-match), and
the audit's old/new values are JSON-cast to arrays rather than
strings. Inspect the parsed arrays directly via data_get and pick
the first DRAFT->INITIALLY_SUBMITTED transition. Unblocks every
consumer of submitted_at — the new manage Overview panel, the
dashboard "Submitted" column, and the GraphQL field generally.

CreatedUpdatedBy trait read auth()->user()->id unconditionally and
crashed any model save outside an authenticated request. Use
auth()->user()?->id and skip the auto-update when no user is
present so seeders, queue workers, and artisan commands can save
models that explicitly set created_by/updated_by themselves.

DashboardDemoSeeder previously synthesized the DRAFT->INITIALLY_SUBMITTED
audit row only in the primary publication's loop, leaving the
~27 supplemental-publication submissions (B/C/D/E sets) without
the audit they need for getSubmittedAt() to resolve. Extract a
synthesizeSubmittedAudit() helper that skips drafts and call it
from both loops so every non-draft demo submission has the row.
Add backend mutations and client UI for theme/color-blind preferences,
per-key dismissable UI elements, and feature opt-in toggles. Includes a
new Lab Features page, an admin-side user settings tab, and full test
coverage on both ends.
The feature adds non-color cues for status, which benefits more than
just color-blind users. Rename the GraphQL field, JSON storage key,
TypeScript composable export, body class (`body--a11y-patterns`), and
i18n labels to match. Also relabel the user-settings dismissed section
header to plain "Dismissed help messages" so the ampersand stops
escaping in the rendered DOM.
The pattern rules in app.sass gate on `.a11y-patterns`, but the
composable was applying `.body--a11y-patterns`, so toggling the
preference had no visible effect on dashboard pages.
Co-authored-by: Copilot <copilot@github.com>
Base automatically changed from feature/admin-tables to master May 29, 2026 19:23
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