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.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) {
toast.error("Create the shared context first before tagging tasks with it.");
return;
+110 -44
View File
@@ -282,11 +282,6 @@ const readScopedPrefs = (rawPrefs: any): 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) => ({
...(rawPrefs || {}),
[PREF_ENVIRONMENTS_KEY]: {
@@ -308,9 +303,44 @@ const unwrapRelationId = (value: unknown): string | 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) =>
id ? `(${field} = "${id}" || ${field} ?= "${id}")` : "false";
const buildUserContextKey = (userId: string) => `user-${userId}`;
export interface ShareContext {
id: string;
key: string;
@@ -318,6 +348,7 @@ export interface ShareContext {
kind: 'user' | 'bucket';
policy: 'collaborative' | 'handoff';
targetUserId?: string | null;
supervisorUserIds?: string[];
color?: string;
description?: string;
deletedAt?: number | null;
@@ -462,9 +493,19 @@ const deriveSystemTagDefinitions = (contexts: ShareContext[]): TagDefinition[] =
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) =>
store.contexts.find(context => context.id === ref.contextId) ||
store.contexts.find(context => context.key === ref.key && context.kind === ref.kind);
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.kind === ref.kind && normalizeContextKey(context.displayName) === ref.key) ||
store.contexts.find(context => !context.deletedAt && normalizeContextKey(context.displayName) === ref.key) ||
null;
const getNoteByRef = (ref: NoteRef) =>
store.notes.find(note => note.id === ref.noteId) ||
@@ -801,7 +842,7 @@ const mapRecordToTask = (r: any): Task => {
: parsedTags
.filter(tag => tag.kind === "share")
.map(tag => {
const context = store.contexts.find(existing => existing.key === tag.key);
const context = findContextForShareTag(tag);
return {
contextId: context?.id || tag.key,
key: tag.key,
@@ -971,6 +1012,7 @@ const mapRecordToContext = (r: any): ShareContext => ({
kind: (r.kind || "bucket") as "user" | "bucket",
policy: (r.policy || (r.kind === "user" ? "collaborative" : "handoff")) as "collaborative" | "handoff",
targetUserId: unwrapRelationId(r.targetUserId),
supervisorUserIds: unwrapRelationIds(r.supervisorUserIds),
color: r.color,
description: r.description,
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : null,
@@ -1172,6 +1214,7 @@ export const subscribeToRealtime = async () => {
setContexts(remaining);
setStore("shareRules", remaining.map(mapContextToShareRule));
setStore("tasks", tasks => tasks.filter(task => shouldTaskBeVisible(task, pb.authStore.model?.id || "")));
void syncSupervisedContexts();
return;
}
@@ -1181,6 +1224,10 @@ export const subscribeToRealtime = async () => {
: [...store.contexts, context];
setContexts(nextContexts);
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({
requestKey: null
}).catch(() => []);
const contextByKey = new Map(existingContexts.map((context: any) => [context.key || normalizeContextKey(context.displayName || context.name), context]));
let createdAny = false;
for (const user of allUsers) {
const displayName = user.name || user.email;
const key = normalizeContextKey(displayName);
if (!displayName || contextByKey.has(key)) continue;
const existingUserContext = existingContexts.find((context: any) =>
(context.kind || "bucket") === "user" &&
unwrapRelationId(context.targetUserId) === user.id
);
if (!displayName || existingUserContext) continue;
const created = await pb.collection(CONTEXTS_COLLECTION).create({
key,
key: buildUserContextKey(user.id),
displayName,
kind: "user",
policy: "collaborative",
targetUserId: user.id
}, { requestKey: null }).catch(() => null);
if (created) {
contextByKey.set(key, created);
createdAny = true;
}
}
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);
setStore("shareRules", nextContexts.map(mapContextToShareRule));
}
@@ -1280,33 +1333,31 @@ const syncSupervisedContexts = async () => {
}
try {
const users = await pb.collection('users').getFullList({
fields: 'id,name,email,Taskgrid_pref',
const contextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({
sort: 'displayName,key',
requestKey: null
});
}).catch(() => []);
const contexts = contextRecords.length > 0 ? contextRecords.map(mapRecordToContext) : store.contexts;
const nextSupervisedContexts = users
.filter((user: any) => user.id !== currentUserId)
.filter((user: any) => {
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
);
if (contextRecords.length > 0) {
setContexts(contexts);
setStore("shareRules", contexts.map(mapContextToShareRule));
}
return personalContext ? {
ownerUserId: user.id,
ownerName,
contextId: personalContext.id,
contextName: personalContext.displayName
} : null;
})
.filter((value): value is SupervisedContextAccess => !!value)
const nextSupervisedContexts = contexts
.filter(context =>
!context.deletedAt &&
context.kind === "user" &&
!!context.targetUserId &&
context.targetUserId !== currentUserId &&
(context.supervisorUserIds || []).includes(currentUserId)
)
.map((context) => ({
ownerUserId: context.targetUserId as string,
ownerName: context.displayName,
contextId: context.id,
contextName: context.displayName
}))
.sort((a, b) => a.ownerName.localeCompare(b.ownerName));
setStore("supervisedContexts", nextSupervisedContexts);
@@ -1453,16 +1504,17 @@ export const initStore = async () => {
try {
const userId = pb.authStore.model?.id;
if (userId) {
const user = await pb.collection('users').getOne(userId, { requestKey: null });
const prefs = readScopedPrefs(user.Taskgrid_pref || {});
const user = await pb.collection('users').getOne(userId, { requestKey: null });
const prefs = readScopedPrefs(user.Taskgrid_pref || {});
const personalContext = getPersonalContext(userId);
setStore({
setStore({
pWeight: prefs.pWeight || 1.0,
uWeight: prefs.uWeight || 1.0,
matrixScaleDays: prefs.matrixScaleDays || 30,
prefId: userId,
subscribedBuckets: prefs.subscribedBuckets || user.subscribedBuckets || [],
personalContextSupervisorIds: prefs.supervisorUserIds || [],
personalContextSupervisorIds: personalContext?.supervisorUserIds || prefs.supervisorUserIds || [],
tagDefinitions: (prefs.tagDefinitions || []).filter((def: TagDefinition) => !def.name.startsWith("@")),
filterTemplates: prefs.filterTemplates || [],
quickloadTasks: prefs.quickloadTasks || [],
@@ -1502,6 +1554,9 @@ export const initStore = async () => {
context.kind === "user" &&
context.targetUserId === pb.authStore.model?.id
);
if (personalContext) {
setStore("personalContextSupervisorIds", personalContext.supervisorUserIds || []);
}
if (personalContext && currentTaskContext() === "mine") {
setCurrentTaskContext({ bucketId: personalContext.id, name: "My Bucket", isPersonal: true });
}
@@ -1836,6 +1891,16 @@ export const savePersonalContextSupervisors = async (supervisorUserIds: string[]
setStore("personalContextSupervisorIds", uniqueSupervisorIds);
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 rawPrefs = user.Taskgrid_pref || {};
const scopedPrefs = readScopedPrefs(rawPrefs);
@@ -1845,6 +1910,7 @@ export const savePersonalContextSupervisors = async (supervisorUserIds: string[]
supervisorUserIds: uniqueSupervisorIds
})
}, { requestKey: null });
await syncSupervisedContexts();
toast.success("Personal supervision updated");
} catch (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
.filter(tag => tag.kind === "share")
.map(tag => {
const context = store.contexts.find(existing => existing.key === tag.key);
const context = findContextForShareTag(tag);
return {
contextId: context?.id || tag.key,
key: tag.key,
@@ -2028,7 +2094,7 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
finalUpdates.shareRefs = parsedTags
.filter(tag => tag.kind === "share")
.map(tag => {
const context = store.contexts.find(existing => existing.key === tag.key);
const context = findContextForShareTag(tag);
return {
contextId: context?.id || tag.key,
key: tag.key,