mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 01:59:39 +02:00
feat(contacts,calendar): integrate shared TagStrip and createTagStore
Contacts: - Replace local TagStrip with shared TagStrip from @manacore/shared-ui - Replace local tags store with createTagStore wrapper (backward-compatible) - Change Tags nav item from link to toggle pill (shows/hides TagStrip overlay) Calendar: - Replace local TagStrip in UnifiedBar with shared TagStrip component - Replace local event-tags store with createTagStore wrapper (backward-compatible) - Both apps now use central mana-core-auth Tags API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ce900d5fd3
commit
69aa837898
4 changed files with 160 additions and 181 deletions
|
|
@ -1,10 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { QuickInputBar } from '@manacore/shared-ui';
|
||||
import { QuickInputBar, TagStrip } from '@manacore/shared-ui';
|
||||
import type { QuickInputItem, CreatePreview } from '@manacore/shared-ui';
|
||||
import { unifiedBarStore } from '$lib/stores/unified-bar.svelte';
|
||||
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import DateStrip from './DateStrip.svelte';
|
||||
import TagStrip from './TagStrip.svelte';
|
||||
import CalendarToolbarContent from './CalendarToolbarContent.svelte';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import type { CalendarViewType } from '@calendar/shared';
|
||||
|
|
@ -126,7 +127,18 @@
|
|||
<!-- Layer 2: Tag Filter Strip -->
|
||||
{#if showCalendarLayers && unifiedBarStore.showTagStrip}
|
||||
<div class="unified-bar-layer tag-layer" transition:fly={flyConfig}>
|
||||
<TagStrip />
|
||||
<TagStrip
|
||||
tags={eventTagsStore.tags.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
}))}
|
||||
selectedIds={settingsStore.selectedTagIds}
|
||||
onToggle={(tagId) => settingsStore.toggleTagSelection(tagId)}
|
||||
onClear={() => settingsStore.clearTagSelection()}
|
||||
managementHref="/tags"
|
||||
loading={eventTagsStore.loading}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,111 +1,93 @@
|
|||
/**
|
||||
* Event Tags Store - Manages event tags using Svelte 5 runes
|
||||
* Event Tags Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
*
|
||||
* Wraps createTagStore to provide a backward-compatible eventTagsStore interface
|
||||
* that existing Calendar components (TagStripModal, EventForm, /tags page) rely on.
|
||||
*/
|
||||
|
||||
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';
|
||||
import type { EventTag } from '@calendar/shared';
|
||||
import * as api from '$lib/api/event-tags';
|
||||
|
||||
// State
|
||||
let tags = $state<EventTag[]>([]);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
// Re-export EventTag for backward compatibility
|
||||
export type { EventTag };
|
||||
|
||||
// Helper to safely get tags array (Svelte 5 runes safety)
|
||||
function getTagsArray(): EventTag[] {
|
||||
const arr = tags ?? [];
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
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 wrapper that returns { data, error } results
|
||||
* to match the old Calendar API client pattern used by TagStripModal.
|
||||
*/
|
||||
async function wrapResult<T>(
|
||||
fn: () => Promise<T>
|
||||
): Promise<{ data: T | null; error: { message: string } | null }> {
|
||||
try {
|
||||
const data = await fn();
|
||||
return { data, error: null };
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
return { data: null, error: { message } };
|
||||
}
|
||||
}
|
||||
|
||||
// Backward-compatible eventTagsStore wrapper
|
||||
export const eventTagsStore = {
|
||||
// Getters
|
||||
get tags() {
|
||||
return tags;
|
||||
return tagStore.tags as unknown as EventTag[];
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
return tagStore.loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
return tagStore.error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all tags
|
||||
*/
|
||||
async fetchTags() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
const result = await api.getEventTags();
|
||||
|
||||
if (result.error) {
|
||||
error = result.error.message;
|
||||
tags = [];
|
||||
} else {
|
||||
tags = result.data || [];
|
||||
}
|
||||
|
||||
loading = false;
|
||||
return result;
|
||||
return tagStore.fetchTags();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new tag
|
||||
*/
|
||||
async createTag(data: api.CreateEventTagInput) {
|
||||
const result = await api.createEventTag(data);
|
||||
|
||||
if (result.data) {
|
||||
tags = [...tags, result.data];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a tag
|
||||
*/
|
||||
async updateTag(id: string, data: api.UpdateEventTagInput) {
|
||||
const result = await api.updateEventTag(id, data);
|
||||
|
||||
if (result.data) {
|
||||
tags = getTagsArray().map((t) => (t.id === id ? result.data! : t));
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a tag
|
||||
*/
|
||||
async deleteTag(id: string) {
|
||||
const result = await api.deleteEventTag(id);
|
||||
|
||||
if (!result.error) {
|
||||
tags = getTagsArray().filter((t) => t.id !== id);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get tag by ID
|
||||
*/
|
||||
getById(id: string) {
|
||||
return getTagsArray().find((t) => t.id === id);
|
||||
return tagStore.getById(id) as unknown as EventTag | undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get tags by IDs
|
||||
*/
|
||||
getByIds(ids: string[]) {
|
||||
return getTagsArray().filter((t) => ids.includes(t.id));
|
||||
return tagStore.getByIds(ids) as unknown as EventTag[];
|
||||
},
|
||||
|
||||
async createTag(data: { name: string; color?: string; groupId?: string | null }) {
|
||||
return wrapResult(() => tagStore.createTag(data)) as Promise<{
|
||||
data: EventTag | null;
|
||||
error: { message: string } | null;
|
||||
}>;
|
||||
},
|
||||
|
||||
async updateTag(id: string, data: { name?: string; color?: string; groupId?: string | null }) {
|
||||
return wrapResult(() => tagStore.updateTag(id, data)) as Promise<{
|
||||
data: EventTag | null;
|
||||
error: { message: string } | null;
|
||||
}>;
|
||||
},
|
||||
|
||||
async deleteTag(id: string) {
|
||||
return wrapResult(() => tagStore.deleteTag(id));
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear store
|
||||
*/
|
||||
clear() {
|
||||
tags = [];
|
||||
error = null;
|
||||
tagStore.clear();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,117 +1,65 @@
|
|||
/**
|
||||
* Tags Store - Manages tag state using Svelte 5 runes
|
||||
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
*
|
||||
* Centralized store for tags, used by TagStrip, TagStripModal, and tags page.
|
||||
* Uses the central Tags API from mana-core-auth.
|
||||
* Wraps createTagStore for backward compatibility with existing ContactTag interface.
|
||||
*/
|
||||
|
||||
import { tagsApi, type ContactTag } from '$lib/api/contacts';
|
||||
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 tags = $state<ContactTag[]>([]);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
// Re-export Tag as ContactTag for backward compatibility
|
||||
export type ContactTag = 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 tagsStore wrapper
|
||||
export const tagsStore = {
|
||||
// Getters
|
||||
get tags() {
|
||||
return tags;
|
||||
return tagStore.tags;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
return tagStore.loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
return tagStore.error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all tags from API
|
||||
*/
|
||||
async fetchTags() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await tagsApi.list();
|
||||
tags = response.tags || [];
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch tags';
|
||||
console.error('Failed to fetch tags:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
return tagStore.fetchTags();
|
||||
},
|
||||
getById(id: string) {
|
||||
return tagStore.getById(id);
|
||||
},
|
||||
getColor(tagId: string) {
|
||||
return tagStore.getColor(tagId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get tag by ID
|
||||
*/
|
||||
getById(id: string): ContactTag | undefined {
|
||||
return tags.find((t) => t.id === id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get tag color by ID
|
||||
*/
|
||||
getColor(tagId: string): string {
|
||||
const tag = tags.find((t) => t.id === tagId);
|
||||
return tag?.color || '#6b7280';
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new tag
|
||||
*/
|
||||
async createTag(data: { name: string; color?: string }) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await tagsApi.create(data);
|
||||
tags = [...tags, response.tag];
|
||||
return response.tag;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create tag';
|
||||
console.error('Failed to create tag:', e);
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
return tagStore.createTag(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing tag
|
||||
*/
|
||||
async updateTag(id: string, data: { name?: string; color?: string }) {
|
||||
error = null;
|
||||
try {
|
||||
const response = await tagsApi.update(id, data);
|
||||
tags = tags.map((t) => (t.id === id ? response.tag : t));
|
||||
return response.tag;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update tag';
|
||||
console.error('Failed to update tag:', e);
|
||||
throw e;
|
||||
}
|
||||
return tagStore.updateTag(id, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a tag
|
||||
*/
|
||||
async deleteTag(id: string) {
|
||||
error = null;
|
||||
try {
|
||||
await tagsApi.delete(id);
|
||||
tags = tags.filter((t) => t.id !== id);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete tag';
|
||||
console.error('Failed to delete tag:', e);
|
||||
throw e;
|
||||
}
|
||||
return tagStore.deleteTag(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all state (for logout)
|
||||
*/
|
||||
clear() {
|
||||
tags = [];
|
||||
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,
|
||||
|
|
@ -41,7 +46,6 @@
|
|||
formatParsedContactPreview,
|
||||
} from '$lib/utils/contact-parser';
|
||||
import ContactsToolbar from '$lib/components/ContactsToolbar.svelte';
|
||||
import TagStrip from '$lib/components/TagStrip.svelte';
|
||||
import { tagsStore } from '$lib/stores/tags.svelte';
|
||||
import { contactsOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
|
|
@ -128,10 +132,23 @@
|
|||
// User email for user dropdown (fallback to 'Menü' when not logged in)
|
||||
let userEmail = $derived(authStore.user?.email || 'Menü');
|
||||
|
||||
// TagStrip visibility (toggle via Tags button in PillNav)
|
||||
let isTagStripVisible = $state(true);
|
||||
|
||||
function handleTagStripToggle() {
|
||||
isTagStripVisible = !isTagStripVisible;
|
||||
}
|
||||
|
||||
// Base navigation items for Contacts
|
||||
const baseNavItems: PillNavItem[] = [
|
||||
{ href: '/', label: 'Kontakte', icon: 'users' },
|
||||
{ href: '/tags', label: 'Tags', icon: 'tag' },
|
||||
{
|
||||
href: '/',
|
||||
label: 'Tags',
|
||||
icon: 'tag',
|
||||
onClick: handleTagStripToggle,
|
||||
active: isTagStripVisible,
|
||||
},
|
||||
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
|
||||
{ href: '/help', label: 'Hilfe', icon: 'help-circle' },
|
||||
{ href: '/spiral', label: 'Spiral', icon: 'sparkles' },
|
||||
|
|
@ -332,8 +349,28 @@
|
|||
ariaLabel="Hauptnavigation"
|
||||
/>
|
||||
|
||||
<!-- TagStrip (above PillNav) -->
|
||||
<TagStrip />
|
||||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagsStore.tags.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
}))}
|
||||
selectedIds={contactsFilterStore.selectedTagId
|
||||
? [contactsFilterStore.selectedTagId]
|
||||
: []}
|
||||
onToggle={(tagId) => {
|
||||
if (contactsFilterStore.selectedTagId === tagId) {
|
||||
contactsFilterStore.setSelectedTagId(null);
|
||||
} else {
|
||||
contactsFilterStore.setSelectedTagId(tagId);
|
||||
}
|
||||
}}
|
||||
onClear={() => contactsFilterStore.setSelectedTagId(null)}
|
||||
managementHref="/tags"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Global Quick Input Bar -->
|
||||
<QuickInputBar
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue