Components · Collaboration
LinkedDocsPanel
Docs and links attached to a record: list, open, unlink, link a doc, add a link, filter by Space.
Docs and links on a record
import { useRef, useState } from 'react';
import { LinkedDocsPanel, type LinkedDocItem } from '@rookcom/monarch-react/shell';
const START: LinkedDocItem[] = [
{
id: 'l1',
kind: 'doc',
docId: 'd1',
title: 'Release runbook',
href: '#doc-release-runbook',
meta: 'Updated 2 hours ago',
status: 'Published',
space: { id: 's-eng', name: 'Engineering' },
},
{
id: 'l2',
kind: 'doc',
docId: 'd2',
title: 'Launch brief',
href: '#doc-launch-brief',
meta: 'Updated yesterday',
status: 'Draft',
space: { id: 's-mkt', name: 'Marketing' },
},
{
id: 'l3',
kind: 'link',
title: 'Design file',
href: 'https://design.example.com/file/123',
},
];
const DOCS = [
{ value: 'd1', label: 'Release runbook', description: 'Engineering' },
{ value: 'd3', label: 'Incident review template', description: 'Engineering' },
{ value: 'd4', label: 'Pricing notes', description: 'Sales' },
];
function Basic() {
const [items, setItems] = useState(START);
const counter = useRef(10);
const nextId = () => `l${(counter.current += 1)}`;
return (
<div style={{ width: '100%', maxWidth: '26rem' }}>
<LinkedDocsPanel
items={items}
docOptions={DOCS}
onLinkDoc={(docId) => {
const doc = DOCS.find((d) => d.value === docId);
if (doc)
setItems((l) => [
...l,
{ id: nextId(), kind: 'doc', docId, title: doc.label, href: `#doc-${docId}` },
]);
}}
onCreateDoc={() => {}}
onAddLink={({ url, title }) =>
setItems((l) => [...l, { id: nextId(), kind: 'link', title, href: url }])
}
onUnlink={(it) => setItems((l) => l.filter((x) => x.id !== it.id))}
/>
</div>
);
}
<Basic />Usage examples
Empty, loading and read-only
import { LinkedDocsPanel, type LinkedDocItem } from '@rookcom/monarch-react/shell';
const START: LinkedDocItem[] = [
{
id: 'l1',
kind: 'doc',
docId: 'd1',
title: 'Release runbook',
href: '#doc-release-runbook',
meta: 'Updated 2 hours ago',
status: 'Published',
space: { id: 's-eng', name: 'Engineering' },
},
{
id: 'l2',
kind: 'doc',
docId: 'd2',
title: 'Launch brief',
href: '#doc-launch-brief',
meta: 'Updated yesterday',
status: 'Draft',
space: { id: 's-mkt', name: 'Marketing' },
},
{
id: 'l3',
kind: 'link',
title: 'Design file',
href: 'https://design.example.com/file/123',
},
];
function States() {
return (
<div style={{ display: 'grid', gap: 'var(--mn-space-6)', width: '100%', maxWidth: '26rem' }}>
<LinkedDocsPanel items={[]} onAddLink={() => {}} messages={{ title: 'Docs (empty)' }} />
<LinkedDocsPanel items={null} messages={{ title: 'Docs (loading)' }} />
<LinkedDocsPanel
items={START.slice(0, 1)}
readOnly
messages={{ title: 'Docs (read-only)' }}
/>
</div>
);
}
<States />Purpose
Records in Rook apps (tickets, tasks, campaigns, audits) link to Rook Dossier docs and to outside pages. LinkedDocsPanel is that panel on Monarch tokens, with the data and the linking left to the product.
Use when
- On a record’s detail page, in a side column or a "Docs" tab.
- Read-only (
readOnly) for people who may see but not change links.
Avoid when
- Attachments uploaded to the record itself: use FileUpload and UploadQueue.
- A general list of documents: use a table or list page.
Design principles
- Data in, intent out:
itemscomes from the product; link, create, add and unlink are callbacks. The panel never fetches. - Both kinds in one list: Dossier docs (status, Space, last update) and external links (the host name shows by default).
- Filter, don’t hide: Space chips appear only when items span several Spaces, and a filtered list says how many more exist.
- Promise-aware: a returned promise shows progress on the row or button; a rejection becomes an alert.
API reference
import { LinkedDocsPanel, parseLinkUrl } from '@rookcom/monarch-react/shell';| Export | Kind | Rendering | Root class |
|---|---|---|---|
LinkedDocsPanel | component | client | .mn-linked-docs-panel |
parseLinkUrl | utility | client | – |
Forwards its ref to the section. "Link doc" uses EntityPicker from @rookcom/monarch-react.
Also exported: parseLinkUrl (the add-link validator: full http(s) URLs only).
Generated from the TypeScript declarations of packages/react/src/components/collaboration/linked-docs-panel.
Properties
items={null} is loading. Each item has a row id (id); doc rows can carry docId so linked docs are left out of the picker.
"Link doc" appears with onLinkDoc and either docOptions or onSearchDocs; "New doc" with onCreateDoc; "Add link" with onAddLink.
filter: a Space id, "" for all, null for items without a Space.
LinkedDocsPanel
The shared panel of docs and links attached to a record (a ticket, a task, a campaign): Rook Dossier docs and external links, with Space filter chips, open, unlink, link a doc (search picker, New doc) and add a link. The product supplies the items and does the linking; the panel never fetches.
| Prop | Type | Description |
|---|---|---|
items required | LinkedDocItem[] | null | What is linked to the record. `null` while loading. |
children | ReactNode | |
className | string | |
defaultFilter | string | null | |
docOptions | ComboboxOption[] | Doc options for the picker (static list). |
error | ReactNode | Something went wrong loading or changing links (shown as an alert). |
filter | string | null | The Space filter (controlled): a Space id, `''` for all, or `null` for no Space. |
headingLevel | "h2" | "h3" | "h4" | Heading level of the title. Default `h3`. |
id | string | |
linkedDocIds | readonly string[] | Doc ids already linked (left out of the picker). Default: the `docId` of the `doc` items. |
messages | Partial<LinkedDocsPanelMessages> | Interface text, for translation. |
notice | ReactNode | Replaces the list, e.g. "You don't have access to Rook Dossier in this workspace." |
onAddLink | ((link: { url: string; title: string; }) => unknown) | Add an external link. Shows "Add link". |
onCreateDoc | (() => unknown) | "New doc" in the picker footer. |
onFilterChange | ((filter: string | null) => void) | |
onLinkDoc | ((docId: string) => unknown) | Link the chosen doc (its option value). Shows "Link doc" with `docOptions` or `onSearchDocs`. |
onOpen | ((item: LinkedDocItem, event: MouseEvent<HTMLAnchorElement>) => void) | Called when a row is opened (the link still navigates unless you prevent it). |
onRetry | (() => void) | With `error`, shows Try again. |
onSearchDocs | ((query: string, context: ComboboxSearchContext) => Promise<ComboboxOption[]>) | Search docs on the server. Already-linked docs are left out of the results. |
onUnlink | ((item: LinkedDocItem) => unknown) | Unlink a row. A returned promise shows progress; a rejection shows an error. |
readOnly | boolean | Hide linking, adding and unlinking. |
spaceFilter | boolean | Show Space filter chips when items span more than one Space. Default true. |
Variants
| Prop | Values | Description |
|---|---|---|
readOnly | 'false''true' | No link, add or unlink controls. |
spaceFilter | 'true''false' | Space chips when items span more than one Space. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Loading | aria-busy="true" | Skeleton rows and a status. |
| Empty | – | A short hint (different when read-only). |
| Filtered | aria-pressed="true" | On the active chip; "N more in other Spaces. Show all". |
| Unlinking | data-busy | The row dims and its button is busy. |
| Notice | – | A product message replaces the list, e.g. no access to Dossier. |
| Error | – | An alert, with Try again when onRetry is given. |
Interaction behaviour
- Rows open in a new tab (unless
external: false);onOpenruns first and may prevent it. - Unlink removes the row once the product updates
items; focus moves to the next row, the previous one, or the header. - Add link opens a small form with the URL focused; a URL that is not full http(s) is refused with a message. The title defaults to the host name.
- Link doc opens a search list without the docs already linked; "New doc" sits under it.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Through chips, rows, unlink buttons and actions. |
| Enter / Space | Press a chip, open a row, unlink, open the picker or form. |
| ↓ / ↑, Enter | Choose a doc in the picker (EntityPicker). |
| Enter | Add the link from the form. |
| Escape | Cancel the form or close the picker; focus returns to its button. |
Accessibility
- Role
- region (section labelled by its heading) with a list
- Accessible name
- The heading (
messages.title, default "Docs") plus the count.
- Chips are toggle buttons (
aria-pressed) in a labelled group. - Unlink buttons are named "Unlink <title>"; new-tab rows say so.
- The URL field reports errors through FieldError (
aria-invalid,aria-describedby). - Errors are alerts; loading is a status inside an
aria-busyregion. - Security: only http(s) URLs are accepted from the add-link form (no
javascript:ordata:); items are rendered as text, and new-tab links getrel="noopener noreferrer". The product must still validate URLs and permissions on the server.
Responsive behaviour
- Fills its column; long titles and meta truncate with an ellipsis.
- Chips wrap; chips and rows are 44px tall on touch screens.
Theme support
- Surface, border and muted text tokens; the active chip uses the accent subtle colours.
- Transitions are removed under reduced motion.
Do and don't
Do
- Return the promise from your callbacks so the panel can show progress and errors.
- Pass
docIdon doc rows so the picker leaves linked docs out. - Pass a
noticewhen the person has no access to Dossier in this workspace.
Don't
- Do not fetch inside the callbacks without handling errors: reject, and the panel shows a message.
- Do not put file uploads here.
Related components
- EntityPicker: The doc picker.
- FileUpload: Attachments stored on the record.
- SpaceSwitcher: The Space the person works in.
Dependencies
Testing coverage
- Labelled region with a list; new-tab links named with meta; host name for links; status chip.
- Space chips (pressed), filtered count and Show all; a stale filter shows all; no chips for one Space or when off.
- Unlink with a labelled button, focus to the next row; failed unlink alert.
- Link doc from the picker without linked docs; New doc; server search drops linked docs.
- Add link: validation, title from the host, focus back; Escape cancels.
- Loading, empty, read-only, notice and error with Try again; onOpen; URL parser; SSR.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/linked-docs-panel; Cross-browser axe: /components/linked-docs-panel |
Tests: packages/react/src/components/collaboration/linked-docs-panel