refactor(manacore/web): unify all module tags into shared globalTags

Migrate 7 modules (picture, photos, storage, memoro, times, uload, todo)
from module-specific tag tables to the shared globalTags system. Each
module now reads tags from the global pool and keeps only its junction
table for entity-tag linking via createTagLinkOps.

Removed: pictureTags, photoTags, storageTags, memoroTags, timeTags,
uloadTags, labels tables from IndexedDB schema and SYNC_APP_MAP.
Kept: all junction tables (imageTags, photoMediaTags, fileTags, etc.)
pointing to globalTags IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 15:34:55 +02:00
parent 48aac82bcb
commit 8c98dd871d
35 changed files with 149 additions and 623 deletions

View file

@ -26,8 +26,7 @@ db.version(1).stores({
tasks:
'id, dueDate, isCompleted, priority, order, projectId, [isCompleted+order], [projectId+order]',
todoProjects: 'id, order, isArchived, isDefault',
labels: 'id',
taskLabels: 'id, taskId, labelId',
taskLabels: 'id, taskId, labelId', // junction to globalTags (labelId = tagId)
reminders: 'id, taskId',
boardViews: 'id, order, groupBy',
@ -47,8 +46,7 @@ db.version(1).stores({
images: 'id, isFavorite, isPublic, archivedAt, prompt',
boards: 'id, isPublic',
boardItems: 'id, boardId, itemType, zIndex, [boardId+zIndex]',
pictureTags: 'id, name',
imageTags: 'id, imageId, tagId, [imageId+tagId]',
imageTags: 'id, imageId, tagId, [imageId+tagId]', // junction to globalTags
// ─── Cards (appId: 'cards') ───
cardDecks: 'id, isPublic',
@ -68,8 +66,7 @@ db.version(1).stores({
// ─── Storage (appId: 'storage') ───
files: 'id, parentFolderId, mimeType, isFavorite, isDeleted, name',
storageFolders: 'id, parentFolderId, path, depth, isFavorite, isDeleted',
storageTags: 'id, name',
fileTags: 'id, fileId, tagId, [fileId+tagId]',
fileTags: 'id, fileId, tagId, [fileId+tagId]', // junction to globalTags
// ─── Presi (appId: 'presi') ───
presiDecks: 'id, isPublic',
@ -85,8 +82,7 @@ db.version(1).stores({
albums: 'id, isAutoGenerated, name',
albumItems: 'id, albumId, mediaId, sortOrder, [albumId+sortOrder]',
photoFavorites: 'id, mediaId',
photoTags: 'id, name',
photoMediaTags: 'id, mediaId, tagId, [mediaId+tagId]',
photoMediaTags: 'id, mediaId, tagId, [mediaId+tagId]', // junction to globalTags
// ─── SkillTree (appId: 'skilltree') ───
skills: 'id, branch, parentId, level',
@ -103,7 +99,6 @@ db.version(1).stores({
timeProjects: 'id, clientId, isArchived, isBillable, guildId, visibility, order',
timeEntries:
'id, projectId, clientId, date, isRunning, [date+projectId], [date+clientId], guildId, visibility',
timeTags: 'id, name, order',
timeTemplates: 'id, usageCount, lastUsedAt, projectId',
timeSettings: 'id',
timeAlarms: 'id, enabled, time',
@ -132,7 +127,6 @@ db.version(1).stores({
// ─── uLoad (appId: 'uload') ───
links: 'id, shortCode, isActive, folderId, order, clickCount, [folderId+order], [isActive+order]',
uloadTags: 'id, slug, name',
uloadFolders: 'id, order',
linkTags: 'id, linkId, tagId, [linkId+tagId]',
@ -147,8 +141,7 @@ db.version(1).stores({
// ─── Memoro (appId: 'memoro') ───
memos: 'id, processingStatus, isArchived, isPinned, language, [isArchived+createdAt]',
memories: 'id, memoId',
memoroTags: 'id, name, sortOrder',
memoTags: 'id, memoId, tagId',
memoTags: 'id, memoId, tagId', // junction to globalTags
memoroSpaces: 'id, ownerId',
spaceMembers: 'id, spaceId, userId',
memoSpaces: 'id, memoId, spaceId',
@ -177,25 +170,24 @@ db.version(1).stores({
export const SYNC_APP_MAP: Record<string, string[]> = {
manacore: ['userSettings', 'dashboardConfigs'],
todo: ['tasks', 'todoProjects', 'labels', 'taskLabels', 'reminders', 'boardViews'],
todo: ['tasks', 'todoProjects', 'taskLabels', 'reminders', 'boardViews'],
calendar: ['calendars', 'events'],
contacts: ['contacts'],
chat: ['conversations', 'messages', 'chatTemplates'],
picture: ['images', 'boards', 'boardItems', 'pictureTags', 'imageTags'],
picture: ['images', 'boards', 'boardItems', 'imageTags'],
cards: ['cardDecks', 'cards'],
zitare: ['zitareFavorites', 'zitareLists'],
mukke: ['songs', 'mukkePlaylists', 'playlistSongs', 'mukkeProjects', 'markers'],
storage: ['files', 'storageFolders', 'storageTags', 'fileTags'],
storage: ['files', 'storageFolders', 'fileTags'],
presi: ['presiDecks', 'slides'],
inventar: ['invCollections', 'invItems', 'invLocations', 'invCategories'],
photos: ['albums', 'albumItems', 'photoFavorites', 'photoTags', 'photoMediaTags'],
photos: ['albums', 'albumItems', 'photoFavorites', 'photoMediaTags'],
skilltree: ['skills', 'activities', 'achievements'],
citycorners: ['cities', 'ccLocations', 'ccFavorites'],
times: [
'timeClients',
'timeProjects',
'timeEntries',
'timeTags',
'timeTemplates',
'timeSettings',
'timeAlarms',
@ -206,18 +198,10 @@ export const SYNC_APP_MAP: Record<string, string[]> = {
questions: ['qCollections', 'questions', 'answers'],
nutriphi: ['meals', 'goals', 'nutriFavorites'],
planta: ['plants', 'plantPhotos', 'wateringSchedules', 'wateringLogs'],
uload: ['links', 'uloadTags', 'uloadFolders', 'linkTags'],
uload: ['links', 'uloadFolders', 'linkTags'],
calc: ['calculations', 'savedFormulas'],
moodlit: ['moods', 'sequences'],
memoro: [
'memos',
'memories',
'memoroTags',
'memoTags',
'memoroSpaces',
'spaceMembers',
'memoSpaces',
],
memoro: ['memos', 'memories', 'memoTags', 'memoroSpaces', 'spaceMembers', 'memoSpaces'],
guides: ['guides', 'sections', 'steps', 'guideCollections', 'runs'],
tags: ['globalTags', 'tagGroups'],
links: ['manaLinks'],
@ -241,7 +225,6 @@ export const TABLE_TO_SYNC_NAME: Record<string, string> = {
// chat
chatTemplates: 'templates',
// picture
pictureTags: 'tags',
// cards
cardDecks: 'decks',
// zitare
@ -252,7 +235,6 @@ export const TABLE_TO_SYNC_NAME: Record<string, string> = {
mukkeProjects: 'projects',
// storage
storageFolders: 'folders',
storageTags: 'tags',
// presi
presiDecks: 'decks',
// inventar
@ -262,7 +244,6 @@ export const TABLE_TO_SYNC_NAME: Record<string, string> = {
invCategories: 'categories',
// photos
photoFavorites: 'favorites',
photoTags: 'tags',
photoMediaTags: 'photoTags',
// citycorners
ccLocations: 'locations',
@ -270,7 +251,6 @@ export const TABLE_TO_SYNC_NAME: Record<string, string> = {
// times
timeClients: 'clients',
timeProjects: 'projects',
timeTags: 'tags',
timeTemplates: 'templates',
timeSettings: 'settings',
timeAlarms: 'alarms',
@ -283,10 +263,8 @@ export const TABLE_TO_SYNC_NAME: Record<string, string> = {
// nutriphi
nutriFavorites: 'favorites',
// memoro
memoroTags: 'tags',
memoroSpaces: 'spaces',
// uload
uloadTags: 'tags',
uloadFolders: 'folders',
// guides
guideCollections: 'collections',

View file

@ -75,7 +75,7 @@ export interface Category {
export type ViewMode = 'list' | 'grid' | 'table';
export interface SortOption {
field: 'name' | 'createdAt' | 'updatedAt' | 'status' | 'quantity';
field: string;
direction: 'asc' | 'desc';
}

View file

@ -8,7 +8,6 @@ import { db } from '$lib/data/database';
import type {
LocalMemo,
LocalMemory,
LocalTag,
LocalMemoTag,
LocalSpace,
LocalSpaceMember,
@ -19,7 +18,6 @@ import type {
export const memoTable = db.table<LocalMemo>('memos');
export const memoryTable = db.table<LocalMemory>('memories');
export const memoroTagTable = db.table<LocalTag>('memoroTags');
export const memoTagTable = db.table<LocalMemoTag>('memoTags');
export const memoroSpaceTable = db.table<LocalSpace>('memoroSpaces');
export const spaceMemberTable = db.table<LocalSpaceMember>('spaceMembers');
@ -55,29 +53,7 @@ export const MEMORO_GUEST_SEED = {
'Memoro bietet Sprachaufnahme, automatische Transkription, KI-gestützte Zusammenfassungen und Tagging.',
},
],
memoroTags: [
{
id: 'tag-ideen',
name: 'Ideen',
color: '#3b82f6',
isPinned: true,
sortOrder: 0,
},
{
id: 'tag-notizen',
name: 'Notizen',
color: '#10b981',
isPinned: false,
sortOrder: 1,
},
],
memoTags: [
{
id: 'mt-demo-1',
memoId: DEMO_MEMO_ID,
tagId: 'tag-notizen',
},
],
memoTags: [] as Record<string, unknown>[],
memoroSpaces: [] as Record<string, unknown>[],
spaceMembers: [] as Record<string, unknown>[],
memoSpaces: [] as Record<string, unknown>[],

View file

@ -3,18 +3,23 @@
*/
export { memosStore } from './stores/memos.svelte';
export { tagsStore } from './stores/tags.svelte';
export {
tagMutations,
useAllTags,
getTagById,
getTagsByIds,
getTagColor,
memoTagOps,
} from './stores/tags.svelte';
export { memoriesStore } from './stores/memories.svelte';
export {
useAllMemos,
useArchivedMemos,
useMemoriesByMemo,
useAllTags,
useAllMemoTags,
useAllSpaces,
toMemo,
toMemory,
toTag,
toSpace,
sortMemos,
filterBySearch,
@ -26,7 +31,6 @@ export {
export {
memoTable,
memoryTable,
memoroTagTable,
memoTagTable,
memoroSpaceTable,
spaceMemberTable,
@ -36,7 +40,6 @@ export {
export type {
LocalMemo,
LocalMemory,
LocalTag,
LocalMemoTag,
LocalSpace,
LocalSpaceMember,

View file

@ -7,12 +7,10 @@ import { db } from '$lib/data/database';
import type {
LocalMemo,
LocalMemory,
LocalTag,
LocalMemoTag,
LocalSpace,
Memo,
Memory,
Tag,
Space,
} from './types';
@ -46,18 +44,6 @@ export function toMemory(local: LocalMemory): Memory {
};
}
export function toTag(local: LocalTag): Tag {
return {
id: local.id,
name: local.name,
color: local.color,
isPinned: local.isPinned ?? false,
sortOrder: local.sortOrder ?? 0,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
export function toSpace(local: LocalSpace): Space {
return {
id: local.id,
@ -98,16 +84,8 @@ export function useMemoriesByMemo(memoId: string) {
});
}
/** All tags, sorted by sortOrder. */
export function useAllTags() {
return liveQuery(async () => {
const locals = await db.table<LocalTag>('memoroTags').toArray();
return locals
.filter((t) => !t.deletedAt)
.map(toTag)
.sort((a, b) => a.sortOrder - b.sortOrder);
});
}
// Tags: use shared global tags from @manacore/shared-stores
export { useAllTags } from '@manacore/shared-stores';
/** All memo-tag associations. */
export function useAllMemoTags() {

View file

@ -1,71 +1,19 @@
/**
* Tags Store Mutations Only
*
* Reads come from liveQuery hooks in queries.ts.
* Handles tag CRUD and memo-tag associations.
* Memoro Tags Uses shared global tags + module-specific junction table.
*/
import { memoroTagTable, memoTagTable } from '../collections';
import { toTag } from '../queries';
import type { LocalTag, LocalMemoTag } from '../types';
import { db } from '$lib/data/database';
import { createTagLinkOps } from '@manacore/shared-stores';
export const tagsStore = {
/** Create a new tag. */
async create(data: { name: string; color?: string }) {
const all = await memoroTagTable.toArray();
const active = all.filter((t) => !t.deletedAt);
const newLocal: LocalTag = {
id: crypto.randomUUID(),
name: data.name,
color: data.color ?? null,
isPinned: false,
sortOrder: active.length,
};
await memoroTagTable.add(newLocal);
return toTag(newLocal);
},
export {
tagMutations,
useAllTags,
getTagById,
getTagsByIds,
getTagColor,
} from '@manacore/shared-stores';
/** Update a tag. */
async update(id: string, data: Partial<Pick<LocalTag, 'name' | 'color' | 'isPinned'>>) {
await memoroTagTable.update(id, {
...data,
updatedAt: new Date().toISOString(),
});
},
/** Soft-delete a tag and its associations. */
async delete(id: string) {
const now = new Date().toISOString();
await memoroTagTable.update(id, { deletedAt: now, updatedAt: now });
// Soft-delete associations
const allMT = await memoTagTable.where('tagId').equals(id).toArray();
for (const mt of allMT) {
await memoTagTable.update(mt.id, { deletedAt: now, updatedAt: now });
}
},
/** Add a tag to a memo. */
async addToMemo(memoId: string, tagId: string) {
// Check if association already exists
const existing = await memoTagTable.toArray();
if (existing.some((mt) => mt.memoId === memoId && mt.tagId === tagId && !mt.deletedAt)) {
return;
}
const newMT: LocalMemoTag = {
id: crypto.randomUUID(),
memoId,
tagId,
};
await memoTagTable.add(newMT);
},
/** Remove a tag from a memo. */
async removeFromMemo(memoId: string, tagId: string) {
const all = await memoTagTable.toArray();
const toRemove = all.find((mt) => mt.memoId === memoId && mt.tagId === tagId && !mt.deletedAt);
if (toRemove) {
const now = new Date().toISOString();
await memoTagTable.update(toRemove.id, { deletedAt: now, updatedAt: now });
}
},
};
export const memoTagOps = createTagLinkOps({
table: () => db.table('memoTags'),
entityIdField: 'memoId',
});

View file

@ -51,14 +51,6 @@ export interface LocalMemory extends BaseRecord {
metadata?: Record<string, unknown>;
}
export interface LocalTag extends BaseRecord {
name: string;
color: string | null;
userId?: string;
isPinned?: boolean;
sortOrder?: number;
}
export interface LocalMemoTag extends BaseRecord {
memoId: string;
tagId: string;

View file

@ -5,14 +5,13 @@
*/
import { db } from '$lib/data/database';
import type { LocalAlbum, LocalAlbumItem, LocalFavorite, LocalTag, LocalPhotoTag } from './types';
import type { LocalAlbum, LocalAlbumItem, LocalFavorite, LocalPhotoTag } from './types';
// ─── Collection Accessors ──────────────────────────────────
export const albumTable = db.table<LocalAlbum>('albums');
export const albumItemTable = db.table<LocalAlbumItem>('albumItems');
export const photoFavoriteTable = db.table<LocalFavorite>('photoFavorites');
export const photoTagTable = db.table<LocalTag>('photoTags');
export const photoMediaTagTable = db.table<LocalPhotoTag>('photoMediaTags');
// ─── Guest Seed ────────────────────────────────────────────
@ -26,21 +25,4 @@ export const PHOTOS_GUEST_SEED = {
isAutoGenerated: false,
},
],
photoTags: [
{
id: 'tag-nature',
name: 'Natur',
color: '#22c55e',
},
{
id: 'tag-people',
name: 'Menschen',
color: '#3b82f6',
},
{
id: 'tag-travel',
name: 'Reisen',
color: '#f59e0b',
},
],
};

View file

@ -5,7 +5,7 @@
export { photoStore } from './stores/photos.svelte';
export { albumMutations } from './stores/albums.svelte';
export {
useAllPhotoTags,
useAllTags,
getTagById,
getTagsByIds,
tagMutations,
@ -28,7 +28,6 @@ export {
albumTable,
albumItemTable,
photoFavoriteTable,
photoTagTable,
photoMediaTagTable,
PHOTOS_GUEST_SEED,
} from './collections';
@ -36,7 +35,6 @@ export type {
LocalAlbum,
LocalAlbumItem,
LocalFavorite,
LocalTag,
LocalPhotoTag,
Photo,
PhotoFilters,

View file

@ -1,146 +1,19 @@
/**
* Photo Tag Store Local-First via Dexie
*
* Tag CRUD and photo-tag junction table operations.
* Photo Tags Uses shared global tags + module-specific junction table.
*/
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalTag, LocalPhotoTag } from '../types';
import { createTagLinkOps } from '@manacore/shared-stores';
// ─── Tag CRUD ─────────────────────────────────────────────
export {
tagMutations,
useAllTags,
getTagById,
getTagsByIds,
getTagColor,
} from '@manacore/shared-stores';
export function useAllPhotoTags() {
return liveQuery(async () => {
const all = await db.table<LocalTag>('photoTags').toArray();
return all.filter((t) => !t.deletedAt);
});
}
export function getTagById(tags: LocalTag[], id: string): LocalTag | undefined {
return tags.find((t) => t.id === id);
}
export function getTagsByIds(tags: LocalTag[], ids: string[]): LocalTag[] {
const idSet = new Set(ids);
return tags.filter((t) => idSet.has(t.id));
}
export const tagMutations = {
async createTag(data: { name: string; color?: string }): Promise<LocalTag | null> {
try {
const now = new Date().toISOString();
const tag: LocalTag = {
id: crypto.randomUUID(),
name: data.name,
color: data.color ?? null,
createdAt: now,
updatedAt: now,
};
await db.table('photoTags').add(tag);
return tag;
} catch (e) {
console.error('Failed to create tag:', e);
return null;
}
},
async deleteTag(id: string): Promise<boolean> {
try {
const now = new Date().toISOString();
await db.table('photoTags').update(id, { deletedAt: now, updatedAt: now });
// Also soft-delete photo-tag associations
const associations = await db.table<LocalPhotoTag>('photoMediaTags').toArray();
for (const a of associations.filter((pt) => pt.tagId === id)) {
await db.table('photoMediaTags').update(a.id, { deletedAt: now, updatedAt: now });
}
return true;
} catch (e) {
console.error('Failed to delete tag:', e);
return false;
}
},
};
// ─── Photo-Tag Junction ───────────────────────────────────
export const photoTagOps = {
/** Get tags for a photo */
async getPhotoTags(mediaId: string): Promise<string[]> {
try {
const all = await db.table<LocalPhotoTag>('photoMediaTags').toArray();
return all.filter((pt) => pt.mediaId === mediaId && !pt.deletedAt).map((pt) => pt.tagId);
} catch (e) {
console.error('Failed to get photo tags:', e);
return [];
}
},
/** Add tag to photo */
async addTagToPhoto(mediaId: string, tagId: string) {
try {
const all = await db.table<LocalPhotoTag>('photoMediaTags').toArray();
const exists = all.some(
(pt) => pt.mediaId === mediaId && pt.tagId === tagId && !pt.deletedAt
);
if (exists) return true;
const now = new Date().toISOString();
await db.table('photoMediaTags').add({
id: crypto.randomUUID(),
mediaId,
tagId,
createdAt: now,
updatedAt: now,
});
return true;
} catch (e) {
console.error('Failed to add tag to photo:', e);
return false;
}
},
/** Remove tag from photo */
async removeTagFromPhoto(mediaId: string, tagId: string) {
try {
const all = await db.table<LocalPhotoTag>('photoMediaTags').toArray();
const item = all.find((pt) => pt.mediaId === mediaId && pt.tagId === tagId && !pt.deletedAt);
if (item) {
const now = new Date().toISOString();
await db.table('photoMediaTags').update(item.id, { deletedAt: now, updatedAt: now });
}
return true;
} catch (e) {
console.error('Failed to remove tag from photo:', e);
return false;
}
},
/** Set all tags for a photo (replace) */
async setPhotoTags(mediaId: string, tagIds: string[]) {
try {
const now = new Date().toISOString();
// Soft-delete existing tags for this photo
const all = await db.table<LocalPhotoTag>('photoMediaTags').toArray();
const existing = all.filter((pt) => pt.mediaId === mediaId && !pt.deletedAt);
for (const item of existing) {
await db.table('photoMediaTags').update(item.id, { deletedAt: now, updatedAt: now });
}
// Add new tags
for (const tagId of tagIds) {
await db.table('photoMediaTags').add({
id: crypto.randomUUID(),
mediaId,
tagId,
createdAt: now,
updatedAt: now,
});
}
return true;
} catch (e) {
console.error('Failed to set photo tags:', e);
return false;
}
},
};
export const photoTagOps = createTagLinkOps({
table: () => db.table('photoMediaTags'),
entityIdField: 'mediaId',
});

View file

@ -23,11 +23,6 @@ export interface LocalFavorite extends BaseRecord {
mediaId: string;
}
export interface LocalTag extends BaseRecord {
name: string;
color?: string | null;
}
export interface LocalPhotoTag extends BaseRecord {
mediaId: string;
tagId: string;

View file

@ -5,20 +5,13 @@
*/
import { db } from '$lib/data/database';
import type {
LocalImage,
LocalBoard,
LocalBoardItem,
LocalPictureTag,
LocalImageTag,
} from './types';
import type { LocalImage, LocalBoard, LocalBoardItem, LocalImageTag } from './types';
// ─── Collection Accessors ──────────────────────────────────
export const imageTable = db.table<LocalImage>('images');
export const boardTable = db.table<LocalBoard>('boards');
export const boardItemTable = db.table<LocalBoardItem>('boardItems');
export const pictureTagTable = db.table<LocalPictureTag>('pictureTags');
export const imageTagTable = db.table<LocalImageTag>('imageTags');
// ─── Guest Seed ────────────────────────────────────────────
@ -75,9 +68,4 @@ export const PICTURE_GUEST_SEED = {
properties: { fontFamily: 'Arial', fontWeight: 'normal', textAlign: 'center' },
},
] satisfies LocalBoardItem[],
pictureTags: [
{ id: 'tag-landscape', name: 'Landschaft', color: '#22c55e' },
{ id: 'tag-portrait', name: 'Portrait', color: '#3b82f6' },
{ id: 'tag-abstract', name: 'Abstrakt', color: '#a855f7' },
] satisfies LocalPictureTag[],
};

View file

@ -24,7 +24,6 @@ export {
imageTable,
boardTable,
boardItemTable,
pictureTagTable,
imageTagTable,
PICTURE_GUEST_SEED,
} from './collections';
@ -32,7 +31,6 @@ export type {
LocalImage,
LocalBoard,
LocalBoardItem,
LocalPictureTag,
LocalImageTag,
ViewMode,
Image,

View file

@ -13,7 +13,6 @@ import type {
LocalImage,
LocalBoard,
LocalBoardItem,
LocalPictureTag,
LocalImageTag,
Image,
Board,
@ -113,13 +112,8 @@ export function useAllBoards() {
}, [] as BoardWithCount[]);
}
/** All picture tags. */
export function useAllPictureTags() {
return useLiveQueryWithDefault(async () => {
const locals = await db.table<LocalPictureTag>('pictureTags').toArray();
return locals.filter((t) => !t.deletedAt);
}, [] as LocalPictureTag[]);
}
// Tags: use shared global tags from @manacore/shared-stores
export { useAllTags as useAllPictureTags } from '@manacore/shared-stores';
/** All image-tag associations. */
export function useAllImageTags() {

View file

@ -55,11 +55,6 @@ export interface LocalBoardItem extends BaseRecord {
properties: Record<string, unknown>;
}
export interface LocalPictureTag extends BaseRecord {
name: string;
color?: string | null;
}
export interface LocalImageTag extends BaseRecord {
imageId: string;
tagId: string;

View file

@ -5,13 +5,12 @@
*/
import { db } from '$lib/data/database';
import type { LocalFile, LocalFolder, LocalTag, LocalFileTag } from './types';
import type { LocalFile, LocalFolder, LocalFileTag } from './types';
// ─── Collection Accessors ──────────────────────────────────
export const fileTable = db.table<LocalFile>('files');
export const storageFolderTable = db.table<LocalFolder>('storageFolders');
export const storageTagTable = db.table<LocalTag>('storageTags');
export const fileTagTable = db.table<LocalFileTag>('fileTags');
// ─── Guest Seed ────────────────────────────────────────────
@ -49,21 +48,4 @@ export const STORAGE_GUEST_SEED = {
isDeleted: false,
},
],
storageTags: [
{
id: 'tag-important',
name: 'Wichtig',
color: '#ef4444',
},
{
id: 'tag-work',
name: 'Arbeit',
color: '#3b82f6',
},
{
id: 'tag-personal',
name: 'Privat',
color: '#22c55e',
},
],
};

View file

@ -3,7 +3,14 @@
*/
export { filesStore } from './stores/files.svelte';
export { storageTagStore } from './stores/tags.svelte';
export {
tagMutations,
useAllTags,
getTagById,
getTagsByIds,
getTagColor,
fileTagOps,
} from './stores/tags.svelte';
export {
useAllFiles,
useAllFolders,
@ -22,11 +29,5 @@ export {
formatFileSize,
} from './queries';
export type { StorageFile, StorageFolder, StorageTag } from './queries';
export {
fileTable,
storageFolderTable,
storageTagTable,
fileTagTable,
STORAGE_GUEST_SEED,
} from './collections';
export type { LocalFile, LocalFolder, LocalTag, LocalFileTag } from './types';
export { fileTable, storageFolderTable, fileTagTable, STORAGE_GUEST_SEED } from './collections';
export type { LocalFile, LocalFolder, LocalFileTag } from './types';

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalFile, LocalFolder, LocalTag, LocalFileTag } from './types';
import type { LocalFile, LocalFolder, LocalFileTag } from './types';
// ─── Shared Types (inline to avoid @storage/shared dependency) ───
@ -92,7 +92,12 @@ export function toFolder(local: LocalFolder): StorageFolder {
};
}
export function toTag(local: LocalTag): StorageTag {
export function toTag(local: {
id: string;
name: string;
color?: string | null;
createdAt?: string;
}): StorageTag {
return {
id: local.id,
userId: 'local',
@ -126,16 +131,8 @@ export function useAllFolders() {
});
}
/** All tags, sorted by name. Auto-updates on any change. */
export function useAllStorageTags() {
return liveQuery(async () => {
const locals = await db.table<LocalTag>('storageTags').toArray();
return locals
.filter((t) => !t.deletedAt)
.map(toTag)
.sort((a, b) => a.name.localeCompare(b.name));
});
}
// Tags: use shared global tags from @manacore/shared-stores
export { useAllTags as useAllStorageTags } from '@manacore/shared-stores';
// ─── Pure Helper Functions (for $derived) ─────────────────

View file

@ -1,56 +1,19 @@
/**
* Storage Tag Store Mutations Only
*
* Reads come from liveQuery hooks in queries.ts.
* This store only handles writes to IndexedDB via the unified database.
* Storage Tags Uses shared global tags + module-specific junction table.
*/
import { storageTagTable, fileTagTable } from '../collections';
import type { LocalTag, LocalFileTag } from '../types';
import { db } from '$lib/data/database';
import { createTagLinkOps } from '@manacore/shared-stores';
export const storageTagStore = {
async create(name: string, color?: string) {
const newTag: LocalTag = {
id: crypto.randomUUID(),
name,
color: color ?? null,
};
await storageTagTable.add(newTag);
return newTag;
},
export {
tagMutations,
useAllTags,
getTagById,
getTagsByIds,
getTagColor,
} from '@manacore/shared-stores';
async update(id: string, data: Partial<Pick<LocalTag, 'name' | 'color'>>) {
await storageTagTable.update(id, {
...data,
updatedAt: new Date().toISOString(),
});
},
async delete(id: string) {
await storageTagTable.update(id, {
deletedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
},
async tagFile(fileId: string, tagId: string) {
const existing = await fileTagTable.where('[fileId+tagId]').equals([fileId, tagId]).first();
if (existing) return;
const newFileTag: LocalFileTag = {
id: crypto.randomUUID(),
fileId,
tagId,
};
await fileTagTable.add(newFileTag);
},
async untagFile(fileId: string, tagId: string) {
const existing = await fileTagTable.where('[fileId+tagId]').equals([fileId, tagId]).first();
if (existing) {
await fileTagTable.update(existing.id, {
deletedAt: new Date().toISOString(),
});
}
},
};
export const fileTagOps = createTagLinkOps({
table: () => db.table('fileTags'),
entityIdField: 'fileId',
});

View file

@ -30,11 +30,6 @@ export interface LocalFolder extends BaseRecord {
isDeleted: boolean;
}
export interface LocalTag extends BaseRecord {
name: string;
color?: string | null;
}
export interface LocalFileTag extends BaseRecord {
fileId: string;
tagId: string;

View file

@ -10,7 +10,6 @@ import type {
LocalClient,
LocalProject,
LocalTimeEntry,
LocalTag,
LocalTemplate,
LocalSettings,
LocalAlarm,
@ -23,7 +22,6 @@ import type {
export const clientTable = db.table<LocalClient>('timeClients');
export const projectTable = db.table<LocalProject>('timeProjects');
export const timeEntryTable = db.table<LocalTimeEntry>('timeEntries');
export const tagTable = db.table<LocalTag>('timeTags');
export const templateTable = db.table<LocalTemplate>('timeTemplates');
export const settingsTable = db.table<LocalSettings>('timeSettings');
@ -181,12 +179,6 @@ export const TIMES_GUEST_SEED = {
source: { app: 'manual' as const },
},
],
timeTags: [
{ id: 'times-tag-design', name: 'design', color: '#f59e0b', order: 0 },
{ id: 'times-tag-dev', name: 'development', color: '#3b82f6', order: 1 },
{ id: 'times-tag-meeting', name: 'meeting', color: '#6b7280', order: 2 },
{ id: 'times-tag-review', name: 'review', color: '#22c55e', order: 3 },
],
timeSettings: [
{
id: 'times-default-settings',

View file

@ -25,7 +25,6 @@ export {
toClient,
toProject,
toTimeEntry,
toTag,
toTemplate,
toSettings,
formatDuration,
@ -67,7 +66,6 @@ export {
clientTable,
projectTable,
timeEntryTable,
tagTable,
templateTable,
settingsTable,
TIMES_GUEST_SEED,
@ -86,7 +84,6 @@ export type {
LocalClient,
LocalProject,
LocalTimeEntry,
LocalTag,
LocalTemplate,
LocalSettings,
BillingRate,

View file

@ -11,7 +11,6 @@ import type {
LocalClient,
LocalProject,
LocalTimeEntry,
LocalTag,
LocalTemplate,
LocalSettings,
LocalAlarm,
@ -88,17 +87,6 @@ export function toTimeEntry(local: LocalTimeEntry): TimeEntry {
};
}
export function toTag(local: LocalTag): Tag {
return {
id: local.id,
name: local.name,
color: local.color,
order: local.order,
createdAt: local.createdAt ?? new Date().toISOString(),
updatedAt: local.updatedAt ?? new Date().toISOString(),
};
}
export function toTemplate(local: LocalTemplate): EntryTemplate {
return {
id: local.id,
@ -200,12 +188,8 @@ export function useAllTimeEntries() {
});
}
export function useAllTags() {
return liveQuery(async () => {
const locals = await db.table<LocalTag>('timeTags').toArray();
return locals.filter((t) => !t.deletedAt).map(toTag);
});
}
// Tags: use shared global tags from @manacore/shared-stores
export { useAllTags } from '@manacore/shared-stores';
export function useAllTemplates() {
return liveQuery(async () => {

View file

@ -33,8 +33,8 @@ export interface ProjectBudget {
}
export interface SortOption {
field: SortField;
direction: SortDirection;
field: string;
direction: 'asc' | 'desc';
}
export interface FilterCriteria {
@ -195,12 +195,6 @@ export interface LocalTimeEntry extends BaseRecord {
source?: EntrySourceRef | null;
}
export interface LocalTag extends BaseRecord {
name: string;
color: string;
order: number;
}
export interface LocalTemplate extends BaseRecord {
name: string;
projectId?: string | null;

View file

@ -7,8 +7,7 @@
import { db } from '$lib/data/database';
import type {
LocalTask,
LocalLabel,
LocalTaskLabel,
LocalTaskTag,
LocalReminder,
LocalBoardView,
LocalTodoProject,
@ -18,8 +17,7 @@ import type {
export const taskTable = db.table<LocalTask>('tasks');
export const todoProjectTable = db.table<LocalTodoProject>('todoProjects');
export const labelTable = db.table<LocalLabel>('labels');
export const taskLabelTable = db.table<LocalTaskLabel>('taskLabels');
export const taskTagTable = db.table<LocalTaskTag>('taskLabels'); // DB table still 'taskLabels' until schema migration
export const reminderTable = db.table<LocalReminder>('reminders');
export const boardViewTable = db.table<LocalBoardView>('boardViews');
@ -32,19 +30,6 @@ const nextWeek = new Date(now);
nextWeek.setDate(nextWeek.getDate() + 7);
export const TODO_GUEST_SEED = {
labels: [
{
id: 'label-important',
name: 'Wichtig',
color: '#ef4444',
},
{
id: 'label-idea',
name: 'Idee',
color: '#f59e0b',
},
] satisfies LocalLabel[],
boardViews: [
{
id: 'view-kanban',

View file

@ -1,7 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte';
import type { Observable } from 'dexie';
import type { LocalLabel } from '../../types';
import { TagSelector as SharedTagSelector } from '@manacore/shared-ui';
import type { Tag } from '@manacore/shared-ui';
@ -12,14 +11,15 @@
let { selectedIds, onChange }: Props = $props();
const allLabels$: Observable<LocalLabel[]> = getContext('labels');
let allLabels = $state<LocalLabel[]>([]);
// Labels come from context (set in todo +layout.svelte via useAllLabels → shared useAllTags)
const allLabels$: Observable<Array<{ id: string; name: string; color: string }>> =
getContext('labels');
let allLabels = $state<Array<{ id: string; name: string; color: string }>>([]);
$effect(() => {
const sub = allLabels$.subscribe((l) => (allLabels = l));
const sub = allLabels$.subscribe((l) => (allLabels = l ?? []));
return () => sub.unsubscribe();
});
// Adapt LocalLabel[] to Tag[] for the shared component
const tags: Tag[] = $derived(allLabels.map((l) => ({ id: l.id, name: l.name, color: l.color })));
const selectedTags: Tag[] = $derived(
selectedIds.map((id) => tags.find((t) => t.id === id)).filter((t): t is Tag => t != null)

View file

@ -37,8 +37,7 @@ export {
export {
taskTable,
todoProjectTable,
labelTable,
taskLabelTable,
taskTagTable,
reminderTable,
boardViewTable,
TODO_GUEST_SEED,
@ -66,7 +65,7 @@ export { useTaskForm } from './composables/useTaskForm.svelte';
export type {
LocalTask,
LocalLabel,
LocalTaskLabel,
LocalTaskTag,
LocalReminder,
LocalBoardView,
LocalTodoProject,

View file

@ -6,7 +6,6 @@ import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type {
LocalTask,
LocalLabel,
LocalBoardView,
LocalReminder,
LocalTodoProject,
@ -50,12 +49,8 @@ export function useAllTasks() {
});
}
export function useAllLabels() {
return liveQuery(async () => {
const locals = await db.table<LocalLabel>('labels').toArray();
return locals.filter((l) => !l.deletedAt);
});
}
// Labels/Tags: use shared global tags from @manacore/shared-stores
export { useAllTags as useAllLabels } from '@manacore/shared-stores';
export function useAllBoardViews() {
return liveQuery(async () => {

View file

@ -1,32 +1,35 @@
/**
* Labels Store Mutation-Only Service
* Todo Tags (formerly Labels) Uses shared global tags + module-specific junction table.
*/
import { labelTable } from '../collections';
import type { LocalLabel } from '../types';
import { db } from '$lib/data/database';
import { createTagLinkOps } from '@manacore/shared-stores';
export {
tagMutations,
useAllTags,
getTagById,
getTagsByIds,
getTagColor,
} from '@manacore/shared-stores';
export const taskTagOps = createTagLinkOps({
table: () => db.table('taskLabels'), // DB table still 'taskLabels' until schema migration
entityIdField: 'taskId',
});
// Backward-compat alias
export const labelsStore = {
async createLabel(data: { name: string; color: string }) {
const newLabel: LocalLabel = {
id: crypto.randomUUID(),
name: data.name,
color: data.color,
};
await labelTable.add(newLabel);
return newLabel;
createLabel: async (data: { name: string; color: string }) => {
const { tagMutations } = await import('@manacore/shared-stores');
return tagMutations.createTag({ name: data.name, color: data.color });
},
async updateLabel(id: string, data: Partial<Pick<LocalLabel, 'name' | 'color'>>) {
await labelTable.update(id, {
...data,
updatedAt: new Date().toISOString(),
});
updateLabel: async (id: string, data: { name?: string; color?: string }) => {
const { tagMutations } = await import('@manacore/shared-stores');
return tagMutations.updateTag(id, data);
},
async deleteLabel(id: string) {
await labelTable.update(id, {
deletedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
deleteLabel: async (id: string) => {
const { tagMutations } = await import('@manacore/shared-stores');
return tagMutations.deleteTag(id);
},
};

View file

@ -3,6 +3,10 @@
*/
import type { BaseRecord } from '@manacore/local-store';
import type { Tag } from '@manacore/shared-tags';
/** @deprecated Use Tag from @manacore/shared-tags. Kept for backward compatibility. */
export type LocalLabel = Tag;
// ─── Local Types (IndexedDB) ──────────────────────────────
@ -35,15 +39,9 @@ export interface LocalTask extends BaseRecord {
metadata?: Record<string, unknown>;
}
export interface LocalLabel extends BaseRecord {
name: string;
color: string;
userId?: string;
}
export interface LocalTaskLabel extends BaseRecord {
export interface LocalTaskTag extends BaseRecord {
taskId: string;
labelId: string;
tagId: string;
}
export interface LocalReminder extends BaseRecord {

View file

@ -5,12 +5,11 @@
*/
import { db } from '$lib/data/database';
import type { LocalLink, LocalTag, LocalFolder, LocalLinkTag } from './types';
import type { LocalLink, LocalFolder, LocalLinkTag } from './types';
// ─── Collection Accessors ──────────────────────────────────
export const linkTable = db.table<LocalLink>('links');
export const uloadTagTable = db.table<LocalTag>('uloadTags');
export const uloadFolderTable = db.table<LocalFolder>('uloadFolders');
export const linkTagTable = db.table<LocalLinkTag>('linkTags');
@ -31,35 +30,6 @@ export const ULOAD_GUEST_SEED = {
order: 1,
},
] satisfies LocalFolder[],
uloadTags: [
{
id: 'tag-social',
name: 'Social Media',
slug: 'social-media',
color: '#8b5cf6',
icon: null,
isPublic: false,
usageCount: 2,
},
{
id: 'tag-docs',
name: 'Dokumentation',
slug: 'dokumentation',
color: '#f59e0b',
icon: null,
isPublic: false,
usageCount: 1,
},
{
id: 'tag-marketing',
name: 'Marketing',
slug: 'marketing',
color: '#ef4444',
icon: null,
isPublic: false,
usageCount: 1,
},
] satisfies LocalTag[],
links: [
{
id: 'link-welcome',
@ -109,10 +79,5 @@ export const ULOAD_GUEST_SEED = {
order: 1,
},
] satisfies LocalLink[],
linkTags: [
{ id: 'lt-1', linkId: 'link-github', tagId: 'tag-social' },
{ id: 'lt-2', linkId: 'link-docs', tagId: 'tag-docs' },
{ id: 'lt-3', linkId: 'link-welcome', tagId: 'tag-social' },
{ id: 'lt-4', linkId: 'link-expired', tagId: 'tag-marketing' },
] satisfies LocalLinkTag[],
linkTags: [] as LocalLinkTag[],
};

View file

@ -23,15 +23,6 @@ export interface LocalLink extends BaseRecord {
order: number;
}
export interface LocalTag extends BaseRecord {
name: string;
slug: string;
color?: string | null;
icon?: string | null;
isPublic: boolean;
usageCount: number;
}
export interface LocalFolder extends BaseRecord {
name: string;
color?: string | null;

View file

@ -4,7 +4,7 @@
import { getContext } from 'svelte';
import { memosStore } from '$lib/modules/memoro/stores/memos.svelte';
import { memoriesStore } from '$lib/modules/memoro/stores/memories.svelte';
import { tagsStore } from '$lib/modules/memoro/stores/tags.svelte';
import { memoTagOps } from '$lib/modules/memoro/stores/tags.svelte';
import {
useMemoriesByMemo,
getTagsForMemo,
@ -75,12 +75,12 @@
}
async function handleAddTag(tagId: string) {
await tagsStore.addToMemo(memoId, tagId);
await memoTagOps.addTag(memoId, tagId);
showTagPicker = false;
}
async function handleRemoveTag(tagId: string) {
await tagsStore.removeFromMemo(memoId, tagId);
await memoTagOps.removeTag(memoId, tagId);
}
// Available tags (not already assigned)

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte';
import { tagsStore } from '$lib/modules/memoro/stores/tags.svelte';
import type { Tag } from '$lib/modules/memoro/types';
import { tagMutations } from '$lib/modules/memoro/stores/tags.svelte';
import type { Tag } from '@manacore/shared-tags';
import {
ArrowLeft,
Plus,
@ -48,22 +48,18 @@
async function handleSubmit() {
if (!formName.trim()) return;
if (editingId) {
await tagsStore.update(editingId, { name: formName.trim(), color: formColor });
await tagMutations.updateTag(editingId, { name: formName.trim(), color: formColor });
} else {
await tagsStore.create({ name: formName.trim(), color: formColor });
await tagMutations.createTag({ name: formName.trim(), color: formColor });
}
showCreateForm = false;
}
async function handleDelete(id: string) {
if (confirm('Tag wirklich loschen?')) {
await tagsStore.delete(id);
await tagMutations.deleteTag(id);
}
}
async function handleTogglePin(tag: Tag) {
await tagsStore.update(tag.id, { isPinned: !tag.isPinned });
}
</script>
<svelte:head>
@ -123,17 +119,7 @@
style="background-color: {tag.color || '#888'}"
></span>
<span class="flex-1 font-medium text-[hsl(var(--foreground))]">{tag.name}</span>
{#if tag.isPinned}
<PushPin size={14} weight="fill" class="text-[hsl(var(--primary))]" />
{/if}
<div class="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<button
onclick={() => handleTogglePin(tag)}
class="rounded p-1 text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--primary))]"
title={tag.isPinned ? 'Loslosen' : 'Anpinnen'}
>
<PushPin size={16} weight={tag.isPinned ? 'fill' : 'regular'} />
</button>
<button
onclick={() => openEditForm(tag)}
class="rounded p-1 text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))]"

View file

@ -3,7 +3,8 @@
import { imagesStore } from '$lib/modules/picture/stores/images.svelte';
import { pictureViewStore } from '$lib/modules/picture/stores/view.svelte';
import { getFavoriteImages, getImagesByTags } from '$lib/modules/picture/queries';
import type { Image, LocalPictureTag, LocalImageTag } from '$lib/modules/picture/types';
import type { Image, LocalImageTag } from '$lib/modules/picture/types';
import type { Tag } from '@manacore/shared-tags';
import {
Heart,
SquaresFour,
@ -16,7 +17,7 @@
} from '@manacore/shared-icons';
const allImages: { value: Image[] } = getContext('allImages');
const allPictureTags: { value: LocalPictureTag[] } = getContext('pictureTags');
const allPictureTags: { value: Tag[] } = getContext('pictureTags');
const allImageTags: { value: LocalImageTag[] } = getContext('allImageTags');
let searchQuery = $state('');