Compare commits
3 Commits
3907de048b
...
41c9e2f3a0
| Author | SHA1 | Date | |
|---|---|---|---|
| 41c9e2f3a0 | |||
| 2abf4ae5a9 | |||
| 3826a2cbe2 |
@@ -1,5 +1,5 @@
|
||||
import { type Component, createSignal, For, Show } from "solid-js";
|
||||
import { Search, X, Hash, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark } from "lucide-solid";
|
||||
import { Search, X, Tag, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark } from "lucide-solid";
|
||||
import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate, loadAllHistory } from "@/store";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -235,7 +235,7 @@ export const FilterBar: Component = () => {
|
||||
setFilter({ tags: newTags });
|
||||
}}
|
||||
>
|
||||
<Show when={tag.excluded} fallback={<Hash size={12} class="opacity-70" />}>
|
||||
<Show when={tag.excluded} fallback={<Tag size={12} class="opacity-70" />}>
|
||||
<Minus size={12} class="opacity-100" />
|
||||
</Show>
|
||||
<span class={cn(tag.excluded && "line-through opacity-70")}>{tag.name}</span>
|
||||
|
||||
@@ -81,7 +81,7 @@ export const NotesSidebar: Component<{
|
||||
<For each={note.tags.slice(0, 3)}>
|
||||
{(t) => (
|
||||
<span class="text-[0.6rem] px-1.5 py-0.5 bg-muted rounded-md text-muted-foreground whitespace-nowrap border border-border/50">
|
||||
#{t}
|
||||
{t}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { store, upsertTagDefinition } from "@/store";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Hash, Plus } from "lucide-solid";
|
||||
import { Tag, Plus } from "lucide-solid";
|
||||
|
||||
interface TagPickerProps {
|
||||
selectedTags: string[];
|
||||
@@ -24,8 +24,8 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
|
||||
const allTagNames = () => {
|
||||
const definedTags = store.tagDefinitions.map(d => d.name);
|
||||
const bucketTags = store.buckets.map(b => b.name);
|
||||
const noteTags = store.notes.map(n => n.title);
|
||||
const bucketTags = store.buckets.map(b => `@${b.name}`);
|
||||
const noteTags = store.notes.map(n => `#${n.title}`);
|
||||
return [...new Set([...definedTags, ...bucketTags, ...noteTags])].sort();
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
if (!name) return;
|
||||
|
||||
// update global definition
|
||||
const bucket = store.buckets.find(b => b.name === name);
|
||||
const bucket = store.buckets.find(b => b.name === name || `@${b.name}` === name);
|
||||
const color = bucket ? bucket.color : undefined;
|
||||
upsertTagDefinition(name, valueScore(), color, resolvedTheme());
|
||||
|
||||
@@ -71,7 +71,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
return (
|
||||
<Popover open={open()} onOpenChange={setOpen}>
|
||||
<PopoverTrigger as={Button} variant="ghost" size="sm" class="h-8 border-dashed flex gap-2 items-center text-muted-foreground hover:text-foreground">
|
||||
<Hash size={14} />
|
||||
<Tag size={14} />
|
||||
<span>Add Tag</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-3 w-[min(calc(100vw-2rem),280px)] flex flex-col gap-3" align="start">
|
||||
|
||||
@@ -33,8 +33,9 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
|
||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
// We are in a bucket view. Hide the tag that matches this bucket's name.
|
||||
const bucketName = (ctx as { name: string }).name.toLowerCase();
|
||||
return tags.filter(t => t.toLowerCase() !== bucketName);
|
||||
// Buckets are now tagged with '@' + name.
|
||||
const bucketTagName = `@${(ctx as { name: string }).name.toLowerCase()}`;
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName);
|
||||
}
|
||||
|
||||
return tags;
|
||||
|
||||
@@ -101,10 +101,10 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
|
||||
// If in a bucket view, ensure the bucket tag is preserved (it was hidden from the UI so it's missing from 'tags')
|
||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
const bucketName = (ctx as { name: string }).name;
|
||||
const bucketTagName = `@${(ctx as { name: string }).name}`;
|
||||
// Check case-insensitive existence
|
||||
if (!newTags.some(t => t.toLowerCase() === bucketName.toLowerCase())) {
|
||||
newTags.push(bucketName);
|
||||
if (!newTags.some(t => t.toLowerCase() === bucketTagName.toLowerCase())) {
|
||||
newTags.push(bucketTagName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +129,9 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
|
||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
// We are in a bucket view. Hide the tag that matches this bucket's name.
|
||||
const bucketName = (ctx as { name: string }).name.toLowerCase();
|
||||
return tags.filter(t => t.toLowerCase() !== bucketName);
|
||||
// Buckets are now tagged with '@' + name.
|
||||
const bucketTagName = `@${(ctx as { name: string }).name.toLowerCase()}`;
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName);
|
||||
}
|
||||
|
||||
return tags;
|
||||
|
||||
+30
-66
@@ -260,28 +260,16 @@ export const matchesFilter = (task: Task) => {
|
||||
task.tags?.includes(r.tagName || "");
|
||||
});
|
||||
|
||||
// Bucket visibility logic for "My Bucket":
|
||||
// Users requested: "Any user who adds that bucket to their workspaces list can view the workspace and see tasks in that bucket from all users."
|
||||
// AND "In settings a user should be able to see and edit all buckets... They should be able to add any bucket to their workspaces list."
|
||||
// When in 'mine', do we show bucket tasks?
|
||||
// Typically 'My Bucket' is just MY stuff + stuff shared explicitly with me.
|
||||
// Buckets seem to be separate "Contexts" in the sidebar.
|
||||
// However, if the user wants them mixed in, they didn't explicitly say "mix into my main list",
|
||||
// they said "add that bucket to their workspaces list". This usually implies a separate view.
|
||||
// BUT, if I create a task and tag it "Shop", I expect to see it.
|
||||
|
||||
// If I own the task, I see it.
|
||||
// If it's shared with me, I see it.
|
||||
|
||||
// Bucket tasks are now tagged with '@' + name.
|
||||
if (!isOwner && !isExplicitlyShared && !isTagRuleShared) return false;
|
||||
} else if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
// Bucket View
|
||||
const bucketProxy = store.buckets.find(b => b.id === (ctx as { bucketId: string }).bucketId);
|
||||
if (!bucketProxy) return false;
|
||||
|
||||
// Show tasks that have the bucket name as a tag
|
||||
// The requirement: "if any user adds a tag that is the name of a bucket to a task that task is then made public and moved to the bucket."
|
||||
if (!task.tags?.includes(bucketProxy.name)) return false;
|
||||
// Show tasks that have the bucket name (prefixed with @) as a tag
|
||||
const bucketTagName = `@${bucketProxy.name}`;
|
||||
if (!task.tags?.includes(bucketTagName)) return false;
|
||||
} else {
|
||||
// Context is Oversight for specific user
|
||||
// Show ONLY tasks owned by that user
|
||||
@@ -545,13 +533,9 @@ const isTaskInSubscribedBucket = (task: any): boolean => {
|
||||
if (taskTags.length === 0) return false;
|
||||
|
||||
// Check if any of the task's tags match a bucket we are subscribed to
|
||||
// We need to look up bucket names from our subscribed bucket IDs
|
||||
// This is slightly expensive if we iterate.
|
||||
// Optimization: Pre-calculate subscribed bucket names?
|
||||
// For now, simple find.
|
||||
return store.subscribedBuckets.some(subId => {
|
||||
const bucket = store.buckets.find(b => b.id === subId);
|
||||
return bucket && taskTags.includes(bucket.name);
|
||||
return bucket && taskTags.includes(`@${bucket.name}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1315,17 +1299,18 @@ const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> =>
|
||||
|
||||
// Check for any tag that triggers a SEND_TASK rule
|
||||
for (const tag of updates.tags) {
|
||||
// Handle @ prefix in rule matching
|
||||
const rule = store.shareRules.find(r =>
|
||||
r.ownerId === currentUserId &&
|
||||
r.type === 'tag' &&
|
||||
r.tagName === tag &&
|
||||
(r.tagName === tag || `@${r.tagName}` === tag) &&
|
||||
r.share_mode === 'SEND_TASK'
|
||||
);
|
||||
|
||||
if (rule) {
|
||||
console.log(`Handoff triggered by tag "${tag}" -> Transferring to ${rule.targetUserId}`);
|
||||
|
||||
// Check if it's a bucket tag (target is self, but tag is a bucket)
|
||||
// Check if it's a bucket tag
|
||||
const isBucketRule = rule.targetUserId === currentUserId && store.tagDefinitions.find(t => t.name === tag)?.isBucket;
|
||||
|
||||
if (isBucketRule) {
|
||||
@@ -1633,12 +1618,13 @@ export const syncSystemTagsAndRules = async () => {
|
||||
|
||||
// --- Sync Bucket Tags ---
|
||||
for (const bucket of store.buckets) {
|
||||
const tagExists = tagDefinitions.find(t => t.name === bucket.name);
|
||||
if (!tagExists) {
|
||||
const bucketTagName = `@${bucket.name}`;
|
||||
const tagExistsExact = tagDefinitions.find(t => t.name.toLowerCase() === bucketTagName.toLowerCase());
|
||||
if (!tagExistsExact) {
|
||||
try {
|
||||
const record = await pb.collection(TAGS_COLLECTION).create({
|
||||
user: currentUserId,
|
||||
name: bucket.name,
|
||||
name: bucketTagName,
|
||||
value: 5,
|
||||
color: bucket.color || "#64748b",
|
||||
theme: "dark",
|
||||
@@ -1653,23 +1639,22 @@ export const syncSystemTagsAndRules = async () => {
|
||||
isBucket: true
|
||||
}]);
|
||||
} catch (e) {
|
||||
console.error(`Failed to create bucket tag ${bucket.name}`, e);
|
||||
console.error(`Failed to create bucket tag ${bucketTagName}`, e);
|
||||
}
|
||||
} else if (!tagExists.isBucket) {
|
||||
} else if (!tagExistsExact.isBucket) {
|
||||
try {
|
||||
await pb.collection(TAGS_COLLECTION).update(tagExists.id, { isBucket: true });
|
||||
setStore("tagDefinitions", d => d.id === tagExists.id, { isBucket: true });
|
||||
await pb.collection(TAGS_COLLECTION).update(tagExistsExact.id, { isBucket: true });
|
||||
setStore("tagDefinitions", d => d.id === tagExistsExact.id, { isBucket: true });
|
||||
} catch (e) {
|
||||
console.error(`Failed to flag tag ${bucket.name} as bucket`, e);
|
||||
console.error(`Failed to flag tag ${bucketTagName} as bucket`, e);
|
||||
}
|
||||
}
|
||||
// --- Sync Bucket System Rule ---
|
||||
// Ensure a ShareRule exists for this bucket so it appears in "System Rules"
|
||||
const bucketRuleExists = normalizedRules.some(r =>
|
||||
r.ownerId === currentUserId &&
|
||||
r.targetUserId === currentUserId &&
|
||||
r.type === 'tag' &&
|
||||
r.tagName === bucket.name
|
||||
(r.tagName === bucket.name || r.tagName === bucketTagName)
|
||||
);
|
||||
|
||||
if (!bucketRuleExists) {
|
||||
@@ -1678,22 +1663,23 @@ export const syncSystemTagsAndRules = async () => {
|
||||
ownerId: currentUserId,
|
||||
targetUserId: currentUserId,
|
||||
type: 'tag',
|
||||
tagName: bucket.name,
|
||||
tagName: bucketTagName,
|
||||
share_mode: 'SEND_TASK'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to ensure system rule for bucket ${bucket.name}`, e);
|
||||
console.error(`Failed to ensure system rule for bucket ${bucketTagName}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sync User Tags ---
|
||||
for (const user of users) {
|
||||
const userName = user.name || user.email;
|
||||
if (!userName) continue;
|
||||
const originalName = user.name || user.email;
|
||||
if (!originalName) continue;
|
||||
const userName = `@${originalName}`;
|
||||
|
||||
const tagExists = tagDefinitions.find(t => t.name === userName);
|
||||
if (!tagExists) {
|
||||
const tagExistsExact = tagDefinitions.find(t => t.name.toLowerCase() === userName.toLowerCase());
|
||||
if (!tagExistsExact) {
|
||||
try {
|
||||
const record = await pb.collection(TAGS_COLLECTION).create({
|
||||
user: currentUserId,
|
||||
@@ -1714,43 +1700,22 @@ export const syncSystemTagsAndRules = async () => {
|
||||
} catch (e) {
|
||||
// console.error(`Failed to create user tag ${userName}`, e);
|
||||
}
|
||||
} else if (!tagExists.isUser) {
|
||||
} else if (!tagExistsExact.isUser) {
|
||||
try {
|
||||
await pb.collection(TAGS_COLLECTION).update(tagExists.id, { isUser: true });
|
||||
setStore("tagDefinitions", d => d.id === tagExists.id, { isUser: true });
|
||||
await pb.collection(TAGS_COLLECTION).update(tagExistsExact.id, { isUser: true });
|
||||
setStore("tagDefinitions", d => d.id === tagExistsExact.id, { isUser: true });
|
||||
} catch (e) {
|
||||
console.error(`Failed to flag tag ${userName} as user`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- User Share Rule Sync ---
|
||||
// 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 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
|
||||
}
|
||||
*/
|
||||
|
||||
// --- Ensure Self-Rule for Current User (Global Subscription) ---
|
||||
// This is the critical piece for "Global Sharing".
|
||||
// We must ensure the user has a rule: Owner=Me, Target=Me, Tag=Me.
|
||||
if (user.id === currentUserId) {
|
||||
const selfRuleExists = normalizedRules.some(r =>
|
||||
r.ownerId === currentUserId &&
|
||||
r.targetUserId === currentUserId &&
|
||||
r.type === 'tag' &&
|
||||
r.tagName === userName
|
||||
(r.tagName === originalName || r.tagName === userName)
|
||||
);
|
||||
|
||||
if (!selfRuleExists) {
|
||||
@@ -1760,10 +1725,9 @@ export const syncSystemTagsAndRules = async () => {
|
||||
targetUserId: currentUserId,
|
||||
type: 'tag',
|
||||
tagName: userName,
|
||||
share_mode: 'ADD_USER' // Mode doesn't strictly matter for self-rule visibility, but ADD_USER is safe default
|
||||
share_mode: 'ADD_USER'
|
||||
});
|
||||
|
||||
// Update local store immediately to ensure loadAllHistory sees it
|
||||
const newRule: ShareRule = {
|
||||
id: record.id,
|
||||
ownerId: currentUserId,
|
||||
|
||||
@@ -2,14 +2,12 @@ import { type Component, createSignal, createMemo, For, Show } from "solid-js";
|
||||
import { store, renameTagDefinition } from "@/store";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { type Note } from "@/store";
|
||||
import { Trash2, Lock, Unlock, Hash, StickyNote, Search, X, Link } from "lucide-solid";
|
||||
import { Trash2, Lock, Unlock, StickyNote, Search, Link, X } from "lucide-solid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TaskEditor } from "@/components/TaskEditor";
|
||||
import { TagPicker } from "@/components/TagPicker";
|
||||
import { NOTES_COLLECTION } from "@/lib/constants";
|
||||
import { TaskCard } from "@/components/TaskCard";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { NotesSidebar } from "@/components/NotesSidebar";
|
||||
|
||||
export const NotepadView: Component<{
|
||||
@@ -53,7 +51,8 @@ export const NotepadView: Component<{
|
||||
await handleUpdateNote(id, { title: newTitle });
|
||||
|
||||
// 2. Rename the TagDefinition (preserves color) and propagate to all tagged tasks
|
||||
await renameTagDefinition(oldTitle, newTitle);
|
||||
// Use '#' prefix for note tags
|
||||
await renameTagDefinition(`#${oldTitle}`, `#${newTitle}`);
|
||||
};
|
||||
const handleDeleteNote = async (id: string) => {
|
||||
try {
|
||||
@@ -86,11 +85,12 @@ export const NotepadView: Component<{
|
||||
const linkedTasks = createMemo(() => {
|
||||
const note = activeNote();
|
||||
if (!note) return [];
|
||||
const noteTagName = `#${note.title}`;
|
||||
return store.tasks.filter(t => {
|
||||
// Explicit relation
|
||||
if (note.tasks && note.tasks.includes(t.id)) return true;
|
||||
// Tag relation (case-insensitive title match)
|
||||
if (t.tags && t.tags.some(tag => tag.toLowerCase() === note.title.toLowerCase())) return true;
|
||||
// Tag relation (case-insensitive title match with # prefix)
|
||||
if (t.tags && t.tags.some(tag => tag.toLowerCase() === noteTagName.toLowerCase())) return true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
@@ -145,7 +145,6 @@ export const NotepadView: Component<{
|
||||
)}>
|
||||
<Show when={activeNote()} fallback={
|
||||
<div class="text-center space-y-4 opacity-40 select-none">
|
||||
<StickyNote size={64} class="mx-auto" />
|
||||
<p class="text-sm font-medium tracking-wide">Select a note or create a new one</p>
|
||||
</div>
|
||||
}>
|
||||
@@ -201,35 +200,6 @@ export const NotepadView: Component<{
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<For each={note().tags}>
|
||||
{(tag) => (
|
||||
<Badge variant="secondary" class="h-6 px-2 text-xs bg-muted/30 border border-border/50 text-foreground shadow-sm">
|
||||
<Hash size={10} class="mr-1 opacity-50" />
|
||||
{tag}
|
||||
<Show when={canEdit}>
|
||||
<button
|
||||
class="ml-2 hover:text-destructive transition-colors shrink-0 outline-none"
|
||||
onClick={() => {
|
||||
handleUpdateNote(note().id, { tags: note().tags.filter(t => t !== tag) });
|
||||
}}
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
</Show>
|
||||
</Badge>
|
||||
)}
|
||||
</For>
|
||||
<Show when={canEdit}>
|
||||
<div class="opacity-70 hover:opacity-100 transition-opacity">
|
||||
<TagPicker
|
||||
selectedTags={note().tags || []}
|
||||
onTagsChange={(newTags) => handleUpdateNote(note().id, { tags: newTags })}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor Instance */}
|
||||
|
||||
+10
-10
@@ -2,7 +2,7 @@ import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-j
|
||||
import { ThemeToggle } from "../components/ThemeToggle";
|
||||
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, HelpCircle } from "lucide-solid";
|
||||
import { Trash2, Undo2, ArrowLeftRight, Tag, ChevronDown, ChevronRight, Share2, Users, HelpCircle, Copy, Plus, Type, Upload, Box, Edit2, Archive } from "lucide-solid";
|
||||
import { TagPicker } from "@/components/TagPicker";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -268,7 +268,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
<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}`}
|
||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName} `}
|
||||
<span class="mx-1">•</span>
|
||||
<button
|
||||
class="hover:text-primary transition-colors hover:underline"
|
||||
@@ -320,7 +320,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
<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}`}
|
||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName} `}
|
||||
<span class="mx-1">•</span>
|
||||
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
|
||||
</p>
|
||||
@@ -528,7 +528,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10 opacity-0 group-hover/bucket:opacity-100 transition-opacity"
|
||||
onClick={() => {
|
||||
if (confirm(`Delete the bucket "${bucket.name}"? This cannot be undone and will affect all users.`)) {
|
||||
if (confirm(`Delete the bucket "${bucket.name}" ? This cannot be undone and will affect all users.`)) {
|
||||
deleteBucket(bucket.id);
|
||||
}
|
||||
}}
|
||||
@@ -889,10 +889,10 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||
<Hash size={16} />
|
||||
Global Tags
|
||||
<Tag size={16} />
|
||||
Tag List
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">Manage your global tag definitions and values.</p>
|
||||
<p class="text-sm text-muted-foreground">Manage tag definitions and values.</p>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isTagsOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
@@ -902,7 +902,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
<Show when={isTagsOpen()}>
|
||||
<div class="space-y-2 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<div class="space-y-2">
|
||||
<For each={store.tagDefinitions.filter(t => !t.isUser && !store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))} fallback={
|
||||
<For each={store.tagDefinitions.filter(t => !t.isUser && !t.isBucket && !t.name.startsWith('#') && !t.name.startsWith('@')).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>
|
||||
@@ -988,10 +988,10 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
<div class="bg-muted/50 p-1 rounded-md group-open/details:rotate-90 transition-transform">
|
||||
<ChevronRight size={12} />
|
||||
</div>
|
||||
System Tags (Users & Buckets)
|
||||
System Tags (Users, Buckets & Notes)
|
||||
</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 || store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))}>
|
||||
<For each={store.tagDefinitions.filter(t => t.isUser || t.isBucket || t.name.startsWith('#') || t.name.startsWith('@')).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">
|
||||
|
||||
Reference in New Issue
Block a user