109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
// A page above any project, on the shell's F10 contract: `/<org>/<workspace>/modules/notes`. It lists the
|
|
// workspace's projects, each linked to its notes, and `?q=` filters them on the server, so the first byte
|
|
// already holds the result and a filter is a link anyone can copy.
|
|
//
|
|
// The shell answers 403 for `permission` before `load` runs; the rest is a `ProjectPage` with two slugs. This is
|
|
// the worked example for a catalogue, a list or a setting that belongs to a workspace, not to a project, and for
|
|
// a page's strings as one record, with a count only the browser knows formatted there (project-picker.tsx).
|
|
|
|
import type { Container, TransportCaller } from "@conusai-kit/core"
|
|
import type { I18n } from "@conusai-kit/i18n"
|
|
import { projectList } from "@conusai-kit/tenancy"
|
|
import { type WorkspacePage, workspacePage } from "@conusai-kit/ui"
|
|
import { FilterBar, Page, PageHeader, PageState, TextField } from "@conusai-kit/ui/patterns"
|
|
import { Button } from "@conusai-kit/ui/primitives"
|
|
import { ProjectPicker } from "./project-picker.js"
|
|
|
|
/** Every string of the page, as one record. `selected` keeps its placeholder: only the browser knows the count. */
|
|
export const WORKSPACE_LABEL_KEYS = [
|
|
"title",
|
|
"intro",
|
|
"filter",
|
|
"apply",
|
|
"clear",
|
|
"empty",
|
|
"noProjects",
|
|
"selected",
|
|
] as const
|
|
|
|
export interface NotesWorkspaceData {
|
|
readonly labels: Readonly<Record<(typeof WORKSPACE_LABEL_KEYS)[number], string>>
|
|
readonly query: string
|
|
readonly projects: readonly { readonly slug: string; readonly name: string }[]
|
|
}
|
|
|
|
export type NotesWorkspacePage = WorkspacePage<TransportCaller, I18n, Container, NotesWorkspaceData>
|
|
|
|
export const notesWorkspacePage: NotesWorkspacePage = workspacePage({
|
|
permission: "notes.note.read",
|
|
async load({ slugs, search, caller, i18n }): Promise<NotesWorkspaceData> {
|
|
const query = search.q ?? ""
|
|
const wanted = query.trim().toLowerCase()
|
|
const { projects } = await caller.call(projectList, slugs)
|
|
return {
|
|
labels: Object.fromEntries(
|
|
WORKSPACE_LABEL_KEYS.map((key) => [key, i18n.message(`notes.workspace.${key}`)]),
|
|
) as NotesWorkspaceData["labels"],
|
|
query,
|
|
projects: projects
|
|
.filter((project) => project.name.toLowerCase().includes(wanted))
|
|
.map(({ slug, name }) => ({ slug, name })),
|
|
}
|
|
},
|
|
View({ slugs, locale, data }) {
|
|
const { labels } = data
|
|
return (
|
|
<Page>
|
|
<PageHeader title={labels.title} description={labels.intro} />
|
|
<FilterBar
|
|
method="get"
|
|
actions={
|
|
<>
|
|
<Button type="submit" variant="secondary">
|
|
{labels.apply}
|
|
</Button>
|
|
{data.query.trim() && data.projects.length > 0 && (
|
|
<Button asChild variant="link">
|
|
<a href={`/${slugs.org}/${slugs.workspace}/modules/notes`}>{labels.clear}</a>
|
|
</Button>
|
|
)}
|
|
</>
|
|
}
|
|
>
|
|
<TextField
|
|
name="q"
|
|
label={labels.filter}
|
|
placeholder={labels.filter}
|
|
type="search"
|
|
defaultValue={data.query}
|
|
/>
|
|
</FilterBar>
|
|
{data.projects.length === 0 ? (
|
|
<PageState
|
|
title={data.query.trim() ? labels.empty : labels.noProjects}
|
|
action={
|
|
data.query.trim() ? (
|
|
<Button asChild variant="outline">
|
|
<a href={`/${slugs.org}/${slugs.workspace}/modules/notes`}>{labels.clear}</a>
|
|
</Button>
|
|
) : undefined
|
|
}
|
|
/>
|
|
) : (
|
|
<ProjectPicker
|
|
label={labels.title}
|
|
column={labels.filter}
|
|
empty={labels.noProjects}
|
|
projects={data.projects.map((project) => ({
|
|
...project,
|
|
href: `/${slugs.org}/${slugs.workspace}/${project.slug}/notes`,
|
|
}))}
|
|
selected={labels.selected}
|
|
locale={locale}
|
|
/>
|
|
)}
|
|
</Page>
|
|
)
|
|
},
|
|
})
|