diff --git a/src/views/SettingsView.tsx b/src/views/SettingsView.tsx index e60c5ac..64b1007 100644 --- a/src/views/SettingsView.tsx +++ b/src/views/SettingsView.tsx @@ -1,6 +1,6 @@ import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js"; import { ThemeToggle } from "../components/ThemeToggle"; -import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription } from "@/store"; +import { store, restoreTask, deleteTaskPermanently, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription } from "@/store"; 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 { TagPicker } from "@/components/TagPicker"; @@ -62,286 +62,193 @@ export const SettingsView: Component = () => { {pb.authStore.model?.email}

- +
+
+ +
+ +
-
- {/* Appearance */} -
-
-
-

Appearance

-

Select your preferred color scheme.

-
-
- -
-
-
+
- {/* Sharing */} -
-
setIsSharingOpen(!isSharingOpen())} - > -
-

- - Task Sharing -

-

Share all tasks or tasks with specific tags with others.

-
-
- {isSharingOpen() ? : } -
-
- -
- {/* Add new share rule */} -
-
- - + {/* Sharing Category */} +
+

Sharing

+
+ {/* Sharing */} +
+
setIsSharingOpen(!isSharingOpen())} + > +
+

+ + Task Sharing +

+

Share all tasks or tasks with specific tags with others.

- - {/* Share Mode Selection */} -
- - +
+ {isSharingOpen() ? : }
- - - - - -
- - value={allUsers().find(u => u.id === shareUserId()) || null} - onChange={(user) => setShareUserId(user?.id || "")} - options={availableUsers()} - optionValue="id" - optionTextValue="name" - placeholder={usersLoading() ? "Loading..." : "Select user..."} - itemComponent={(itemProps: any) => ( - {itemProps.item.rawValue.name} - )} - > - - > - {(state) => state.selectedOption()?.name || "Select user..."} - - - - - -
-

- {shareMode() === 'ADD_USER' - ? "User will be added to the task's shared list." - : "Task owner will change to the recipient immediately when tagged."} -

-
- {/* Active share rules */} - {/* Active share rules - Outgoing */} -
-

-
- Sharing With Others -

- { - 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; - return !isSystem && isOwner; - })} fallback={ -
- You aren't sharing with anyone yet. -
- }> - {(rule) => ( -
-
-
- {rule.type === 'all' ? : } -
-
-

To: {getUserName(rule.targetUserId)}

-

- {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`} - - -

-
-
+ +
+ {/* Add new share rule */} +
+
+
- )} - -
- {/* Active share rules - Incoming */} -
-

-
- Shared With You -

- { - 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; - return !isSystem && isTarget; - })} fallback={ -
- No one has shared with you yet. -
- }> - {(rule) => ( -
-
-
- {rule.type === 'all' ? : } -
-
-

From: {getUserName(rule.ownerId)}

-

- {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`} - - {rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'} -

-
-
- {/* No delete button for incoming rules unless we add leave logic later */} + {/* Share Mode Selection */} +
+ +
- )} - -
- {/* Active share rules - System */} -
- -
- + + + + +
+ + value={allUsers().find(u => u.id === shareUserId()) || null} + onChange={(user) => setShareUserId(user?.id || "")} + options={availableUsers()} + optionValue="id" + optionTextValue="name" + placeholder={usersLoading() ? "Loading..." : "Select user..."} + itemComponent={(itemProps: any) => ( + {itemProps.item.rawValue.name} + )} + > + + > + {(state) => state.selectedOption()?.name || "Select user..."} + + + + + +
+

+ {shareMode() === 'ADD_USER' + ? "User will be added to the task's shared list." + : "Task owner will change to the recipient immediately when tagged."} +

+
- System Rules (Auto-Generated) -
-
- { - 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); - return tagDef?.isUser || tagDef?.isBucket || isBucketTag; - })}> - {(rule) => { - const tagDef = store.tagDefinitions.find(t => t.name === rule.tagName); - return ( -
+ + {/* Active share rules */} + {/* Active share rules - Outgoing */} +
+

+
+ Sharing With Others +

+ { + 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; + return !isSystem && isOwner; + })} fallback={ +
+ You aren't sharing with anyone yet. +
+ }> + {(rule) => ( +
- +
+ {rule.type === 'all' ? : } +
-

- {/* If it's a user tag, show the person's name. If it's a bucket, show the bucket name (tagName). */} - {tagDef?.isUser ? getUserName(rule.targetUserId) : rule.tagName} - {tagDef?.isUser ? : } -

-

- Auto-shared via {tagDef?.isUser ? 'User' : 'Bucket'} Tag +

To: {getUserName(rule.targetUserId)}

+

+ {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}

+ +
+ )} +
+
+ + {/* Active share rules - Incoming */} +
+

+
+ Shared With You +

+ { + 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; + return !isSystem && isTarget; + })} fallback={ +
+ No one has shared with you yet. +
+ }> + {(rule) => ( +
+
+
+ {rule.type === 'all' ? : } +
+
+

From: {getUserName(rule.ownerId)}

+

+ {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`} + + {rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'} +

+
+
+ {/* No delete button for incoming rules unless we add leave logic later */} +
+ )} +
+
+ + {/* Active share rules - System */} +
+ +
+ +
+ System Rules (Auto-Generated) +
+
+ { + 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); + return tagDef?.isUser || tagDef?.isBucket || isBucketTag; + })}> + {(rule) => { + const tagDef = store.tagDefinitions.find(t => t.name === rule.tagName); + return ( +
+
+ +
+

+ {/* If it's a user tag, show the person's name. If it's a bucket, show the bucket name (tagName). */} + {tagDef?.isUser ? getUserName(rule.targetUserId) : rule.tagName} + {tagDef?.isUser ? : } +

+

+ Auto-shared via {tagDef?.isUser ? 'User' : 'Bucket'} Tag + + +

+
+
+
+ ); + }} +
+
+
+
+ +
+ + + {/* Buckets Manager */} +
+
setIsBucketsOpen(!isBucketsOpen())} + > +
+

+ + Shared Global Buckets +

+

Manage public task buckets and your subscriptions.

+
+
+ {isBucketsOpen() ? : } +
+
+ + +
+ {/* Create Bucket */} +
+

Create New Bucket

+
{ + e.preventDefault(); + const form = e.currentTarget; + const nameInput = form.elements.namedItem('name') as HTMLInputElement; + const colorInput = form.elements.namedItem('color') as HTMLInputElement; + const descInput = form.elements.namedItem('desc') as HTMLInputElement; + + if (nameInput.value) { + createBucket(nameInput.value, colorInput.value || "#6366f1", descInput.value); + nameInput.value = ""; + descInput.value = ""; + form.reset(); + } + }} + > +
+ + +
+
+ + +
+
+
+ + {/* List Buckets */} +
+ + No buckets found. +
+ }> + {(bucket) => { + const isSubscribed = () => store.subscribedBuckets.includes(bucket.id); + const [isEditing, setIsEditing] = createSignal(false); + const [editName, setEditName] = createSignal(bucket.name); + + return ( +
+
+
+
+ +

{bucket.name}

+ +
+ }> + setEditName(e.currentTarget.value)} + onBlur={() => { + if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() }); + setIsEditing(false); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() }); + setIsEditing(false); + } + }} + class="h-6 w-full max-w-[150px] rounded border border-input bg-transparent px-1 text-sm" + autofocus + /> + +

{bucket.description || "No description"}

+
+
+ +
+ + + +
+
+ ); + }} + +
+
+ +
+ + + + + +
+
+ + {/* Tasks Category */} +
+

Tasks

+
+ {/* Task Template Manager */} +
+
+
setIsTemplatesOpen(!isTemplatesOpen())} + > +
+

+ + Task Templates +

+

Save frequent task configurations for quick reuse.

+
+
+ {isTemplatesOpen() ? : } +
+
+ +
+ + +
+ + No templates created yet. +
+ }> + {(template) => { + const [isEditingName, setIsEditingName] = createSignal(false); + const [editName, setEditName] = createSignal(template.name); + const [editTitle, setEditTitle] = createSignal(template.title); + + const isExpanded = () => expandedTemplateId() === template.id; + + const handleSaveName = () => { + if (editName() && editName() !== template.name) { + updateTemplate(template.id, { name: editName() }); + } + setIsEditingName(false); + }; + + const handleSaveTitle = () => { + if (editTitle() !== template.title) { + updateTemplate(template.id, { title: editTitle() }); + } + }; + + return ( +
+
+
+
setExpandedTemplateId(isExpanded() ? null : template.id)} + > + +
+ {isEditingName() ? ( + setEditName(e.currentTarget.value)} + onBlur={handleSaveName} + onKeyDown={(e) => e.key === "Enter" && handleSaveName()} + autofocus + /> + ) : ( +
setExpandedTemplateId(isExpanded() ? null : template.id)}> +

{template.name}

+

+ P:{template.priority} / U:{template.urgency} / {template.tags?.length || 0} Tags +

+
+ )} +
+
+ + +
+
+ + +
+
+
+ + setEditTitle(e.currentTarget.value)} + onBlur={handleSaveTitle} + placeholder="Default task title..." + /> +
+
+
+ + +
+
+ + +
+
+
+ +
+ +
+ updateTemplate(template.id, { tags })} + /> + + {(tag) => ( + + )} + +
+
+ +
+ +
+ }> + { + // For the rich editor, we can use a slightly debounced store update + // to avoid losing focus if the parent re-renders, + // although Tiptap usually handles its own state well. + // But to be safe, let's just update the store. + updateTemplate(template.id, { content: html }); + }} + class="min-h-[100px]" + /> + +
+
+
+
); }}
- -
- - + + - - {/* Buckets Manager */} -
-
setIsBucketsOpen(!isBucketsOpen())} - > -
-

- - Shared Global Buckets -

-

Manage public task buckets and your subscriptions.

-
-
- {isBucketsOpen() ? : } -
-
- - -
- {/* Create Bucket */} -
-

Create New Bucket

-
{ - e.preventDefault(); - const form = e.currentTarget; - const nameInput = form.elements.namedItem('name') as HTMLInputElement; - const colorInput = form.elements.namedItem('color') as HTMLInputElement; - const descInput = form.elements.namedItem('desc') as HTMLInputElement; - - if (nameInput.value) { - createBucket(nameInput.value, colorInput.value || "#6366f1", descInput.value); - nameInput.value = ""; - descInput.value = ""; - form.reset(); - } - }} - > -
- - -
-
- - -
-
-
- - {/* List Buckets */} -
- - No buckets found. -
- }> - {(bucket) => { - const isSubscribed = () => store.subscribedBuckets.includes(bucket.id); - const [isEditing, setIsEditing] = createSignal(false); - const [editName, setEditName] = createSignal(bucket.name); - - return ( -
-
-
-
- -

{bucket.name}

- -
- }> - setEditName(e.currentTarget.value)} - onBlur={() => { - if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() }); - setIsEditing(false); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() }); - setIsEditing(false); - } - }} - class="h-6 w-full max-w-[150px] rounded border border-input bg-transparent px-1 text-sm" - autofocus - /> - -

{bucket.description || "No description"}

-
-
- -
- - - -
-
- ); - }} - -
- -
-
- - {/* Matrix Configuration */} -
-
-
-

- - Matrix Horizon -

-

Adjust the time scale for your strategy grid.

-
-
- -
-
-
- - {/* Global Tag Manager */} -
-
setIsTagsOpen(!isTagsOpen())} - > -
-

- - Global Tags -

-

Manage your global tag definitions and values.

-
-
- {isTagsOpen() ? : } -
-
- - -
-
- !t.isUser && !store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))} fallback={ -
- No custom tags found. -
- }> - {(tag) => { - const [isEditingName, setIsEditingName] = createSignal(false); - const [tempName, setTempName] = createSignal(tag.name); - - const handleRename = () => { - if (tempName() && tempName() !== tag.name) { - renameTagDefinition(tag.name, tempName()); - } - setIsEditingName(false); - }; - - return ( -
-
- upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())} - class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden" - title="Tag accent color" - /> - - {isEditingName() ? ( - setTempName(e.currentTarget.value)} - onBlur={handleRename} - onKeyDown={(e) => e.key === "Enter" && handleRename()} - autofocus - /> - ) : ( - setIsEditingName(true)} - title="Click to rename" - > - {tag.name} - - )} -
- -
-
- Val - -
- - -
-
- ); - }} -
+
+

+ + Import Tasks +

+

Paste a list of tasks from another app to import them.

+
+
+ {isImportOpen() ? : } +
- {/* User/System Tags Expansion */} -
- -
- -
- System Tags (Users & Buckets) -
-
- t.isUser || store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))}> - {(tag) => ( -
-
- upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())} - class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden" - title="Tag accent color" - /> - - {tag.name} - - -
+ +
+ }> + + +
+
+
-
-
- Val - + + Recover + +
- - )} + ); + }} - - - - - - {/* Task Template Manager */} -
-
-
setIsTemplatesOpen(!isTemplatesOpen())} - > -
-

- - Task Templates -

-

Save frequent task configurations for quick reuse.

-
-
- {isTemplatesOpen() ? : } -
-
- + +
+ - -
- - No templates created yet. + {/* Organization Category */} +
+

Organization

+
+ {/* Global Tag Manager */} +
+
setIsTagsOpen(!isTagsOpen())} + > +
+

+ + Global Tags +

+

Manage your global tag definitions and values.

- }> - {(template) => { - const [isEditingName, setIsEditingName] = createSignal(false); - const [editName, setEditName] = createSignal(template.name); - const [editTitle, setEditTitle] = createSignal(template.title); +
+ {isTagsOpen() ? : } +
+
- const isExpanded = () => expandedTemplateId() === template.id; - - const handleSaveName = () => { - if (editName() && editName() !== template.name) { - updateTemplate(template.id, { name: editName() }); - } - setIsEditingName(false); - }; - - const handleSaveTitle = () => { - if (editTitle() !== template.title) { - updateTemplate(template.id, { title: editTitle() }); - } - }; - - return ( -
-
-
-
setExpandedTemplateId(isExpanded() ? null : template.id)} - > - -
- {isEditingName() ? ( - setEditName(e.currentTarget.value)} - onBlur={handleSaveName} - onKeyDown={(e) => e.key === "Enter" && handleSaveName()} - autofocus - /> - ) : ( -
setExpandedTemplateId(isExpanded() ? null : template.id)}> -

{template.name}

-

- P:{template.priority} / U:{template.urgency} / {template.tags?.length || 0} Tags -

-
- )} -
-
- - -
+ +
+
+ !t.isUser && !store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))} fallback={ +
+ No custom tags found.
+ }> + {(tag) => { + const [isEditingName, setIsEditingName] = createSignal(false); + const [tempName, setTempName] = createSignal(tag.name); - -
-
-
- + const handleRename = () => { + if (tempName() && tempName() !== tag.name) { + renameTagDefinition(tag.name, tempName()); + } + setIsEditingName(false); + }; + + return ( +
+
setEditTitle(e.currentTarget.value)} - onBlur={handleSaveTitle} - placeholder="Default task title..." + type="color" + value={tag.color || "#6366f1"} + onInput={(e) => upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())} + class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden" + title="Tag accent color" /> -
-
-
- - -
-
- - -
-
-
-
- -
- updateTemplate(template.id, { tags })} - /> - - {(tag) => ( - - )} - -
-
- -
- -
- }> - { - // For the rich editor, we can use a slightly debounced store update - // to avoid losing focus if the parent re-renders, - // although Tiptap usually handles its own state well. - // But to be safe, let's just update the store. - updateTemplate(template.id, { content: html }); - }} - class="min-h-[100px]" + {isEditingName() ? ( + setTempName(e.currentTarget.value)} + onBlur={handleRename} + onKeyDown={(e) => e.key === "Enter" && handleRename()} + autofocus /> - + ) : ( + setIsEditingName(true)} + title="Click to rename" + > + {tag.name} + + )} +
+ +
+
+ Val + +
+ +
-
- + ); + }} + +
+ + {/* User/System Tags Expansion */} +
+ +
+ +
+ System Tags (Users & Buckets) +
+
+ t.isUser || store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))}> + {(tag) => ( +
+
+ upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())} + class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden" + title="Tag accent color" + /> + + {tag.name} + + +
+ +
+
+ Val + +
+
+
+ )} +
- ); - }} - -
-
-
- - {/* Import Tasks */} -
-
setIsImportOpen(!isImportOpen())} - > -
-

- - Import Tasks -

-

Paste a list of tasks from another app to import them.

-
-
- {isImportOpen() ? : } -
-
- - -
- }> - - -
-
-
- - {/* Trash Manager */} -
-
setIsTrashOpen(!isTrashOpen())} - > -
-

- - Trash -

-

Items are permanently deleted after 7 days.

-
-
- {isTrashOpen() ? : } -
-
- - -
- t.deletedAt)} fallback={ -
- Trash is empty. +
- }> - {(task) => { - const daysLeft = Math.ceil(((task.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24)); - return ( -
-
-

{task.title || "Untitled"}

-

Expires in {Math.max(0, daysLeft)} days

-
-
- - -
-
- ); - }} -
-
-
-
+ + +
+
);