added system user tags/sharing and workspaces for recieving share-all rules
This commit is contained in:
+184
-96
@@ -179,33 +179,73 @@ export const SettingsView: Component = () => {
|
||||
</div>
|
||||
|
||||
{/* Active share rules */}
|
||||
<For each={store.shareRules} fallback={
|
||||
<div class="text-center py-6 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||
No active share rules.
|
||||
</div>
|
||||
}>
|
||||
{(rule) => (
|
||||
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
{rule.type === 'all' ? <Users size={14} class="text-primary shrink-0" /> : <Tag size={14} class="text-primary shrink-0" />}
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium truncate">{getUserName(rule.targetUserId)}</p>
|
||||
<p class="text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={() => removeShareRule(rule.id)}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
{/* Active share rules - Custom */}
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-xs font-bold uppercase tracking-wider text-muted-foreground pb-1">Custom Rules</h4>
|
||||
<For each={store.shareRules.filter(r => {
|
||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
||||
return !isSystem;
|
||||
})} fallback={
|
||||
<div class="text-center py-4 text-muted-foreground text-xs italic border border-dashed border-border rounded-xl">
|
||||
No custom share rules.
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
}>
|
||||
{(rule) => (
|
||||
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
{rule.type === 'all' ? <Users size={14} class="text-primary shrink-0" /> : <Tag size={14} class="text-primary shrink-0" />}
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium truncate">{getUserName(rule.targetUserId)}</p>
|
||||
<p class="text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={() => removeShareRule(rule.id)}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</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 isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
||||
return isSystem;
|
||||
})}>
|
||||
{(rule) => (
|
||||
<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">
|
||||
{getUserName(rule.targetUserId)}
|
||||
<Users size={10} class="text-muted-foreground" />
|
||||
</p>
|
||||
<p class="text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
Auto-shared via User Tag
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
@@ -257,84 +297,132 @@ export const SettingsView: Component = () => {
|
||||
|
||||
<Show when={isTagsOpen()}>
|
||||
<div class="space-y-2 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<For each={store.tagDefinitions.sort((a, b) => a.name.localeCompare(b.name))} fallback={
|
||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||
No specific tag definitions found.
|
||||
</div>
|
||||
}>
|
||||
{(tag) => {
|
||||
const [isEditingName, setIsEditingName] = createSignal(false);
|
||||
const [tempName, setTempName] = createSignal(tag.name);
|
||||
<div class="space-y-2">
|
||||
<For each={store.tagDefinitions.filter(t => !t.isUser).sort((a, b) => a.name.localeCompare(b.name))} fallback={
|
||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||
No custom tags found.
|
||||
</div>
|
||||
}>
|
||||
{(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);
|
||||
};
|
||||
const handleRename = () => {
|
||||
if (tempName() && tempName() !== tag.name) {
|
||||
renameTagDefinition(tag.name, tempName());
|
||||
}
|
||||
setIsEditingName(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded-xl bg-muted/30 border border-border/50 gap-3 sm:gap-4 group">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<input
|
||||
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"
|
||||
/>
|
||||
|
||||
{isEditingName() ? (
|
||||
return (
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded-xl bg-muted/30 border border-border/50 gap-3 sm:gap-4 group">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<input
|
||||
class="flex h-7 min-w-0 w-full rounded-md border border-input bg-background px-2 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={tempName()}
|
||||
onInput={(e) => setTempName(e.currentTarget.value)}
|
||||
onBlur={handleRename}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleRename()}
|
||||
autofocus
|
||||
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"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
class="font-medium cursor-pointer hover:underline decoration-dashed underline-offset-4 truncate"
|
||||
onClick={() => setIsEditingName(true)}
|
||||
title="Click to rename"
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center grow sm:grow-0 justify-between sm:justify-end gap-3 sm:gap-4 shrink-0 border-t sm:border-t-0 pt-2 sm:pt-0 border-border/20">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<select
|
||||
value={String(tag.value)}
|
||||
onChange={(e) => upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
|
||||
class="flex h-8 w-16 items-center justify-between rounded-md border border-input bg-background px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
>
|
||||
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1", "0"]}>
|
||||
{(v) => <option value={v}>{v}</option>}
|
||||
</For>
|
||||
</select>
|
||||
{isEditingName() ? (
|
||||
<input
|
||||
class="flex h-7 min-w-0 w-full rounded-md border border-input bg-background px-2 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={tempName()}
|
||||
onInput={(e) => setTempName(e.currentTarget.value)}
|
||||
onBlur={handleRename}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleRename()}
|
||||
autofocus
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
class="font-medium cursor-pointer hover:underline decoration-dashed underline-offset-4 truncate"
|
||||
onClick={() => setIsEditingName(true)}
|
||||
title="Click to rename"
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => {
|
||||
if (confirm(`Delete tag definition for "${tag.name}" ? `)) {
|
||||
removeTagDefinition(tag.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
<div class="flex items-center grow sm:grow-0 justify-between sm:justify-end gap-3 sm:gap-4 shrink-0 border-t sm:border-t-0 pt-2 sm:pt-0 border-border/20">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<select
|
||||
value={String(tag.value)}
|
||||
onChange={(e) => upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
|
||||
class="flex h-8 w-16 items-center justify-between rounded-md border border-input bg-background px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
>
|
||||
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1", "0"]}>
|
||||
{(v) => <option value={v}>{v}</option>}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => {
|
||||
if (confirm(`Delete tag definition for "${tag.name}" ? `)) {
|
||||
removeTagDefinition(tag.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* User Tags Expansion */}
|
||||
<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 pt-4">
|
||||
<div class="bg-muted/50 p-1 rounded-md group-open/details:rotate-90 transition-transform">
|
||||
<ChevronRight size={12} />
|
||||
</div>
|
||||
User Tags (System)
|
||||
</summary>
|
||||
<div class="pt-2 pl-4 space-y-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<For each={store.tagDefinitions.filter(t => t.isUser).sort((a, b) => a.name.localeCompare(b.name))}>
|
||||
{(tag) => (
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-2 rounded-lg bg-muted/10 border border-border/20 gap-3 sm:gap-4 group opacity-80">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<input
|
||||
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"
|
||||
/>
|
||||
<span class="font-medium truncate flex items-center gap-2 text-sm">
|
||||
{tag.name}
|
||||
<Users size={12} class="text-muted-foreground opacity-70" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center grow sm:grow-0 justify-between sm:justify-end gap-3 sm:gap-4 shrink-0 border-t sm:border-t-0 pt-2 sm:pt-0 border-border/20">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<select
|
||||
value={String(tag.value)}
|
||||
onChange={(e) => upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
|
||||
class="flex h-7 w-14 items-center justify-between rounded-md border border-input bg-background/50 px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
>
|
||||
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1", "0"]}>
|
||||
{(v) => <option value={v}>{v}</option>}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user