diff --git a/src/backend/Database.test.ts b/src/backend/Database.test.ts index a7e0afe5..ab24b4eb 100644 --- a/src/backend/Database.test.ts +++ b/src/backend/Database.test.ts @@ -57,29 +57,63 @@ test('index is correct', async () => { assert.is(entries[1]._index, second) }) -test('remove child entries', async () => { +test('archive child entries', async () => { const example = createExample() const {Page, Container} = example.schema - const parent = Edit.create({type: Container, set: {title: 'Parent'}}) - const sub = Edit.create({ + const parent = await example.create({type: Container, set: {title: 'Parent'}}) + const sub = await example.create({ type: Container, - parentId: parent.id, + parentId: parent._id, set: {title: 'Sub'} }) - const entry = Edit.create({ + const entry = await example.create({ type: Page, - parentId: sub.id, + parentId: sub._id, set: {title: 'Deepest'} }) - await example.commit(parent) - await example.commit(sub) - await example.commit(entry) - const res1 = await example.get({ - id: entry.id + assert.is(entry._parentId, sub._id) + await example.update({id: parent._id, status: EntryStatus.Archived}) + assert.not.ok( + await example.first({id: parent._id}), + 'Parent entry should be archived' + ) + assert.not.ok( + await example.first({id: sub._id}), + 'Sub entry should be archived' + ) + assert.not.ok( + await example.first({id: entry._id}), + 'Deepest entry should be archived' + ) + await example.update({id: parent._id, status: EntryStatus.Published}) + assert.ok( + await example.first({id: parent._id}), + 'Parent entry should be published' + ) + assert.ok(await example.first({id: sub._id}), 'Sub entry should be published') + assert.ok( + await example.first({id: entry._id}), + 'Deepest entry should be published' + ) +}) + +test('remove child entries', async () => { + const example = createExample() + const {Page, Container} = example.schema + const parent = await example.create({type: Container, set: {title: 'Parent'}}) + const sub = await example.create({ + type: Container, + parentId: parent._id, + set: {title: 'Sub'} + }) + const entry = await example.create({ + type: Page, + parentId: sub._id, + set: {title: 'Deepest'} }) - assert.is(res1._parentId, sub.id) - await example.commit(Edit.remove(parent.id)) - const res2 = await example.first({id: entry.id}) + assert.is(entry._parentId, sub._id) + await example.remove(parent._id) + const res2 = await example.first({id: entry._id}) assert.not.ok(res2) }) diff --git a/src/backend/Database.ts b/src/backend/Database.ts index f3bbfe2d..c4537c51 100644 --- a/src/backend/Database.ts +++ b/src/backend/Database.ts @@ -203,20 +203,21 @@ export class Database implements Syncable { return children } - async logEntries() { + async logEntries( + keys: Array = [ + 'url', + 'id', + 'parentId', + 'locale', + 'status', + 'title' + ] + ) { const entries = await this.store .select() .from(EntryRow) .orderBy(asc(EntryRow.url), asc(EntryRow.index)) - for (const entry of entries) { - console.info( - entry.url.padEnd(35), - entry.id.padEnd(12), - (entry.locale ?? '').padEnd(5), - entry.status.padEnd(12), - entry.title - ) - } + console.table(entries, keys) } private async applyMutation( @@ -246,9 +247,8 @@ export class Database implements Syncable { await tx.delete(EntryRow).where(condition) await tx.insert(EntryRow).values(entry) let children: Array = [] - if (entry.status === EntryStatus.Published) { - if (current) children = await this.updateChildren(tx, current, entry) - } + if (entry.status === EntryStatus.Published && current) + children = await this.updateChildren(tx, current, entry) return () => { return this.updateHash(tx, condition).then(self => this.updateHash( @@ -301,12 +301,21 @@ export class Database implements Syncable { await tx.delete(EntryRow).where(archived) await tx .update(EntryRow) - .set({ - status: EntryStatus.Archived, - filePath - }) + .set({status: EntryStatus.Archived, filePath}) .where(condition) - return () => this.updateHash(tx, archived) + const children = await tx + .update(EntryRow) + .set({status: EntryStatus.Archived}) + .where( + eq(EntryRow.status, EntryStatus.Published), + or( + eq(EntryRow.parentDir, published.childrenDir), + like(EntryRow.childrenDir, published.childrenDir + '/%') + ) + ) + .returning(EntryRow.id) + return () => + this.updateHash(tx, or(archived, inArray(EntryRow.id, children))) } case MutationType.Publish: { const promoting = await tx @@ -673,7 +682,6 @@ export class Database implements Syncable { seeded: EntryRow.seeded }) .from(EntryRow) - .where( eq(EntryRow.filePath, file.filePath), eq(EntryRow.workspace, file.workspace), @@ -697,9 +705,7 @@ export class Database implements Syncable { try { const raw = JsonLoader.parse(this.config.schema, file.contents) const {meta, data, v0Id} = parseRecord(raw) - if (v0Id) { - v0Ids.set(v0Id, meta.id) - } + if (v0Id) v0Ids.set(v0Id, meta.id) const seeded = meta.seeded const key = seedKey( file.workspace, @@ -743,6 +749,7 @@ export class Database implements Syncable { seenVersions.push([entry.id, entry.locale ?? 'null', entry.status]) inserted.push([entry.id, entry.locale ?? 'null', entry.status]) } catch (e: any) { + // Reminder: this runs browser side too so cannot use reportHalt here console.warn(`${e.message} @ ${file.filePath}`) process.exit(1) } @@ -842,14 +849,26 @@ export class Database implements Syncable { EntryRow.locale, sql`'null'` )}, ${EntryRow.status}) in ${values(...inserted)}` + const archivedPaths = await tx + .select(EntryRow.childrenDir) + .from(EntryRow) + .where(eq(EntryRow.status, EntryStatus.Archived)) + for (const archivedPath of archivedPaths) { + const isChildOf = or( + eq(EntryRow.parentDir, archivedPath), + like(EntryRow.childrenDir, archivedPath + '/%') + ) + await tx + .update(EntryRow) + .set({status: EntryStatus.Archived}) + .where(isChildOf, eq(EntryRow.status, EntryStatus.Published)) + } const entries = await tx.select().from(EntryRow).where(isInserted) for (const entry of entries) { const rowHash = await createRowHash(entry) await tx .update(EntryRow) - .set({ - rowHash - }) + .set({rowHash}) .where( eq(EntryRow.id, entry.id), is(EntryRow.locale, entry.locale), diff --git a/src/backend/Store.ts b/src/backend/Store.ts index 41619f86..851dde7d 100644 --- a/src/backend/Store.ts +++ b/src/backend/Store.ts @@ -1,3 +1,3 @@ -import {Database} from 'rado' +import {SyncDatabase} from 'rado' -export type Store = Database +export type Store = SyncDatabase<'sqlite'> diff --git a/src/dashboard/atoms/EntryEditorAtoms.ts b/src/dashboard/atoms/EntryEditorAtoms.ts index fd67d582..afe71352 100644 --- a/src/dashboard/atoms/EntryEditorAtoms.ts +++ b/src/dashboard/atoms/EntryEditorAtoms.ts @@ -151,7 +151,8 @@ export const entryEditorAtoms = atomFamily( parents: {}, select: { id: Entry.id, - path: Entry.path + path: Entry.path, + status: Entry.status } } }, @@ -178,7 +179,14 @@ export const entryEditorAtoms = atomFamily( locale: searchLocale, status: 'preferDraft' })) + const untranslated = Boolean( + entry.locale && searchLocale !== entry.locale + ) const parentNeedsTranslation = entry.parentId ? !parentLink : false + const parents = withParents?.parents ?? [] + const canPublish = parents.every( + parent => parent.status === EntryStatus.Published + ) if (versions.length === 0) return undefined const statuses = fromEntries( versions.map(version => [version.status, version]) @@ -187,8 +195,10 @@ export const entryEditorAtoms = atomFamily( status => statuses[status] !== undefined ) return createEntryEditor({ - parents: withParents?.parents ?? [], + parents, + canPublish, translations, + untranslated, parentNeedsTranslation, client, config, @@ -204,7 +214,7 @@ export const entryEditorAtoms = atomFamily( ) export interface EntryData { - parents: Array<{id: string; path: string}> + parents: Array<{id: string; path: string; status: EntryStatus}> client: Connection config: Config entryId: string @@ -212,6 +222,8 @@ export interface EntryData { statuses: Record availableStatuses: Array translations: Array<{locale: string; entryId: string}> + untranslated: boolean + canPublish: boolean parentNeedsTranslation: boolean edits: Edits } @@ -725,7 +737,7 @@ export function createEntryEditor(entryData: EntryData) { }) const form = atom(get => { const doc = get(currentDoc) - const readOnly = doc !== edits.doc ? true : undefined + const readOnly = doc !== edits.doc ? true : !entryData.canPublish return new FormAtoms(type, doc.getMap(DOC_KEY), '', {readOnly}) }) diff --git a/src/dashboard/view/EntryEdit.tsx b/src/dashboard/view/EntryEdit.tsx index 00828b65..3f62bed1 100644 --- a/src/dashboard/view/EntryEdit.tsx +++ b/src/dashboard/view/EntryEdit.tsx @@ -67,10 +67,9 @@ export function EntryEdit({editor}: EntryEditProps) { useEffect(() => { ref.current?.scrollTo({top: 0}) }, [editor.entryId, mode, selectedStatus]) - const untranslated = locale && locale !== editor.activeVersion.locale const {isBlocking, nextRoute, confirm, cancel} = useRouteBlocker( 'Are you sure you want to discard changes?', - !untranslated && hasChanges + !editor.untranslated && hasChanges ) const isNavigationChange = (nextRoute?.data.editor as EntryEditor)?.entryId !== editor.entryId @@ -93,7 +92,7 @@ export function EntryEdit({editor}: EntryEditProps) { alert('todo') return } - if (untranslated && hasChanges) { + if (editor.untranslated && hasChanges) { translate() } else if (config.enableDrafts) { if (hasChanges) saveDraft() @@ -219,7 +218,7 @@ export function EntryEdit({editor}: EntryEditProps) { )} - {untranslated && ( + {editor.untranslated && (
- - Publish - + {canPublish && ( + + Publish + + )}
@@ -288,7 +291,7 @@ export function EntryHeader({editor, editable = true}: EntryHeaderProps) { {!currentTransition && untranslated && - !editor.parentNeedsTranslation && + !parentNeedsTranslation && !hasChanges && ( <>