Task sharing wording improved

This commit is contained in:
2026-03-19 18:03:32 -05:00
parent 7f781edd89
commit 9438cffd58
+244 -244
View File
@@ -119,7 +119,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
<Share2 size={16} /> <Share2 size={16} />
Tag Sharing Tag Sharing
</h3> </h3>
<p class="text-sm text-muted-foreground">Share through tags only: `@people` collaborate, `@buckets` route work, and `#notes` stay link-only.</p> <p class="text-sm text-muted-foreground">Share through tags: `@people` to collaborate, `@buckets` to move tasks to buckets, and `#notes` to link tasks to notes.</p>
</div> </div>
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors"> <div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
{isSharingOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />} {isSharingOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
@@ -131,7 +131,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
<div class="p-3 rounded-xl bg-muted/30 border border-border/50 space-y-3"> <div class="p-3 rounded-xl bg-muted/30 border border-border/50 space-y-3">
<h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">User Tag Policies</h4> <h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">User Tag Policies</h4>
<p class="text-[0.6875rem] text-muted-foreground"> <p class="text-[0.6875rem] text-muted-foreground">
Control how each <span class="font-semibold text-foreground">@user</span> tag behaves. Collaborative tags keep work shared. Handoff tags route work into that user&apos;s queue. Control how each <span class="font-semibold text-foreground">@user</span> share tag behaves. Collaborative keeps tasks in both of your lists. Handoff moves tasks from your list to theirs.
</p> </p>
<div class="space-y-2"> <div class="space-y-2">
<For each={userContexts()} fallback={ <For each={userContexts()} fallback={
@@ -234,261 +234,261 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
</Show> </Show>
<Show when={false}> <Show when={false}>
{/* Add new share rule */} {/* Add new share rule */}
<div class="p-3 rounded-xl bg-muted/30 border border-border/50 space-y-3"> <div class="p-3 rounded-xl bg-muted/30 border border-border/50 space-y-3">
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
variant={shareType() === 'all' ? 'default' : 'outline'} variant={shareType() === 'all' ? 'default' : 'outline'}
size="sm" size="sm"
class="h-8 text-xs gap-1" class="h-8 text-xs gap-1"
disabled disabled
title="All-task sharing is no longer supported" title="All-task sharing is no longer supported"
> >
<Users size={12} /> <Users size={12} />
Legacy Off Legacy Off
</Button> </Button>
<Button <Button
variant={shareType() === 'tag' ? 'default' : 'outline'} variant={shareType() === 'tag' ? 'default' : 'outline'}
size="sm" size="sm"
class="h-8 text-xs gap-1" class="h-8 text-xs gap-1"
onClick={() => setShareType('tag')} onClick={() => setShareType('tag')}
> >
<Tag size={12} /> <Tag size={12} />
By Tag By Tag
</Button> </Button>
</div> </div>
{/* Share Mode Selection */} {/* Share Mode Selection */}
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
variant={shareMode() === 'ADD_USER' ? 'default' : 'outline'} variant={shareMode() === 'ADD_USER' ? 'default' : 'outline'}
size="sm" size="sm"
class="h-7 text-[0.625rem] gap-1 px-2" class="h-7 text-[0.625rem] gap-1 px-2"
title="Recipient gets edit access (Collaboration)" title="Recipient gets edit access (Collaboration)"
onClick={() => setShareMode('ADD_USER')} onClick={() => setShareMode('ADD_USER')}
> >
<Users size={10} /> <Users size={10} />
Collaborate Collaborate
</Button> </Button>
<Button <Button
variant={shareMode() === 'SEND_TASK' ? 'default' : 'outline'} variant={shareMode() === 'SEND_TASK' ? 'default' : 'outline'}
size="sm" size="sm"
class="h-7 text-[0.625rem] gap-1 px-2" class="h-7 text-[0.625rem] gap-1 px-2"
title="Task ownership is transferred to recipient (Handoff)" title="Task ownership is transferred to recipient (Handoff)"
onClick={() => setShareMode('SEND_TASK')} onClick={() => setShareMode('SEND_TASK')}
> >
<ArrowLeftRight size={10} /> <ArrowLeftRight size={10} />
Handoff Handoff
</Button> </Button>
</div> </div>
<Show when={shareType() === 'tag'}> <Show when={shareType() === 'tag'}>
<Select <Select
value={shareTag()} value={shareTag()}
onChange={(val) => { onChange={(val) => {
if (val) { if (val) {
setShareTag(val); setShareTag(val);
const tagDef = store.tagDefinitions.find(t => t.name === val); const tagDef = store.tagDefinitions.find(t => t.name === val);
const isBucket = tagDef?.isBucket || store.buckets.some(b => b.name === val); const isBucket = tagDef?.isBucket || store.buckets.some(b => b.name === val);
const isUser = tagDef?.isUser; const isUser = tagDef?.isUser;
if (isBucket) { if (isBucket) {
setShareMode('SEND_TASK'); setShareMode('SEND_TASK');
} else if (isUser) { } else if (isUser) {
setShareMode('ADD_USER'); setShareMode('ADD_USER');
}
} }
} }}
}} options={store.tagDefinitions.filter(t => t.name.startsWith("@")).map(t => t.name)}
options={store.tagDefinitions.filter(t => t.name.startsWith("@")).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>} >
> <SelectTrigger class="h-9 text-sm">
<SelectTrigger class="h-9 text-sm"> <span>{shareTag() || "Select a tag..."}</span>
<span>{shareTag() || "Select a tag..."}</span> </SelectTrigger>
</SelectTrigger> <SelectContent />
<SelectContent /> </Select>
</Select> </Show>
</Show>
<div class="flex gap-2">
<Select<{ id: string; name: string }>
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) => (
<SelectItem item={itemProps.item}>{itemProps.item.rawValue.name}</SelectItem>
)}
>
<SelectTrigger class="flex-1 h-9 text-sm">
<SelectValue<{ id: string; name: string }>>
{(state) => state.selectedOption()?.name || "Select user..."}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
<Button
size="sm"
class="h-9 gap-2"
disabled={!shareUserId() || !shareTag()}
onClick={async () => {
if (shareUserId() && shareTag()) {
await addShareRule('tag', shareUserId(), shareTag(), shareMode());
setShareUserId("");
}
}}
>
<Plus size={14} />
Share
</Button>
</div>
<p class="text-[0.625rem] text-muted-foreground italic px-1 pt-1">
{shareMode() === 'ADD_USER'
? "Collaborative contexts add access through the selected @tag."
: "Handoff contexts transfer ownership when that @tag is applied."}
</p>
<div class="flex gap-2">
<Select<{ id: string; name: string }>
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) => (
<SelectItem item={itemProps.item}>{itemProps.item.rawValue.name}</SelectItem>
)}
>
<SelectTrigger class="flex-1 h-9 text-sm">
<SelectValue<{ id: string; name: string }>>
{(state) => state.selectedOption()?.name || "Select user..."}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
<Button
size="sm"
class="h-9 gap-2"
disabled={!shareUserId() || !shareTag()}
onClick={async () => {
if (shareUserId() && shareTag()) {
await addShareRule('tag', shareUserId(), shareTag(), shareMode());
setShareUserId("");
}
}}
>
<Plus size={14} />
Share
</Button>
</div> </div>
<p class="text-[0.625rem] text-muted-foreground italic px-1 pt-1">
{shareMode() === 'ADD_USER'
? "Collaborative contexts add access through the selected @tag."
: "Handoff contexts transfer ownership when that @tag is applied."}
</p>
</div> {/* Active share rules */}
{/* Active share rules - Outgoing */}
{/* Active share rules */} <div class="space-y-3">
{/* Active share rules - Outgoing */} <h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
<div class="space-y-3"> <div class="w-1 h-1 rounded-full bg-primary" />
<h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2"> Sharing With Others
<div class="w-1 h-1 rounded-full bg-primary" /> </h4>
Sharing With Others
</h4>
<For each={store.shareRules.filter(r => {
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={
<div class="text-center py-4 text-muted-foreground text-[0.625rem] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
You aren't sharing with anyone yet.
</div>
}>
{(rule) => (
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3 group/rule hover:bg-muted/30 transition-all">
<div class="flex items-center gap-3 min-w-0 flex-1">
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0 transition-transform group-hover/rule:scale-110">
{rule.type === 'all' ? <Users size={14} /> : <Tag size={14} />}
</div>
<div class="min-w-0">
<p class="text-xs font-bold truncate">To: {getUserName(rule.targetUserId)}</p>
<p class="text-[0.5625rem] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName} `}
<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>
</div>
</div>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 hover:bg-destructive/10 hover:text-destructive rounded-lg transition-all opacity-0 group-hover/rule:opacity-100"
onClick={() => removeShareRule(rule.id)}
>
<Trash2 size={14} />
</Button>
</div>
)}
</For>
</div>
{/* Active share rules - Incoming */}
<div class="space-y-3 pt-4 border-t border-border/20">
<h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
<div class="w-1 h-1 rounded-full bg-indigo-500" />
Shared With You
</h4>
<For each={store.shareRules.filter(r => {
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={
<div class="text-center py-4 text-muted-foreground text-[0.625rem] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
No one has shared with you yet.
</div>
}>
{(rule) => (
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3 group/rule hover:bg-muted/30 transition-all opacity-80">
<div class="flex items-center gap-3 min-w-0 flex-1">
<div class="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center text-indigo-500 shrink-0">
{rule.type === 'all' ? <Users size={14} /> : <Tag size={14} />}
</div>
<div class="min-w-0">
<p class="text-xs font-bold truncate">From: {getUserName(rule.ownerId)}</p>
<p class="text-[0.5625rem] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName} `}
<span class="mx-1">•</span>
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
</p>
</div>
</div>
{/* No delete button for incoming rules unless we add leave logic later */}
</div>
)}
</For>
</div>
{/* Active share rules - System */}
<details class="group/details">
<summary class="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground cursor-pointer hover:text-foreground transition-colors py-2 select-none">
<div class="bg-muted/50 p-1 rounded-md group-open/details:rotate-90 transition-transform">
<ChevronRight size={12} />
</div>
System Rules (Auto-Generated)
</summary>
<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 tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null; 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 isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
return tagDef?.isUser || tagDef?.isBucket || isBucketTag; const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag;
})}> const isOwner = r.ownerId === pb.authStore.model?.id;
{(rule) => { return !isSystem && isOwner;
const tagDef = store.tagDefinitions.find(t => t.name === rule.tagName); })} fallback={
return ( <div class="text-center py-4 text-muted-foreground text-[0.625rem] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
<div class="flex items-center justify-between p-2 rounded-lg bg-muted/10 border border-border/20 gap-3 opacity-70"> You aren't sharing with anyone yet.
<div class="flex items-center gap-3 min-w-0 flex-1"> </div>
<Tag size={14} class="text-muted-foreground shrink-0" /> }>
<div class="min-w-0"> {(rule) => (
<p class="text-sm font-medium truncate flex items-center gap-2"> <div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3 group/rule hover:bg-muted/30 transition-all">
{/* If it's a user tag, show the person's name. If it's a bucket, show the bucket name (tagName). */} <div class="flex items-center gap-3 min-w-0 flex-1">
{tagDef?.isUser ? getUserName(rule.targetUserId) : rule.tagName} <div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0 transition-transform group-hover/rule:scale-110">
{tagDef?.isUser ? <Users size={10} class="text-muted-foreground" /> : <Archive size={10} class="text-muted-foreground" />} {rule.type === 'all' ? <Users size={14} /> : <Tag size={14} />}
</p> </div>
<p class="text-[0.625rem] text-muted-foreground uppercase tracking-wider flex items-center gap-1"> <div class="min-w-0">
Auto-shared via {tagDef?.isUser ? 'User' : 'Bucket'} Tag <p class="text-xs font-bold truncate">To: {getUserName(rule.targetUserId)}</p>
<span class="mx-1"></span> <p class="text-[0.5625rem] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
<button {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName} `}
class="hover:text-primary transition-colors hover:underline" <span class="mx-1">•</span>
onClick={() => updateShareRule(rule.id, { <button
share_mode: rule.share_mode === 'SEND_TASK' ? 'ADD_USER' : 'SEND_TASK' 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> {rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
</div> </button>
</p>
</div> </div>
</div> </div>
); <Button
}} variant="ghost"
size="icon"
class="h-8 w-8 hover:bg-destructive/10 hover:text-destructive rounded-lg transition-all opacity-0 group-hover/rule:opacity-100"
onClick={() => removeShareRule(rule.id)}
>
<Trash2 size={14} />
</Button>
</div>
)}
</For> </For>
</div> </div>
</details>
{/* Active share rules - Incoming */}
<div class="space-y-3 pt-4 border-t border-border/20">
<h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
<div class="w-1 h-1 rounded-full bg-indigo-500" />
Shared With You
</h4>
<For each={store.shareRules.filter(r => {
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={
<div class="text-center py-4 text-muted-foreground text-[0.625rem] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
No one has shared with you yet.
</div>
}>
{(rule) => (
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3 group/rule hover:bg-muted/30 transition-all opacity-80">
<div class="flex items-center gap-3 min-w-0 flex-1">
<div class="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center text-indigo-500 shrink-0">
{rule.type === 'all' ? <Users size={14} /> : <Tag size={14} />}
</div>
<div class="min-w-0">
<p class="text-xs font-bold truncate">From: {getUserName(rule.ownerId)}</p>
<p class="text-[0.5625rem] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName} `}
<span class="mx-1">•</span>
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
</p>
</div>
</div>
{/* No delete button for incoming rules unless we add leave logic later */}
</div>
)}
</For>
</div>
{/* Active share rules - System */}
<details class="group/details">
<summary class="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground cursor-pointer hover:text-foreground transition-colors py-2 select-none">
<div class="bg-muted/50 p-1 rounded-md group-open/details:rotate-90 transition-transform">
<ChevronRight size={12} />
</div>
System Rules (Auto-Generated)
</summary>
<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 => {
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 (
<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">
<Tag size={14} class="text-muted-foreground shrink-0" />
<div class="min-w-0">
<p class="text-sm font-medium truncate flex items-center gap-2">
{/* 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 ? <Users size={10} class="text-muted-foreground" /> : <Archive size={10} class="text-muted-foreground" />}
</p>
<p class="text-[0.625rem] text-muted-foreground uppercase tracking-wider flex items-center gap-1">
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>
</div>
</div>
</div>
);
}}
</For>
</div>
</details>
</Show> </Show>
</div> </div>
</Show> </Show>
@@ -506,7 +506,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
<Box size={16} /> <Box size={16} />
Shared Buckets Shared Buckets
</h3> </h3>
<p class="text-sm text-muted-foreground">Each bucket is a real `@context` with a policy, metadata, soft delete, and personal pinning.</p> <p class="text-sm text-muted-foreground">Buckets are shared spaces where tasks can be moved and collaborated on.</p>
</div> </div>
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors"> <div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
{isBucketsOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />} {isBucketsOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}