mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(manacore/web): add entity descriptor system with cross-module drag-and-drop
Introduce EntityDescriptor pattern: each module declares how its items can be displayed, dragged, dropped, and created from other modules. - Entity types + registry with executeDrop orchestration - Entity descriptors for todo, calendar, contacts - List items are now draggable (dragSource) across pages - Pages accept cross-module drops (event→todo, contact→calendar, etc.) - Drops create new items + bidirectional manaLinks via shared-links - LinkedItems component shows cross-module links in DetailViews - Visual feedback: page glows purple on valid hover, green on success Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d73591865c
commit
7ee57b7afd
14 changed files with 450 additions and 4 deletions
|
|
@ -0,0 +1,116 @@
|
|||
<!--
|
||||
LinkedItems — Shows cross-module links for a record.
|
||||
Displays linked items as clickable pills with app color and cached title.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import {
|
||||
useLinksForRecord,
|
||||
type ManaRecordRef,
|
||||
type LocalManaLink,
|
||||
} from '@manacore/shared-links';
|
||||
import { getAppEntry } from '$lib/components/workbench/app-registry';
|
||||
import { ArrowSquareOut } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
recordRef: ManaRecordRef;
|
||||
}
|
||||
|
||||
let { recordRef }: Props = $props();
|
||||
|
||||
const linksQuery = useLinksForRecord(recordRef);
|
||||
let links = $derived(linksQuery.value ?? []);
|
||||
</script>
|
||||
|
||||
{#if links.length > 0}
|
||||
<div class="section">
|
||||
<span class="section-label">Verknüpfungen</span>
|
||||
<div class="links-list">
|
||||
{#each links as link (link.id)}
|
||||
{@const appEntry = getAppEntry(link.targetApp)}
|
||||
{@const color = link.cachedTarget?.color ?? appEntry?.color ?? '#6B7280'}
|
||||
<div class="link-item">
|
||||
<span class="link-dot" style="background: {color}"></span>
|
||||
<div class="link-content">
|
||||
<span class="link-title">{link.cachedTarget?.title ?? link.targetId}</span>
|
||||
{#if link.cachedTarget?.subtitle}
|
||||
<span class="link-subtitle">{link.cachedTarget.subtitle}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="link-app">{link.cachedTarget?.appName ?? link.targetApp}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
.section-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.links-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.link-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.link-item:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
:global(.dark) .link-item {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
:global(.dark) .link-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.link-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.link-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.0625rem;
|
||||
}
|
||||
.link-title {
|
||||
font-size: 0.8125rem;
|
||||
color: #374151;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:global(.dark) .link-title {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.link-subtitle {
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.link-app {
|
||||
font-size: 0.625rem;
|
||||
color: #b0afa8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -8,6 +8,9 @@
|
|||
import { PageShell } from '$lib/components/page-carousel';
|
||||
import { getAppEntry } from './app-registry';
|
||||
import type { Component } from 'svelte';
|
||||
import { dropTarget } from '@manacore/shared-ui/dnd';
|
||||
import { getEntity, getEntityByDragType, canDrop, executeDrop } from '$lib/entities';
|
||||
import type { DragPayload } from '@manacore/shared-ui/dnd';
|
||||
|
||||
interface Props {
|
||||
appId: string;
|
||||
|
|
@ -35,6 +38,16 @@
|
|||
let appName = $derived(appEntry?.name ?? appId);
|
||||
let appColor = $derived(appEntry?.color ?? '#6B7280');
|
||||
|
||||
// ── Cross-module drop target ────────────────────────────
|
||||
let targetEntity = $derived(getEntity(appId));
|
||||
let acceptedDropTypes = $derived(targetEntity?.acceptsDropFrom ?? []);
|
||||
|
||||
function handleCrossModuleDrop(payload: DragPayload) {
|
||||
const sourceEntity = getEntityByDragType(payload.type);
|
||||
if (!sourceEntity) return;
|
||||
executeDrop(payload.data as Record<string, unknown>, sourceEntity.appId, appId);
|
||||
}
|
||||
|
||||
// ── List View (always loaded) ───────────────────────────
|
||||
let ListComponent = $state<Component | null>(null);
|
||||
let loadError = $state(false);
|
||||
|
|
@ -163,7 +176,14 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="app-page-wrapper">
|
||||
<div
|
||||
class="app-page-wrapper"
|
||||
use:dropTarget={{
|
||||
accepts: acceptedDropTypes,
|
||||
onDrop: handleCrossModuleDrop,
|
||||
canDrop: (p) => canDrop(p.type, appId),
|
||||
}}
|
||||
>
|
||||
<!-- Base: PageShell with list view (always visible) -->
|
||||
<PageShell
|
||||
{widthPx}
|
||||
|
|
@ -239,6 +259,15 @@
|
|||
.app-page-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
:global(.app-page-wrapper.mana-drop-target-hover) :global(.page-shell) {
|
||||
outline: 2px solid rgba(139, 92, 246, 0.5);
|
||||
outline-offset: -2px;
|
||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.15);
|
||||
}
|
||||
:global(.app-page-wrapper.mana-drop-target-success) :global(.page-shell) {
|
||||
outline: 2px solid rgba(34, 197, 94, 0.5);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.load-state {
|
||||
display: flex;
|
||||
|
|
|
|||
17
apps/manacore/apps/web/src/lib/entities/index.ts
Normal file
17
apps/manacore/apps/web/src/lib/entities/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Types
|
||||
export type { EntityDescriptor, EntityDisplayData, DropResult } from './types';
|
||||
|
||||
// Registry
|
||||
export {
|
||||
registerEntity,
|
||||
getEntity,
|
||||
getEntityByDragType,
|
||||
canDrop,
|
||||
executeDrop,
|
||||
getAllEntities,
|
||||
} from './registry';
|
||||
|
||||
// Register module entities (side-effect imports)
|
||||
import '$lib/modules/todo/entity';
|
||||
import '$lib/modules/calendar/entity';
|
||||
import '$lib/modules/contacts/entity';
|
||||
79
apps/manacore/apps/web/src/lib/entities/registry.ts
Normal file
79
apps/manacore/apps/web/src/lib/entities/registry.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Entity Registry — Collects module descriptors and orchestrates cross-module drops.
|
||||
*/
|
||||
|
||||
import type { DragType } from '@manacore/shared-ui/dnd';
|
||||
import { linkMutations, buildCachedData } from '@manacore/shared-links';
|
||||
import type { EntityDescriptor, DropResult } from './types';
|
||||
|
||||
const entities = new Map<string, EntityDescriptor>();
|
||||
|
||||
export function registerEntity(descriptor: EntityDescriptor): void {
|
||||
entities.set(descriptor.appId, descriptor);
|
||||
}
|
||||
|
||||
export function getEntity(appId: string): EntityDescriptor | undefined {
|
||||
return entities.get(appId);
|
||||
}
|
||||
|
||||
export function getEntityByDragType(type: DragType): EntityDescriptor | undefined {
|
||||
for (const e of entities.values()) {
|
||||
if (e.dragType === type) return e;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function canDrop(sourceType: DragType, targetAppId: string): boolean {
|
||||
const target = entities.get(targetAppId);
|
||||
if (!target?.acceptsDropFrom?.includes(sourceType)) return false;
|
||||
if (!target.createItem) return false;
|
||||
if (!target.transformIncoming?.[sourceType]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function executeDrop(
|
||||
sourceItem: Record<string, unknown>,
|
||||
sourceAppId: string,
|
||||
targetAppId: string
|
||||
): Promise<DropResult> {
|
||||
const source = entities.get(sourceAppId);
|
||||
const target = entities.get(targetAppId);
|
||||
if (!source || !target)
|
||||
throw new Error(`Entity not registered: ${sourceAppId} or ${targetAppId}`);
|
||||
if (!target.createItem) throw new Error(`Target ${targetAppId} has no createItem`);
|
||||
|
||||
const transform = target.transformIncoming?.[source.dragType];
|
||||
if (!transform) throw new Error(`No transform for ${source.dragType} → ${targetAppId}`);
|
||||
|
||||
// 1. Transform source data into target shape
|
||||
const transformedData = transform(sourceItem);
|
||||
|
||||
// 2. Create new item in target module
|
||||
const newItemId = await target.createItem(transformedData);
|
||||
|
||||
// 3. Build cached display data for link
|
||||
const sourceDisplay = source.getDisplayData(sourceItem);
|
||||
const targetDisplay = target.getDisplayData({ ...transformedData, id: newItemId });
|
||||
|
||||
const cachedSource = buildCachedData(sourceAppId, sourceDisplay.title, sourceDisplay.subtitle);
|
||||
const cachedTarget = buildCachedData(targetAppId, targetDisplay.title, targetDisplay.subtitle);
|
||||
|
||||
// 4. Create bidirectional link
|
||||
const { forward } = await linkMutations.createLink({
|
||||
sourceApp: sourceAppId,
|
||||
sourceCollection: source.collection,
|
||||
sourceId: sourceItem.id as string,
|
||||
targetApp: targetAppId,
|
||||
targetCollection: target.collection,
|
||||
targetId: newItemId,
|
||||
linkType: 'related',
|
||||
cachedSource,
|
||||
cachedTarget,
|
||||
});
|
||||
|
||||
return { newItemId, linkPairId: forward.pairId };
|
||||
}
|
||||
|
||||
export function getAllEntities(): EntityDescriptor[] {
|
||||
return Array.from(entities.values());
|
||||
}
|
||||
40
apps/manacore/apps/web/src/lib/entities/types.ts
Normal file
40
apps/manacore/apps/web/src/lib/entities/types.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Entity Descriptor System — Types
|
||||
*
|
||||
* Each module declares an EntityDescriptor that describes how its items
|
||||
* can be displayed, dragged, dropped, and created from other modules.
|
||||
*/
|
||||
|
||||
import type { DragType } from '@manacore/shared-ui/dnd';
|
||||
|
||||
export interface EntityDisplayData {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export interface EntityDescriptor {
|
||||
appId: string;
|
||||
collection: string;
|
||||
|
||||
/** How to display an item in link previews and drag ghosts. */
|
||||
getDisplayData: (item: Record<string, unknown>) => EntityDisplayData;
|
||||
|
||||
/** DragType used when dragging items from this module. */
|
||||
dragType: DragType;
|
||||
|
||||
/** Which DragTypes this module accepts as drop targets. */
|
||||
acceptsDropFrom?: DragType[];
|
||||
|
||||
/** Transform incoming items from other modules into this module's create shape. */
|
||||
transformIncoming?: Partial<
|
||||
Record<DragType, (sourceItem: Record<string, unknown>) => Record<string, unknown>>
|
||||
>;
|
||||
|
||||
/** Create a new item in this module. Returns the new item's ID. */
|
||||
createItem?: (data: Record<string, unknown>) => Promise<string>;
|
||||
}
|
||||
|
||||
export interface DropResult {
|
||||
newItemId: string;
|
||||
linkPairId: string;
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
import { eventsStore } from './stores/events.svelte';
|
||||
import { Plus } from '@manacore/shared-icons';
|
||||
import type { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||
import { dropTarget } from '@manacore/shared-ui/dnd';
|
||||
import { dropTarget, dragSource } from '@manacore/shared-ui/dnd';
|
||||
import type { TagDragData } from '@manacore/shared-ui/dnd';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
|
||||
|
|
@ -145,6 +145,17 @@
|
|||
_siblingIds: todayEvents.map((e) => e.id),
|
||||
_siblingKey: 'eventId',
|
||||
})}
|
||||
use:dragSource={{
|
||||
type: 'event',
|
||||
data: () => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
}),
|
||||
}}
|
||||
use:dropTarget={{
|
||||
accepts: ['tag'],
|
||||
onDrop: (p) => handleTagDrop(event.id, p.data as unknown as TagDragData),
|
||||
|
|
|
|||
67
apps/manacore/apps/web/src/lib/modules/calendar/entity.ts
Normal file
67
apps/manacore/apps/web/src/lib/modules/calendar/entity.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { registerEntity } from '$lib/entities/registry';
|
||||
import { eventsStore } from './stores/events.svelte';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { EntityDescriptor } from '$lib/entities/types';
|
||||
|
||||
const calendarEntity: EntityDescriptor = {
|
||||
appId: 'calendar',
|
||||
collection: 'events',
|
||||
|
||||
getDisplayData: (item) => ({
|
||||
title: (item.title as string) || 'Termin',
|
||||
subtitle: item.startDate
|
||||
? new Date(item.startDate as string).toLocaleDateString('de', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: undefined,
|
||||
}),
|
||||
|
||||
dragType: 'event',
|
||||
acceptsDropFrom: ['task', 'contact'],
|
||||
|
||||
transformIncoming: {
|
||||
task: (source) => {
|
||||
const dueDate = (source.dueDate as string) || new Date().toISOString();
|
||||
const start = new Date(dueDate);
|
||||
const end = new Date(start.getTime() + 60 * 60 * 1000);
|
||||
return {
|
||||
title: source.title as string,
|
||||
startTime: start.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
description: source.description as string | undefined,
|
||||
};
|
||||
},
|
||||
contact: (source) => {
|
||||
const name = [source.firstName, source.lastName].filter(Boolean).join(' ');
|
||||
const now = new Date();
|
||||
const end = new Date(now.getTime() + 60 * 60 * 1000);
|
||||
return {
|
||||
title: `Treffen mit ${name}`,
|
||||
startTime: now.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
createItem: async (data) => {
|
||||
const calendars = await db.table('calendars').toArray();
|
||||
const defaultCal = calendars.find((c: Record<string, unknown>) => !c.deletedAt);
|
||||
const calendarId = (defaultCal?.id as string) ?? 'default';
|
||||
|
||||
const result = await eventsStore.createEvent({
|
||||
calendarId,
|
||||
title: data.title as string,
|
||||
startTime: data.startTime as string,
|
||||
endTime: data.endTime as string,
|
||||
description: (data.description as string) ?? undefined,
|
||||
});
|
||||
|
||||
if (!result.success || !result.data) throw new Error(result.error || 'Failed to create event');
|
||||
return result.data.id;
|
||||
},
|
||||
};
|
||||
|
||||
registerEntity(calendarEntity);
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
import type { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||
import type { LocalEvent } from '../types';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
import LinkedItems from '$lib/components/links/LinkedItems.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
let eventId = $derived(params.eventId as string);
|
||||
|
|
@ -178,6 +179,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Links -->
|
||||
<LinkedItems recordRef={{ app: 'calendar', collection: 'events', id: eventId }} />
|
||||
|
||||
<!-- Description -->
|
||||
<div class="section">
|
||||
<span class="section-label">Beschreibung</span>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import { contactsStore } from './stores/contacts.svelte';
|
||||
import { Plus, Star } from '@manacore/shared-icons';
|
||||
import type { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||
import { dropTarget } from '@manacore/shared-ui/dnd';
|
||||
import { dropTarget, dragSource } from '@manacore/shared-ui/dnd';
|
||||
import type { TagDragData } from '@manacore/shared-ui/dnd';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
|
||||
|
|
@ -110,6 +110,16 @@
|
|||
_siblingIds: filtered().map((c) => c.id),
|
||||
_siblingKey: 'contactId',
|
||||
})}
|
||||
use:dragSource={{
|
||||
type: 'contact',
|
||||
data: () => ({
|
||||
id: contact.id,
|
||||
firstName: contact.firstName,
|
||||
lastName: contact.lastName,
|
||||
company: contact.company,
|
||||
email: contact.email,
|
||||
}),
|
||||
}}
|
||||
use:dropTarget={{
|
||||
accepts: ['tag'],
|
||||
onDrop: (p) => handleTagDrop(contact.id, p.data as unknown as TagDragData),
|
||||
|
|
|
|||
20
apps/manacore/apps/web/src/lib/modules/contacts/entity.ts
Normal file
20
apps/manacore/apps/web/src/lib/modules/contacts/entity.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { registerEntity } from '$lib/entities/registry';
|
||||
import type { EntityDescriptor } from '$lib/entities/types';
|
||||
|
||||
const contactsEntity: EntityDescriptor = {
|
||||
appId: 'contacts',
|
||||
collection: 'contacts',
|
||||
|
||||
getDisplayData: (item) => {
|
||||
const name = [item.firstName, item.lastName].filter(Boolean).join(' ');
|
||||
return {
|
||||
title: name || (item.email as string) || 'Kontakt',
|
||||
subtitle: (item.company as string) ?? undefined,
|
||||
};
|
||||
},
|
||||
|
||||
dragType: 'contact',
|
||||
// Contacts are drag sources only — dropping onto contacts doesn't create a new contact
|
||||
};
|
||||
|
||||
registerEntity(contactsEntity);
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
import type { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||
import type { LocalContact } from '../types';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
import LinkedItems from '$lib/components/links/LinkedItems.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
let contactId = $derived(params.contactId as string);
|
||||
|
|
@ -289,6 +290,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Links -->
|
||||
<LinkedItems recordRef={{ app: 'contacts', collection: 'contacts', id: contactId }} />
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="section">
|
||||
<span class="section-label">Notizen</span>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
import { tasksStore } from './stores/tasks.svelte';
|
||||
import { Circle, Check } from '@manacore/shared-icons';
|
||||
import type { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||
import { dropTarget } from '@manacore/shared-ui/dnd';
|
||||
import { dropTarget, dragSource } from '@manacore/shared-ui/dnd';
|
||||
import type { TagDragData } from '@manacore/shared-ui/dnd';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
|
||||
|
|
@ -121,6 +121,15 @@
|
|||
_siblingKey: 'taskId',
|
||||
})}
|
||||
class="task-item"
|
||||
use:dragSource={{
|
||||
type: 'task',
|
||||
data: () => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
dueDate: task.dueDate,
|
||||
description: task.description,
|
||||
}),
|
||||
}}
|
||||
use:dropTarget={{
|
||||
accepts: ['tag'],
|
||||
onDrop: (p) => handleTagDrop(task.id, p.data as unknown as TagDragData),
|
||||
|
|
|
|||
36
apps/manacore/apps/web/src/lib/modules/todo/entity.ts
Normal file
36
apps/manacore/apps/web/src/lib/modules/todo/entity.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { registerEntity } from '$lib/entities/registry';
|
||||
import { tasksStore } from './stores/tasks.svelte';
|
||||
import type { EntityDescriptor } from '$lib/entities/types';
|
||||
|
||||
const todoEntity: EntityDescriptor = {
|
||||
appId: 'todo',
|
||||
collection: 'tasks',
|
||||
|
||||
getDisplayData: (item) => ({
|
||||
title: (item.title as string) || 'Aufgabe',
|
||||
subtitle: item.dueDate ? new Date(item.dueDate as string).toLocaleDateString('de') : undefined,
|
||||
}),
|
||||
|
||||
dragType: 'task',
|
||||
acceptsDropFrom: ['event', 'contact'],
|
||||
|
||||
transformIncoming: {
|
||||
event: (source) => ({
|
||||
title: source.title as string,
|
||||
dueDate: source.startDate as string,
|
||||
description: source.description as string | undefined,
|
||||
}),
|
||||
contact: (source) => ({
|
||||
title: `Kontaktieren: ${[source.firstName, source.lastName].filter(Boolean).join(' ')}`,
|
||||
}),
|
||||
},
|
||||
|
||||
createItem: async (data) => {
|
||||
const task = await tasksStore.createTask(
|
||||
data as { title: string; dueDate?: string; description?: string }
|
||||
);
|
||||
return task.id;
|
||||
},
|
||||
};
|
||||
|
||||
registerEntity(todoEntity);
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
import type { ViewProps } from '$lib/components/workbench/nav-stack';
|
||||
import type { LocalTask, TaskPriority } from '../types';
|
||||
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
|
||||
import LinkedItems from '$lib/components/links/LinkedItems.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
let taskId = $derived(params.taskId as string);
|
||||
|
|
@ -190,6 +191,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Links -->
|
||||
<LinkedItems recordRef={{ app: 'todo', collection: 'tasks', id: taskId }} />
|
||||
|
||||
<!-- Description -->
|
||||
<div class="section">
|
||||
<span class="section-label">Beschreibung</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue