improved sharing not finished
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
+106
-40
@@ -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);
|
|
||||||
})
|
const nextSupervisedContexts = contexts
|
||||||
.map((user: any) => {
|
.filter(context =>
|
||||||
const ownerName = user.name || user.email || user.id;
|
|
||||||
const personalContext = store.contexts.find(context =>
|
|
||||||
!context.deletedAt &&
|
!context.deletedAt &&
|
||||||
context.kind === "user" &&
|
context.kind === "user" &&
|
||||||
context.targetUserId === user.id
|
!!context.targetUserId &&
|
||||||
);
|
context.targetUserId !== currentUserId &&
|
||||||
|
(context.supervisorUserIds || []).includes(currentUserId)
|
||||||
return personalContext ? {
|
)
|
||||||
ownerUserId: user.id,
|
.map((context) => ({
|
||||||
ownerName,
|
ownerUserId: context.targetUserId as string,
|
||||||
contextId: personalContext.id,
|
ownerName: context.displayName,
|
||||||
contextName: personalContext.displayName
|
contextId: context.id,
|
||||||
} : null;
|
contextName: context.displayName
|
||||||
})
|
}))
|
||||||
.filter((value): value is SupervisedContextAccess => !!value)
|
|
||||||
.sort((a, b) => a.ownerName.localeCompare(b.ownerName));
|
.sort((a, b) => a.ownerName.localeCompare(b.ownerName));
|
||||||
|
|
||||||
setStore("supervisedContexts", nextSupervisedContexts);
|
setStore("supervisedContexts", nextSupervisedContexts);
|
||||||
@@ -1455,6 +1506,7 @@ export const initStore = async () => {
|
|||||||
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,
|
||||||
@@ -1462,7 +1514,7 @@ export const initStore = async () => {
|
|||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user