This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The one route every module's pages are mounted through: `/<org>/<workspace>/<project>/<module>/...`.
|
||||
//
|
||||
// A module that brings pages lists them in `modules/index.ts` (`modulePages`), keyed by the segment
|
||||
// it answers; this route looks the segment up, calls the module's server component with the
|
||||
// request's own caller and messages, and wraps what it returns in the transport its client
|
||||
// components call procedures through. A new module therefore adds no route file here. A segment no
|
||||
// module answers, a path the module does not know and a record of another tenant are the same
|
||||
// not-found page.
|
||||
|
||||
import { modulePages } from "@app/modules"
|
||||
import { AppError } from "@conusai-kit/core"
|
||||
import { notFound } from "next/navigation"
|
||||
import type { ReactElement } from "react"
|
||||
import { webRuntime } from "../../../../../../src/server/app"
|
||||
import { pageContext } from "../../../../../../src/server/page"
|
||||
import { AppTransport } from "../../../../../../src/ui/transport"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ModulePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ org: string; workspace: string; project: string; module: string; rest?: string[] }>
|
||||
}): Promise<ReactElement> {
|
||||
const { module, rest, ...slugs } = await params
|
||||
if (!webRuntime().registry.modules.some((entry) => entry.id === module)) notFound()
|
||||
// `Object.hasOwn`: a segment named `constructor` is not a module.
|
||||
const load = Object.hasOwn(modulePages, module) ? modulePages[module] : undefined
|
||||
if (!load) notFound()
|
||||
const { caller, i18n, container } = await pageContext()
|
||||
const page = await load()
|
||||
const content = await page({ slugs, segments: rest ?? [], caller, i18n, container }).catch((error: unknown) => {
|
||||
if (error instanceof AppError && error.status === 404) notFound()
|
||||
throw error
|
||||
})
|
||||
return <AppTransport>{content}</AppTransport>
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// One document: its extracted lines, each with where it was read, and the reviewer's decisions.
|
||||
// Approve, edit and reject are procedure calls from the client component; nothing is exported
|
||||
// until a line is approved, and the export is the last control on the page.
|
||||
|
||||
import { PROFILE } from "@app/config"
|
||||
import { documentGet } from "@app/modules/documents"
|
||||
import { ExportButton, exportLabels, LineReview, reviewLabels } from "@app/modules/documents/ui"
|
||||
import { AppError } from "@conusai-kit/core"
|
||||
import { notFound } from "next/navigation"
|
||||
import type { ReactElement } from "react"
|
||||
import { pageContext } from "../../../../../../src/server/page"
|
||||
import { AppTransport } from "../../../../../../src/ui/transport"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function DocumentPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ org: string; workspace: string; project: string; fileId: string }>
|
||||
}): Promise<ReactElement> {
|
||||
const { fileId, ...slugs } = await params
|
||||
if (PROFILE === "convex") notFound()
|
||||
const { caller, i18n } = await pageContext()
|
||||
const detail = await caller.call(documentGet, { ...slugs, fileId }).catch((error: unknown) => {
|
||||
// A document of another organization and one that does not exist are the same page.
|
||||
if (error instanceof AppError && error.status === 404) notFound()
|
||||
throw error
|
||||
})
|
||||
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`documents.${key}`, values)
|
||||
const failed = i18n.message("app.failed")
|
||||
const { document, lines } = detail
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<a href={`/${slugs.org}/${slugs.workspace}/${slugs.project}/documents`}>
|
||||
{i18n.message("documents.review.back")}
|
||||
</a>
|
||||
</p>
|
||||
<h1>{i18n.message("documents.review.title", { filename: document.filename })}</h1>
|
||||
<p>
|
||||
{i18n.message("documents.list.status")}: {i18n.message(`documents.status.${document.status}`)}
|
||||
{document.failureCode ? ` ${i18n.message(`documents.failure.${document.failureCode}`)}` : ""}
|
||||
</p>
|
||||
<p>
|
||||
{i18n.message("documents.review.pendingCount", { count: document.pending })}.{" "}
|
||||
{i18n.message("documents.review.approvedCount", { count: document.approved })}.
|
||||
</p>
|
||||
{document.status === "queued" || document.status === "processing" ? (
|
||||
<p role="status">{i18n.message("documents.review.waiting")}</p>
|
||||
) : null}
|
||||
{document.skippedPages.length > 0 ? (
|
||||
<section aria-labelledby="skipped-pages">
|
||||
<h2 id="skipped-pages">{i18n.message("documents.review.skippedTitle")}</h2>
|
||||
<ul>
|
||||
{document.skippedPages.map((skipped) => (
|
||||
<li key={skipped.page}>
|
||||
{i18n.message("documents.review.skippedPage", {
|
||||
page: skipped.page,
|
||||
kind: i18n.message(`documents.review.pageClass.${skipped.pageClass}`),
|
||||
})}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
<AppTransport>
|
||||
<LineReview
|
||||
slugs={slugs}
|
||||
fileId={fileId}
|
||||
lines={lines}
|
||||
labels={reviewLabels(t, {
|
||||
failed,
|
||||
aiDisclosure: i18n.message("ui.aiDisclosure"),
|
||||
pageWord: i18n.message("ui.page"),
|
||||
})}
|
||||
locale={i18n.locale}
|
||||
/>
|
||||
<ExportButton slugs={slugs} fileId={fileId} approved={document.approved} labels={exportLabels(t, failed)} />
|
||||
</AppTransport>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The project's documents: upload one, and see where each is in being read and reviewed. Every
|
||||
// read is a procedure through the same caller the API uses; the upload is a client form that
|
||||
// calls the files module's own procedure (spec §6.2: one API path).
|
||||
|
||||
import { PROFILE } from "@app/config"
|
||||
import { documentList, READABLE_CONTENT_TYPES } from "@app/modules/documents"
|
||||
import { DocumentList, listLabels, UploadForm, uploadLabels } from "@app/modules/documents/ui"
|
||||
import { downloadHref } from "@conusai-kit/ui/download"
|
||||
import { notFound } from "next/navigation"
|
||||
import type { ReactElement } from "react"
|
||||
import { pageContext } from "../../../../../src/server/page"
|
||||
import { AppTransport } from "../../../../../src/ui/transport"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function DocumentsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ org: string; workspace: string; project: string }>
|
||||
}): Promise<ReactElement> {
|
||||
const slugs = await params
|
||||
if (PROFILE === "convex") notFound()
|
||||
const { caller, i18n } = await pageContext()
|
||||
const { documents } = await caller.call(documentList, slugs)
|
||||
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`documents.${key}`, values)
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{i18n.message("documents.title")}</h1>
|
||||
<p>{i18n.message("documents.intro")}</p>
|
||||
<AppTransport>
|
||||
<UploadForm
|
||||
slugs={slugs}
|
||||
accept={READABLE_CONTENT_TYPES}
|
||||
labels={uploadLabels(t, i18n.message("app.failed"))}
|
||||
/>
|
||||
</AppTransport>
|
||||
<DocumentList
|
||||
documents={documents}
|
||||
hrefFor={(fileId) => `/${slugs.org}/${slugs.workspace}/${slugs.project}/documents/${fileId}`}
|
||||
downloadHrefFor={(fileId) => downloadHref("files.file.download", { ...slugs, fileId })}
|
||||
labels={listLabels(t)}
|
||||
formatDate={(iso) => i18n.date(new Date(iso))}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The project layout: the project has to exist in this workspace of this organization, and a project
|
||||
// of another tenant, an unknown slug and a project the person has no place in are one 404, answered
|
||||
// before the response streams (spec §3.3). `tenancy.project.get` resolves the whole path from the
|
||||
// organization down, each step a scoped read, so nothing here re-implements the wall.
|
||||
|
||||
import { projectGet } from "@conusai-kit/tenancy"
|
||||
import type { ReactNode } from "react"
|
||||
import { orFallback } from "../../../../src/server/guard"
|
||||
import { pageContext } from "../../../../src/server/page"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ProjectLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: ReactNode
|
||||
params: Promise<{ org: string; workspace: string; project: string }>
|
||||
}): Promise<ReactNode> {
|
||||
const { org, workspace, project } = await params
|
||||
const { caller, path } = await pageContext()
|
||||
await orFallback(path, () => caller.call(projectGet, { org, workspace, project }))
|
||||
return children
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// A project's home: what it is, and the pages of the modules that work in it (spec §3.1, §4.1).
|
||||
// The entries come from the modules' manifests (src/ui/projects.ts), so a module added tomorrow
|
||||
// appears here with no edit to this file.
|
||||
|
||||
import { projectGet, workspaceList } from "@conusai-kit/tenancy"
|
||||
import type { ReactElement } from "react"
|
||||
import { orFallback } from "../../../../src/server/guard"
|
||||
import { pageContext } from "../../../../src/server/page"
|
||||
import { Breadcrumb } from "../../../../src/ui/breadcrumb"
|
||||
import { PROJECT_ENTRIES, projectHref } from "../../../../src/ui/projects"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ProjectHome({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ org: string; workspace: string; project: string }>
|
||||
}): Promise<ReactElement> {
|
||||
const { org, workspace, project } = await params
|
||||
const { caller, i18n, path } = await pageContext()
|
||||
const { found, workspaceName } = await orFallback(path, async () => {
|
||||
const [found, { workspaces }] = await Promise.all([
|
||||
caller.call(projectGet, { org, workspace, project }),
|
||||
caller.call(workspaceList, { org }),
|
||||
])
|
||||
return { found, workspaceName: workspaces.find((entry) => entry.slug === workspace)?.name ?? workspace }
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
label={i18n.message("shell.breadcrumb")}
|
||||
crumbs={[
|
||||
{ text: i18n.message("app.workspaces"), href: `/${org}/workspaces` },
|
||||
{ text: workspaceName, href: `/${org}/${workspace}` },
|
||||
{ text: found.name, href: projectHref(org, workspace, project) },
|
||||
]}
|
||||
/>
|
||||
<h1>{i18n.message("project.title", { name: found.name })}</h1>
|
||||
<p>{i18n.message("project.module", { module: found.moduleKey })}</p>
|
||||
{found.archived ? <p role="status">{i18n.message("project.archived")}</p> : null}
|
||||
<h2>{i18n.message("project.work")}</h2>
|
||||
<ul>
|
||||
{PROJECT_ENTRIES.map((entry) => (
|
||||
<li key={entry.href}>
|
||||
<a href={projectHref(org, workspace, project, entry)}>{i18n.message(entry.label)}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user