feat(documents): syncEntityFolderName + entity-rename hooks
Per-entity subfolder names mirror the entity's current display string. Wired into updateClient / updateCompany / updateYacht; runs only when the name field changes. Best-effort (logged + swallowed) so a folder- sync error never fails an entity update. Preserves the (archived) suffix when present; skips entirely when the folder has been demoted to (deleted) — the rep owns the name at that point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,6 +138,7 @@ import {
|
||||
deleteFolderSoftRescue,
|
||||
moveFolder,
|
||||
renameFolder,
|
||||
syncEntityFolderName,
|
||||
} from '@/lib/services/document-folders.service';
|
||||
|
||||
describe('document-folders service · system folder protection', () => {
|
||||
@@ -188,3 +189,66 @@ describe('document-folders service · system folder protection', () => {
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('document-folders service · syncEntityFolderName', () => {
|
||||
let portId: string;
|
||||
let clientId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const port = await makePort();
|
||||
portId = port.id;
|
||||
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
|
||||
await ensureSystemRoots(portId, TEST_USER_ID);
|
||||
const originalFullName = `John Smith ${crypto.randomUUID().slice(0, 6)}`;
|
||||
const [client] = await db
|
||||
.insert(clients)
|
||||
.values({
|
||||
portId,
|
||||
fullName: originalFullName,
|
||||
})
|
||||
.returning();
|
||||
clientId = client!.id;
|
||||
await ensureEntityFolder(portId, 'client', clientId, TEST_USER_ID);
|
||||
});
|
||||
|
||||
it('renames the entity subfolder when the entity is renamed', async () => {
|
||||
const newName = `Jonathan Smith ${crypto.randomUUID().slice(0, 6)}`;
|
||||
await db.update(clients).set({ fullName: newName }).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)),
|
||||
});
|
||||
expect(folder?.name).toBe(newName);
|
||||
});
|
||||
|
||||
it('is a no-op when the folder does not exist (lazy creation)', async () => {
|
||||
const otherPort = await makePort();
|
||||
await ensureSystemRoots(otherPort.id, TEST_USER_ID);
|
||||
const [otherClient] = await db
|
||||
.insert(clients)
|
||||
.values({ portId: otherPort.id, fullName: `Jane Doe ${crypto.randomUUID().slice(0, 6)}` })
|
||||
.returning();
|
||||
// No folder created. Sync should not throw.
|
||||
await expect(
|
||||
syncEntityFolderName(otherPort.id, 'client', otherClient!.id, TEST_USER_ID),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('appends numeric suffix on rename collision (target name already taken)', async () => {
|
||||
const sharedName = `Jane Smith ${crypto.randomUUID().slice(0, 6)}`;
|
||||
const [collider] = await db
|
||||
.insert(clients)
|
||||
.values({ portId, fullName: sharedName })
|
||||
.returning();
|
||||
await ensureEntityFolder(portId, 'client', collider!.id, TEST_USER_ID);
|
||||
|
||||
// Rename John → same as collider.
|
||||
await db.update(clients).set({ fullName: sharedName }).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)),
|
||||
});
|
||||
expect(folder?.name).toBe(`${sharedName} (2)`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user