Compare commits
2 Commits
23e3ddd667
...
0f4d374658
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f4d374658 | |||
| 609163f933 |
@@ -223,7 +223,7 @@ export const ImportTool: Component = () => {
|
||||
<div class="space-y-1">
|
||||
<label class="text-[0.5625rem] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Tags</label>
|
||||
<div class="flex items-center gap-1 flex-wrap">
|
||||
<TagPicker selectedTags={bulkTags()} onTagsChange={setBulkTags} />
|
||||
<TagPicker selectedTags={bulkTags()} onTagsChange={(tags) => setBulkTags(tags)} />
|
||||
<For each={bulkTags()}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { toast } from "solid-sonner";
|
||||
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants";
|
||||
import { buildNoteTag } from "@/lib/tags";
|
||||
|
||||
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
|
||||
@@ -27,6 +28,7 @@ const [input, setInput] = createSignal("");
|
||||
const [priority, setPriority] = createSignal<string>("");
|
||||
const [urgency, setUrgency] = createSignal<string>("");
|
||||
const [tags, setTags] = createSignal<string[]>([]);
|
||||
const [tagShareModes, setTagShareModes] = createSignal<Record<string, "handoff" | "collaborative">>({});
|
||||
const [description, setDescription] = createSignal("");
|
||||
const [size, setSize] = createSignal<string>("3");
|
||||
const [dateString, setDateString] = createSignal<string>("");
|
||||
@@ -39,6 +41,7 @@ const resetQuickEntryState = () => {
|
||||
setPriority("");
|
||||
setUrgency("");
|
||||
setTags([]);
|
||||
setTagShareModes({});
|
||||
setDescription("");
|
||||
setSize("3");
|
||||
setDateString("");
|
||||
@@ -153,6 +156,7 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
let finalPriority = parseInt(priority());
|
||||
let finalUrgency = parseInt(urgency());
|
||||
const finalTags = [...tags()];
|
||||
const finalTagShareModes = { ...tagShareModes() };
|
||||
|
||||
const pMatch = text.match(/\/p\s*(\d+)/);
|
||||
if (pMatch) {
|
||||
@@ -217,8 +221,8 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
const activeNoteId = (window as any)._activeNoteId;
|
||||
if (activeNoteId) {
|
||||
const note = store.notes.find(n => n.id === activeNoteId);
|
||||
if (note && note.title) {
|
||||
const noteTagName = note.title.trim();
|
||||
if (note) {
|
||||
const noteTagName = buildNoteTag(note.key || note.title);
|
||||
if (noteTagName && !finalTags.includes(noteTagName)) finalTags.push(noteTagName);
|
||||
}
|
||||
}
|
||||
@@ -243,7 +247,8 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
dueDate: finalDueDate || undefined,
|
||||
tags: finalTags,
|
||||
content: description(),
|
||||
size: parseInt(size())
|
||||
size: parseInt(size()),
|
||||
shareModeByTag: finalTagShareModes
|
||||
});
|
||||
|
||||
resetQuickEntryState();
|
||||
@@ -413,7 +418,16 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
|
||||
<div class="flex flex-col md:flex-row items-stretch md:items-center px-4 py-3 gap-4 bg-muted/10 border-b border-border">
|
||||
<div class="flex-1 flex flex-wrap gap-2 items-center">
|
||||
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
|
||||
<TagPicker
|
||||
selectedTags={tags()}
|
||||
getShareModeForTag={(tag) => tagShareModes()[tag.toLowerCase()]}
|
||||
onTagsChange={(nextTags, shareModeOverrides) => {
|
||||
setTags(nextTags);
|
||||
if (shareModeOverrides) {
|
||||
setTagShareModes(prev => ({ ...prev, ...shareModeOverrides }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<For each={tags().filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t))}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
@@ -435,7 +449,14 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
return getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()) || "inherit";
|
||||
})()
|
||||
}}
|
||||
onClick={() => setTags(tags().filter(t => t !== tag))}
|
||||
onClick={() => {
|
||||
setTags(tags().filter(t => t !== tag));
|
||||
setTagShareModes(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[tag.toLowerCase()];
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Component, createSignal, For, createEffect } from "solid-js";
|
||||
import { store, upsertTagDefinition, isCurrentUserContextTag, setContextPolicy, setNoteContextPolicy, getNoteSharePolicy } from "@/store";
|
||||
import { store, upsertTagDefinition, isCurrentUserContextTag, getNoteSharePolicy } from "@/store";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -10,7 +10,8 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
interface TagPickerProps {
|
||||
selectedTags: string[];
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
onTagsChange: (tags: string[], shareModeOverrides?: Record<string, "handoff" | "collaborative">) => void;
|
||||
getShareModeForTag?: (tag: string) => "handoff" | "collaborative" | undefined;
|
||||
}
|
||||
|
||||
export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
@@ -75,6 +76,11 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const explicitMode = props.getShareModeForTag?.(selectedTagName().trim());
|
||||
if (explicitMode) {
|
||||
setShareMode(explicitMode);
|
||||
return;
|
||||
}
|
||||
const context = selectedShareContext();
|
||||
if (context) {
|
||||
setShareMode(context.policy);
|
||||
@@ -94,9 +100,6 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
const name = selectedTagName().trim();
|
||||
if (!name) return;
|
||||
|
||||
const context = selectedShareContext();
|
||||
const noteContext = selectedNoteContext();
|
||||
|
||||
if (name.startsWith("@")) {
|
||||
const normalized = normalizeContextKey(name);
|
||||
const exists = store.contexts.some(context =>
|
||||
@@ -109,11 +112,6 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
toast.error("Create the shared context first before tagging tasks with it.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (context && context.policy !== shareMode()) {
|
||||
const updated = await setContextPolicy(context.id, shareMode());
|
||||
if (!updated) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (name.startsWith("#")) {
|
||||
@@ -122,11 +120,6 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
toast.error("Create the canonical note first before linking it with a #tag.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (noteContext && getNoteSharePolicy(noteContext) !== shareMode()) {
|
||||
const updated = await setNoteContextPolicy(noteContext.id, shareMode());
|
||||
if (!updated) return;
|
||||
}
|
||||
}
|
||||
|
||||
// update global definition
|
||||
@@ -138,9 +131,13 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
|
||||
// Toggle in current task selection if not already there
|
||||
const currentSelected = props.selectedTags;
|
||||
if (!currentSelected.includes(name)) {
|
||||
props.onTagsChange([...currentSelected, name]);
|
||||
}
|
||||
const nextTags = currentSelected.includes(name) ? currentSelected : [...currentSelected, name];
|
||||
props.onTagsChange(
|
||||
nextTags,
|
||||
name.startsWith("@") || name.startsWith("#")
|
||||
? { [name.toLowerCase()]: shareMode() }
|
||||
: undefined
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
setSelectedTagName("");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Component, createMemo, For, Show } from "solid-js";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag, isCurrentUserContextTag, dismissTaskUpdateIndicator, isTaskUpdatedByAnotherUser, collapseTaskUntilUpdated, expandCollapsedTask, isTaskCollapsedUntilUpdated, isTaskCollaborativelyShared } from "@/store";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag, dismissTaskUpdateIndicator, isTaskUpdatedByAnotherUser, collapseTaskUntilUpdated, expandCollapsedTask, isTaskCollapsedUntilUpdated, isTaskCollaborativelyShared, getVisibleTaskTags } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Clock, ArrowUpCircle, Copy, Users2, Star, ChevronRight } from "lucide-solid";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -8,7 +8,6 @@ import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
import { getStatusOptionsForTask } from "@/lib/constants";
|
||||
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||
import { buildShareTag } from "@/lib/tags";
|
||||
import { StatusCircle } from "./StatusCircle";
|
||||
|
||||
export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props) => {
|
||||
@@ -28,18 +27,8 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
// Filter out bucket tags if we are currently INSIDE that bucket view
|
||||
const visibleTags = createMemo(() => {
|
||||
const tags = props.task.tags || [];
|
||||
const ctx = currentTaskContext();
|
||||
|
||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
||||
const bucketTagName = buildShareTag(bucketContext?.displayName || ctx.name).toLowerCase();
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||
}
|
||||
|
||||
return tags.filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||
return getVisibleTaskTags(props.task, currentTaskContext());
|
||||
});
|
||||
|
||||
const isDueWithin12Hours = createMemo(() => {
|
||||
@@ -187,13 +176,13 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
<span class="text-[0.625rem] font-medium">P{props.task.priority}</span>
|
||||
</div>
|
||||
|
||||
{/* Shared Indicator - driven by @tag share contexts */}
|
||||
{/* Shared Indicator - driven by task context tags */}
|
||||
{(() => {
|
||||
if (props.task.shareRefs.length === 0) return null;
|
||||
|
||||
const tooltip = props.task.shareRefs.length === 1
|
||||
? 'Shared via 1 @context'
|
||||
: `Shared via ${props.task.shareRefs.length} @contexts`;
|
||||
? 'Shared via 1 context'
|
||||
: `Shared via ${props.task.shareRefs.length} contexts`;
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-1 shrink-0" title={tooltip}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Component, createEffect, createSignal, For, createMemo, onCleanup, Show } from "solid-js";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now, isCurrentUserContextTag } from "@/store";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now, isCurrentUserContextTag, getVisibleTaskTags, isTaskLockedNoteTag, deriveTaskRelationsFromTags, getTaskShareModeForTag, getTaskSharePolicy } from "@/store";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Star, FileText } from "lucide-solid";
|
||||
@@ -17,7 +17,7 @@ import { lazy, Suspense } from "solid-js";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||
import { buildShareTag } from "@/lib/tags";
|
||||
import { buildNoteTag, buildShareTag } from "@/lib/tags";
|
||||
import { TextBubbleMenu } from "@/components/TextBubbleMenu";
|
||||
import { TableBubbleMenu } from "@/components/TableBubbleMenu";
|
||||
|
||||
@@ -168,7 +168,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateTags = (tags: string[]) => {
|
||||
const updateTags = (tags: string[], shareModeOverrides?: Record<string, "handoff" | "collaborative">) => {
|
||||
const ctx = currentTaskContext();
|
||||
const currentTags = props.task.tags || [];
|
||||
let newTags = [...tags];
|
||||
@@ -191,7 +191,16 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
updateTask(props.task.id, { tags: newTags });
|
||||
props.task.tags
|
||||
.filter(tag => isTaskLockedNoteTag(props.task, tag))
|
||||
.forEach(tag => {
|
||||
if (!newTags.some(existing => existing.toLowerCase() === tag.toLowerCase())) {
|
||||
newTags.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
const { labelTags, shareRefs, noteRefs } = deriveTaskRelationsFromTags(newTags, props.task.shareRefs, shareModeOverrides);
|
||||
updateTask(props.task.id, { tags: newTags, labelTags, shareRefs, noteRefs });
|
||||
};
|
||||
|
||||
const onDateInputChange = (e: Event) => {
|
||||
@@ -207,16 +216,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
|
||||
// Filter out bucket tags if we are currently INSIDE that bucket view
|
||||
const visibleTags = createMemo(() => {
|
||||
const tags = props.task.tags || [];
|
||||
const ctx = currentTaskContext();
|
||||
|
||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
||||
const bucketTagName = buildShareTag(bucketContext?.displayName || ctx.name).toLowerCase();
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||
}
|
||||
|
||||
return tags.filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||
return getVisibleTaskTags(props.task, currentTaskContext());
|
||||
});
|
||||
|
||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||
@@ -511,6 +511,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
<div class="flex flex-nowrap overflow-x-auto no-scrollbar items-center gap-1.5 min-w-0 flex-1 pb-1">
|
||||
<TagPicker
|
||||
selectedTags={visibleTags()}
|
||||
getShareModeForTag={(tag) => getTaskShareModeForTag(props.task, tag)}
|
||||
onTagsChange={updateTags}
|
||||
/>
|
||||
<div class="flex flex-nowrap items-center gap-1.5 shrink-0 pr-2">
|
||||
@@ -694,7 +695,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
<div class="px-2 py-1.5 rounded-lg bg-muted/20 border border-border/30 space-y-2">
|
||||
<p class="text-[0.6875rem] text-muted-foreground leading-relaxed">
|
||||
Sharing is driven by the task's <span class="font-semibold text-foreground">@tags</span>. Add or remove
|
||||
<span class="font-semibold text-foreground"> @people</span> and <span class="font-semibold text-foreground">@buckets</span> in the tag list above.
|
||||
<span class="font-semibold text-foreground"> @people</span>, <span class="font-semibold text-foreground">@buckets</span>, and <span class="font-semibold text-foreground">#notes</span> in the tag list above.
|
||||
</p>
|
||||
<Show
|
||||
when={props.task.shareRefs.length > 0}
|
||||
@@ -702,13 +703,25 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={props.task.shareRefs.filter(ref => {
|
||||
if (ref.kind === "note") {
|
||||
const label = buildNoteTag(store.notes.find(note => note.id === ref.contextId || note.key === ref.key)?.key || ref.key);
|
||||
return !isTaskLockedNoteTag(props.task, label);
|
||||
}
|
||||
|
||||
const context = store.contexts.find(existing => existing.id === ref.contextId) || store.contexts.find(existing => existing.key === ref.key);
|
||||
return !isCurrentUserContextTag(`@${context?.displayName || ref.key}`);
|
||||
})}>
|
||||
{(ref) => {
|
||||
const context = store.contexts.find(existing => existing.id === ref.contextId) || store.contexts.find(existing => existing.key === ref.key);
|
||||
const label = `@${context?.displayName || ref.key}`;
|
||||
const policy = context?.policy || (ref.kind === "bucket" ? "handoff" : "collaborative");
|
||||
const note = ref.kind === "note"
|
||||
? store.notes.find(existing => existing.id === ref.contextId || existing.key === ref.key)
|
||||
: null;
|
||||
const context = ref.kind === "note"
|
||||
? null
|
||||
: store.contexts.find(existing => existing.id === ref.contextId) || store.contexts.find(existing => existing.key === ref.key);
|
||||
const label = ref.kind === "note"
|
||||
? buildNoteTag(note?.key || ref.key)
|
||||
: `@${context?.displayName || ref.key}`;
|
||||
const policy = getTaskSharePolicy(ref);
|
||||
return (
|
||||
<Badge variant="secondary" class="h-6 px-2 text-[0.625rem] gap-1 bg-muted/40 border-border/40">
|
||||
<span>{label}</span>
|
||||
|
||||
+186
-126
@@ -373,6 +373,7 @@ export interface TaskShareRef {
|
||||
contextId: string;
|
||||
key: string;
|
||||
kind: 'user' | 'bucket' | 'note';
|
||||
policy?: 'collaborative' | 'handoff';
|
||||
}
|
||||
|
||||
export interface NoteRef {
|
||||
@@ -545,11 +546,31 @@ const getNoteByRef = (ref: NoteRef) =>
|
||||
store.notes.find(note => note.id === ref.noteId) ||
|
||||
store.notes.find(note => note.key === ref.key);
|
||||
|
||||
const DEFAULT_SHARE_POLICY_BY_KIND: Record<TaskShareRef["kind"], "collaborative" | "handoff"> = {
|
||||
user: "collaborative",
|
||||
bucket: "handoff",
|
||||
note: "handoff"
|
||||
};
|
||||
|
||||
const NOTE_HANDOFF_POLICY_TAG = "__note_policy_handoff__";
|
||||
|
||||
export const getNoteSharePolicy = (note: Pick<Note, "tags"> | null | undefined): "collaborative" | "handoff" =>
|
||||
note?.tags?.includes(NOTE_HANDOFF_POLICY_TAG) ? "handoff" : "collaborative";
|
||||
|
||||
export const getTaskSharePolicy = (ref: Pick<TaskShareRef, "kind" | "key" | "policy" | "contextId">): "collaborative" | "handoff" => {
|
||||
if (ref.policy) return ref.policy;
|
||||
|
||||
if (ref.kind === "note") {
|
||||
const note = store.notes.find(existing => existing.id === ref.contextId || existing.key === ref.key);
|
||||
return getNoteSharePolicy(note);
|
||||
}
|
||||
|
||||
const context = getContextByRef(ref as TaskShareRef);
|
||||
if (context?.policy) return context.policy;
|
||||
|
||||
return DEFAULT_SHARE_POLICY_BY_KIND[ref.kind];
|
||||
};
|
||||
|
||||
const buildNoteContext = (note: Note): ShareContext => ({
|
||||
id: note.id,
|
||||
key: note.key,
|
||||
@@ -604,6 +625,82 @@ const dedupeShareRefs = (refs: TaskShareRef[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
const buildTaskShareRef = (
|
||||
parsedTag: Extract<ReturnType<typeof parseTags>[number], { kind: "share" | "note" }>,
|
||||
existingRefs: TaskShareRef[] = [],
|
||||
shareModeOverrides: Record<string, "collaborative" | "handoff"> = {}
|
||||
): TaskShareRef => {
|
||||
if (parsedTag.kind === "note") {
|
||||
const note = store.notes.find(existing => existing.key === parsedTag.key);
|
||||
const nextRefBase = {
|
||||
contextId: note?.id || parsedTag.key,
|
||||
key: parsedTag.key,
|
||||
kind: "note" as const
|
||||
};
|
||||
const existing = existingRefs.find(ref => ref.kind === "note" && (ref.contextId === nextRefBase.contextId || ref.key === nextRefBase.key));
|
||||
return {
|
||||
...nextRefBase,
|
||||
policy: shareModeOverrides[parsedTag.raw.toLowerCase()] || existing?.policy || getTaskSharePolicy(nextRefBase)
|
||||
};
|
||||
}
|
||||
|
||||
const context = findContextForShareTag(parsedTag);
|
||||
const nextRefBase = {
|
||||
contextId: context?.id || parsedTag.key,
|
||||
key: parsedTag.key,
|
||||
kind: (context?.kind || "bucket") as "user" | "bucket"
|
||||
};
|
||||
const existing = existingRefs.find(ref =>
|
||||
ref.kind === nextRefBase.kind &&
|
||||
(ref.contextId === nextRefBase.contextId || ref.key === nextRefBase.key)
|
||||
);
|
||||
return {
|
||||
...nextRefBase,
|
||||
policy: shareModeOverrides[parsedTag.raw.toLowerCase()] || existing?.policy || getTaskSharePolicy(nextRefBase)
|
||||
};
|
||||
};
|
||||
|
||||
export const getTaskShareModeForTag = (task: Pick<Task, "shareRefs">, tag: string): "collaborative" | "handoff" | undefined => {
|
||||
const parsed = parseTags([tag])[0];
|
||||
if (!parsed || (parsed.kind !== "share" && parsed.kind !== "note")) return undefined;
|
||||
|
||||
const ref = task.shareRefs.find(existing =>
|
||||
existing.key === parsed.key && (
|
||||
(parsed.kind === "note" && existing.kind === "note") ||
|
||||
(parsed.kind === "share" && existing.kind !== "note")
|
||||
)
|
||||
);
|
||||
|
||||
return ref ? getTaskSharePolicy(ref) : undefined;
|
||||
};
|
||||
|
||||
export const deriveTaskRelationsFromTags = (
|
||||
tags: string[],
|
||||
existingShareRefs: TaskShareRef[] = [],
|
||||
shareModeOverrides: Record<string, "collaborative" | "handoff"> = {}
|
||||
) => {
|
||||
const parsedTags = parseTags(tags);
|
||||
const labelTags = parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display);
|
||||
const shareRefs = parsedTags
|
||||
.filter((tag): tag is Extract<typeof tag, { kind: "share" | "note" }> => tag.kind === "share" || tag.kind === "note")
|
||||
.map(tag => buildTaskShareRef(tag, existingShareRefs, shareModeOverrides));
|
||||
const noteRefs = parsedTags
|
||||
.filter(tag => tag.kind === "note")
|
||||
.map(tag => {
|
||||
const note = store.notes.find(existing => existing.key === tag.key);
|
||||
return {
|
||||
noteId: note?.id || tag.key,
|
||||
key: tag.key
|
||||
} satisfies NoteRef;
|
||||
});
|
||||
|
||||
return {
|
||||
labelTags,
|
||||
shareRefs: dedupeShareRefs(shareRefs),
|
||||
noteRefs: dedupeNoteRefs(noteRefs)
|
||||
};
|
||||
};
|
||||
|
||||
const dedupeNoteRefs = (refs: NoteRef[]) => {
|
||||
const seen = new Set<string>();
|
||||
return refs.filter(ref => {
|
||||
@@ -790,6 +887,43 @@ export const isCurrentUserContextTag = (tag: string) => {
|
||||
return hiddenTags.has(normalizedTag);
|
||||
};
|
||||
|
||||
export const isTaskLockedNoteTag = (task: Pick<Task, "tags" | "shareRefs" | "noteRefs">, tag: string) => {
|
||||
const normalizedTag = tag.trim();
|
||||
if (!normalizedTag.startsWith("#")) return false;
|
||||
|
||||
const noteKey = normalizeNoteKey(normalizedTag);
|
||||
const matchingNoteRefs = task.noteRefs.filter(ref => ref.key === noteKey || ref.noteId === noteKey);
|
||||
if (matchingNoteRefs.length === 0) return false;
|
||||
|
||||
const nonNoteContexts = task.shareRefs.filter(ref => ref.kind !== "note");
|
||||
const uniqueNoteRefs = dedupeNoteRefs(task.noteRefs);
|
||||
return uniqueNoteRefs.length === matchingNoteRefs.length && nonNoteContexts.length === 0;
|
||||
};
|
||||
|
||||
export const getVisibleTaskTags = (task: Pick<Task, "tags" | "shareRefs" | "noteRefs">, context: TaskContext = currentTaskContext()) => {
|
||||
const tags = task.tags || [];
|
||||
|
||||
return tags.filter(tag => {
|
||||
if (tag.endsWith("_favorite__") || isCurrentUserContextTag(tag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isTaskLockedNoteTag(task, tag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof context === "object" && "bucketId" in context) {
|
||||
const bucketContext = store.contexts.find(existing => existing.id === context.bucketId);
|
||||
const bucketTagName = buildShareTag(bucketContext?.displayName || context.name).toLowerCase();
|
||||
if (tag.toLowerCase() === bucketTagName) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const getSubscribedBucketContexts = () =>
|
||||
store.subscribedBuckets
|
||||
.map(id => store.contexts.find(context => context.id === id))
|
||||
@@ -803,16 +937,15 @@ export const matchesFilter = (task: Task) => {
|
||||
const activeShareContexts = task.shareRefs
|
||||
.map(ref => getContextByRef(ref))
|
||||
.filter((context): context is ShareContext => !!context && !context.deletedAt);
|
||||
const collaborativeUserShare = activeShareContexts.some(context =>
|
||||
context.kind === "user" &&
|
||||
context.policy === "collaborative" &&
|
||||
context.targetUserId === currentUserId
|
||||
);
|
||||
const bucketRefs = activeShareContexts.filter(context => context.kind === "bucket");
|
||||
const hasHandoffBucket = rawBucketRefs.some(ref => {
|
||||
const collaborativeUserShare = task.shareRefs.some(ref => {
|
||||
if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false;
|
||||
const context = getContextByRef(ref);
|
||||
return !context || context.policy === "handoff";
|
||||
return context?.targetUserId === currentUserId;
|
||||
});
|
||||
const bucketRefs = activeShareContexts.filter(context => context.kind === "bucket");
|
||||
const hasHandoffContext = task.shareRefs.some(ref =>
|
||||
ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff"
|
||||
);
|
||||
const inPinnedBucket = rawBucketRefs.some(ref =>
|
||||
store.subscribedBuckets.includes(ref.contextId) ||
|
||||
store.subscribedBuckets.some(bucketId => store.contexts.find(context => context.id === bucketId)?.key === ref.key)
|
||||
@@ -835,7 +968,7 @@ export const matchesFilter = (task: Task) => {
|
||||
|
||||
// 0. Context Filter
|
||||
if (isPersonalView) {
|
||||
if (hasHandoffBucket) return false;
|
||||
if (hasHandoffContext) return false;
|
||||
const isOwner = task.ownerId === currentUserId;
|
||||
if (!isOwner && !collaborativeUserShare && !inPersonalContext) return false;
|
||||
} else if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
@@ -940,7 +1073,7 @@ export const isTaskCollaborativelyShared = (task: Task) =>
|
||||
task.shareRefs.some(ref => {
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
const context = getContextByRef(ref);
|
||||
const policy = context?.policy || (ref.kind === "user" ? "collaborative" : "handoff");
|
||||
const policy = getTaskSharePolicy(ref);
|
||||
const targetUserId = context?.targetUserId || null;
|
||||
return ref.kind === "user" && policy === "collaborative" && !!targetUserId && targetUserId !== currentUserId;
|
||||
});
|
||||
@@ -1091,25 +1224,15 @@ const mapRecordToTask = (r: any): Task => {
|
||||
const legacyTags: string[] = r.tags || [];
|
||||
const parsedTags = parseTags(legacyTags).filter(tag => tag.raw !== "__template__");
|
||||
const shareRefs: TaskShareRef[] = Array.isArray(r.shareRefs)
|
||||
? r.shareRefs
|
||||
? r.shareRefs.map((ref: any) => ({
|
||||
contextId: ref.contextId,
|
||||
key: ref.key,
|
||||
kind: ref.kind,
|
||||
policy: ref.policy
|
||||
}))
|
||||
: parsedTags
|
||||
.filter(tag => tag.kind === "share" || tag.kind === "note")
|
||||
.map(tag => {
|
||||
if (tag.kind === "note") {
|
||||
const note = store.notes.find(existing => existing.key === tag.key);
|
||||
return {
|
||||
contextId: note?.id || tag.key,
|
||||
key: tag.key,
|
||||
kind: "note"
|
||||
};
|
||||
}
|
||||
const context = findContextForShareTag(tag);
|
||||
return {
|
||||
contextId: context?.id || tag.key,
|
||||
key: tag.key,
|
||||
kind: context?.kind || "bucket"
|
||||
};
|
||||
});
|
||||
.filter((tag): tag is Extract<(typeof parsedTags)[number], { kind: "share" | "note" }> => tag.kind === "share" || tag.kind === "note")
|
||||
.map(tag => buildTaskShareRef(tag));
|
||||
const noteRefs: NoteRef[] = Array.isArray(r.noteRefs)
|
||||
? r.noteRefs
|
||||
: parsedTags
|
||||
@@ -1124,7 +1247,8 @@ const mapRecordToTask = (r: any): Task => {
|
||||
const noteShareRefs: TaskShareRef[] = noteRefs.map(ref => ({
|
||||
contextId: ref.noteId || ref.key,
|
||||
key: ref.key,
|
||||
kind: "note"
|
||||
kind: "note",
|
||||
policy: shareRefs.find(existing => existing.kind === "note" && (existing.contextId === ref.noteId || existing.key === ref.key))?.policy
|
||||
}));
|
||||
const mergedShareRefs = dedupeShareRefs([...shareRefs, ...noteShareRefs]);
|
||||
const labelTags: string[] = Array.isArray(r.labelTags)
|
||||
@@ -1182,10 +1306,7 @@ const buildTaskMigrationPayload = (
|
||||
const ownerContext = task.ownerId ? ownerContextByUserId.get(task.ownerId) : undefined;
|
||||
let nextShareRefs = dedupeShareRefs(task.shareRefs);
|
||||
let nextNoteRefs = dedupeNoteRefs([...task.noteRefs, ...linkedNoteRefs]);
|
||||
const hasHandoffBucket = nextShareRefs.some(ref => {
|
||||
const context = getContextByRef(ref);
|
||||
return context?.kind === "bucket" && context.policy === "handoff";
|
||||
});
|
||||
const hasHandoffContext = nextShareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff");
|
||||
|
||||
if (ownerContext) {
|
||||
const matchesOwnerContext = (ref: TaskShareRef) =>
|
||||
@@ -1195,14 +1316,15 @@ const buildTaskMigrationPayload = (
|
||||
normalizeContextKey(ownerContext.displayName) === ref.key
|
||||
);
|
||||
|
||||
if (hasHandoffBucket) {
|
||||
if (hasHandoffContext) {
|
||||
nextShareRefs = nextShareRefs.filter(ref => !matchesOwnerContext(ref));
|
||||
} else if (!nextShareRefs.some(matchesOwnerContext)) {
|
||||
nextShareRefs = [
|
||||
{
|
||||
contextId: ownerContext.id,
|
||||
key: ownerContext.key,
|
||||
kind: "user" as const
|
||||
kind: "user" as const,
|
||||
policy: getTaskSharePolicy({ contextId: ownerContext.id, key: ownerContext.key, kind: "user" })
|
||||
},
|
||||
...nextShareRefs
|
||||
];
|
||||
@@ -1495,15 +1617,11 @@ const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
||||
|
||||
if (mappedTask.shareRefs.some(ref => ref.kind === "bucket")) return true;
|
||||
|
||||
const contexts = mappedTask.shareRefs
|
||||
.map(ref => getContextByRef(ref))
|
||||
.filter((context): context is ShareContext => !!context && !context.deletedAt);
|
||||
|
||||
return contexts.some(context =>
|
||||
context.kind === "user" &&
|
||||
context.policy === "collaborative" &&
|
||||
context.targetUserId === currentUserId
|
||||
);
|
||||
return mappedTask.shareRefs.some(ref => {
|
||||
if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false;
|
||||
const context = getContextByRef(ref);
|
||||
return !!context && !context.deletedAt && context.targetUserId === currentUserId;
|
||||
});
|
||||
};
|
||||
|
||||
export const subscribeToRealtime = async () => {
|
||||
@@ -2092,7 +2210,6 @@ export const initStore = async () => {
|
||||
.filter(context =>
|
||||
!context.deletedAt &&
|
||||
context.kind === "user" &&
|
||||
context.policy === "collaborative" &&
|
||||
context.targetUserId === currentUserId
|
||||
)
|
||||
.map(context => buildShareTag(context.displayName));
|
||||
@@ -2464,7 +2581,7 @@ export const savePersonalContextSupervisors = async (supervisorUserIds: string[]
|
||||
}
|
||||
};
|
||||
|
||||
export const addTask = async (title: string, options?: { dueDate?: Date | string, priority?: number, tags?: string[], content?: string, size?: number }) => {
|
||||
export const addTask = async (title: string, options?: { dueDate?: Date | string, priority?: number, tags?: string[], content?: string, size?: number, shareModeByTag?: Record<string, "collaborative" | "handoff"> }) => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
// Default to ~24h from now if no date provided
|
||||
@@ -2478,35 +2595,7 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
|
||||
const tags = withActiveContextTags(options?.tags ?? ["work"]);
|
||||
const content = options?.content ?? "";
|
||||
const size = options?.size ?? 3;
|
||||
const parsedTags = parseTags(tags);
|
||||
const labelTags = parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display);
|
||||
const shareRefs = parsedTags
|
||||
.filter(tag => tag.kind === "share" || tag.kind === "note")
|
||||
.map(tag => {
|
||||
if (tag.kind === "note") {
|
||||
const note = store.notes.find(existing => existing.key === tag.key);
|
||||
return {
|
||||
contextId: note?.id || tag.key,
|
||||
key: tag.key,
|
||||
kind: "note"
|
||||
} satisfies TaskShareRef;
|
||||
}
|
||||
const context = findContextForShareTag(tag);
|
||||
return {
|
||||
contextId: context?.id || tag.key,
|
||||
key: tag.key,
|
||||
kind: context?.kind || "bucket"
|
||||
} satisfies TaskShareRef;
|
||||
});
|
||||
const noteRefs = parsedTags
|
||||
.filter(tag => tag.kind === "note")
|
||||
.map(tag => {
|
||||
const note = store.notes.find(existing => existing.key === tag.key);
|
||||
return {
|
||||
noteId: note?.id || tag.key,
|
||||
key: tag.key
|
||||
} satisfies NoteRef;
|
||||
});
|
||||
const { labelTags, shareRefs, noteRefs } = deriveTaskRelationsFromTags(tags, [], options?.shareModeByTag || {});
|
||||
|
||||
const tempId = "temp-" + Date.now();
|
||||
const initialTask: Task = {
|
||||
@@ -2596,13 +2685,24 @@ export const copyTask = async (id: string) => {
|
||||
const original = store.tasks.find(t => t.id === id);
|
||||
if (!original) return;
|
||||
|
||||
const shareModeByTag = Object.fromEntries(
|
||||
original.shareRefs.map(ref => {
|
||||
const context = getContextByRef(ref);
|
||||
const tag = ref.kind === "note"
|
||||
? buildNoteTag(context?.key || ref.key)
|
||||
: buildShareTag(context?.displayName || ref.key);
|
||||
return [tag.toLowerCase(), getTaskSharePolicy(ref)];
|
||||
})
|
||||
) as Record<string, "collaborative" | "handoff">;
|
||||
|
||||
// Create a new task based on the original
|
||||
await addTask(`${original.title} (Copy)`, {
|
||||
priority: original.priority,
|
||||
dueDate: original.dueDate,
|
||||
tags: [...(original.tags || [])],
|
||||
content: original.content,
|
||||
size: original.size
|
||||
size: original.size,
|
||||
shareModeByTag
|
||||
});
|
||||
|
||||
toast.success("Task duplicated");
|
||||
@@ -2624,7 +2724,7 @@ const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> =>
|
||||
|
||||
for (const ref of refs) {
|
||||
const context = getContextByRef(ref);
|
||||
if (!context || context.policy !== "handoff") continue;
|
||||
if (!context || getTaskSharePolicy(ref) !== "handoff") continue;
|
||||
const nextShareRefs = stripSenderPersonalContext(refs);
|
||||
const labelTags = updates.labelTags || task.labelTags;
|
||||
const noteRefs = updates.noteRefs || task.noteRefs;
|
||||
@@ -2639,7 +2739,7 @@ const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> =>
|
||||
};
|
||||
}
|
||||
|
||||
if (context.kind === "bucket") {
|
||||
if (context.kind === "bucket" || context.kind === "note") {
|
||||
return {
|
||||
...updates,
|
||||
shareRefs: nextShareRefs,
|
||||
@@ -2680,35 +2780,12 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
|
||||
let favoriteTagOverride: string | null | undefined;
|
||||
|
||||
if (finalUpdates.tags) {
|
||||
const parsedTags = parseTags(finalUpdates.tags);
|
||||
finalUpdates.labelTags = parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display);
|
||||
finalUpdates.shareRefs = parsedTags
|
||||
.filter(tag => tag.kind === "share" || tag.kind === "note")
|
||||
.map(tag => {
|
||||
if (tag.kind === "note") {
|
||||
const note = store.notes.find(existing => existing.key === tag.key);
|
||||
return {
|
||||
contextId: note?.id || tag.key,
|
||||
key: tag.key,
|
||||
kind: "note"
|
||||
} satisfies TaskShareRef;
|
||||
}
|
||||
const context = findContextForShareTag(tag);
|
||||
return {
|
||||
contextId: context?.id || tag.key,
|
||||
key: tag.key,
|
||||
kind: context?.kind || "bucket"
|
||||
} satisfies TaskShareRef;
|
||||
});
|
||||
finalUpdates.noteRefs = parsedTags
|
||||
.filter(tag => tag.kind === "note")
|
||||
.map(tag => {
|
||||
const note = store.notes.find(existing => existing.key === tag.key);
|
||||
return {
|
||||
noteId: note?.id || tag.key,
|
||||
key: tag.key
|
||||
} satisfies NoteRef;
|
||||
});
|
||||
if (!finalUpdates.shareRefs || !finalUpdates.noteRefs || !finalUpdates.labelTags) {
|
||||
const derived = deriveTaskRelationsFromTags(finalUpdates.tags, currentTask.shareRefs);
|
||||
finalUpdates.labelTags = finalUpdates.labelTags || derived.labelTags;
|
||||
finalUpdates.shareRefs = finalUpdates.shareRefs || derived.shareRefs;
|
||||
finalUpdates.noteRefs = finalUpdates.noteRefs || derived.noteRefs;
|
||||
}
|
||||
favoriteTagOverride = finalUpdates.tags.find(tag => tag.endsWith("_favorite__")) || null;
|
||||
}
|
||||
|
||||
@@ -3068,7 +3145,6 @@ export const loadAllHistory = async () => {
|
||||
.filter(context =>
|
||||
!context.deletedAt &&
|
||||
context.kind === "user" &&
|
||||
context.policy === "collaborative" &&
|
||||
context.targetUserId === userId
|
||||
)
|
||||
.map(context => buildShareTag(context.displayName));
|
||||
@@ -3149,33 +3225,17 @@ export const loadTasksLinkedToNote = async (note: Pick<Note, "id" | "key" | "tit
|
||||
|
||||
const noteTag = buildNoteTag(note.key || note.title);
|
||||
const escapedNoteTag = noteTag.replace(/"/g, '\\"');
|
||||
const linkedTaskIds = [...new Set((note.tasks || []).filter(Boolean))];
|
||||
const idFilter = linkedTaskIds.length > 0
|
||||
? ` || (${linkedTaskIds.map(taskId => `id = "${taskId}"`).join(" || ")})`
|
||||
: "";
|
||||
|
||||
try {
|
||||
const idFetchResults = await Promise.allSettled(
|
||||
linkedTaskIds.map(taskId =>
|
||||
pb.collection(TASGRID_COLLECTION).getOne(taskId, {
|
||||
fields: getTaskListFields(),
|
||||
requestKey: null
|
||||
})
|
||||
)
|
||||
);
|
||||
const idRecords = idFetchResults
|
||||
.filter((result): result is PromiseFulfilledResult<any> => result.status === "fulfilled")
|
||||
.map(result => result.value);
|
||||
|
||||
const listRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||
filter: `((tags ~ "${escapedNoteTag}")${idFilter}) && tags !~ "__template__"`,
|
||||
filter: `(tags ~ "${escapedNoteTag}") && tags !~ "__template__"`,
|
||||
sort: "-updated",
|
||||
fields: getTaskListFields(),
|
||||
requestKey: `note-linked-${note.id}`
|
||||
}).catch(() => []);
|
||||
|
||||
const recordMap = new Map<string, any>();
|
||||
[...idRecords, ...listRecords].forEach(record => {
|
||||
listRecords.forEach(record => {
|
||||
if (!record?.id) return;
|
||||
if (record.tags?.includes("__template__")) return;
|
||||
recordMap.set(record.id, record);
|
||||
|
||||
+33
-18
@@ -1,5 +1,5 @@
|
||||
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
||||
import { store, renameTagDefinition, updateNote, updateTask, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag, loadNoteContent, requestImmediateNotesMetadataLoad, loadTasksLinkedToNote } from "@/store";
|
||||
import { store, renameTagDefinition, updateNote, updateTask, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag, loadNoteContent, requestImmediateNotesMetadataLoad, loadTasksLinkedToNote, getCombinedScore } from "@/store";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { type Note } from "@/store";
|
||||
import { Plus, X, Lock, Unlock, Trash2, ChevronDown, Link as LinkIcon, MoreHorizontal, Type, FileText, Search, Star, ChevronRight } from "lucide-solid";
|
||||
@@ -81,7 +81,7 @@ export const NotepadView: Component<{
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const note = activeNote();
|
||||
const note = taskLinkOwnerNote();
|
||||
if (!note) return;
|
||||
const linkageSignature = `${note.id}|${note.key || note.title}|${(note.tasks || []).join(",")}|${note.updated}`;
|
||||
void linkageSignature;
|
||||
@@ -94,6 +94,22 @@ export const NotepadView: Component<{
|
||||
return store.notes.find(n => n.id === id) || null;
|
||||
});
|
||||
|
||||
const getImmediateParentNote = (note: Note | null): Note | null => {
|
||||
if (!note) return null;
|
||||
const parentTag = note.tags?.find(t => t.startsWith("child-of-"));
|
||||
if (!parentTag) return null;
|
||||
const parentId = parentTag.replace("child-of-", "");
|
||||
return store.notes.find(n => n.id === parentId) || null;
|
||||
};
|
||||
|
||||
const taskLinkOwnerNote = createMemo(() => {
|
||||
const note = activeNote();
|
||||
if (!note) return null;
|
||||
return getImmediateParentNote(note) || note;
|
||||
});
|
||||
|
||||
const isChildNote = createMemo(() => !!getImmediateParentNote(activeNote()));
|
||||
|
||||
const handleUpdateNote = async (id: string, data: Partial<Note>) => {
|
||||
await updateNote(id, data);
|
||||
};
|
||||
@@ -141,12 +157,10 @@ export const NotepadView: Component<{
|
||||
};
|
||||
|
||||
const handleLinkTask = async (taskId: string) => {
|
||||
const note = activeNote();
|
||||
const note = taskLinkOwnerNote();
|
||||
if (!note) return;
|
||||
const task = store.tasks.find(existing => existing.id === taskId);
|
||||
const noteTag = buildNoteTag(note.key || note.title);
|
||||
const newTasks = [...(note.tasks || []), taskId];
|
||||
await handleUpdateNote(note.id, { tasks: newTasks });
|
||||
if (task && !task.tags.some(tag => tag.toLowerCase() === noteTag.toLowerCase())) {
|
||||
await updateTask(taskId, { tags: [...task.tags, noteTag] });
|
||||
}
|
||||
@@ -155,12 +169,11 @@ export const NotepadView: Component<{
|
||||
};
|
||||
|
||||
const handleUnlinkTask = async (taskId: string) => {
|
||||
const note = activeNote();
|
||||
if (isChildNote()) return;
|
||||
const note = taskLinkOwnerNote();
|
||||
if (!note) return;
|
||||
const task = store.tasks.find(existing => existing.id === taskId);
|
||||
const noteTag = buildNoteTag(note.key || note.title).toLowerCase();
|
||||
const newTasks = (note.tasks || []).filter(id => id !== taskId);
|
||||
await handleUpdateNote(note.id, { tasks: newTasks });
|
||||
if (task && task.tags.some(tag => tag.toLowerCase() === noteTag)) {
|
||||
await updateTask(taskId, { tags: task.tags.filter(tag => tag.toLowerCase() !== noteTag) });
|
||||
}
|
||||
@@ -168,20 +181,22 @@ export const NotepadView: Component<{
|
||||
|
||||
// Linked tasks calculation
|
||||
const linkedTasks = createMemo(() => {
|
||||
const note = activeNote();
|
||||
const note = taskLinkOwnerNote();
|
||||
if (!note) return [];
|
||||
const noteTagName = buildNoteTag(note.key || note.title).toLowerCase();
|
||||
return store.tasks.filter(t => {
|
||||
// Explicit relation
|
||||
if (note.tasks && note.tasks.includes(t.id)) return true;
|
||||
if (t.deletedAt) return false;
|
||||
if (t.noteRefs && t.noteRefs.some(ref => ref.key === note.key || ref.noteId === note.id)) return true;
|
||||
if (t.tags && t.tags.some(tag => tag.toLowerCase() === noteTagName)) return true;
|
||||
return false;
|
||||
}).sort((a, b) => {
|
||||
if (a.completed !== b.completed) return a.completed ? 1 : -1;
|
||||
return getCombinedScore(b) - getCombinedScore(a);
|
||||
});
|
||||
});
|
||||
|
||||
const unlinkedTasks = createMemo(() => {
|
||||
const note = activeNote();
|
||||
const note = taskLinkOwnerNote();
|
||||
if (!note) return [];
|
||||
const q = linkSearchQuery().toLowerCase();
|
||||
|
||||
@@ -204,7 +219,6 @@ export const NotepadView: Component<{
|
||||
}
|
||||
|
||||
const isAlreadyLinked =
|
||||
(note.tasks && note.tasks.includes(t.id)) ||
|
||||
t.noteRefs?.some(ref => ref.key === note.key || ref.noteId === note.id) ||
|
||||
t.tags?.some(tag => tag.toLowerCase() === buildNoteTag(note.key || note.title).toLowerCase());
|
||||
if (isAlreadyLinked) return false;
|
||||
@@ -271,6 +285,10 @@ export const NotepadView: Component<{
|
||||
};
|
||||
|
||||
const openQuickEntry = () => {
|
||||
const ownerNote = taskLinkOwnerNote();
|
||||
if (ownerNote) {
|
||||
(window as any)._activeNoteId = ownerNote.id;
|
||||
}
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'k' }));
|
||||
};
|
||||
|
||||
@@ -390,9 +408,6 @@ export const NotepadView: Component<{
|
||||
return s.startsWith('<div data-type="file-viewer-wrapper"') ||
|
||||
(s.startsWith('<div data-type="file-attachment"') && s.includes('data-filename') && s.toLowerCase().includes('.pdf'));
|
||||
});
|
||||
const isChildNote = createMemo(() => {
|
||||
return (note()?.tags || []).some(t => t.startsWith("child-of-"));
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -744,14 +759,14 @@ export const NotepadView: Component<{
|
||||
<div class="text-center text-xs text-muted-foreground p-6 bg-muted/10 border border-dashed border-border/50 rounded-xl flex flex-col items-center gap-2">
|
||||
<LinkIcon size={24} class="opacity-20" />
|
||||
<p>No tasks linked yet.</p>
|
||||
<p class="text-[0.6rem] opacity-70 max-w-[200px]">Link an existing task, create a new one, or add the tag #{note().title} to a task.</p>
|
||||
<p class="text-[0.6rem] opacity-70 max-w-[200px]">Link an existing task, create a new one, or add the tag #{taskLinkOwnerNote()?.title || note().title} to a task.</p>
|
||||
</div>
|
||||
}>
|
||||
{(task) => (
|
||||
<div class="relative group">
|
||||
<TaskCard task={task} />
|
||||
{/* If explicitly linked, allow unlinking. If linked by tag, they should remove the tag from the task, which we can't easily do here unless we implement it. */}
|
||||
<Show when={note().tasks && note().tasks.includes(task.id)}>
|
||||
<Show when={!isChildNote() && taskLinkOwnerNote()?.tasks?.includes(task.id)}>
|
||||
<button
|
||||
class="absolute -top-2 -right-2 bg-destructive text-destructive-foreground rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity shadow-sm hover:scale-110"
|
||||
onClick={(e) => {
|
||||
|
||||
Reference in New Issue
Block a user