summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/routes')
-rw-r--r--makima/frontend/src/routes/document-directives.tsx54
1 files changed, 46 insertions, 8 deletions
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx
index 06e427a..479dcd8 100644
--- a/makima/frontend/src/routes/document-directives.tsx
+++ b/makima/frontend/src/routes/document-directives.tsx
@@ -266,6 +266,11 @@ function DirectiveFolder({
return { activeDocs: active, shippedDocs: shipped };
}, [docs]);
+ // When a directive owns more than one contract, the two `tasks/` folders
+ // would otherwise be ambiguous. We pass this down to DocumentTasksFolder
+ // so it can rename itself to `tasks - <contract name>/` for clarity.
+ const multipleContracts = activeDocs.length + shippedDocs.length > 1;
+
const handleCreate = useCallback(async () => {
if (creating) return;
setCreating(true);
@@ -433,6 +438,8 @@ function DirectiveFolder({
refreshNonce={refreshNonce}
selectedTaskId={selectedTaskIdForFolder}
onSelectTask={onSelectTask}
+ contractLabel={fileLabel(doc, directive)}
+ multipleContracts={multipleContracts}
/>
</div>
))}
@@ -477,6 +484,8 @@ function DirectiveFolder({
refreshNonce={refreshNonce}
selectedTaskId={selectedTaskIdForFolder}
onSelectTask={onSelectTask}
+ contractLabel={fileLabel(doc, directive)}
+ multipleContracts={multipleContracts}
/>
</div>
))}
@@ -627,6 +636,13 @@ interface DocumentTasksFolderProps {
selectedTaskId: string | null;
/** Click handler for step/task rows — navigates to the live transcript. */
onSelectTask: (directiveId: string, taskId: string) => void;
+ /** Human-readable contract label (already resolved via fileLabel). Used to
+ * disambiguate multiple tasks/ folders under the same directive. */
+ contractLabel: string;
+ /** True when the parent directive owns more than one contract — drives the
+ * `tasks - <contractLabel>/` rename so the two sibling tasks/ folders are
+ * distinguishable. Single-contract directives keep the plain `tasks/`. */
+ multipleContracts: boolean;
}
function DocumentTasksFolder({
@@ -637,6 +653,8 @@ function DocumentTasksFolder({
refreshNonce,
selectedTaskId,
onSelectTask,
+ contractLabel,
+ multipleContracts,
}: DocumentTasksFolderProps) {
const [open, setOpen] = useState(defaultOpen);
const [data, setData] = useState<ContractTasksResponse | null>(null);
@@ -672,24 +690,29 @@ function DocumentTasksFolder({
const total = (data?.steps.length ?? 0) + (data?.tasks.length ?? 0);
- // Don't render the folder at all if we've fetched and the document has
- // no tasks. This is the cleanest visual: a draft document just shows up
- // as a single row with no children. The empty-folder check is gated on
- // a successful fetch so we don't flash "no tasks/" rows during loading.
- if (data && total === 0 && !loading && !error) {
- return null;
- }
+ // Folder always renders (even when empty) so the user can click into a
+ // fresh contract's tasks/ folder and see it stay visible. The empty state
+ // shows a muted "no tasks yet" placeholder inside the open body — same
+ // visual weight as the existing "Loading tasks…" / error placeholders.
+ //
+ // When the parent directive owns multiple contracts, both tasks/ folders
+ // are disambiguated as `tasks - <contractLabel>/` so the user can tell
+ // them apart. Single-contract directives keep the plain `tasks/` label.
+ const headerLabel = multipleContracts
+ ? `tasks - ${contractLabel}/`
+ : "tasks/";
return (
<div>
<button
type="button"
onClick={() => setOpen((v) => !v)}
+ title={headerLabel}
className={`w-full flex items-center gap-1.5 ${headerPadLeft} pr-3 py-1 font-mono text-[11px] text-[#7788aa] hover:bg-[rgba(117,170,252,0.06)]`}
>
<Caret open={open} />
<FolderIcon open={open} />
- <span>tasks/</span>
+ <span className="truncate text-left">{headerLabel}</span>
{total > 0 && (
<span className="ml-auto text-[10px] text-[#556677]">{total}</span>
)}
@@ -706,6 +729,11 @@ function DocumentTasksFolder({
{error}
</div>
)}
+ {data && total === 0 && !loading && !error && (
+ <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-[#556677] italic`}>
+ no tasks yet
+ </div>
+ )}
{data?.steps.map((step) => (
<StepRow
key={`step-${step.id}`}
@@ -1428,6 +1456,16 @@ function EditorShell({
onPickUpOrders={async () => {
await pickUpOrders();
}}
+ // Locked-and-started (`active`), `queued`, `shipped`, and
+ // `archived` contracts must be unlocked before edits are
+ // accepted. Only `draft` is freely editable; everything else
+ // shows the in-editor Unlock affordance.
+ editable={doc.status === "draft"}
+ onRequestUnlock={async () => {
+ const updated = await unlockDirectiveContract(doc.id);
+ setDoc(updated);
+ onDocumentChanged();
+ }}
/>
</div>
);