The workspaces grid: a hero banner leading a card grid.
Declared only by MishkaDocument.Workspace, and it reads that record directly rather than by column
position — the design's cards are specific shapes (accent bar, completion track, stat tiles, tags,
language editions) that a generic position mapping cannot describe.
The page's first featured workspace wears the banner (flagship_id/1); every other one stays a card
with its badge. A card's chrome is banded by how far along it is and its badges keep their language's
colour (MishkaDocument.WorkspaceAccent), and it reaches its translations through the editions row —
the list itself shows one row per document (MishkaDocument.WorkspaceScope).
Features
:filter- the control row: archive switch, search, and a drawer holding the grouped filters:select- card selection for bulk operations:bulk_actions- bulk actions on selected cards:paginate- numbered pagination under the grid
Options
:columns- the minimum card width the grid's tracks are built from (2, 3, or 4)
Performance
Uses @static.* for columns, ui_adapter, etc. (no re-render on user interaction)
Uses @state.* for page, filter_values, etc. (re-renders when changed)
Pairs with MishkaCmsCoreWeb.UIAdapters.Admin, which draws the archive switch and the buttons.
Summary
Functions
Whether a workspace reads as archived — by its status or by having been archived.
How far along the workspace is, 0–100.
How many people have touched the workspace. maintainers_info reads the event log and is gated the
same way health_metrics is, and it keys its count by string.
Where an edition chip goes: that language's own workspace, on the page edition_view/1 names. The
list only shows masters, so this is the only way to reach a translation without searching for it.
Which page an edition chip opens for the table's viewer: the edition's sections for a section admin, its versions for a workspace admin — the page its card's own actions open for them.
The workspace's sibling editions — the same document in other languages.
Which workspace on this page wears the banner: the first featured one, or nothing if none is.
One health_metrics figure, or 0 when the viewer cannot see the calculation.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_bulk_actions/1.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_empty/1.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_error/1.
The panel's control row: the archive switch, the search box, and a drawer holding every filter the
resource grouped. Nothing here names a filter — a :text filter becomes the search box and the rest
follow their group, so the resource stays the single source of truth.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_header/1.
One workspace card, wearing two accents: its chrome is banded by how far along it is, its language
badge and edition chips keep their language's colour, and archiving turns both grey
(MishkaDocument.WorkspaceAccent).
The count on the left, the page buttons on the right, under the grid.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_template_switcher/1.
How much of the flagship's progress ring to leave undrawn.
The words a workspace's status pill reads, in the viewer's language.
The tag chips the card carries, from the tag_names aggregate.
How many versions the workspace has, from the versions_count aggregate.
Functions
Whether a workspace reads as archived — by its status or by having been archived.
How far along the workspace is, 0–100.
health_metrics is field-policy gated, so it arrives as %Ash.NotLoaded{} for a viewer who cannot
see it — every read here has to survive that rather than take the page down with it.
How many people have touched the workspace. maintainers_info reads the event log and is gated the
same way health_metrics is, and it keys its count by string.
Where an edition chip goes: that language's own workspace, on the page edition_view/1 names. The
list only shows masters, so this is the only way to reach a translation without searching for it.
@spec edition_view(map()) :: :sections | :versions
Which page an edition chip opens for the table's viewer: the edition's sections for a section admin, its versions for a workspace admin — the page its card's own actions open for them.
The workspace's sibling editions — the same document in other languages.
The collapsing rule and the self-join are MishkaCmsCore.LanguageEditions', shared with every other
translatable admin list. What is workspace-specific is only what a chip shows beyond its language:
its own accent, and how far along that edition is, so the chip can read FA · 76%.
Which workspace on this page wears the banner: the first featured one, or nothing if none is.
A stream never tells a row its position, but it does not have to — the page's own records are on
state.records_result, which is where the stream's contents came from. Reading them here keeps the
decision with the page rather than pushing a "first?" flag onto every record.
Archived workspaces are skipped: the banner is a hero for live work, and a grey gradient reads as broken rather than as archived.
One health_metrics figure, or 0 when the viewer cannot see the calculation.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_bulk_actions/1.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_empty/1.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_error/1.
The panel's control row: the archive switch, the search box, and a drawer holding every filter the
resource grouped. Nothing here names a filter — a :text filter becomes the search box and the rest
follow their group, so the resource stays the single source of truth.
The phx-change="filter" form, the input names and clear_filters are Gervaz's contract; only the
look is ours. The archive switch comes from the ui_adapter (MishkaCmsCoreWeb.UIAdapters.Admin).
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_header/1.
One workspace card, wearing two accents: its chrome is banded by how far along it is, its language
badge and edition chips keep their language's colour, and archiving turns both grey
(MishkaDocument.WorkspaceAccent).
The page's one flagship gets the design's banner instead: full grid width, a gradient head and a progress ring rather than a bar. Every other featured workspace stays a card and keeps its badge — the design leads the grid with a single hero, and a wall of banners is not that grid.
Unlike the generic card templates this one reads the record directly rather than by column position:
it is declared only by MishkaDocument.Workspace, and the design's cards are specific shapes (accent
bar, completion track, stat tiles, editions) that positions cannot describe.
The count on the left, the page buttons on the right, under the grid.
prev_page / next_page / go_to_page and every figure here are Gervaz's — only the look is ours.
A resource that asked for :load_more or :infinite instead keeps the shared pagination, which is
the only thing that knows how to draw those.
Callback implementation for MishkaGervaz.Table.Behaviours.Template.render_template_switcher/1.
How much of the flagship's progress ring to leave undrawn.
The ring is a 41px-radius circle, so its circumference is 2π×41 ≈ 257.6 — the dash length the design hard-codes. Offsetting by the undone share leaves the done share drawn.
The words a workspace's status pill reads, in the viewer's language.
The tag chips the card carries, from the tag_names aggregate.
A workspace has no tags of its own — its sections do — so this is a list of names gathered three relationships away, and it can arrive unloaded like any other aggregate. Sorted so a card's chips keep their order between renders, and capped: the design gives the row one line, and a workspace with forty tags would push everything below it off the card.
How many versions the workspace has, from the versions_count aggregate.