improved sharing not finished

This commit is contained in:
2026-03-19 17:30:04 -05:00
parent 727cfe0083
commit a4d3da984b
2 changed files with 117 additions and 45 deletions
+7 -1
View File
@@ -56,7 +56,13 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
if (!name) return; if (!name) return;
if (name.startsWith("@")) { if (name.startsWith("@")) {
const exists = store.contexts.some(context => context.key === normalizeContextKey(name)); const normalized = normalizeContextKey(name);
const exists = store.contexts.some(context =>
!context.deletedAt && (
context.key === normalized ||
normalizeContextKey(context.displayName) === normalized
)
);
if (!exists) { if (!exists) {
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;
+110 -44
View File
@@ -282,11 +282,6 @@ const readScopedPrefs = (rawPrefs: any): ScopedTaskgridPrefs => {
return (scoped || rawPrefs || {}) as ScopedTaskgridPrefs; return (scoped || rawPrefs || {}) as ScopedTaskgridPrefs;
}; };
const readScopedPrefsForMode = (rawPrefs: any, mode: string): ScopedTaskgridPrefs => {
const scoped = rawPrefs?.[PREF_ENVIRONMENTS_KEY]?.[mode];
return (scoped || rawPrefs || {}) as ScopedTaskgridPrefs;
};
const writeScopedPrefs = (rawPrefs: any, scopedPrefs: ScopedTaskgridPrefs) => ({ const writeScopedPrefs = (rawPrefs: any, scopedPrefs: ScopedTaskgridPrefs) => ({
...(rawPrefs || {}), ...(rawPrefs || {}),
[PREF_ENVIRONMENTS_KEY]: { [PREF_ENVIRONMENTS_KEY]: {
@@ -308,9 +303,44 @@ const unwrapRelationId = (value: unknown): string | null => {
return null; return null;
}; };
const unwrapRelationIds = (value: unknown): string[] => {
if (Array.isArray(value)) {
return value
.map(item => {
if (typeof item === "string") return item;
if (item && typeof item === "object" && "id" in item && typeof (item as any).id === "string") {
return (item as any).id;
}
return null;
})
.filter((item): item is string => !!item);
}
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) return [];
try {
const parsed = JSON.parse(trimmed);
return unwrapRelationIds(parsed);
} catch {
return trimmed.includes(",")
? trimmed.split(",").map(item => item.trim()).filter(Boolean)
: [trimmed];
}
}
if (value && typeof value === "object" && "id" in value && typeof (value as any).id === "string") {
return [(value as any).id];
}
return [];
};
const relationFilter = (field: string, id: string | undefined | null) => const relationFilter = (field: string, id: string | undefined | null) =>
id ? `(${field} = "${id}" || ${field} ?= "${id}")` : "false"; id ? `(${field} = "${id}" || ${field} ?= "${id}")` : "false";
const buildUserContextKey = (userId: string) => `user-${userId}`;
export interface ShareContext { export interface ShareContext {
id: string; id: string;
key: string; key: string;
@@ -318,6 +348,7 @@ export interface ShareContext {
kind: 'user' | 'bucket'; kind: 'user' | 'bucket';
policy: 'collaborative' | 'handoff'; policy: 'collaborative' | 'handoff';
targetUserId?: string | null; targetUserId?: string | null;
supervisorUserIds?: string[];
color?: string; color?: string;
description?: string; description?: string;
deletedAt?: number | null; deletedAt?: number | null;
@@ -462,9 +493,19 @@ const deriveSystemTagDefinitions = (contexts: ShareContext[]): TagDefinition[] =
isBucket: context.kind === "bucket" isBucket: context.kind === "bucket"
})); }));
const findContextForShareTag = (tag: { key: string; raw: string }) =>
store.contexts.find(context => !context.deletedAt && context.key === tag.key) ||
store.contexts.find(context => !context.deletedAt && context.kind === "user" && normalizeContextKey(context.displayName) === tag.key) ||
store.contexts.find(context => !context.deletedAt && normalizeContextKey(context.displayName) === tag.key) ||
store.contexts.find(context => !context.deletedAt && buildShareTag(context.displayName).toLowerCase() === tag.raw.toLowerCase()) ||
null;
const getContextByRef = (ref: TaskShareRef) => const getContextByRef = (ref: TaskShareRef) =>
store.contexts.find(context => context.id === ref.contextId) || store.contexts.find(context => !context.deletedAt && context.id === ref.contextId) ||
store.contexts.find(context => 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 && normalizeContextKey(context.displayName) === ref.key) ||
null;
const getNoteByRef = (ref: NoteRef) => const getNoteByRef = (ref: NoteRef) =>
store.notes.find(note => note.id === ref.noteId) || store.notes.find(note => note.id === ref.noteId) ||
@@ -801,7 +842,7 @@ const mapRecordToTask = (r: any): Task => {
: parsedTags : parsedTags
.filter(tag => tag.kind === "share") .filter(tag => tag.kind === "share")
.map(tag => { .map(tag => {
const context = store.contexts.find(existing => existing.key === tag.key); const context = findContextForShareTag(tag);
return { return {
contextId: context?.id || tag.key, contextId: context?.id || tag.key,
key: tag.key, key: tag.key,
@@ -971,6 +1012,7 @@ const mapRecordToContext = (r: any): ShareContext => ({
kind: (r.kind || "bucket") as "user" | "bucket", kind: (r.kind || "bucket") as "user" | "bucket",
policy: (r.policy || (r.kind === "user" ? "collaborative" : "handoff")) as "collaborative" | "handoff", policy: (r.policy || (r.kind === "user" ? "collaborative" : "handoff")) as "collaborative" | "handoff",
targetUserId: unwrapRelationId(r.targetUserId), targetUserId: unwrapRelationId(r.targetUserId),
supervisorUserIds: unwrapRelationIds(r.supervisorUserIds),
color: r.color, color: r.color,
description: r.description, description: r.description,
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : null, deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : null,
@@ -1172,6 +1214,7 @@ export const subscribeToRealtime = async () => {
setContexts(remaining); setContexts(remaining);
setStore("shareRules", remaining.map(mapContextToShareRule)); setStore("shareRules", remaining.map(mapContextToShareRule));
setStore("tasks", tasks => tasks.filter(task => shouldTaskBeVisible(task, pb.authStore.model?.id || ""))); setStore("tasks", tasks => tasks.filter(task => shouldTaskBeVisible(task, pb.authStore.model?.id || "")));
void syncSupervisedContexts();
return; return;
} }
@@ -1181,6 +1224,10 @@ export const subscribeToRealtime = async () => {
: [...store.contexts, context]; : [...store.contexts, context];
setContexts(nextContexts); setContexts(nextContexts);
setStore("shareRules", nextContexts.map(mapContextToShareRule)); setStore("shareRules", nextContexts.map(mapContextToShareRule));
if (context.targetUserId === pb.authStore.model?.id) {
setStore("personalContextSupervisorIds", context.supervisorUserIds || []);
}
void syncSupervisedContexts();
}); });
}; };
@@ -1245,28 +1292,34 @@ const syncUserContexts = async () => {
const existingContexts = await pb.collection(CONTEXTS_COLLECTION).getFullList({ const existingContexts = await pb.collection(CONTEXTS_COLLECTION).getFullList({
requestKey: null requestKey: null
}).catch(() => []); }).catch(() => []);
const contextByKey = new Map(existingContexts.map((context: any) => [context.key || normalizeContextKey(context.displayName || context.name), context]));
let createdAny = false; let createdAny = false;
for (const user of allUsers) { for (const user of allUsers) {
const displayName = user.name || user.email; const displayName = user.name || user.email;
const key = normalizeContextKey(displayName); const existingUserContext = existingContexts.find((context: any) =>
if (!displayName || contextByKey.has(key)) continue; (context.kind || "bucket") === "user" &&
unwrapRelationId(context.targetUserId) === user.id
);
if (!displayName || existingUserContext) continue;
const created = await pb.collection(CONTEXTS_COLLECTION).create({ const created = await pb.collection(CONTEXTS_COLLECTION).create({
key, key: buildUserContextKey(user.id),
displayName, displayName,
kind: "user", kind: "user",
policy: "collaborative", policy: "collaborative",
targetUserId: user.id targetUserId: user.id
}, { requestKey: null }).catch(() => null); }, { requestKey: null }).catch(() => null);
if (created) { if (created) {
contextByKey.set(key, created);
createdAny = true; createdAny = true;
} }
} }
if (createdAny || store.contexts.length === 0) { if (createdAny || store.contexts.length === 0) {
const nextContexts = Array.from(contextByKey.values()).map(mapRecordToContext); const nextContextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({
sort: 'displayName,key',
requestKey: null
}).catch(() => []);
const nextContexts = nextContextRecords.map(mapRecordToContext);
setContexts(nextContexts); setContexts(nextContexts);
setStore("shareRules", nextContexts.map(mapContextToShareRule)); setStore("shareRules", nextContexts.map(mapContextToShareRule));
} }
@@ -1280,33 +1333,31 @@ const syncSupervisedContexts = async () => {
} }
try { try {
const users = await pb.collection('users').getFullList({ const contextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({
fields: 'id,name,email,Taskgrid_pref', sort: 'displayName,key',
requestKey: null requestKey: null
}); }).catch(() => []);
const contexts = contextRecords.length > 0 ? contextRecords.map(mapRecordToContext) : store.contexts;
const nextSupervisedContexts = users if (contextRecords.length > 0) {
.filter((user: any) => user.id !== currentUserId) setContexts(contexts);
.filter((user: any) => { setStore("shareRules", contexts.map(mapContextToShareRule));
const prefs = readScopedPrefsForMode(user.Taskgrid_pref || {}, TASGRID_DATA_MODE); }
return (prefs.supervisorUserIds || []).includes(currentUserId);
})
.map((user: any) => {
const ownerName = user.name || user.email || user.id;
const personalContext = store.contexts.find(context =>
!context.deletedAt &&
context.kind === "user" &&
context.targetUserId === user.id
);
return personalContext ? { const nextSupervisedContexts = contexts
ownerUserId: user.id, .filter(context =>
ownerName, !context.deletedAt &&
contextId: personalContext.id, context.kind === "user" &&
contextName: personalContext.displayName !!context.targetUserId &&
} : null; context.targetUserId !== currentUserId &&
}) (context.supervisorUserIds || []).includes(currentUserId)
.filter((value): value is SupervisedContextAccess => !!value) )
.map((context) => ({
ownerUserId: context.targetUserId as string,
ownerName: context.displayName,
contextId: context.id,
contextName: context.displayName
}))
.sort((a, b) => a.ownerName.localeCompare(b.ownerName)); .sort((a, b) => a.ownerName.localeCompare(b.ownerName));
setStore("supervisedContexts", nextSupervisedContexts); setStore("supervisedContexts", nextSupervisedContexts);
@@ -1453,16 +1504,17 @@ export const initStore = async () => {
try { try {
const userId = pb.authStore.model?.id; const userId = pb.authStore.model?.id;
if (userId) { if (userId) {
const user = await pb.collection('users').getOne(userId, { requestKey: null }); const user = await pb.collection('users').getOne(userId, { requestKey: null });
const prefs = readScopedPrefs(user.Taskgrid_pref || {}); const prefs = readScopedPrefs(user.Taskgrid_pref || {});
const personalContext = getPersonalContext(userId);
setStore({ setStore({
pWeight: prefs.pWeight || 1.0, pWeight: prefs.pWeight || 1.0,
uWeight: prefs.uWeight || 1.0, uWeight: prefs.uWeight || 1.0,
matrixScaleDays: prefs.matrixScaleDays || 30, matrixScaleDays: prefs.matrixScaleDays || 30,
prefId: userId, prefId: userId,
subscribedBuckets: prefs.subscribedBuckets || user.subscribedBuckets || [], subscribedBuckets: prefs.subscribedBuckets || user.subscribedBuckets || [],
personalContextSupervisorIds: prefs.supervisorUserIds || [], personalContextSupervisorIds: personalContext?.supervisorUserIds || prefs.supervisorUserIds || [],
tagDefinitions: (prefs.tagDefinitions || []).filter((def: TagDefinition) => !def.name.startsWith("@")), tagDefinitions: (prefs.tagDefinitions || []).filter((def: TagDefinition) => !def.name.startsWith("@")),
filterTemplates: prefs.filterTemplates || [], filterTemplates: prefs.filterTemplates || [],
quickloadTasks: prefs.quickloadTasks || [], quickloadTasks: prefs.quickloadTasks || [],
@@ -1502,6 +1554,9 @@ export const initStore = async () => {
context.kind === "user" && context.kind === "user" &&
context.targetUserId === pb.authStore.model?.id context.targetUserId === pb.authStore.model?.id
); );
if (personalContext) {
setStore("personalContextSupervisorIds", personalContext.supervisorUserIds || []);
}
if (personalContext && currentTaskContext() === "mine") { if (personalContext && currentTaskContext() === "mine") {
setCurrentTaskContext({ bucketId: personalContext.id, name: "My Bucket", isPersonal: true }); setCurrentTaskContext({ bucketId: personalContext.id, name: "My Bucket", isPersonal: true });
} }
@@ -1836,6 +1891,16 @@ export const savePersonalContextSupervisors = async (supervisorUserIds: string[]
setStore("personalContextSupervisorIds", uniqueSupervisorIds); setStore("personalContextSupervisorIds", uniqueSupervisorIds);
try { try {
await syncUserContexts();
const personalContext = getPersonalContext(userId);
if (!personalContext) {
throw new Error("Missing personal context record");
}
await pb.collection(CONTEXTS_COLLECTION).update(personalContext.id, {
supervisorUserIds: uniqueSupervisorIds
}, { requestKey: null });
const user = await pb.collection('users').getOne(userId, { requestKey: null }); const user = await pb.collection('users').getOne(userId, { requestKey: null });
const rawPrefs = user.Taskgrid_pref || {}; const rawPrefs = user.Taskgrid_pref || {};
const scopedPrefs = readScopedPrefs(rawPrefs); const scopedPrefs = readScopedPrefs(rawPrefs);
@@ -1845,6 +1910,7 @@ export const savePersonalContextSupervisors = async (supervisorUserIds: string[]
supervisorUserIds: uniqueSupervisorIds supervisorUserIds: uniqueSupervisorIds
}) })
}, { requestKey: null }); }, { requestKey: null });
await syncSupervisedContexts();
toast.success("Personal supervision updated"); toast.success("Personal supervision updated");
} catch (err) { } catch (err) {
console.error("Failed to update personal supervision:", err); console.error("Failed to update personal supervision:", err);
@@ -1871,7 +1937,7 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
const shareRefs = parsedTags const shareRefs = parsedTags
.filter(tag => tag.kind === "share") .filter(tag => tag.kind === "share")
.map(tag => { .map(tag => {
const context = store.contexts.find(existing => existing.key === tag.key); const context = findContextForShareTag(tag);
return { return {
contextId: context?.id || tag.key, contextId: context?.id || tag.key,
key: tag.key, key: tag.key,
@@ -2028,7 +2094,7 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
finalUpdates.shareRefs = parsedTags finalUpdates.shareRefs = parsedTags
.filter(tag => tag.kind === "share") .filter(tag => tag.kind === "share")
.map(tag => { .map(tag => {
const context = store.contexts.find(existing => existing.key === tag.key); const context = findContextForShareTag(tag);
return { return {
contextId: context?.id || tag.key, contextId: context?.id || tag.key,
key: tag.key, key: tag.key,