improved sharing not finished
This commit is contained in:
@@ -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;
|
||||
|
||||
+106
-40
@@ -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 =>
|
||||
if (contextRecords.length > 0) {
|
||||
setContexts(contexts);
|
||||
setStore("shareRules", contexts.map(mapContextToShareRule));
|
||||
}
|
||||
|
||||
const nextSupervisedContexts = contexts
|
||||
.filter(context =>
|
||||
!context.deletedAt &&
|
||||
context.kind === "user" &&
|
||||
context.targetUserId === user.id
|
||||
);
|
||||
|
||||
return personalContext ? {
|
||||
ownerUserId: user.id,
|
||||
ownerName,
|
||||
contextId: personalContext.id,
|
||||
contextName: personalContext.displayName
|
||||
} : null;
|
||||
})
|
||||
.filter((value): value is SupervisedContextAccess => !!value)
|
||||
!!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);
|
||||
@@ -1455,6 +1506,7 @@ export const initStore = async () => {
|
||||
if (userId) {
|
||||
const user = await pb.collection('users').getOne(userId, { requestKey: null });
|
||||
const prefs = readScopedPrefs(user.Taskgrid_pref || {});
|
||||
const personalContext = getPersonalContext(userId);
|
||||
|
||||
setStore({
|
||||
pWeight: prefs.pWeight || 1.0,
|
||||
@@ -1462,7 +1514,7 @@ export const initStore = async () => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user