share modes partially implemented

This commit is contained in:
2026-02-17 13:23:25 -06:00
parent 01348ce229
commit e5c7666a2d
2 changed files with 240 additions and 67 deletions
+147 -44
View File
@@ -128,6 +128,7 @@ export interface TagDefinition {
color?: string;
theme?: "light" | "dark";
isUser?: boolean; // New flag to identify user-based tags
isBucket?: boolean; // New flag to identify bucket-based tags
}
export interface FilterTag {
@@ -158,6 +159,7 @@ export interface ShareRule {
targetUserId: string;
type: 'tag' | 'all';
tagName?: string;
share_mode?: 'ADD_USER' | 'SEND_TASK'; // Default to ADD_USER if undefined
}
export interface Bucket {
@@ -404,7 +406,8 @@ const mapRecordToShareRule = (r: any): ShareRule => {
ownerId: r.ownerId,
targetUserId: r.targetUserId,
type: r.type as 'tag' | 'all',
tagName: r.tagName
tagName: r.tagName,
share_mode: r.share_mode || 'ADD_USER'
};
};
@@ -707,15 +710,17 @@ export const initStore = async () => {
name: r.name,
value: r.value,
color: r.color,
theme: r.theme
theme: r.theme,
isUser: r.isUser || false,
isBucket: r.isBucket || false
}));
setStore("tagDefinitions", reconcile(loadedTags));
} catch (tagErr) {
console.warn("Failed to load tags:", tagErr);
}
// 1.8 Sync User Tags & Share Rules
await syncUserTagsAndRules();
// 1.8 Sync System Tags & Share Rules
await syncSystemTagsAndRules();
// 2. Fetch Tasks & Templates + Shared Tasks in PARALLEL
const currentUserId = pb.authStore.model?.id;
@@ -955,6 +960,7 @@ export const createBucket = async (name: string, color: string, description?: st
color,
description
});
await syncSystemTagsAndRules();
toast.success("Bucket created");
} catch (err) {
console.error("Failed to create bucket:", err);
@@ -965,6 +971,9 @@ export const createBucket = async (name: string, color: string, description?: st
export const updateBucket = async (id: string, data: { name?: string, color?: string, description?: string }) => {
try {
await pb.collection(BUCKETS_COLLECTION).update(id, data);
if (data.name) {
await syncSystemTagsAndRules();
}
toast.success("Bucket updated");
} catch (err) {
console.error("Failed to update bucket:", err);
@@ -1070,12 +1079,50 @@ export const copyTask = async (id: string) => {
toast.success("Task duplicated");
};
const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> => {
// Only check if tags are being modified
if (!updates.tags) return updates;
const currentUserId = pb.authStore.model?.id;
if (!currentUserId || task.ownerId !== currentUserId) return updates;
// Check for any tag that triggers a SEND_TASK rule
for (const tag of updates.tags) {
const rule = store.shareRules.find(r =>
r.ownerId === currentUserId &&
r.type === 'tag' &&
r.tagName === tag &&
r.share_mode === 'SEND_TASK'
);
if (rule) {
console.log(`Handoff triggered by tag "${tag}" -> Transferring to ${rule.targetUserId}`);
return {
...updates,
ownerId: rule.targetUserId,
// Also clear sharedWith for the specific target user if they were already shared
sharedWith: (task.sharedWith || []).filter(s => s.userId !== rule.targetUserId)
};
}
}
return updates;
};
export const updateTask = async (id: string, updates: Partial<Task>) => {
if (!pb.authStore.isValid) return;
// 0. Check for Hand-off Logic (Tag-based ownership transfer)
const currentTask = store.tasks.find(t => t.id === id);
let finalUpdates = updates;
if (currentTask) {
finalUpdates = checkForHandoffs(currentTask, updates);
}
// Optimistic update with timestamp to prevent stale realtime events
const optimisticUpdates = {
...updates,
...finalUpdates,
updated: new Date().toISOString()
};
setStore("tasks", (t) => t.id === id, optimisticUpdates);
@@ -1083,15 +1130,21 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
try {
// Map Task fields to PB fields if needed (dates are ISO strings, so should matches)
// Check for specific fields being updated
const pbUpdates: any = { ...updates };
if (updates.deletedAt !== undefined) {
const pbUpdates: any = { ...finalUpdates };
if (finalUpdates.ownerId) {
pbUpdates.user = finalUpdates.ownerId; // Map ownerId to 'user' field in PB
delete pbUpdates.ownerId;
}
if (finalUpdates.deletedAt !== undefined) {
// PB expects a date string or null for date fields usually,
// but if we defined deletedAt as a 'date' field in PB.
// If we defined it as a number in PB? User didn't specify.
// Assuming it's a 'Date' field in PB based on previous conversation
// "deletedAt | Date | No"
if (updates.deletedAt) {
pbUpdates.deletedAt = new Date(updates.deletedAt).toISOString();
if (finalUpdates.deletedAt) {
pbUpdates.deletedAt = new Date(finalUpdates.deletedAt).toISOString();
} else {
pbUpdates.deletedAt = null; // restore
}
@@ -1223,7 +1276,8 @@ export const upsertTagDefinition = async (name: string, value: number, color?: s
name: record.name,
value: record.value,
color: record.color,
theme: record.theme as "light" | "dark"
theme: record.theme as "light" | "dark",
isBucket: record.isBucket || false
};
setStore("tagDefinitions", (prev) => [...prev, newDef]);
}
@@ -1257,28 +1311,28 @@ export const removeTagDefinition = async (name: string) => {
export const syncUserTagsAndRules = async () => {
export const syncSystemTagsAndRules = async () => {
if (!pb.authStore.isValid) return;
const currentUserId = pb.authStore.model?.id;
try {
// 1. Fetch only verified users
const usersPromise = pb.collection('users').getFullList({
filter: 'verified = true',
fields: 'id,name,email,verified',
requestKey: null
});
// 1. Fetch data
const [users, pbRules, tagDefinitions] = await Promise.all([
pb.collection('users').getFullList({
filter: 'verified = true',
fields: 'id,name,email,verified',
requestKey: null
}),
pb.collection(SHARE_RULES_COLLECTION).getFullList({
filter: `ownerId = "${currentUserId}"`,
requestKey: null
}),
pb.collection(TAGS_COLLECTION).getFullList({
filter: `user = "${currentUserId}"`,
requestKey: null
})
]);
// 2. Fetch existing rules DIRECTLY from PB to avoid store race conditions
const rulesPromise = pb.collection(SHARE_RULES_COLLECTION).getFullList({
filter: `ownerId = "${currentUserId}"`,
requestKey: null
});
const [users, pbRules] = await Promise.all([usersPromise, rulesPromise]);
// 2a. Map PB rules to check for duplicates
const existingTags = store.tagDefinitions;
const normalizedRules = pbRules.map(r => ({
id: r.id,
targetUserId: r.targetUserId,
@@ -1286,21 +1340,55 @@ export const syncUserTagsAndRules = async () => {
tagName: r.tagName
}));
// --- Sync Bucket Tags ---
for (const bucket of store.buckets) {
const tagExists = tagDefinitions.find(t => t.name === bucket.name);
if (!tagExists) {
try {
const record = await pb.collection(TAGS_COLLECTION).create({
user: currentUserId,
name: bucket.name,
value: 5,
color: bucket.color || "#64748b",
theme: "dark",
isBucket: true
});
setStore("tagDefinitions", prev => [...prev, {
id: record.id,
name: record.name,
value: record.value,
color: record.color,
theme: record.theme,
isBucket: true
}]);
} catch (e) {
console.error(`Failed to create bucket tag ${bucket.name}`, e);
}
} else if (!tagExists.isBucket) {
try {
await pb.collection(TAGS_COLLECTION).update(tagExists.id, { isBucket: true });
setStore("tagDefinitions", d => d.id === tagExists.id, { isBucket: true });
} catch (e) {
console.error(`Failed to flag tag ${bucket.name} as bucket`, e);
}
}
}
// --- Sync User Tags ---
for (const user of users) {
const userName = user.name || user.email;
if (!userName) continue;
// --- Tag Sync ---
const tagExists = existingTags.find(t => t.name === userName);
const tagExists = tagDefinitions.find(t => t.name === userName);
if (!tagExists) {
// ... create tag (logic unchanged) ...
try {
const record = await pb.collection(TAGS_COLLECTION).create({
user: currentUserId,
name: userName,
value: 5,
color: "#64748b",
theme: "dark"
theme: "dark",
isUser: true
});
setStore("tagDefinitions", prev => [...prev, {
id: record.id,
@@ -1313,15 +1401,17 @@ export const syncUserTagsAndRules = async () => {
} catch (e) {
// console.error(`Failed to create user tag ${userName}`, e);
}
} else {
if (!tagExists.isUser) {
} else if (!tagExists.isUser) {
try {
await pb.collection(TAGS_COLLECTION).update(tagExists.id, { isUser: true });
setStore("tagDefinitions", d => d.id === tagExists.id, { isUser: true });
} catch (e) {
console.error(`Failed to flag tag ${userName} as user`, e);
}
}
// --- Share Rule Sync ---
// --- User Share Rule Sync ---
if (user.id !== currentUserId) {
// Find ALL rules matching this criteria
const matchingRules = normalizedRules.filter(r =>
r.targetUserId === user.id &&
r.type === 'tag' &&
@@ -1329,16 +1419,12 @@ export const syncUserTagsAndRules = async () => {
);
if (matchingRules.length === 0) {
console.log(`Creating auto-share rule for ${userName}`);
await addShareRule('tag', user.id, userName);
await addShareRule('tag', user.id, userName, 'ADD_USER');
} else if (matchingRules.length > 1) {
// DUPLICATE DETECTED: Keep first, delete others
console.warn(`Duplicate share rules found for ${userName}. Cleaning up...`);
const [, ...remove] = matchingRules;
for (const rule of remove) {
try {
await pb.collection(SHARE_RULES_COLLECTION).delete(rule.id);
// Update local store immediately to reflect deletion
setStore("shareRules", list => list.filter(r => r.id !== rule.id));
} catch (e) {
console.error("Failed to delete duplicate rule:", e);
@@ -1349,7 +1435,7 @@ export const syncUserTagsAndRules = async () => {
}
} catch (err) {
console.error("Failed to sync user tags/rules:", err);
console.error("Failed to sync system tags/rules:", err);
}
};
@@ -1589,7 +1675,8 @@ export const loadShareRules = async () => {
ownerId: r.ownerId,
targetUserId: r.targetUserId,
type: r.type as 'tag' | 'all',
tagName: r.tagName
tagName: r.tagName,
share_mode: r.share_mode || 'ADD_USER'
}));
setStore("shareRules", reconcile(rules));
@@ -1598,7 +1685,7 @@ export const loadShareRules = async () => {
}
};
export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string) => {
export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string, share_mode: 'ADD_USER' | 'SEND_TASK' = 'ADD_USER') => {
if (!pb.authStore.isValid) return;
try {
@@ -1606,7 +1693,8 @@ export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, ta
ownerId: pb.authStore.model?.id,
targetUserId,
type,
tagName: type === 'tag' ? tagName : null
tagName: type === 'tag' ? tagName : null,
share_mode: share_mode || 'ADD_USER'
});
// Realtime subscription will handle adding to store
toast.success(`Share rule created`);
@@ -1616,6 +1704,21 @@ export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, ta
}
};
export const updateShareRule = async (ruleId: string, updates: Partial<ShareRule>) => {
if (!pb.authStore.isValid) return;
try {
await pb.collection(SHARE_RULES_COLLECTION).update(ruleId, updates);
setStore("shareRules", (rules) =>
rules.map(r => r.id === ruleId ? { ...r, ...updates } : r)
);
toast.success("Share rule updated");
} catch (err) {
console.error("Failed to update share rule:", err);
toast.error("Failed to update share rule.");
}
};
export const removeShareRule = async (ruleId: string) => {
if (!pb.authStore.isValid) return;