chore: prettier format pass on branch files

Auto-format all files modified during the documents-hub-split feature
branch that were not yet aligned with the project's Prettier config
(single quotes, semicolons, trailing commas).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 13:01:47 +02:00
parent eceb77a6c4
commit 0e8feb1073
77 changed files with 1174 additions and 1356 deletions

View File

@@ -428,10 +428,7 @@ type SystemRootName = (typeof SYSTEM_ROOT_NAMES)[number];
* can't race two inserts of the same root. Re-SELECTs on conflict so
* the return shape is always populated.
*/
export async function ensureSystemRoots(
portId: string,
userId: string,
): Promise<DocumentFolder[]> {
export async function ensureSystemRoots(portId: string, userId: string): Promise<DocumentFolder[]> {
// Try to insert all three; collect existing ids on conflict.
const values = SYSTEM_ROOT_NAMES.map((name) => ({
portId,
@@ -443,13 +440,16 @@ export async function ensureSystemRoots(
createdBy: userId,
}));
await db.insert(documentFolders).values(values).onConflictDoNothing({
target: [
documentFolders.portId,
sql`COALESCE(${documentFolders.parentId}, '__root__')`,
sql`LOWER(${documentFolders.name})`,
],
});
await db
.insert(documentFolders)
.values(values)
.onConflictDoNothing({
target: [
documentFolders.portId,
sql`COALESCE(${documentFolders.parentId}, '__root__')`,
sql`LOWER(${documentFolders.name})`,
],
});
// Re-SELECT — the rows that already existed are not in `.returning()`
// when ON CONFLICT DO NOTHING is used. SELECT is the authoritative
@@ -457,9 +457,7 @@ export async function ensureSystemRoots(
const rows = await db
.select()
.from(documentFolders)
.where(
and(eq(documentFolders.portId, portId), eq(documentFolders.entityType, 'root')),
);
.where(and(eq(documentFolders.portId, portId), eq(documentFolders.entityType, 'root')));
// Preserve SYSTEM_ROOT_NAMES order for callers (stable test assertions
// and a stable UI render order).
@@ -879,8 +877,7 @@ async function assertNotSystemManaged(
});
if (!folder) throw new NotFoundError('Folder');
if (folder.systemManaged) {
const verb =
action === 'rename' ? 'renamed' : action === 'move' ? 'moved' : 'deleted';
const verb = action === 'rename' ? 'renamed' : action === 'move' ? 'moved' : 'deleted';
throw new ConflictError(`System folders can't be ${verb}`);
}
return folder;
@@ -1009,10 +1006,7 @@ describe('document-folders service · syncEntityFolderName', () => {
});
it('renames the entity subfolder when the entity is renamed', async () => {
await db
.update(clients)
.set({ firstName: 'Jonathan' })
.where(eq(clients.id, clientId));
await db.update(clients).set({ firstName: 'Jonathan' }).where(eq(clients.id, clientId));
await syncEntityFolderName(portId, 'client', clientId, TEST_USER_ID);
const folder = await db.query.documentFolders.findFirst({
where: and(eq(documentFolders.entityType, 'client'), eq(documentFolders.entityId, clientId)),
@@ -1052,10 +1046,7 @@ describe('document-folders service · syncEntityFolderName', () => {
await ensureEntityFolder(portId, 'client', collider!.id, TEST_USER_ID);
// Rename John → Jane (collision with the other Smith, Jane).
await db
.update(clients)
.set({ firstName: 'Jane' })
.where(eq(clients.id, clientId));
await db.update(clients).set({ firstName: 'Jane' }).where(eq(clients.id, clientId));
await syncEntityFolderName(portId, 'client', clientId, TEST_USER_ID);
const folder = await db.query.documentFolders.findFirst({
@@ -1115,9 +1106,7 @@ export async function syncEntityFolderName(
for (let attempt = 0; attempt < 50; attempt += 1) {
const candidate =
attempt === 0
? `${baseName}${targetSuffix}`
: `${baseName} (${attempt + 1})${targetSuffix}`;
attempt === 0 ? `${baseName}${targetSuffix}` : `${baseName} (${attempt + 1})${targetSuffix}`;
if (candidate === folder.name) return; // No-op rename.
try {
const [updated] = await db
@@ -1692,9 +1681,12 @@ interface ResolvedOwner {
* linked interest's primary entity. Returns null when no owner is
* resolvable (signed PDF will land at root).
*/
async function resolveDocumentOwner(
doc: { clientId: string | null; companyId: string | null; yachtId: string | null; interestId: string | null },
): Promise<ResolvedOwner | null> {
async function resolveDocumentOwner(doc: {
clientId: string | null;
companyId: string | null;
yachtId: string | null;
interestId: string | null;
}): Promise<ResolvedOwner | null> {
if (doc.clientId) return { entityType: 'client', entityId: doc.clientId };
if (doc.companyId) return { entityType: 'company', entityId: doc.companyId };
if (doc.yachtId) return { entityType: 'yacht', entityId: doc.yachtId };
@@ -1853,24 +1845,40 @@ describe('files service · listFilesAggregatedByEntity', () => {
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
await ensureSystemRoots(portId, TEST_USER_ID);
const [c] = await db.insert(clients).values({
portId, firstName: 'John', lastName: 'Smith',
email: `john-${crypto.randomUUID()}@example.com`,
}).returning();
const [c] = await db
.insert(clients)
.values({
portId,
firstName: 'John',
lastName: 'Smith',
email: `john-${crypto.randomUUID()}@example.com`,
})
.returning();
clientId = c!.id;
const [co] = await db.insert(companies).values({
portId, name: 'Smith Marine LLC',
}).returning();
const [co] = await db
.insert(companies)
.values({
portId,
name: 'Smith Marine LLC',
})
.returning();
companyId = co!.id;
const [y] = await db.insert(yachts).values({
portId, name: 'MV Serenity', currentOwnerType: 'client', currentOwnerId: clientId,
}).returning();
const [y] = await db
.insert(yachts)
.values({
portId,
name: 'MV Serenity',
currentOwnerType: 'client',
currentOwnerId: clientId,
})
.returning();
yachtId = y!.id;
await db.insert(companyMemberships).values({
companyId, clientId,
companyId,
clientId,
});
clientFolderId = (await ensureEntityFolder(portId, 'client', clientId, TEST_USER_ID)).id;
@@ -1885,19 +1893,22 @@ describe('files service · listFilesAggregatedByEntity', () => {
folderId: string;
filename: string;
}) {
const [row] = await db.insert(files).values({
portId,
clientId: opts.clientId ?? null,
companyId: opts.companyId ?? null,
yachtId: opts.yachtId ?? null,
folderId: opts.folderId,
filename: opts.filename,
originalName: opts.filename,
mimeType: 'application/pdf',
storagePath: `test/${crypto.randomUUID()}`,
storageBucket: 'test',
uploadedBy: TEST_USER_ID,
}).returning();
const [row] = await db
.insert(files)
.values({
portId,
clientId: opts.clientId ?? null,
companyId: opts.companyId ?? null,
yachtId: opts.yachtId ?? null,
folderId: opts.folderId,
filename: opts.filename,
originalName: opts.filename,
mimeType: 'application/pdf',
storagePath: `test/${crypto.randomUUID()}`,
storageBucket: 'test',
uploadedBy: TEST_USER_ID,
})
.returning();
return row!;
}
@@ -1934,14 +1945,24 @@ describe('files service · listFilesAggregatedByEntity', () => {
it('snapshots are file-FK-based — yacht transfer does not move historical files', async () => {
const file = await insertFile({
yachtId, clientId, folderId: yachtFolderId, filename: 'Historic.pdf',
yachtId,
clientId,
folderId: yachtFolderId,
filename: 'Historic.pdf',
});
// Transfer the yacht to a different owner.
const [mary] = await db.insert(clients).values({
portId, firstName: 'Mary', lastName: 'Brown',
email: `mary-${crypto.randomUUID()}@example.com`,
}).returning();
await db.update(yachts).set({ currentOwnerType: 'client', currentOwnerId: mary!.id })
const [mary] = await db
.insert(clients)
.values({
portId,
firstName: 'Mary',
lastName: 'Brown',
email: `mary-${crypto.randomUUID()}@example.com`,
})
.returning();
await db
.update(yachts)
.set({ currentOwnerType: 'client', currentOwnerId: mary!.id })
.where(eq(yachts.id, yachtId));
// John's view still shows the file (via DIRECTLY ATTACHED or FROM YACHT).
@@ -1957,10 +1978,15 @@ describe('files service · listFilesAggregatedByEntity', () => {
it('rejects cross-port leakage with defense-in-depth port filter', async () => {
const otherPort = await setupTestPort();
const [otherClient] = await db.insert(clients).values({
portId: otherPort, firstName: 'Other', lastName: 'Port',
email: `other-${crypto.randomUUID()}@example.com`,
}).returning();
const [otherClient] = await db
.insert(clients)
.values({
portId: otherPort,
firstName: 'Other',
lastName: 'Port',
email: `other-${crypto.randomUUID()}@example.com`,
})
.returning();
// Try to query the other port's client from our port — should return empty.
const result = await listFilesAggregatedByEntity(portId, 'client', otherClient!.id);
expect(result.groups.flatMap((g) => g.files)).toHaveLength(0);
@@ -2041,9 +2067,11 @@ export async function listFilesAggregatedByEntity(
// DIRECTLY ATTACHED — files whose own FK matches the requested entity.
const directColumn =
entityType === 'client' ? files.clientId
: entityType === 'company' ? files.companyId
: files.yachtId;
entityType === 'client'
? files.clientId
: entityType === 'company'
? files.companyId
: files.yachtId;
const direct = await fetchGroupRows(portId, eq(directColumn, entityId), GROUP_LIMIT);
if (direct.rows.length > 0) {
groups.push({
@@ -2169,7 +2197,10 @@ async function collectRelatedEntities(
and(
eq(yachts.portId, portId),
eq(yachts.currentOwnerType, 'company'),
inArray(yachts.currentOwnerId, memberCompanies.map((c) => c.id)),
inArray(
yachts.currentOwnerId,
memberCompanies.map((c) => c.id),
),
),
);
}
@@ -2228,10 +2259,7 @@ async function collectRelatedEntities(
? [
{
id: owner.id,
name: `${owner.lastName ?? ''}, ${owner.firstName ?? ''}`.replace(
/^,\s*|,\s*$/,
'',
),
name: `${owner.lastName ?? ''}, ${owner.firstName ?? ''}`.replace(/^,\s*|,\s*$/, ''),
},
]
: [],
@@ -2310,10 +2338,7 @@ Append to `src/lib/services/documents.service.ts` — reuses the same `collectRe
Then append to `src/lib/services/documents.service.ts`:
```typescript
import {
collectRelatedEntities,
type AggregatedFileGroup,
} from '@/lib/services/files';
import { collectRelatedEntities, type AggregatedFileGroup } from '@/lib/services/files';
export interface AggregatedWorkflowGroup {
label: string;
@@ -2339,9 +2364,11 @@ export async function listInflightWorkflowsAggregatedByEntity(
const groups: AggregatedWorkflowGroup[] = [];
const directColumn =
entityType === 'client' ? documents.clientId
: entityType === 'company' ? documents.companyId
: documents.yachtId;
entityType === 'client'
? documents.clientId
: entityType === 'company'
? documents.companyId
: documents.yachtId;
const direct = await fetchWorkflowGroupRows(portId, eq(directColumn, entityId));
if (direct.rows.length > 0) {
@@ -2432,22 +2459,35 @@ describe('documents service · listInflightWorkflowsAggregatedByEntity', () => {
portId = await setupTestPort();
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
await ensureSystemRoots(portId, TEST_USER_ID);
const [c] = await db.insert(clients).values({
portId, firstName: 'John', lastName: 'Smith',
email: `john-${crypto.randomUUID()}@example.com`,
}).returning();
const [c] = await db
.insert(clients)
.values({
portId,
firstName: 'John',
lastName: 'Smith',
email: `john-${crypto.randomUUID()}@example.com`,
})
.returning();
clientId = c!.id;
});
it('returns in-flight workflows in DIRECTLY ATTACHED group', async () => {
const { documents: documentsTable } = await import('@/lib/db/schema/documents');
await db.insert(documentsTable).values({
portId, clientId, title: 'EOI', documentType: 'eoi',
status: 'sent', createdBy: TEST_USER_ID,
portId,
clientId,
title: 'EOI',
documentType: 'eoi',
status: 'sent',
createdBy: TEST_USER_ID,
});
await db.insert(documentsTable).values({
portId, clientId, title: 'Old signed EOI', documentType: 'eoi',
status: 'completed', createdBy: TEST_USER_ID,
portId,
clientId,
title: 'Old signed EOI',
documentType: 'eoi',
status: 'completed',
createdBy: TEST_USER_ID,
});
const result = await listInflightWorkflowsAggregatedByEntity(portId, 'client', clientId);
const direct = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED');
@@ -2476,18 +2516,17 @@ Append to `src/lib/services/files.ts`:
* Returns the mutated insert payload so callers can keep their
* single-insert flow.
*/
export async function applyEntityFkFromFolder<T extends {
clientId?: string | null;
companyId?: string | null;
yachtId?: string | null;
folderId?: string | null;
}>(portId: string, payload: T): Promise<T> {
export async function applyEntityFkFromFolder<
T extends {
clientId?: string | null;
companyId?: string | null;
yachtId?: string | null;
folderId?: string | null;
},
>(portId: string, payload: T): Promise<T> {
if (!payload.folderId) return payload;
const folder = await db.query.documentFolders.findFirst({
where: and(
eq(documentFolders.id, payload.folderId),
eq(documentFolders.portId, portId),
),
where: and(eq(documentFolders.id, payload.folderId), eq(documentFolders.portId, portId)),
columns: { systemManaged: true, entityType: true, entityId: true },
});
if (!folder || !folder.systemManaged || !folder.entityType || !folder.entityId) {
@@ -2521,10 +2560,15 @@ describe('files service · applyEntityFkFromFolder', () => {
beforeEach(async () => {
portId = await setupTestPort();
await ensureSystemRoots(portId, TEST_USER_ID);
const [c] = await db.insert(clients).values({
portId, firstName: 'A', lastName: 'B',
email: `a-${crypto.randomUUID()}@example.com`,
}).returning();
const [c] = await db
.insert(clients)
.values({
portId,
firstName: 'A',
lastName: 'B',
email: `a-${crypto.randomUUID()}@example.com`,
})
.returning();
clientId = c!.id;
folderId = (await ensureEntityFolder(portId, 'client', clientId, TEST_USER_ID)).id;
});
@@ -2540,9 +2584,15 @@ describe('files service · applyEntityFkFromFolder', () => {
});
it('is a no-op for non-system folders', async () => {
const [user] = await db.insert(documentFolders).values({
portId, parentId: null, name: 'My templates', createdBy: TEST_USER_ID,
}).returning();
const [user] = await db
.insert(documentFolders)
.values({
portId,
parentId: null,
name: 'My templates',
createdBy: TEST_USER_ID,
})
.returning();
const out = await applyEntityFkFromFolder(portId, { folderId: user!.id, clientId: null });
expect(out.clientId).toBeUndefined();
});
@@ -2767,20 +2817,41 @@ import { describe, it, expect, beforeEach } from 'vitest';
it('hides completed workflows when folderId is set', async () => {
const portId = await setupTestPort();
await ensureSystemRoots(portId, TEST_USER_ID);
const [folder] = await db.insert(documentFolders).values({
portId, parentId: null, name: 'Deals 2026', createdBy: TEST_USER_ID,
}).returning();
const [folder] = await db
.insert(documentFolders)
.values({
portId,
parentId: null,
name: 'Deals 2026',
createdBy: TEST_USER_ID,
})
.returning();
await db.insert(documents).values([
{
portId, folderId: folder!.id, title: 'In flight',
documentType: 'eoi', status: 'sent', createdBy: TEST_USER_ID,
portId,
folderId: folder!.id,
title: 'In flight',
documentType: 'eoi',
status: 'sent',
createdBy: TEST_USER_ID,
},
{
portId, folderId: folder!.id, title: 'Done',
documentType: 'eoi', status: 'completed', createdBy: TEST_USER_ID,
portId,
folderId: folder!.id,
title: 'Done',
documentType: 'eoi',
status: 'completed',
createdBy: TEST_USER_ID,
},
]);
const result = await listDocuments(portId, { folderId: folder!.id, page: 1, limit: 50, sort: 'createdAt', order: 'desc', tab: 'all' });
const result = await listDocuments(portId, {
folderId: folder!.id,
page: 1,
limit: 50,
sort: 'createdAt',
order: 'desc',
tab: 'all',
});
expect(result.data.map((d) => d.title)).toEqual(['In flight']);
});
```
@@ -2859,51 +2930,79 @@ describe('backfill-document-folders · idempotency + isolation', () => {
beforeEach(async () => {
portId = await setupTestPort();
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
const [c] = await db.insert(clients).values({
portId, firstName: 'John', lastName: 'Smith',
email: `john-${crypto.randomUUID()}@example.com`,
}).returning();
const [c] = await db
.insert(clients)
.values({
portId,
firstName: 'John',
lastName: 'Smith',
email: `john-${crypto.randomUUID()}@example.com`,
})
.returning();
clientId = c!.id;
});
it('creates system roots and entity subfolders for entities with attached files', async () => {
await db.insert(files).values({
portId, clientId, filename: 'a.pdf', originalName: 'a.pdf',
storagePath: `test/${crypto.randomUUID()}`, storageBucket: 'test',
portId,
clientId,
filename: 'a.pdf',
originalName: 'a.pdf',
storagePath: `test/${crypto.randomUUID()}`,
storageBucket: 'test',
uploadedBy: TEST_USER_ID,
});
await runBackfill({ portId });
const roots = await db.select().from(documentFolders).where(
and(eq(documentFolders.portId, portId), eq(documentFolders.entityType, 'root')),
);
const roots = await db
.select()
.from(documentFolders)
.where(and(eq(documentFolders.portId, portId), eq(documentFolders.entityType, 'root')));
expect(roots.map((r) => r.name).sort()).toEqual(['Clients', 'Companies', 'Yachts']);
const sub = await db.select().from(documentFolders).where(
and(eq(documentFolders.entityType, 'client'), eq(documentFolders.entityId, clientId)),
);
const sub = await db
.select()
.from(documentFolders)
.where(and(eq(documentFolders.entityType, 'client'), eq(documentFolders.entityId, clientId)));
expect(sub).toHaveLength(1);
});
it('sets files.folder_id from entity FKs', async () => {
const [file] = await db.insert(files).values({
portId, clientId, filename: 'a.pdf', originalName: 'a.pdf',
storagePath: `test/${crypto.randomUUID()}`, storageBucket: 'test',
uploadedBy: TEST_USER_ID,
}).returning();
const [file] = await db
.insert(files)
.values({
portId,
clientId,
filename: 'a.pdf',
originalName: 'a.pdf',
storagePath: `test/${crypto.randomUUID()}`,
storageBucket: 'test',
uploadedBy: TEST_USER_ID,
})
.returning();
await runBackfill({ portId });
const updated = await db.query.files.findFirst({ where: eq(files.id, file!.id) });
expect(updated?.folderId).not.toBeNull();
});
it('copies entity FKs from completed workflows onto signed files', async () => {
const [signedFile] = await db.insert(files).values({
portId, // NOTE: no clientId — legacy completion left it blank
filename: 'signed.pdf', originalName: 'signed.pdf',
storagePath: `test/${crypto.randomUUID()}`, storageBucket: 'test',
uploadedBy: 'system',
}).returning();
const [signedFile] = await db
.insert(files)
.values({
portId, // NOTE: no clientId — legacy completion left it blank
filename: 'signed.pdf',
originalName: 'signed.pdf',
storagePath: `test/${crypto.randomUUID()}`,
storageBucket: 'test',
uploadedBy: 'system',
})
.returning();
await db.insert(documents).values({
portId, clientId, signedFileId: signedFile!.id, title: 'EOI',
documentType: 'eoi', status: 'completed', createdBy: TEST_USER_ID,
portId,
clientId,
signedFileId: signedFile!.id,
title: 'EOI',
documentType: 'eoi',
status: 'completed',
createdBy: TEST_USER_ID,
});
await runBackfill({ portId });
const updated = await db.query.files.findFirst({ where: eq(files.id, signedFile!.id) });
@@ -2913,30 +3012,52 @@ describe('backfill-document-folders · idempotency + isolation', () => {
it('is idempotent — second run produces the same result', async () => {
await db.insert(files).values({
portId, clientId, filename: 'a.pdf', originalName: 'a.pdf',
storagePath: `test/${crypto.randomUUID()}`, storageBucket: 'test',
portId,
clientId,
filename: 'a.pdf',
originalName: 'a.pdf',
storagePath: `test/${crypto.randomUUID()}`,
storageBucket: 'test',
uploadedBy: TEST_USER_ID,
});
await runBackfill({ portId });
const after1 = await db.select().from(documentFolders).where(eq(documentFolders.portId, portId));
const after1 = await db
.select()
.from(documentFolders)
.where(eq(documentFolders.portId, portId));
await runBackfill({ portId });
const after2 = await db.select().from(documentFolders).where(eq(documentFolders.portId, portId));
const after2 = await db
.select()
.from(documentFolders)
.where(eq(documentFolders.portId, portId));
expect(after2).toHaveLength(after1.length);
});
it('respects port isolation — does not touch other ports', async () => {
const otherPort = await setupTestPort();
const [otherClient] = await db.insert(clients).values({
portId: otherPort, firstName: 'Other', lastName: 'Port',
email: `other-${crypto.randomUUID()}@example.com`,
}).returning();
const [otherClient] = await db
.insert(clients)
.values({
portId: otherPort,
firstName: 'Other',
lastName: 'Port',
email: `other-${crypto.randomUUID()}@example.com`,
})
.returning();
await db.insert(files).values({
portId: otherPort, clientId: otherClient!.id, filename: 'b.pdf',
originalName: 'b.pdf', storagePath: `test/${crypto.randomUUID()}`,
storageBucket: 'test', uploadedBy: TEST_USER_ID,
portId: otherPort,
clientId: otherClient!.id,
filename: 'b.pdf',
originalName: 'b.pdf',
storagePath: `test/${crypto.randomUUID()}`,
storageBucket: 'test',
uploadedBy: TEST_USER_ID,
});
await runBackfill({ portId }); // only this port
const otherRoots = await db.select().from(documentFolders).where(eq(documentFolders.portId, otherPort));
const otherRoots = await db
.select()
.from(documentFolders)
.where(eq(documentFolders.portId, otherPort));
expect(otherRoots).toHaveLength(0);
});
});
@@ -3009,11 +3130,13 @@ export async function runBackfill(opts: BackfillOptions = {}): Promise<void> {
);
for (const d of completedDocs) {
if (!d.signedFileId) continue;
const owner =
d.clientId ? ({ type: 'client', id: d.clientId } as const)
: d.companyId ? ({ type: 'company', id: d.companyId } as const)
: d.yachtId ? ({ type: 'yacht', id: d.yachtId } as const)
: null;
const owner = d.clientId
? ({ type: 'client', id: d.clientId } as const)
: d.companyId
? ({ type: 'company', id: d.companyId } as const)
: d.yachtId
? ({ type: 'yacht', id: d.yachtId } as const)
: null;
if (!owner) continue;
await tx
.update(files)
@@ -3030,8 +3153,8 @@ export async function runBackfill(opts: BackfillOptions = {}): Promise<void> {
owner.type === 'client'
? files.clientId
: owner.type === 'company'
? files.companyId
: files.yachtId,
? files.companyId
: files.yachtId,
),
),
);
@@ -3043,11 +3166,13 @@ export async function runBackfill(opts: BackfillOptions = {}): Promise<void> {
.from(files)
.where(and(eq(files.portId, portId), isNull(files.folderId)));
for (const f of fileRows) {
const owner: { type: EntityType; id: string } | null =
f.clientId ? { type: 'client', id: f.clientId }
: f.companyId ? { type: 'company', id: f.companyId }
: f.yachtId ? { type: 'yacht', id: f.yachtId }
: null;
const owner: { type: EntityType; id: string } | null = f.clientId
? { type: 'client', id: f.clientId }
: f.companyId
? { type: 'company', id: f.companyId }
: f.yachtId
? { type: 'yacht', id: f.yachtId }
: null;
if (!owner) continue;
try {
const folder = await ensureEntityFolder(portId, owner.type, owner.id, systemUser);
@@ -3172,8 +3297,7 @@ export function useAggregatedFiles(
) {
return useQuery<{ data: { groups: AggregatedGroup<AggregatedFile>[] } }>({
queryKey: ['files', 'aggregated', entityType, entityId],
queryFn: () =>
apiFetch(`/api/v1/files?entityType=${entityType}&entityId=${entityId}`),
queryFn: () => apiFetch(`/api/v1/files?entityType=${entityType}&entityId=${entityId}`),
enabled: Boolean(entityType && entityId),
staleTime: 10_000,
});
@@ -3186,8 +3310,7 @@ export function useAggregatedWorkflows(
) {
return useQuery<{ data: { groups: AggregatedGroup<AggregatedWorkflow>[] } }>({
queryKey: ['documents', 'aggregated', entityType, entityId],
queryFn: () =>
apiFetch(`/api/v1/documents?entityType=${entityType}&entityId=${entityId}`),
queryFn: () => apiFetch(`/api/v1/documents?entityType=${entityType}&entityId=${entityId}`),
enabled: Boolean(entityType && entityId),
staleTime: 10_000,
});
@@ -3434,7 +3557,10 @@ export function SigningDetailsDialog({ documentId, open, onOpenChange }: Props)
</h5>
<ul className="divide-y rounded border bg-muted/30">
{data.data.signers.map((s) => (
<li key={s.id} className="flex items-center justify-between gap-2 px-3 py-2 text-xs">
<li
key={s.id}
className="flex items-center justify-between gap-2 px-3 py-2 text-xs"
>
<div className="min-w-0">
<span className="font-medium">{s.signerName}</span>
<span className="ml-2 text-muted-foreground">{s.signerEmail}</span>
@@ -3536,8 +3662,10 @@ import { Lock } from 'lucide-react';
>
{/* existing chevron + folder icon */}
<span className="truncate">{node.name}</span>
{node.systemManaged ? <Lock className="ml-1 h-3 w-3 text-muted-foreground" aria-label="System folder" /> : null}
</button>
{node.systemManaged ? (
<Lock className="ml-1 h-3 w-3 text-muted-foreground" aria-label="System folder" />
) : null}
</button>;
```
Read the actual existing template first — the file is ~160 lines, copy its current row JSX and patch in the lock icon + archived muted class. Do not invent props or rewrite the structure.
@@ -3548,15 +3676,13 @@ The actions menu shows Create / Rename / Move / Delete buttons or menu items key
```tsx
// Find the selected folder in the tree
const selected = selectedFolderId
? findInTree(tree, selectedFolderId)
: null;
const selected = selectedFolderId ? findInTree(tree, selectedFolderId) : null;
const isSystem = selected?.systemManaged ?? false;
// Disable Rename + Move + Delete buttons (or hide them) when isSystem:
<Button disabled={isSystem || pending} onClick={handleRename}>
Rename
</Button>
</Button>;
// + a tooltip explaining "System folders can't be renamed/moved/deleted"
```
@@ -3576,18 +3702,20 @@ function findInTree(tree: FolderNode[], id: string): FolderNode | null {
Wrap each disabled button in a `<Tooltip>` (from `@/components/ui/tooltip`) explaining why when `isSystem`:
```tsx
{isSystem ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button disabled>Rename</Button>
</span>
</TooltipTrigger>
<TooltipContent>System folders can't be renamed.</TooltipContent>
</Tooltip>
) : (
<Button onClick={handleRename}>Rename</Button>
)}
{
isSystem ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button disabled>Rename</Button>
</span>
</TooltipTrigger>
<TooltipContent>System folders can't be renamed.</TooltipContent>
</Tooltip>
) : (
<Button onClick={handleRename}>Rename</Button>
);
}
```
- [ ] **Step 4: Verify type + visual sanity in dev**
@@ -3742,10 +3870,7 @@ import { ClipboardSignature, FileText, Eye } from 'lucide-react';
import { AggregatedSection } from './aggregated-section';
import { SigningDetailsDialog } from './signing-details-dialog';
import {
useAggregatedFiles,
useAggregatedWorkflows,
} from '@/hooks/use-aggregated-listing';
import { useAggregatedFiles, useAggregatedWorkflows } from '@/hooks/use-aggregated-listing';
import { StatusPill } from '@/components/ui/status-pill';
interface Props {
@@ -3879,7 +4004,9 @@ return (
{selectedFolderId === undefined ? (
<HubRootView portSlug={portSlug} />
) : selectedFolder?.entityType && selectedFolder.entityType !== 'root' && selectedFolder.entityId ? (
) : selectedFolder?.entityType &&
selectedFolder.entityType !== 'root' &&
selectedFolder.entityId ? (
<EntityFolderView
portSlug={portSlug}
entityType={selectedFolder.entityType as 'client' | 'company' | 'yacht'}
@@ -4069,7 +4196,7 @@ grep -rln 'deploy' docs/ | head -5
If a runbook exists, add a step **after** the migration step:
```markdown
````markdown
### Documents hub split (Wave 11.B+)
Run after the `0051_documents_hub_split.sql` migration applies:
@@ -4077,9 +4204,11 @@ Run after the `0051_documents_hub_split.sql` migration applies:
```bash
pnpm db:backfill:doc-folders
```
````
Idempotent — safe to re-run if the deploy is interrupted.
```
````
If no runbook exists, add the step to the README or a fresh `docs/deploy.md`. Don't create a docs file unless one exists or is the obvious home — the user-facing repo conventions matter more than perfect docs.
@@ -4092,7 +4221,7 @@ PGPASSWORD=changeme psql -h localhost -p 5434 -U crm -d port_nimara_crm \
-f src/lib/db/migrations/0051_documents_hub_split.sql
# Run backfill:
pnpm db:backfill:doc-folders
```
````
Expected: zero errors, system roots populated, file folder_ids set.
@@ -4145,7 +4274,10 @@ test('open client entity folder, see aggregated groups, view signing details', a
await page.getByRole('button', { name: /Smith, John/ }).click();
await expect(page.getByText(/DIRECTLY ATTACHED/i)).toBeVisible();
await expect(page.getByRole('button', { name: /view signing details/i })).toBeVisible();
await page.getByRole('button', { name: /view signing details/i }).first().click();
await page
.getByRole('button', { name: /view signing details/i })
.first()
.click();
await expect(page.getByRole('dialog', { name: /Signing details/i })).toBeVisible();
});
```