Compare commits
3 Commits
de8cea7d2e
...
23e3ddd667
| Author | SHA1 | Date | |
|---|---|---|---|
| 23e3ddd667 | |||
| 6abb1835f0 | |||
| 8e4eae92f8 |
@@ -1,11 +1,12 @@
|
|||||||
import { type Component, createSignal, For, createEffect } from "solid-js";
|
import { type Component, createSignal, For, createEffect } from "solid-js";
|
||||||
import { store, upsertTagDefinition, isCurrentUserContextTag } 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";
|
||||||
import { Tag, Plus } from "lucide-solid";
|
import { Tag, Plus } from "lucide-solid";
|
||||||
import { toast } from "solid-sonner";
|
import { toast } from "solid-sonner";
|
||||||
import { buildNoteTag, buildShareTag, normalizeContextKey, normalizeNoteKey } from "@/lib/tags";
|
import { buildNoteTag, buildShareTag, normalizeContextKey, normalizeNoteKey } from "@/lib/tags";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface TagPickerProps {
|
interface TagPickerProps {
|
||||||
selectedTags: string[];
|
selectedTags: string[];
|
||||||
@@ -17,6 +18,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
|||||||
const [open, setOpen] = createSignal(false);
|
const [open, setOpen] = createSignal(false);
|
||||||
const [selectedTagName, setSelectedTagName] = createSignal("");
|
const [selectedTagName, setSelectedTagName] = createSignal("");
|
||||||
const [valueScore, setValueScore] = createSignal(5);
|
const [valueScore, setValueScore] = createSignal(5);
|
||||||
|
const [shareMode, setShareMode] = createSignal<"handoff" | "collaborative">("handoff");
|
||||||
|
|
||||||
// Filter tags for the "Name" part
|
// Filter tags for the "Name" part
|
||||||
const localTagNames = () => {
|
const localTagNames = () => {
|
||||||
@@ -40,6 +42,27 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
|||||||
return allTagNames();
|
return allTagNames();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedShareContext = () => {
|
||||||
|
const name = selectedTagName().trim();
|
||||||
|
if (!name.startsWith("@")) return null;
|
||||||
|
|
||||||
|
const normalized = normalizeContextKey(name);
|
||||||
|
return store.contexts.find(context =>
|
||||||
|
!context.deletedAt && (
|
||||||
|
context.key === normalized ||
|
||||||
|
normalizeContextKey(context.displayName) === normalized
|
||||||
|
)
|
||||||
|
) || 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();
|
||||||
@@ -51,10 +74,29 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleApply = () => {
|
createEffect(() => {
|
||||||
|
const context = selectedShareContext();
|
||||||
|
if (context) {
|
||||||
|
setShareMode(context.policy);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const note = selectedNoteContext();
|
||||||
|
if (note) {
|
||||||
|
setShareMode(getNoteSharePolicy(note));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedTagName().trim().startsWith("@") || selectedTagName().trim().startsWith("#")) {
|
||||||
|
setShareMode("handoff");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleApply = async () => {
|
||||||
const name = selectedTagName().trim();
|
const name = selectedTagName().trim();
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
|
|
||||||
|
const context = selectedShareContext();
|
||||||
|
const noteContext = selectedNoteContext();
|
||||||
|
|
||||||
if (name.startsWith("@")) {
|
if (name.startsWith("@")) {
|
||||||
const normalized = normalizeContextKey(name);
|
const normalized = normalizeContextKey(name);
|
||||||
const exists = store.contexts.some(context =>
|
const exists = store.contexts.some(context =>
|
||||||
@@ -67,6 +109,11 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
|||||||
toast.error("Create the shared context first before tagging tasks with it.");
|
toast.error("Create the shared context first before tagging tasks with it.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context && context.policy !== shareMode()) {
|
||||||
|
const updated = await setContextPolicy(context.id, shareMode());
|
||||||
|
if (!updated) return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.startsWith("#")) {
|
if (name.startsWith("#")) {
|
||||||
@@ -75,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
|
||||||
@@ -134,6 +186,40 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(selectedTagName().trim().startsWith("@") || selectedTagName().trim().startsWith("#")) && (
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<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={cn(
|
||||||
|
"absolute top-1 bottom-1 w-[calc(50%-4px)] bg-primary rounded-lg shadow-sm transition-transform duration-200 ease-out z-0",
|
||||||
|
shareMode() === "handoff" ? "translate-x-0" : "translate-x-[calc(100%+0px)]"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={cn(
|
||||||
|
"flex-1 h-8 text-[0.7rem] font-bold uppercase tracking-widest z-10 transition-colors duration-200 flex items-center justify-center rounded-lg",
|
||||||
|
shareMode() === "handoff" ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
||||||
|
)}
|
||||||
|
onClick={() => setShareMode("handoff")}
|
||||||
|
>
|
||||||
|
Handoff
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={cn(
|
||||||
|
"flex-1 h-8 text-[0.7rem] font-bold uppercase tracking-widest z-10 transition-colors duration-200 flex items-center justify-center rounded-lg",
|
||||||
|
shareMode() === "collaborative" ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
||||||
|
)}
|
||||||
|
onClick={() => setShareMode("collaborative")}
|
||||||
|
>
|
||||||
|
Collaborate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button size="sm" class="w-full mt-1" onClick={handleApply}>
|
<Button size="sm" class="w-full mt-1" onClick={handleApply}>
|
||||||
<Plus size={14} class="mr-2" />
|
<Plus size={14} class="mr-2" />
|
||||||
{store.tagDefinitions.find(d => d.name === selectedTagName().trim()) ? "Update & Add" : "Create & Add"}
|
{store.tagDefinitions.find(d => d.name === selectedTagName().trim()) ? "Update & Add" : "Create & Add"}
|
||||||
|
|||||||
+175
-11
@@ -286,6 +286,7 @@ interface ScopedTaskgridPrefs {
|
|||||||
collapsedUntilUpdatedTasks?: Record<string, string>;
|
collapsedUntilUpdatedTasks?: Record<string, string>;
|
||||||
migratedStructuredTagsV2?: boolean;
|
migratedStructuredTagsV2?: boolean;
|
||||||
migratedPersonalContextRefsV1?: boolean;
|
migratedPersonalContextRefsV1?: boolean;
|
||||||
|
lastCompletedTaskCleanupDate?: string; // YYYY-MM-DD, per user/environment
|
||||||
}
|
}
|
||||||
|
|
||||||
const PREF_ENVIRONMENTS_KEY = "environments";
|
const PREF_ENVIRONMENTS_KEY = "environments";
|
||||||
@@ -358,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[];
|
||||||
@@ -371,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 {
|
||||||
@@ -432,6 +433,7 @@ interface TaskStore {
|
|||||||
isNotepadMode: boolean;
|
isNotepadMode: boolean;
|
||||||
quickloadTasks: string[];
|
quickloadTasks: string[];
|
||||||
noteFilter: Filter;
|
noteFilter: Filter;
|
||||||
|
lastCompletedTaskCleanupDate: string; // YYYY-MM-DD
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial empty state
|
// Initial empty state
|
||||||
@@ -472,6 +474,7 @@ export const [store, setStore] = createStore<TaskStore>({
|
|||||||
notes: [],
|
notes: [],
|
||||||
isNotepadMode: false,
|
isNotepadMode: false,
|
||||||
quickloadTasks: [],
|
quickloadTasks: [],
|
||||||
|
lastCompletedTaskCleanupDate: "",
|
||||||
noteFilter: {
|
noteFilter: {
|
||||||
query: "",
|
query: "",
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -526,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) ||
|
||||||
@@ -536,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[],
|
||||||
@@ -546,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);
|
||||||
}
|
}
|
||||||
@@ -587,6 +614,12 @@ const dedupeNoteRefs = (refs: NoteRef[]) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runDailyCleanupIfNeeded = () => {
|
||||||
|
const today = new Date().toLocaleDateString('en-CA');
|
||||||
|
if (store.lastCompletedTaskCleanupDate === today) return;
|
||||||
|
setStore("lastCompletedTaskCleanupDate", today);
|
||||||
|
};
|
||||||
|
|
||||||
const recordHasField = (record: any, field: string) =>
|
const recordHasField = (record: any, field: string) =>
|
||||||
!!record && Object.prototype.hasOwnProperty.call(record, field);
|
!!record && Object.prototype.hasOwnProperty.call(record, field);
|
||||||
|
|
||||||
@@ -1060,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,
|
||||||
@@ -1080,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,
|
||||||
@@ -1095,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),
|
||||||
@@ -1714,7 +1761,7 @@ const syncUserContexts = async () => {
|
|||||||
key: buildUserContextKey(user.id),
|
key: buildUserContextKey(user.id),
|
||||||
displayName,
|
displayName,
|
||||||
kind: "user",
|
kind: "user",
|
||||||
policy: "collaborative",
|
policy: "handoff",
|
||||||
targetUserId: user.id
|
targetUserId: user.id
|
||||||
}, { requestKey: null }).catch(() => null);
|
}, { requestKey: null }).catch(() => null);
|
||||||
if (created) {
|
if (created) {
|
||||||
@@ -1953,6 +2000,7 @@ export const initStore = async () => {
|
|||||||
heartbeatInterval = setInterval(() => {
|
heartbeatInterval = setInterval(() => {
|
||||||
setNow(Date.now());
|
setNow(Date.now());
|
||||||
checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.)
|
checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.)
|
||||||
|
runDailyCleanupIfNeeded(); // Once-per-day completed task cleanup
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -2329,12 +2377,55 @@ export const deleteBucket = async (id: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateUserContextPolicy = async (contextId: string, policy: "collaborative" | "handoff") => {
|
export const updateUserContextPolicy = async (contextId: string, policy: "collaborative" | "handoff") => {
|
||||||
|
const success = await setContextPolicy(contextId, policy);
|
||||||
|
if (success) {
|
||||||
|
toast.success("User context updated");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setContextPolicy = async (contextId: string, policy: "collaborative" | "handoff") => {
|
||||||
|
const previousContext = store.contexts.find(context => context.id === contextId);
|
||||||
|
const previousPolicy = previousContext?.policy;
|
||||||
|
|
||||||
|
if (previousContext && previousPolicy !== policy) {
|
||||||
|
setStore("contexts", context => context.id === contextId, "policy", policy);
|
||||||
|
setStore("shareRules", store.contexts.map(mapContextToShareRule));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pb.collection(CONTEXTS_COLLECTION).update(contextId, { policy }, { requestKey: null });
|
await pb.collection(CONTEXTS_COLLECTION).update(contextId, { policy }, { requestKey: null });
|
||||||
toast.success("User context updated");
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to update user context:", err);
|
console.error("Failed to update user context:", err);
|
||||||
|
if (previousContext && previousPolicy) {
|
||||||
|
setStore("contexts", context => context.id === contextId, "policy", previousPolicy);
|
||||||
|
setStore("shareRules", store.contexts.map(mapContextToShareRule));
|
||||||
|
}
|
||||||
toast.error("Failed to update user context");
|
toast.error("Failed to update user context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2390,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,
|
||||||
@@ -2584,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,
|
||||||
@@ -3037,6 +3144,62 @@ export const loadTaskContent = async (id: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const loadTasksLinkedToNote = async (note: Pick<Note, "id" | "key" | "title" | "tasks">) => {
|
||||||
|
if (!pb.authStore.isValid || !note?.id) return;
|
||||||
|
|
||||||
|
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__"`,
|
||||||
|
sort: "-updated",
|
||||||
|
fields: getTaskListFields(),
|
||||||
|
requestKey: `note-linked-${note.id}`
|
||||||
|
}).catch(() => []);
|
||||||
|
|
||||||
|
const recordMap = new Map<string, any>();
|
||||||
|
[...idRecords, ...listRecords].forEach(record => {
|
||||||
|
if (!record?.id) return;
|
||||||
|
if (record.tags?.includes("__template__")) return;
|
||||||
|
recordMap.set(record.id, record);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (recordMap.size === 0) return;
|
||||||
|
|
||||||
|
setStore("tasks", (currentTasks) => {
|
||||||
|
const taskMap = new Map(currentTasks.map(task => [task.id, task]));
|
||||||
|
recordMap.forEach((record: any) => {
|
||||||
|
const mapped = mapRecordToTask(record);
|
||||||
|
const existing = taskMap.get(mapped.id);
|
||||||
|
if (existing?.content) {
|
||||||
|
mapped.content = existing.content;
|
||||||
|
}
|
||||||
|
taskMap.set(mapped.id, mapped);
|
||||||
|
});
|
||||||
|
return Array.from(taskMap.values());
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Failed to load note-linked tasks:", err?.status, err?.response || err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// -- Templates --
|
// -- Templates --
|
||||||
|
|
||||||
@@ -3059,7 +3222,8 @@ export const syncPreferences = async () => {
|
|||||||
collapsedUntilUpdatedTasks: store.collapsedUntilUpdatedTasks,
|
collapsedUntilUpdatedTasks: store.collapsedUntilUpdatedTasks,
|
||||||
noteFilter: store.noteFilter,
|
noteFilter: store.noteFilter,
|
||||||
subscribedBuckets: store.subscribedBuckets,
|
subscribedBuckets: store.subscribedBuckets,
|
||||||
supervisorUserIds: store.personalContextSupervisorIds
|
supervisorUserIds: store.personalContextSupervisorIds,
|
||||||
|
lastCompletedTaskCleanupDate: store.lastCompletedTaskCleanupDate
|
||||||
};
|
};
|
||||||
|
|
||||||
await pb.collection('users').update(userId, {
|
await pb.collection('users').update(userId, {
|
||||||
@@ -3230,7 +3394,7 @@ export const loadShareRules = async () => {
|
|||||||
setStore("shareRules", reconcile(store.contexts.map(mapContextToShareRule)));
|
setStore("shareRules", reconcile(store.contexts.map(mapContextToShareRule)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string, share_mode: 'ADD_USER' | 'SEND_TASK' = 'ADD_USER') => {
|
export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string, share_mode: 'ADD_USER' | 'SEND_TASK' = 'SEND_TASK') => {
|
||||||
if (!pb.authStore.isValid) return;
|
if (!pb.authStore.isValid) return;
|
||||||
if (type !== 'tag' || !tagName?.startsWith("@")) {
|
if (type !== 'tag' || !tagName?.startsWith("@")) {
|
||||||
toast.error("Only @tag contexts are supported now.");
|
toast.error("Only @tag contexts are supported now.");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
||||||
import { store, renameTagDefinition, updateNote, updateTask, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag, loadNoteContent, requestImmediateNotesMetadataLoad } from "@/store";
|
import { store, renameTagDefinition, updateNote, updateTask, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag, loadNoteContent, requestImmediateNotesMetadataLoad, loadTasksLinkedToNote } from "@/store";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { type Note } from "@/store";
|
import { type Note } from "@/store";
|
||||||
import { Plus, X, Lock, Unlock, Trash2, ChevronDown, Link as LinkIcon, MoreHorizontal, Type, FileText, Search, Star, ChevronRight } from "lucide-solid";
|
import { Plus, X, Lock, Unlock, Trash2, ChevronDown, Link as LinkIcon, MoreHorizontal, Type, FileText, Search, Star, ChevronRight } from "lucide-solid";
|
||||||
@@ -80,6 +80,14 @@ export const NotepadView: Component<{
|
|||||||
void loadNoteContent(id);
|
void loadNoteContent(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const note = activeNote();
|
||||||
|
if (!note) return;
|
||||||
|
const linkageSignature = `${note.id}|${note.key || note.title}|${(note.tasks || []).join(",")}|${note.updated}`;
|
||||||
|
void linkageSignature;
|
||||||
|
void loadTasksLinkedToNote(note);
|
||||||
|
});
|
||||||
|
|
||||||
// Derived states
|
// Derived states
|
||||||
const activeNote = createMemo(() => {
|
const activeNote = createMemo(() => {
|
||||||
const id = props.selectedNoteId?.trim();
|
const id = props.selectedNoteId?.trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user