mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 02:41:25 +02:00
feat(shared-stores): add createArchiveOps factory + unify archive pattern
Standardize two-stage deletion across modules: archive (isArchived) + soft-delete (deletedAt). Add shared factory, Archivable type mixin, filterActive/filterArchived/filterNotDeleted query helpers. 13 tests. Migrate memoro, chat, picture, contacts to use createArchiveOps. Standardize picture from archivedAt timestamp to isArchived boolean. Picture toggleFavorite now uses shared toggleField (1 param, not 2). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d3114df16
commit
779a8ba322
12 changed files with 324 additions and 112 deletions
|
|
@ -46,7 +46,7 @@ db.version(1).stores({
|
|||
conversationTags: 'id, conversationId, tagId, [conversationId+tagId]',
|
||||
|
||||
// ─── Picture (appId: 'picture') ───
|
||||
images: 'id, isFavorite, isPublic, archivedAt, prompt',
|
||||
images: 'id, isFavorite, isPublic, isArchived, prompt',
|
||||
boards: 'id, isPublic',
|
||||
boardItems: 'id, boardId, itemType, zIndex, [boardId+zIndex]',
|
||||
imageTags: 'id, imageId, tagId, [imageId+tagId]', // junction to globalTags
|
||||
|
|
|
|||
|
|
@ -7,8 +7,14 @@
|
|||
|
||||
import { conversationTable, messageTable } from '../collections';
|
||||
import { toConversation } from '../queries';
|
||||
import { createArchiveOps } from '@manacore/shared-stores';
|
||||
import type { LocalConversation } from '../types';
|
||||
|
||||
/** Archive/soft-delete ops for conversations. */
|
||||
export const conversationArchive = createArchiveOps({
|
||||
table: () => conversationTable,
|
||||
});
|
||||
|
||||
export const conversationsStore = {
|
||||
/** Create a new conversation. */
|
||||
async create(data: {
|
||||
|
|
@ -50,21 +56,9 @@ export const conversationsStore = {
|
|||
});
|
||||
},
|
||||
|
||||
/** Archive a conversation. */
|
||||
async archive(id: string) {
|
||||
await conversationTable.update(id, {
|
||||
isArchived: true,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
|
||||
/** Unarchive a conversation. */
|
||||
async unarchive(id: string) {
|
||||
await conversationTable.update(id, {
|
||||
isArchived: false,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
// Archive ops (delegated to shared factory)
|
||||
archive: (id: string) => conversationArchive.archive(id),
|
||||
unarchive: (id: string) => conversationArchive.unarchive(id),
|
||||
|
||||
/** Pin a conversation. */
|
||||
async pin(id: string) {
|
||||
|
|
@ -86,7 +80,7 @@ export const conversationsStore = {
|
|||
async delete(id: string) {
|
||||
const now = new Date().toISOString();
|
||||
await conversationTable.update(id, { deletedAt: now, updatedAt: now });
|
||||
// Soft-delete all messages for this conversation
|
||||
// Cascade soft-delete to messages
|
||||
const msgs = await messageTable.where('conversationId').equals(id).toArray();
|
||||
for (const msg of msgs) {
|
||||
await messageTable.update(msg.id, { deletedAt: now, updatedAt: now });
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@
|
|||
|
||||
import { contactTable } from '../collections';
|
||||
import { toContact } from '../queries';
|
||||
import { createArchiveOps } from '@manacore/shared-stores';
|
||||
import type { LocalContact, Contact } from '../types';
|
||||
|
||||
/** Archive/soft-delete ops for contacts. */
|
||||
export const contactArchive = createArchiveOps({ table: () => contactTable });
|
||||
|
||||
export const contactsStore = {
|
||||
async createContact(data: Partial<Contact> & Record<string, unknown>) {
|
||||
const newLocal: LocalContact = {
|
||||
|
|
@ -86,13 +90,5 @@ export const contactsStore = {
|
|||
});
|
||||
},
|
||||
|
||||
async toggleArchive(id: string) {
|
||||
const local = await contactTable.get(id);
|
||||
if (!local) return;
|
||||
|
||||
await contactTable.update(id, {
|
||||
isArchived: !local.isArchived,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
toggleArchive: (id: string) => contactArchive.toggleArchive(id),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,8 +7,14 @@
|
|||
|
||||
import { memoTable } from '../collections';
|
||||
import { toMemo } from '../queries';
|
||||
import { createArchiveOps } from '@manacore/shared-stores';
|
||||
import type { LocalMemo } from '../types';
|
||||
|
||||
/** Archive/soft-delete ops for memos. */
|
||||
export const memoArchive = createArchiveOps({
|
||||
table: () => memoTable,
|
||||
});
|
||||
|
||||
export const memosStore = {
|
||||
/** Create a new memo (e.g., after recording). */
|
||||
async create(data: {
|
||||
|
|
@ -45,21 +51,9 @@ export const memosStore = {
|
|||
});
|
||||
},
|
||||
|
||||
/** Archive a memo. */
|
||||
async archive(id: string) {
|
||||
await memoTable.update(id, {
|
||||
isArchived: true,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
|
||||
/** Unarchive a memo. */
|
||||
async unarchive(id: string) {
|
||||
await memoTable.update(id, {
|
||||
isArchived: false,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
// Archive ops (delegated to shared factory)
|
||||
archive: (id: string) => memoArchive.archive(id),
|
||||
unarchive: (id: string) => memoArchive.unarchive(id),
|
||||
|
||||
/** Pin a memo. */
|
||||
async pin(id: string) {
|
||||
|
|
@ -78,8 +72,5 @@ export const memosStore = {
|
|||
},
|
||||
|
||||
/** Soft-delete a memo. */
|
||||
async delete(id: string) {
|
||||
const now = new Date().toISOString();
|
||||
await memoTable.update(id, { deletedAt: now, updatedAt: now });
|
||||
},
|
||||
delete: (id: string) => memoArchive.softDelete(id),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
$effect(() => {
|
||||
const sub = liveQuery(async () => {
|
||||
const all = await db.table<LocalImage>('images').toArray();
|
||||
return all.filter((i) => !i.deletedAt && !i.archivedAt);
|
||||
return all.filter((i) => !i.deletedAt && !i.isArchived);
|
||||
}).subscribe((val) => {
|
||||
images = val ?? [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function toImage(local: LocalImage): Image {
|
|||
isFavorite: local.isFavorite,
|
||||
downloadCount: local.downloadCount,
|
||||
rating: local.rating ?? undefined,
|
||||
archivedAt: local.archivedAt ?? undefined,
|
||||
isArchived: local.isArchived ?? undefined,
|
||||
generationId: local.generationId ?? undefined,
|
||||
sourceImageId: local.sourceImageId ?? undefined,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
|
|
@ -70,7 +70,7 @@ export function useAllImages() {
|
|||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalImage>('images').toArray();
|
||||
return locals
|
||||
.filter((img) => !img.archivedAt && !img.deletedAt)
|
||||
.filter((img) => !img.isArchived && !img.deletedAt)
|
||||
.map(toImage)
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
}, [] as Image[]);
|
||||
|
|
@ -81,7 +81,7 @@ export function useArchivedImages() {
|
|||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await db.table<LocalImage>('images').toArray();
|
||||
return locals
|
||||
.filter((img) => !!img.archivedAt && !img.deletedAt)
|
||||
.filter((img) => !!img.isArchived && !img.deletedAt)
|
||||
.map(toImage)
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
}, [] as Image[]);
|
||||
|
|
@ -128,7 +128,7 @@ export function allImages$() {
|
|||
return liveQuery(async () => {
|
||||
const locals = await db.table<LocalImage>('images').toArray();
|
||||
return locals
|
||||
.filter((img) => !img.archivedAt && !img.deletedAt)
|
||||
.filter((img) => !img.isArchived && !img.deletedAt)
|
||||
.map(toImage)
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@
|
|||
*/
|
||||
|
||||
import { db } from '$lib/data/database';
|
||||
import { createArchiveOps, toggleField } from '@manacore/shared-stores';
|
||||
import type { LocalImage } from '../types';
|
||||
import { toImage } from '../queries';
|
||||
|
||||
const imageTable = () => db.table<LocalImage>('images');
|
||||
|
||||
/** Archive/soft-delete ops for images. */
|
||||
export const imageArchive = createArchiveOps({ table: imageTable });
|
||||
|
||||
let error = $state<string | null>(null);
|
||||
let selectedImageId = $state<string | null>(null);
|
||||
|
|
@ -33,16 +38,10 @@ export const imagesStore = {
|
|||
showFavoritesOnly = !showFavoritesOnly;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle favorite status of an image.
|
||||
*/
|
||||
async toggleFavorite(id: string, currentIsFavorite: boolean) {
|
||||
async toggleFavorite(id: string) {
|
||||
error = null;
|
||||
try {
|
||||
await db.table('images').update(id, {
|
||||
isFavorite: !currentIsFavorite,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
await toggleField(imageTable(), id, 'isFavorite');
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to toggle favorite';
|
||||
|
|
@ -50,54 +49,8 @@ export const imagesStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Archive an image.
|
||||
*/
|
||||
async archiveImage(id: string) {
|
||||
error = null;
|
||||
try {
|
||||
await db.table('images').update(id, {
|
||||
archivedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to archive image';
|
||||
return { success: false, error };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore an archived image.
|
||||
*/
|
||||
async restoreImage(id: string) {
|
||||
error = null;
|
||||
try {
|
||||
await db.table('images').update(id, {
|
||||
archivedAt: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to restore image';
|
||||
return { success: false, error };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an image -- soft-deletes from IndexedDB instantly.
|
||||
*/
|
||||
async deleteImage(id: string) {
|
||||
error = null;
|
||||
try {
|
||||
await db.table('images').update(id, {
|
||||
deletedAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete image';
|
||||
return { success: false, error };
|
||||
}
|
||||
},
|
||||
// Archive ops (delegated to shared factory)
|
||||
archiveImage: (id: string) => imageArchive.archive(id),
|
||||
restoreImage: (id: string) => imageArchive.unarchive(id),
|
||||
deleteImage: (id: string) => imageArchive.softDelete(id),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export interface LocalImage extends BaseRecord {
|
|||
isFavorite: boolean;
|
||||
downloadCount: number;
|
||||
rating?: number | null;
|
||||
archivedAt?: string | null;
|
||||
isArchived?: boolean;
|
||||
generationId?: string | null;
|
||||
sourceImageId?: string | null;
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ export interface Image {
|
|||
isFavorite: boolean;
|
||||
downloadCount: number;
|
||||
rating?: number;
|
||||
archivedAt?: string;
|
||||
isArchived?: boolean;
|
||||
generationId?: string;
|
||||
sourceImageId?: string;
|
||||
createdAt: string;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
let selectedImage = $state<Image | null>(null);
|
||||
|
||||
async function handleToggleFavorite(img: Image) {
|
||||
await imagesStore.toggleFavorite(img.id, img.isFavorite);
|
||||
await imagesStore.toggleFavorite(img.id);
|
||||
}
|
||||
|
||||
async function handleArchive(img: Image) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue