share modes partially implemented
This commit is contained in:
+142
-39
@@ -128,6 +128,7 @@ export interface TagDefinition {
|
|||||||
color?: string;
|
color?: string;
|
||||||
theme?: "light" | "dark";
|
theme?: "light" | "dark";
|
||||||
isUser?: boolean; // New flag to identify user-based tags
|
isUser?: boolean; // New flag to identify user-based tags
|
||||||
|
isBucket?: boolean; // New flag to identify bucket-based tags
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterTag {
|
export interface FilterTag {
|
||||||
@@ -158,6 +159,7 @@ export interface ShareRule {
|
|||||||
targetUserId: string;
|
targetUserId: string;
|
||||||
type: 'tag' | 'all';
|
type: 'tag' | 'all';
|
||||||
tagName?: string;
|
tagName?: string;
|
||||||
|
share_mode?: 'ADD_USER' | 'SEND_TASK'; // Default to ADD_USER if undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Bucket {
|
export interface Bucket {
|
||||||
@@ -404,7 +406,8 @@ const mapRecordToShareRule = (r: any): ShareRule => {
|
|||||||
ownerId: r.ownerId,
|
ownerId: r.ownerId,
|
||||||
targetUserId: r.targetUserId,
|
targetUserId: r.targetUserId,
|
||||||
type: r.type as 'tag' | 'all',
|
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,
|
name: r.name,
|
||||||
value: r.value,
|
value: r.value,
|
||||||
color: r.color,
|
color: r.color,
|
||||||
theme: r.theme
|
theme: r.theme,
|
||||||
|
isUser: r.isUser || false,
|
||||||
|
isBucket: r.isBucket || false
|
||||||
}));
|
}));
|
||||||
setStore("tagDefinitions", reconcile(loadedTags));
|
setStore("tagDefinitions", reconcile(loadedTags));
|
||||||
} catch (tagErr) {
|
} catch (tagErr) {
|
||||||
console.warn("Failed to load tags:", tagErr);
|
console.warn("Failed to load tags:", tagErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.8 Sync User Tags & Share Rules
|
// 1.8 Sync System Tags & Share Rules
|
||||||
await syncUserTagsAndRules();
|
await syncSystemTagsAndRules();
|
||||||
|
|
||||||
// 2. Fetch Tasks & Templates + Shared Tasks in PARALLEL
|
// 2. Fetch Tasks & Templates + Shared Tasks in PARALLEL
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
@@ -955,6 +960,7 @@ export const createBucket = async (name: string, color: string, description?: st
|
|||||||
color,
|
color,
|
||||||
description
|
description
|
||||||
});
|
});
|
||||||
|
await syncSystemTagsAndRules();
|
||||||
toast.success("Bucket created");
|
toast.success("Bucket created");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to create bucket:", 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 }) => {
|
export const updateBucket = async (id: string, data: { name?: string, color?: string, description?: string }) => {
|
||||||
try {
|
try {
|
||||||
await pb.collection(BUCKETS_COLLECTION).update(id, data);
|
await pb.collection(BUCKETS_COLLECTION).update(id, data);
|
||||||
|
if (data.name) {
|
||||||
|
await syncSystemTagsAndRules();
|
||||||
|
}
|
||||||
toast.success("Bucket updated");
|
toast.success("Bucket updated");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to update bucket:", err);
|
console.error("Failed to update bucket:", err);
|
||||||
@@ -1070,12 +1079,50 @@ export const copyTask = async (id: string) => {
|
|||||||
toast.success("Task duplicated");
|
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>) => {
|
export const updateTask = async (id: string, updates: Partial<Task>) => {
|
||||||
if (!pb.authStore.isValid) return;
|
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
|
// Optimistic update with timestamp to prevent stale realtime events
|
||||||
const optimisticUpdates = {
|
const optimisticUpdates = {
|
||||||
...updates,
|
...finalUpdates,
|
||||||
updated: new Date().toISOString()
|
updated: new Date().toISOString()
|
||||||
};
|
};
|
||||||
setStore("tasks", (t) => t.id === id, optimisticUpdates);
|
setStore("tasks", (t) => t.id === id, optimisticUpdates);
|
||||||
@@ -1083,15 +1130,21 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
|
|||||||
try {
|
try {
|
||||||
// Map Task fields to PB fields if needed (dates are ISO strings, so should matches)
|
// Map Task fields to PB fields if needed (dates are ISO strings, so should matches)
|
||||||
// Check for specific fields being updated
|
// Check for specific fields being updated
|
||||||
const pbUpdates: any = { ...updates };
|
const pbUpdates: any = { ...finalUpdates };
|
||||||
if (updates.deletedAt !== undefined) {
|
|
||||||
|
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,
|
// PB expects a date string or null for date fields usually,
|
||||||
// but if we defined deletedAt as a 'date' field in PB.
|
// but if we defined deletedAt as a 'date' field in PB.
|
||||||
// If we defined it as a number in PB? User didn't specify.
|
// 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
|
// Assuming it's a 'Date' field in PB based on previous conversation
|
||||||
// "deletedAt | Date | No"
|
// "deletedAt | Date | No"
|
||||||
if (updates.deletedAt) {
|
if (finalUpdates.deletedAt) {
|
||||||
pbUpdates.deletedAt = new Date(updates.deletedAt).toISOString();
|
pbUpdates.deletedAt = new Date(finalUpdates.deletedAt).toISOString();
|
||||||
} else {
|
} else {
|
||||||
pbUpdates.deletedAt = null; // restore
|
pbUpdates.deletedAt = null; // restore
|
||||||
}
|
}
|
||||||
@@ -1223,7 +1276,8 @@ export const upsertTagDefinition = async (name: string, value: number, color?: s
|
|||||||
name: record.name,
|
name: record.name,
|
||||||
value: record.value,
|
value: record.value,
|
||||||
color: record.color,
|
color: record.color,
|
||||||
theme: record.theme as "light" | "dark"
|
theme: record.theme as "light" | "dark",
|
||||||
|
isBucket: record.isBucket || false
|
||||||
};
|
};
|
||||||
setStore("tagDefinitions", (prev) => [...prev, newDef]);
|
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;
|
if (!pb.authStore.isValid) return;
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Fetch only verified users
|
// 1. Fetch data
|
||||||
const usersPromise = pb.collection('users').getFullList({
|
const [users, pbRules, tagDefinitions] = await Promise.all([
|
||||||
|
pb.collection('users').getFullList({
|
||||||
filter: 'verified = true',
|
filter: 'verified = true',
|
||||||
fields: 'id,name,email,verified',
|
fields: 'id,name,email,verified',
|
||||||
requestKey: null
|
requestKey: null
|
||||||
});
|
}),
|
||||||
|
pb.collection(SHARE_RULES_COLLECTION).getFullList({
|
||||||
// 2. Fetch existing rules DIRECTLY from PB to avoid store race conditions
|
|
||||||
const rulesPromise = pb.collection(SHARE_RULES_COLLECTION).getFullList({
|
|
||||||
filter: `ownerId = "${currentUserId}"`,
|
filter: `ownerId = "${currentUserId}"`,
|
||||||
requestKey: null
|
requestKey: null
|
||||||
});
|
}),
|
||||||
|
pb.collection(TAGS_COLLECTION).getFullList({
|
||||||
|
filter: `user = "${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 => ({
|
const normalizedRules = pbRules.map(r => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
targetUserId: r.targetUserId,
|
targetUserId: r.targetUserId,
|
||||||
@@ -1286,21 +1340,55 @@ export const syncUserTagsAndRules = async () => {
|
|||||||
tagName: r.tagName
|
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) {
|
for (const user of users) {
|
||||||
const userName = user.name || user.email;
|
const userName = user.name || user.email;
|
||||||
if (!userName) continue;
|
if (!userName) continue;
|
||||||
|
|
||||||
// --- Tag Sync ---
|
const tagExists = tagDefinitions.find(t => t.name === userName);
|
||||||
const tagExists = existingTags.find(t => t.name === userName);
|
|
||||||
if (!tagExists) {
|
if (!tagExists) {
|
||||||
// ... create tag (logic unchanged) ...
|
|
||||||
try {
|
try {
|
||||||
const record = await pb.collection(TAGS_COLLECTION).create({
|
const record = await pb.collection(TAGS_COLLECTION).create({
|
||||||
user: currentUserId,
|
user: currentUserId,
|
||||||
name: userName,
|
name: userName,
|
||||||
value: 5,
|
value: 5,
|
||||||
color: "#64748b",
|
color: "#64748b",
|
||||||
theme: "dark"
|
theme: "dark",
|
||||||
|
isUser: true
|
||||||
});
|
});
|
||||||
setStore("tagDefinitions", prev => [...prev, {
|
setStore("tagDefinitions", prev => [...prev, {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
@@ -1313,15 +1401,17 @@ export const syncUserTagsAndRules = async () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error(`Failed to create user tag ${userName}`, e);
|
// console.error(`Failed to create user tag ${userName}`, e);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!tagExists.isUser) {
|
||||||
if (!tagExists.isUser) {
|
try {
|
||||||
|
await pb.collection(TAGS_COLLECTION).update(tagExists.id, { isUser: true });
|
||||||
setStore("tagDefinitions", d => d.id === 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) {
|
if (user.id !== currentUserId) {
|
||||||
// Find ALL rules matching this criteria
|
|
||||||
const matchingRules = normalizedRules.filter(r =>
|
const matchingRules = normalizedRules.filter(r =>
|
||||||
r.targetUserId === user.id &&
|
r.targetUserId === user.id &&
|
||||||
r.type === 'tag' &&
|
r.type === 'tag' &&
|
||||||
@@ -1329,16 +1419,12 @@ export const syncUserTagsAndRules = async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (matchingRules.length === 0) {
|
if (matchingRules.length === 0) {
|
||||||
console.log(`Creating auto-share rule for ${userName}`);
|
await addShareRule('tag', user.id, userName, 'ADD_USER');
|
||||||
await addShareRule('tag', user.id, userName);
|
|
||||||
} else if (matchingRules.length > 1) {
|
} else if (matchingRules.length > 1) {
|
||||||
// DUPLICATE DETECTED: Keep first, delete others
|
|
||||||
console.warn(`Duplicate share rules found for ${userName}. Cleaning up...`);
|
|
||||||
const [, ...remove] = matchingRules;
|
const [, ...remove] = matchingRules;
|
||||||
for (const rule of remove) {
|
for (const rule of remove) {
|
||||||
try {
|
try {
|
||||||
await pb.collection(SHARE_RULES_COLLECTION).delete(rule.id);
|
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));
|
setStore("shareRules", list => list.filter(r => r.id !== rule.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to delete duplicate rule:", e);
|
console.error("Failed to delete duplicate rule:", e);
|
||||||
@@ -1349,7 +1435,7 @@ export const syncUserTagsAndRules = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} 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,
|
ownerId: r.ownerId,
|
||||||
targetUserId: r.targetUserId,
|
targetUserId: r.targetUserId,
|
||||||
type: r.type as 'tag' | 'all',
|
type: r.type as 'tag' | 'all',
|
||||||
tagName: r.tagName
|
tagName: r.tagName,
|
||||||
|
share_mode: r.share_mode || 'ADD_USER'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setStore("shareRules", reconcile(rules));
|
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;
|
if (!pb.authStore.isValid) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1606,7 +1693,8 @@ export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, ta
|
|||||||
ownerId: pb.authStore.model?.id,
|
ownerId: pb.authStore.model?.id,
|
||||||
targetUserId,
|
targetUserId,
|
||||||
type,
|
type,
|
||||||
tagName: type === 'tag' ? tagName : null
|
tagName: type === 'tag' ? tagName : null,
|
||||||
|
share_mode: share_mode || 'ADD_USER'
|
||||||
});
|
});
|
||||||
// Realtime subscription will handle adding to store
|
// Realtime subscription will handle adding to store
|
||||||
toast.success(`Share rule created`);
|
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) => {
|
export const removeShareRule = async (ruleId: string) => {
|
||||||
if (!pb.authStore.isValid) return;
|
if (!pb.authStore.isValid) return;
|
||||||
|
|
||||||
|
|||||||
+84
-14
@@ -1,6 +1,6 @@
|
|||||||
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
|
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
|
||||||
import { ThemeToggle } from "../components/ThemeToggle";
|
import { ThemeToggle } from "../components/ThemeToggle";
|
||||||
import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription } from "@/store";
|
import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription } from "@/store";
|
||||||
import { useTheme } from "@/components/ThemeProvider";
|
import { useTheme } from "@/components/ThemeProvider";
|
||||||
import { Trash2, Undo2, ArrowLeftRight, Hash, Copy, Plus, ChevronDown, ChevronRight, Type, Share2, Users, Tag, Upload, Box, Edit2, Archive } from "lucide-solid";
|
import { Trash2, Undo2, ArrowLeftRight, Hash, Copy, Plus, ChevronDown, ChevronRight, Type, Share2, Users, Tag, Upload, Box, Edit2, Archive } from "lucide-solid";
|
||||||
import { TagPicker } from "@/components/TagPicker";
|
import { TagPicker } from "@/components/TagPicker";
|
||||||
@@ -26,6 +26,7 @@ export const SettingsView: Component = () => {
|
|||||||
const [expandedTemplateId, setExpandedTemplateId] = createSignal<string | null>(null);
|
const [expandedTemplateId, setExpandedTemplateId] = createSignal<string | null>(null);
|
||||||
const [shareUserId, setShareUserId] = createSignal("");
|
const [shareUserId, setShareUserId] = createSignal("");
|
||||||
const [shareType, setShareType] = createSignal<'all' | 'tag'>('all');
|
const [shareType, setShareType] = createSignal<'all' | 'tag'>('all');
|
||||||
|
const [shareMode, setShareMode] = createSignal<'ADD_USER' | 'SEND_TASK'>('ADD_USER');
|
||||||
const [shareTag, setShareTag] = createSignal<string>("");
|
const [shareTag, setShareTag] = createSignal<string>("");
|
||||||
const [allUsers, setAllUsers] = createSignal<Array<{ id: string; name: string }>>([]);
|
const [allUsers, setAllUsers] = createSignal<Array<{ id: string; name: string }>>([]);
|
||||||
const [usersLoading, setUsersLoading] = createSignal(false);
|
const [usersLoading, setUsersLoading] = createSignal(false);
|
||||||
@@ -128,10 +129,47 @@ export const SettingsView: Component = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Share Mode Selection */}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant={shareMode() === 'ADD_USER' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
class="h-7 text-[10px] gap-1 px-2"
|
||||||
|
title="Recipient gets edit access (Collaboration)"
|
||||||
|
onClick={() => setShareMode('ADD_USER')}
|
||||||
|
>
|
||||||
|
<Users size={10} />
|
||||||
|
Collaborate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={shareMode() === 'SEND_TASK' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
class="h-7 text-[10px] gap-1 px-2"
|
||||||
|
title="Task ownership is transferred to recipient (Handoff)"
|
||||||
|
onClick={() => setShareMode('SEND_TASK')}
|
||||||
|
>
|
||||||
|
<ArrowLeftRight size={10} />
|
||||||
|
Handoff
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Show when={shareType() === 'tag'}>
|
<Show when={shareType() === 'tag'}>
|
||||||
<Select
|
<Select
|
||||||
value={shareTag()}
|
value={shareTag()}
|
||||||
onChange={(val) => val && setShareTag(val)}
|
onChange={(val) => {
|
||||||
|
if (val) {
|
||||||
|
setShareTag(val);
|
||||||
|
const tagDef = store.tagDefinitions.find(t => t.name === val);
|
||||||
|
const isBucket = tagDef?.isBucket || store.buckets.some(b => b.name === val);
|
||||||
|
const isUser = tagDef?.isUser;
|
||||||
|
|
||||||
|
if (isBucket) {
|
||||||
|
setShareMode('SEND_TASK');
|
||||||
|
} else if (isUser) {
|
||||||
|
setShareMode('ADD_USER');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
options={store.tagDefinitions.map(t => t.name)}
|
options={store.tagDefinitions.map(t => t.name)}
|
||||||
placeholder="Select a tag"
|
placeholder="Select a tag"
|
||||||
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||||
@@ -168,7 +206,7 @@ export const SettingsView: Component = () => {
|
|||||||
disabled={!shareUserId()}
|
disabled={!shareUserId()}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (shareUserId()) {
|
if (shareUserId()) {
|
||||||
await addShareRule(shareType(), shareUserId(), shareType() === 'tag' ? shareTag() : undefined);
|
await addShareRule(shareType(), shareUserId(), shareType() === 'tag' ? shareTag() : undefined, shareMode());
|
||||||
setShareUserId("");
|
setShareUserId("");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -177,6 +215,12 @@ export const SettingsView: Component = () => {
|
|||||||
Share
|
Share
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-[10px] text-muted-foreground italic px-1 pt-1">
|
||||||
|
{shareMode() === 'ADD_USER'
|
||||||
|
? "User will be added to the task's shared list."
|
||||||
|
: "Task owner will change to the recipient immediately when tagged."}
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Active share rules */}
|
{/* Active share rules */}
|
||||||
@@ -187,7 +231,9 @@ export const SettingsView: Component = () => {
|
|||||||
Sharing With Others
|
Sharing With Others
|
||||||
</h4>
|
</h4>
|
||||||
<For each={store.shareRules.filter(r => {
|
<For each={store.shareRules.filter(r => {
|
||||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
|
||||||
|
const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
|
||||||
|
const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag;
|
||||||
const isOwner = r.ownerId === pb.authStore.model?.id;
|
const isOwner = r.ownerId === pb.authStore.model?.id;
|
||||||
return !isSystem && isOwner;
|
return !isSystem && isOwner;
|
||||||
})} fallback={
|
})} fallback={
|
||||||
@@ -205,7 +251,15 @@ export const SettingsView: Component = () => {
|
|||||||
<p class="text-xs font-bold truncate">To: {getUserName(rule.targetUserId)}</p>
|
<p class="text-xs font-bold truncate">To: {getUserName(rule.targetUserId)}</p>
|
||||||
<p class="text-[9px] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
<p class="text-[9px] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
||||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
||||||
<ArrowLeftRight size={8} class="rotate-45" />
|
<span class="mx-1">•</span>
|
||||||
|
<button
|
||||||
|
class="hover:text-primary transition-colors hover:underline"
|
||||||
|
onClick={() => updateShareRule(rule.id, {
|
||||||
|
share_mode: rule.share_mode === 'SEND_TASK' ? 'ADD_USER' : 'SEND_TASK'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +283,9 @@ export const SettingsView: Component = () => {
|
|||||||
Shared With You
|
Shared With You
|
||||||
</h4>
|
</h4>
|
||||||
<For each={store.shareRules.filter(r => {
|
<For each={store.shareRules.filter(r => {
|
||||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
|
||||||
|
const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
|
||||||
|
const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag;
|
||||||
const isTarget = r.targetUserId === pb.authStore.model?.id;
|
const isTarget = r.targetUserId === pb.authStore.model?.id;
|
||||||
return !isSystem && isTarget;
|
return !isSystem && isTarget;
|
||||||
})} fallback={
|
})} fallback={
|
||||||
@@ -247,7 +303,8 @@ export const SettingsView: Component = () => {
|
|||||||
<p class="text-xs font-bold truncate">From: {getUserName(rule.ownerId)}</p>
|
<p class="text-xs font-bold truncate">From: {getUserName(rule.ownerId)}</p>
|
||||||
<p class="text-[9px] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
<p class="text-[9px] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
||||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
||||||
<ArrowLeftRight size={8} class="-rotate-45" />
|
<span class="mx-1">•</span>
|
||||||
|
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -267,25 +324,38 @@ export const SettingsView: Component = () => {
|
|||||||
</summary>
|
</summary>
|
||||||
<div class="pt-2 pl-4 space-y-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
<div class="pt-2 pl-4 space-y-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||||
<For each={store.shareRules.filter(r => {
|
<For each={store.shareRules.filter(r => {
|
||||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
|
||||||
return isSystem;
|
const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
|
||||||
|
return tagDef?.isUser || tagDef?.isBucket || isBucketTag;
|
||||||
})}>
|
})}>
|
||||||
{(rule) => (
|
{(rule) => {
|
||||||
|
const tagDef = store.tagDefinitions.find(t => t.name === rule.tagName);
|
||||||
|
return (
|
||||||
<div class="flex items-center justify-between p-2 rounded-lg bg-muted/10 border border-border/20 gap-3 opacity-70">
|
<div class="flex items-center justify-between p-2 rounded-lg bg-muted/10 border border-border/20 gap-3 opacity-70">
|
||||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
<Tag size={14} class="text-muted-foreground shrink-0" />
|
<Tag size={14} class="text-muted-foreground shrink-0" />
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="text-sm font-medium truncate flex items-center gap-2">
|
<p class="text-sm font-medium truncate flex items-center gap-2">
|
||||||
{getUserName(rule.targetUserId)}
|
{getUserName(rule.targetUserId)}
|
||||||
<Users size={10} class="text-muted-foreground" />
|
{tagDef?.isUser ? <Users size={10} class="text-muted-foreground" /> : <Archive size={10} class="text-muted-foreground" />}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[10px] text-muted-foreground uppercase tracking-wider">
|
<p class="text-[10px] text-muted-foreground uppercase tracking-wider flex items-center gap-1">
|
||||||
Auto-shared via User Tag
|
Auto-shared via {tagDef?.isUser ? 'User' : 'Bucket'} Tag
|
||||||
|
<span class="mx-1">•</span>
|
||||||
|
<button
|
||||||
|
class="hover:text-primary transition-colors hover:underline"
|
||||||
|
onClick={() => updateShareRule(rule.id, {
|
||||||
|
share_mode: rule.share_mode === 'SEND_TASK' ? 'ADD_USER' : 'SEND_TASK'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
Reference in New Issue
Block a user