From ce7398b660bbf67b926fafc693d2657b253b6608 Mon Sep 17 00:00:00 2001 From: Timothy Cardoza Date: Thu, 19 Feb 2026 12:01:54 -0600 Subject: [PATCH] share rule optimization. NOT YET OPENED OR TESTED --- src/store/index.ts | 160 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 133 insertions(+), 27 deletions(-) diff --git a/src/store/index.ts b/src/store/index.ts index 7df1fea..2f41360 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -435,6 +435,16 @@ const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => { // ShareRule match const matchingRule = store.shareRules.find(rule => { if (rule.targetUserId !== currentUserId) return false; + + // Global Rule Logic: If owner == target, it's a "Global Subscription" to that tag. + // We ignore the rule.ownerId check (which would match the task.user) + // and instead match purely on the tag. + if (rule.ownerId === rule.targetUserId && rule.type === 'tag') { + const tags = task.tags || []; + return tags.some(t => t.toLowerCase() === (rule.tagName || "").toLowerCase()); + } + + // Standard Rule Logic: Match specific owner if (task.user !== rule.ownerId) return false; if (rule.type === 'all') return true; if (rule.type === 'tag') { @@ -799,18 +809,48 @@ export const initStore = async () => { // Share Rules Incomplete if (incomingRules.length > 0) { - const ownerRules = new Map(); + // Separate "Global Rules" (Self-Rules) from "Specific User Rules" + const globalTagRules: string[] = []; + const specificUserRules = new Map(); + incomingRules.forEach((rule: any) => { - const existing = ownerRules.get(rule.ownerId) || []; - existing.push({ type: rule.type, tagName: rule.tagName }); - ownerRules.set(rule.ownerId, existing); + if (rule.ownerId === rule.targetUserId && rule.type === 'tag' && rule.tagName) { + globalTagRules.push(rule.tagName); + } else { + const existing = specificUserRules.get(rule.ownerId) || []; + existing.push({ type: rule.type, tagName: rule.tagName }); + specificUserRules.set(rule.ownerId, existing); + } }); - Array.from(ownerRules.entries()).forEach(([ownerId, rules]) => { + // 1. Fetch Global Rule Tasks (from ANY user) + if (globalTagRules.length > 0) { + // We want tasks that have ANY of these tags, AND are not completed + // filter: (tags ~ "A" || tags ~ "B") && completed = false + const tagFilter = globalTagRules.map(t => `tags ~ "${t}"`).join(' || '); + incompletePromises.push(pb.collection(TASGRID_COLLECTION).getFullList({ + filter: `(${tagFilter}) && completed = false`, + sort: '-created', + fields: LIST_FIELDS, + requestKey: null + }).catch(() => [])); + } + + // 2. Fetch Specific User Rule Tasks (from specific owner) + Array.from(specificUserRules.entries()).forEach(([ownerId, rules]) => { const hasAllRule = rules.some(r => r.type === 'all'); const tagRules = rules.filter(r => r.type === 'tag').map(r => r.tagName).filter(Boolean); + let filter = `user = "${ownerId}" && completed = false`; - if (!hasAllRule && tagRules.length > 0) filter += ` && (${tagRules.map(t => `tags ~ "${t}"`).join(' || ')})`; + + if (hasAllRule) { + // Get everything from this user + } else if (tagRules.length > 0) { + // Get specific tags from this user + filter += ` && (${tagRules.map(t => `tags ~ "${t}"`).join(' || ')})`; + } else { + return; // No valid rules for this user + } incompletePromises.push(pb.collection(TASGRID_COLLECTION).getFullList({ filter, @@ -1363,6 +1403,31 @@ export const upsertTagDefinition = async (name: string, value: number, color?: s theme: theme !== undefined ? theme : existing.theme }); } else { + + // Check for case-insensitive duplicate + const nameLower = name.toLowerCase(); + const duplicate = store.tagDefinitions.find(d => d.name.toLowerCase() === nameLower); + + if (duplicate) { + if (duplicate.name !== name) { + // Case difference only -> Update to new casing? + // Or just warn? "Tag already exists with different casing". + // For now, we'll just return the existing one to be safe, or update it if needed. + // A strict interpretation of "There should never be two tags with the same name" + // usually implies normalization. + // But if the user typed "Work" and "work" exists, maybe they mean "work". + toast.error(`Tag "${duplicate.name}" already exists.`); + return; + } + // Exact match logic handled by 'existing' check above usually, but 'existing' uses strict equality? + // The existing check at top of function is strict: `existing = store.tagDefinitions.find(d => d.name === name);` + // So this block handles case-insensitive duplicates that weren't caught by strict check. + // We should update the existing one if we want to change casing, or just reject. + // Rejection is safer for unique constraint. + toast.error(`Tag "${duplicate.name}" already exists.`); + return; + } + const record = await pb.collection(TAGS_COLLECTION).create({ user: pb.authStore.model?.id, name, @@ -1537,27 +1602,23 @@ export const syncSystemTagsAndRules = async () => { } // --- User Share Rule Sync --- - if (user.id !== currentUserId) { - const matchingRules = normalizedRules.filter(r => - r.targetUserId === user.id && - r.type === 'tag' && - r.tagName === userName - ); + // OPTIMIZATION: We no longer create individual share rules for every user-pair. + // Instead, we rely on the "Global Rule" (Self-Rule) created above for the user's own name. + // When User A tags a task "Bob", the Global Rule for Bob (Owner=Bob, Target=Bob, Tag=Bob) + // will pick it up via the updated `shouldTaskBeVisible` logic. - if (matchingRules.length === 0) { - await addShareRule('tag', user.id, userName, 'ADD_USER'); - } else if (matchingRules.length > 1) { - const [, ...remove] = matchingRules; - for (const rule of remove) { - try { - await pb.collection(SHARE_RULES_COLLECTION).delete(rule.id); - setStore("shareRules", list => list.filter(r => r.id !== rule.id)); - } catch (e) { - console.error("Failed to delete duplicate rule:", e); - } - } - } + // If we want to support "Collab" (Edit access), we might still need specific rules or specific shares, + // but for "View" access (Handoff), the Global Rule is sufficient. + + // We do NOT delete existing rules here to avoid data loss during transition, + // unless strictly requested. The user said "I want to optimize share rules... make them ownerless". + // So we stop creating new ones. + + /* + if (user.id !== currentUserId) { + // Legacy loop removed to prevent exponential growth } + */ } } catch (err) { @@ -1573,6 +1634,14 @@ export const renameTagDefinition = async (oldName: string, newName: string) => { if (!def) return; const finalName = newName.trim(); + const finalNameLower = finalName.toLowerCase(); + + // Check availability (case-insensitive) + const duplicate = store.tagDefinitions.find(d => d.name.toLowerCase() === finalNameLower && d.id !== def.id); + if (duplicate) { + toast.error(`Tag "${duplicate.name}" already exists.`); + return; + } try { await pb.collection(TAGS_COLLECTION).update(def.id, { name: finalName }); @@ -1621,12 +1690,32 @@ export const loadAllHistory = async () => { }).catch(() => [])); } + + // Add Share Rules const incomingRules = store.shareRules.filter(r => r.targetUserId === userId); if (incomingRules.length > 0) { + const rulesToFetch: string[] = []; + incomingRules.forEach((rule: any) => { - let filter = `user = "${rule.ownerId}"`; - if (rule.type === 'tag' && rule.tagName) filter += ` && tags ~ "${rule.tagName}"`; + // Global Rule (Self-Targeted) + if (rule.ownerId === rule.targetUserId && rule.type === 'tag' && rule.tagName) { + rulesToFetch.push(`tags ~ "${rule.tagName}"`); + } else { + // Standard Rule + let f = `user = "${rule.ownerId}"`; + if (rule.type === 'tag' && rule.tagName) f += ` && tags ~ "${rule.tagName}"`; + rulesToFetch.push(f); + } + }); + + // Optimization: Group Global Rules? + // For now, simpler to just push them all. + // But if we have many global rules (e.g. buckets), we might want to group them. + // The Global Rules usually behave like buckets. + // Let's just push them as individual queries for safety. + + rulesToFetch.forEach(filter => { promises.push(pb.collection(TASGRID_COLLECTION).getFullList({ filter, sort: '-created', fields: LIST_FIELDS, requestKey: null }).catch(() => [])); }); } @@ -1896,6 +1985,23 @@ export const loadShareRules = async () => { export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string, share_mode: 'ADD_USER' | 'SEND_TASK' = 'ADD_USER') => { if (!pb.authStore.isValid) return; + // Check for duplicates + // We check against the local store to avoid network round-trip delay issues + const currentUserId = pb.authStore.model?.id; + const exists = store.shareRules.some(r => + r.ownerId === currentUserId && + r.targetUserId === targetUserId && + r.type === type && + (type === 'all' || r.tagName === tagName) + // We don't strictly check share_mode because having two rules for same tag/person with different modes is confusing. + // Usually we update the existing one if mode changes. + ); + + if (exists) { + // toast.info("Share rule already exists."); + return; // Silent return or update? + } + try { await pb.collection(SHARE_RULES_COLLECTION).create({ ownerId: pb.authStore.model?.id,