share mode toggle added. Partial note task linkage change (stable)

This commit is contained in:
2026-03-23 12:03:15 -05:00
parent de8cea7d2e
commit 8e4eae92f8
3 changed files with 168 additions and 7 deletions
+90 -4
View File
@@ -286,6 +286,7 @@ interface ScopedTaskgridPrefs {
collapsedUntilUpdatedTasks?: Record<string, string>;
migratedStructuredTagsV2?: boolean;
migratedPersonalContextRefsV1?: boolean;
lastCompletedTaskCleanupDate?: string; // YYYY-MM-DD, per user/environment
}
const PREF_ENVIRONMENTS_KEY = "environments";
@@ -432,6 +433,7 @@ interface TaskStore {
isNotepadMode: boolean;
quickloadTasks: string[];
noteFilter: Filter;
lastCompletedTaskCleanupDate: string; // YYYY-MM-DD
}
// Initial empty state
@@ -472,6 +474,7 @@ export const [store, setStore] = createStore<TaskStore>({
notes: [],
isNotepadMode: false,
quickloadTasks: [],
lastCompletedTaskCleanupDate: "",
noteFilter: {
query: "",
tags: [],
@@ -1714,7 +1717,7 @@ const syncUserContexts = async () => {
key: buildUserContextKey(user.id),
displayName,
kind: "user",
policy: "collaborative",
policy: "handoff",
targetUserId: user.id
}, { requestKey: null }).catch(() => null);
if (created) {
@@ -1953,6 +1956,7 @@ export const initStore = async () => {
heartbeatInterval = setInterval(() => {
setNow(Date.now());
checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.)
runDailyCleanupIfNeeded(); // Once-per-day completed task cleanup
}, 60000);
try {
@@ -2329,12 +2333,32 @@ export const deleteBucket = async (id: string) => {
};
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 {
await pb.collection(CONTEXTS_COLLECTION).update(contextId, { policy }, { requestKey: null });
toast.success("User context updated");
return true;
} catch (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");
return false;
}
};
@@ -3037,6 +3061,67 @@ 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 escapedNoteIdNeedle = `\\"noteId\\":\\"${note.id}\\"`;
const escapedNoteKeyNeedle = note.key ? `\\"key\\":\\"${note.key.replace(/"/g, '\\"')}\\"` : "";
const linkedTaskIds = [...new Set((note.tasks || []).filter(Boolean))];
const idFilter = linkedTaskIds.length > 0
? ` || (${linkedTaskIds.map(taskId => `id = "${taskId}"`).join(" || ")})`
: "";
const noteRefsFilter = escapedNoteKeyNeedle
? `(noteRefs ~ "${escapedNoteIdNeedle}" || noteRefs ~ "${escapedNoteKeyNeedle}")`
: `(noteRefs ~ "${escapedNoteIdNeedle}")`;
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}") || ${noteRefsFilter}${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) {
console.error("Failed to load note-linked tasks:", err);
}
};
// -- Templates --
@@ -3059,7 +3144,8 @@ export const syncPreferences = async () => {
collapsedUntilUpdatedTasks: store.collapsedUntilUpdatedTasks,
noteFilter: store.noteFilter,
subscribedBuckets: store.subscribedBuckets,
supervisorUserIds: store.personalContextSupervisorIds
supervisorUserIds: store.personalContextSupervisorIds,
lastCompletedTaskCleanupDate: store.lastCompletedTaskCleanupDate
};
await pb.collection('users').update(userId, {
@@ -3230,7 +3316,7 @@ export const loadShareRules = async () => {
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 (type !== 'tag' || !tagName?.startsWith("@")) {
toast.error("Only @tag contexts are supported now.");