mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 03:41:23 +02:00
feat(todo): integrate shared TagStrip and createTagStore
- Add createTagStore factory to @manacore/shared-stores (Svelte 5 runes, backed by @manacore/shared-tags) - Replace Todo's local TagStrip with shared TagStrip from @manacore/shared-ui - Replace Todo's labels store with createTagStore wrapper (backward-compatible) - Remove "Tags" tab from PillNav TabGroup, add it as toggle pill (like Filter) - Tags pill toggles TagStrip overlay visibility instead of navigating to /tags - TagStrip has "Tags verwalten" pill linking to /tags management page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1316ef57f3
commit
ce900d5fd3
5 changed files with 333 additions and 103 deletions
|
|
@ -1,119 +1,62 @@
|
|||
/**
|
||||
* Labels Store - Manages label state using Svelte 5 runes
|
||||
*
|
||||
* Uses the central Tags API from mana-core-auth. Labels and Tags are now
|
||||
* unified across all Manacore apps (Todo, Calendar, Contacts).
|
||||
* Labels Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
*/
|
||||
|
||||
import type { Label } from '$lib/api/labels';
|
||||
import * as labelsApi from '$lib/api/labels';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore, type TagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
// State
|
||||
let labels = $state<Label[]>([]);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
// Re-export Tag as Label for backward compatibility
|
||||
export type Label = Tag;
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Create the shared tag store
|
||||
const tagStore: TagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
|
||||
// Backward-compatible labelsStore wrapper
|
||||
export const labelsStore = {
|
||||
// Getters
|
||||
get labels() {
|
||||
return labels;
|
||||
return tagStore.tags;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
return tagStore.loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
return tagStore.error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all labels from API
|
||||
*/
|
||||
async fetchLabels() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
labels = await labelsApi.getLabels();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch labels';
|
||||
console.error('Failed to fetch labels:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
return tagStore.fetchTags();
|
||||
},
|
||||
getById(id: string) {
|
||||
return tagStore.getById(id);
|
||||
},
|
||||
getColor(labelId: string) {
|
||||
return tagStore.getColor(labelId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get label by ID
|
||||
*/
|
||||
getById(id: string): Label | undefined {
|
||||
return labels.find((l) => l.id === id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get label color by ID
|
||||
*/
|
||||
getColor(labelId: string): string {
|
||||
const label = labels.find((l) => l.id === labelId);
|
||||
return label?.color || '#6b7280';
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new label
|
||||
*/
|
||||
async createLabel(data: { name: string; color?: string }) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const newLabel = await labelsApi.createLabel(data);
|
||||
labels = [...labels, newLabel];
|
||||
TodoEvents.labelCreated();
|
||||
return newLabel;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create label';
|
||||
console.error('Failed to create label:', e);
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
return tagStore.createTag(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing label
|
||||
*/
|
||||
async updateLabel(id: string, data: { name?: string; color?: string }) {
|
||||
error = null;
|
||||
try {
|
||||
const updatedLabel = await labelsApi.updateLabel(id, data);
|
||||
labels = labels.map((l) => (l.id === id ? updatedLabel : l));
|
||||
return updatedLabel;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update label';
|
||||
console.error('Failed to update label:', e);
|
||||
throw e;
|
||||
}
|
||||
return tagStore.updateTag(id, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a label
|
||||
*/
|
||||
async deleteLabel(id: string) {
|
||||
error = null;
|
||||
try {
|
||||
await labelsApi.deleteLabel(id);
|
||||
labels = labels.filter((l) => l.id !== id);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete label';
|
||||
console.error('Failed to delete label:', e);
|
||||
throw e;
|
||||
}
|
||||
return tagStore.deleteTag(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all state (for logout)
|
||||
*/
|
||||
clear() {
|
||||
labels = [];
|
||||
loading = false;
|
||||
error = null;
|
||||
tagStore.clear();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar, ImmersiveModeToggle } from '@manacore/shared-ui';
|
||||
import {
|
||||
PillNavigation,
|
||||
QuickInputBar,
|
||||
ImmersiveModeToggle,
|
||||
TagStrip,
|
||||
} from '@manacore/shared-ui';
|
||||
import {
|
||||
SplitPaneContainer,
|
||||
setSplitPanelContext,
|
||||
|
|
@ -23,7 +28,6 @@
|
|||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import TaskFilters from '$lib/components/TaskFilters.svelte';
|
||||
import TagStrip from '$lib/components/TagStrip.svelte';
|
||||
import { viewStore, type SortBy } from '$lib/stores/view.svelte';
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import {
|
||||
|
|
@ -171,11 +175,17 @@
|
|||
todoSettings.toggleFilterStrip();
|
||||
}
|
||||
|
||||
// TagStrip visibility (toggle via Tags button in PillNav)
|
||||
let isTagStripVisible = $state(true);
|
||||
|
||||
function handleTagStripToggle() {
|
||||
isTagStripVisible = !isTagStripVisible;
|
||||
}
|
||||
|
||||
// View routes for the tab group (pages that navigate)
|
||||
const viewRoutes: Record<string, string> = {
|
||||
liste: '/',
|
||||
kanban: '/kanban',
|
||||
tags: '/tags',
|
||||
};
|
||||
|
||||
// Determine active view tab from current path
|
||||
|
|
@ -183,13 +193,12 @@
|
|||
Object.entries(viewRoutes).find(([_, path]) => $page.url.pathname === path)?.[0] || 'liste'
|
||||
);
|
||||
|
||||
// Tab group for view switching (Liste, Kanban, Tags) - grouped in one pill
|
||||
// Tab group for view switching (Liste, Kanban) - grouped in one pill
|
||||
let viewTabGroup = $derived<PillNavElement>({
|
||||
type: 'tabs' as const,
|
||||
options: [
|
||||
{ id: 'liste', icon: 'list', label: 'Liste', title: 'Listenansicht' },
|
||||
{ id: 'kanban', icon: 'columns', label: 'Kanban', title: 'Kanban-Board' },
|
||||
{ id: 'tags', icon: 'tag', label: 'Tags', title: 'Tags verwalten' },
|
||||
],
|
||||
value: activeViewTab,
|
||||
onChange: (id: string) => {
|
||||
|
|
@ -198,7 +207,7 @@
|
|||
},
|
||||
});
|
||||
|
||||
// Filter stays as a standalone pill (toggle behavior, not navigation)
|
||||
// Filter and Tags stay as standalone pills (toggle behavior, not navigation)
|
||||
let baseNavItems = $derived<PillNavItem[]>([
|
||||
{
|
||||
href: '/',
|
||||
|
|
@ -207,6 +216,13 @@
|
|||
onClick: handleFilterToggle,
|
||||
active: isFilterStripVisible,
|
||||
},
|
||||
{
|
||||
href: '/',
|
||||
label: 'Tags',
|
||||
icon: 'tag',
|
||||
onClick: handleTagStripToggle,
|
||||
active: isTagStripVisible,
|
||||
},
|
||||
]);
|
||||
|
||||
// Navigation items filtered by visibility settings (with fallback for guest mode)
|
||||
|
|
@ -353,8 +369,28 @@
|
|||
ariaLabel="Hauptnavigation"
|
||||
/>
|
||||
|
||||
<!-- TagStrip (above PillNav, always visible when PillNav is open) -->
|
||||
<TagStrip filterStripVisible={isFilterStripVisible} />
|
||||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={labelsStore.labels.map((l) => ({
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
color: l.color || '#6b7280',
|
||||
}))}
|
||||
selectedIds={viewStore.filterLabelIds}
|
||||
onToggle={(tagId) => {
|
||||
const current = viewStore.filterLabelIds;
|
||||
if (current.includes(tagId)) {
|
||||
viewStore.setFilterLabelIds(current.filter((id) => id !== tagId));
|
||||
} else {
|
||||
viewStore.setFilterLabelIds([...current, tagId]);
|
||||
}
|
||||
}}
|
||||
onClear={() => viewStore.setFilterLabelIds([])}
|
||||
managementHref="/tags"
|
||||
aboveFilterStrip={isFilterStripVisible}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- TaskFilters strip (shown when Filter pill is active in PillNav) -->
|
||||
{#if isFilterStripVisible}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue