note-linked tasks are now efficient
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m7s

This commit is contained in:
2026-03-23 12:36:46 -05:00
parent 6abb1835f0
commit 23e3ddd667
2 changed files with 106 additions and 10 deletions
+22 -3
View File
@@ -1,5 +1,5 @@
import { type Component, createSignal, For, createEffect } from "solid-js"; import { type Component, createSignal, For, createEffect } from "solid-js";
import { store, upsertTagDefinition, isCurrentUserContextTag, setContextPolicy } from "@/store"; import { store, upsertTagDefinition, isCurrentUserContextTag, setContextPolicy, setNoteContextPolicy, getNoteSharePolicy } from "@/store";
import { useTheme } from "./ThemeProvider"; import { useTheme } from "./ThemeProvider";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -55,6 +55,14 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
) || null; ) || null;
}; };
const selectedNoteContext = () => {
const name = selectedTagName().trim();
if (!name.startsWith("#")) return null;
const key = normalizeNoteKey(name);
return store.notes.find(note => !note.deletedAt && note.key === key) || null;
};
// When a tag name is selected, update the value score to match its current definition // When a tag name is selected, update the value score to match its current definition
createEffect(() => { createEffect(() => {
const name = selectedTagName().trim(); const name = selectedTagName().trim();
@@ -72,7 +80,12 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
setShareMode(context.policy); setShareMode(context.policy);
return; return;
} }
if (selectedTagName().trim().startsWith("@")) { const note = selectedNoteContext();
if (note) {
setShareMode(getNoteSharePolicy(note));
return;
}
if (selectedTagName().trim().startsWith("@") || selectedTagName().trim().startsWith("#")) {
setShareMode("handoff"); setShareMode("handoff");
} }
}); });
@@ -82,6 +95,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
if (!name) return; if (!name) return;
const context = selectedShareContext(); const context = selectedShareContext();
const noteContext = selectedNoteContext();
if (name.startsWith("@")) { if (name.startsWith("@")) {
const normalized = normalizeContextKey(name); const normalized = normalizeContextKey(name);
@@ -108,6 +122,11 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
toast.error("Create the canonical note first before linking it with a #tag."); toast.error("Create the canonical note first before linking it with a #tag.");
return; return;
} }
if (noteContext && getNoteSharePolicy(noteContext) !== shareMode()) {
const updated = await setNoteContextPolicy(noteContext.id, shareMode());
if (!updated) return;
}
} }
// update global definition // update global definition
@@ -167,7 +186,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
</select> </select>
</div> </div>
{selectedTagName().trim().startsWith("@") && ( {(selectedTagName().trim().startsWith("@") || selectedTagName().trim().startsWith("#")) && (
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground">Share Mode</label> <label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground">Share Mode</label>
<div class="bg-background border border-border p-1 rounded-xl flex items-center shadow-sm relative min-w-[160px] h-10"> <div class="bg-background border border-border p-1 rounded-xl flex items-center shadow-sm relative min-w-[160px] h-10">
+84 -7
View File
@@ -359,7 +359,7 @@ export interface ShareContext {
id: string; id: string;
key: string; key: string;
displayName: string; displayName: string;
kind: 'user' | 'bucket'; kind: 'user' | 'bucket' | 'note';
policy: 'collaborative' | 'handoff'; policy: 'collaborative' | 'handoff';
targetUserId?: string | null; targetUserId?: string | null;
supervisorUserIds?: string[]; supervisorUserIds?: string[];
@@ -372,7 +372,7 @@ export interface ShareContext {
export interface TaskShareRef { export interface TaskShareRef {
contextId: string; contextId: string;
key: string; key: string;
kind: 'user' | 'bucket'; kind: 'user' | 'bucket' | 'note';
} }
export interface NoteRef { export interface NoteRef {
@@ -529,6 +529,12 @@ const findContextForShareTag = (tag: { key: string; raw: string }) =>
null; null;
const getContextByRef = (ref: TaskShareRef) => const getContextByRef = (ref: TaskShareRef) =>
(ref.kind === "note"
? (() => {
const note = store.notes.find(existing => existing.id === ref.contextId || existing.key === ref.key);
return note ? buildNoteContext(note) : null;
})()
: null) ||
store.contexts.find(context => !context.deletedAt && context.id === ref.contextId) || store.contexts.find(context => !context.deletedAt && context.id === ref.contextId) ||
store.contexts.find(context => !context.deletedAt && context.key === ref.key && context.kind === ref.kind) || store.contexts.find(context => !context.deletedAt && context.key === ref.key && context.kind === ref.kind) ||
store.contexts.find(context => !context.deletedAt && context.kind === ref.kind && normalizeContextKey(context.displayName) === ref.key) || store.contexts.find(context => !context.deletedAt && context.kind === ref.kind && normalizeContextKey(context.displayName) === ref.key) ||
@@ -539,6 +545,21 @@ const getNoteByRef = (ref: NoteRef) =>
store.notes.find(note => note.id === ref.noteId) || store.notes.find(note => note.id === ref.noteId) ||
store.notes.find(note => note.key === ref.key); store.notes.find(note => note.key === ref.key);
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";
const buildNoteContext = (note: Note): ShareContext => ({
id: note.id,
key: note.key,
displayName: note.key || note.title,
kind: "note",
policy: getNoteSharePolicy(note),
targetUserId: note.user,
deletedAt: note.deletedAt ?? null
});
const buildTaskTags = ( const buildTaskTags = (
labelTags: string[], labelTags: string[],
shareRefs: TaskShareRef[], shareRefs: TaskShareRef[],
@@ -549,6 +570,9 @@ const buildTaskTags = (
...labelTags, ...labelTags,
...shareRefs.map(ref => { ...shareRefs.map(ref => {
const context = getContextByRef(ref); const context = getContextByRef(ref);
if (ref.kind === "note") {
return buildNoteTag(context?.key || ref.key);
}
if (context?.displayName) { if (context?.displayName) {
return buildShareTag(context.displayName); return buildShareTag(context.displayName);
} }
@@ -1069,8 +1093,16 @@ const mapRecordToTask = (r: any): Task => {
const shareRefs: TaskShareRef[] = Array.isArray(r.shareRefs) const shareRefs: TaskShareRef[] = Array.isArray(r.shareRefs)
? r.shareRefs ? r.shareRefs
: parsedTags : parsedTags
.filter(tag => tag.kind === "share") .filter(tag => tag.kind === "share" || tag.kind === "note")
.map(tag => { .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); const context = findContextForShareTag(tag);
return { return {
contextId: context?.id || tag.key, contextId: context?.id || tag.key,
@@ -1089,11 +1121,17 @@ const mapRecordToTask = (r: any): Task => {
key: tag.key key: tag.key
}; };
}); });
const noteShareRefs: TaskShareRef[] = noteRefs.map(ref => ({
contextId: ref.noteId || ref.key,
key: ref.key,
kind: "note"
}));
const mergedShareRefs = dedupeShareRefs([...shareRefs, ...noteShareRefs]);
const labelTags: string[] = Array.isArray(r.labelTags) const labelTags: string[] = Array.isArray(r.labelTags)
? r.labelTags ? r.labelTags
: parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display); : parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display);
const favoriteTag = legacyTags.find((tag: string) => tag.endsWith("_favorite__")); const favoriteTag = legacyTags.find((tag: string) => tag.endsWith("_favorite__"));
const tags = buildTaskTags(labelTags, shareRefs, noteRefs, favoriteTag); const tags = buildTaskTags(labelTags, mergedShareRefs, noteRefs, favoriteTag);
return { return {
id: r.id, id: r.id,
title: r.title, title: r.title,
@@ -1104,7 +1142,7 @@ const mapRecordToTask = (r: any): Task => {
completed: r.status === 10 || r.completed, completed: r.status === 10 || r.completed,
tags: tags, tags: tags,
labelTags, labelTags,
shareRefs, shareRefs: mergedShareRefs,
noteRefs, noteRefs,
ownerId: unwrapRelationId(r.user), ownerId: unwrapRelationId(r.user),
createdBy: unwrapRelationId(r.createdBy) || unwrapRelationId(r.user), createdBy: unwrapRelationId(r.createdBy) || unwrapRelationId(r.user),
@@ -2368,6 +2406,29 @@ export const setContextPolicy = async (contextId: string, policy: "collaborative
} }
}; };
export const setNoteContextPolicy = async (noteId: string, policy: "collaborative" | "handoff") => {
if (!pb.authStore.isValid || !noteId) return false;
const note = store.notes.find(existing => existing.id === noteId);
if (!note) return false;
const baseTags = (note.tags || []).filter(tag => tag !== NOTE_HANDOFF_POLICY_TAG);
const nextTags = policy === "handoff" ? [...baseTags, NOTE_HANDOFF_POLICY_TAG] : baseTags;
const previousTags = note.tags || [];
setStore("notes", n => n.id === noteId, "tags", nextTags);
try {
await pb.collection(NOTES_COLLECTION).update(noteId, { tags: nextTags }, { requestKey: null });
return true;
} catch (err) {
console.error("Failed to update note context policy:", err);
setStore("notes", n => n.id === noteId, "tags", previousTags);
toast.error("Failed to update note sharing mode");
return false;
}
};
export const savePersonalContextSupervisors = async (supervisorUserIds: string[]) => { export const savePersonalContextSupervisors = async (supervisorUserIds: string[]) => {
const userId = pb.authStore.model?.id; const userId = pb.authStore.model?.id;
if (!userId) return; if (!userId) return;
@@ -2420,8 +2481,16 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
const parsedTags = parseTags(tags); const parsedTags = parseTags(tags);
const labelTags = parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display); const labelTags = parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display);
const shareRefs = parsedTags const shareRefs = parsedTags
.filter(tag => tag.kind === "share") .filter(tag => tag.kind === "share" || tag.kind === "note")
.map(tag => { .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); const context = findContextForShareTag(tag);
return { return {
contextId: context?.id || tag.key, contextId: context?.id || tag.key,
@@ -2614,8 +2683,16 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
const parsedTags = parseTags(finalUpdates.tags); const parsedTags = parseTags(finalUpdates.tags);
finalUpdates.labelTags = parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display); finalUpdates.labelTags = parsedTags.filter(tag => tag.kind === "label").map(tag => tag.display);
finalUpdates.shareRefs = parsedTags finalUpdates.shareRefs = parsedTags
.filter(tag => tag.kind === "share") .filter(tag => tag.kind === "share" || tag.kind === "note")
.map(tag => { .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); const context = findContextForShareTag(tag);
return { return {
contextId: context?.id || tag.key, contextId: context?.id || tag.key,