handoff and collab modes support per task for @user @bucket and #note
This commit is contained in:
@@ -223,7 +223,7 @@ export const ImportTool: Component = () => {
|
||||
<div class="space-y-1">
|
||||
<label class="text-[0.5625rem] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Tags</label>
|
||||
<div class="flex items-center gap-1 flex-wrap">
|
||||
<TagPicker selectedTags={bulkTags()} onTagsChange={setBulkTags} />
|
||||
<TagPicker selectedTags={bulkTags()} onTagsChange={(tags) => setBulkTags(tags)} />
|
||||
<For each={bulkTags()}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { toast } from "solid-sonner";
|
||||
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants";
|
||||
import { buildNoteTag } from "@/lib/tags";
|
||||
|
||||
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
|
||||
@@ -27,6 +28,7 @@ const [input, setInput] = createSignal("");
|
||||
const [priority, setPriority] = createSignal<string>("");
|
||||
const [urgency, setUrgency] = createSignal<string>("");
|
||||
const [tags, setTags] = createSignal<string[]>([]);
|
||||
const [tagShareModes, setTagShareModes] = createSignal<Record<string, "handoff" | "collaborative">>({});
|
||||
const [description, setDescription] = createSignal("");
|
||||
const [size, setSize] = createSignal<string>("3");
|
||||
const [dateString, setDateString] = createSignal<string>("");
|
||||
@@ -39,6 +41,7 @@ const resetQuickEntryState = () => {
|
||||
setPriority("");
|
||||
setUrgency("");
|
||||
setTags([]);
|
||||
setTagShareModes({});
|
||||
setDescription("");
|
||||
setSize("3");
|
||||
setDateString("");
|
||||
@@ -153,6 +156,7 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
let finalPriority = parseInt(priority());
|
||||
let finalUrgency = parseInt(urgency());
|
||||
const finalTags = [...tags()];
|
||||
const finalTagShareModes = { ...tagShareModes() };
|
||||
|
||||
const pMatch = text.match(/\/p\s*(\d+)/);
|
||||
if (pMatch) {
|
||||
@@ -217,8 +221,8 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
const activeNoteId = (window as any)._activeNoteId;
|
||||
if (activeNoteId) {
|
||||
const note = store.notes.find(n => n.id === activeNoteId);
|
||||
if (note && note.title) {
|
||||
const noteTagName = note.title.trim();
|
||||
if (note) {
|
||||
const noteTagName = buildNoteTag(note.key || note.title);
|
||||
if (noteTagName && !finalTags.includes(noteTagName)) finalTags.push(noteTagName);
|
||||
}
|
||||
}
|
||||
@@ -243,7 +247,8 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
dueDate: finalDueDate || undefined,
|
||||
tags: finalTags,
|
||||
content: description(),
|
||||
size: parseInt(size())
|
||||
size: parseInt(size()),
|
||||
shareModeByTag: finalTagShareModes
|
||||
});
|
||||
|
||||
resetQuickEntryState();
|
||||
@@ -413,7 +418,16 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
|
||||
<div class="flex flex-col md:flex-row items-stretch md:items-center px-4 py-3 gap-4 bg-muted/10 border-b border-border">
|
||||
<div class="flex-1 flex flex-wrap gap-2 items-center">
|
||||
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
|
||||
<TagPicker
|
||||
selectedTags={tags()}
|
||||
getShareModeForTag={(tag) => tagShareModes()[tag.toLowerCase()]}
|
||||
onTagsChange={(nextTags, shareModeOverrides) => {
|
||||
setTags(nextTags);
|
||||
if (shareModeOverrides) {
|
||||
setTagShareModes(prev => ({ ...prev, ...shareModeOverrides }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<For each={tags().filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t))}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
@@ -435,7 +449,14 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
return getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()) || "inherit";
|
||||
})()
|
||||
}}
|
||||
onClick={() => setTags(tags().filter(t => t !== tag))}
|
||||
onClick={() => {
|
||||
setTags(tags().filter(t => t !== tag));
|
||||
setTagShareModes(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[tag.toLowerCase()];
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Component, createSignal, For, createEffect } from "solid-js";
|
||||
import { store, upsertTagDefinition, isCurrentUserContextTag, setContextPolicy, setNoteContextPolicy, getNoteSharePolicy } from "@/store";
|
||||
import { store, upsertTagDefinition, isCurrentUserContextTag, getNoteSharePolicy } from "@/store";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -10,7 +10,8 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
interface TagPickerProps {
|
||||
selectedTags: string[];
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
onTagsChange: (tags: string[], shareModeOverrides?: Record<string, "handoff" | "collaborative">) => void;
|
||||
getShareModeForTag?: (tag: string) => "handoff" | "collaborative" | undefined;
|
||||
}
|
||||
|
||||
export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
@@ -75,6 +76,11 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const explicitMode = props.getShareModeForTag?.(selectedTagName().trim());
|
||||
if (explicitMode) {
|
||||
setShareMode(explicitMode);
|
||||
return;
|
||||
}
|
||||
const context = selectedShareContext();
|
||||
if (context) {
|
||||
setShareMode(context.policy);
|
||||
@@ -94,9 +100,6 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
const name = selectedTagName().trim();
|
||||
if (!name) return;
|
||||
|
||||
const context = selectedShareContext();
|
||||
const noteContext = selectedNoteContext();
|
||||
|
||||
if (name.startsWith("@")) {
|
||||
const normalized = normalizeContextKey(name);
|
||||
const exists = store.contexts.some(context =>
|
||||
@@ -109,11 +112,6 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
toast.error("Create the shared context first before tagging tasks with it.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (context && context.policy !== shareMode()) {
|
||||
const updated = await setContextPolicy(context.id, shareMode());
|
||||
if (!updated) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (name.startsWith("#")) {
|
||||
@@ -122,11 +120,6 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
toast.error("Create the canonical note first before linking it with a #tag.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (noteContext && getNoteSharePolicy(noteContext) !== shareMode()) {
|
||||
const updated = await setNoteContextPolicy(noteContext.id, shareMode());
|
||||
if (!updated) return;
|
||||
}
|
||||
}
|
||||
|
||||
// update global definition
|
||||
@@ -138,9 +131,13 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
|
||||
// Toggle in current task selection if not already there
|
||||
const currentSelected = props.selectedTags;
|
||||
if (!currentSelected.includes(name)) {
|
||||
props.onTagsChange([...currentSelected, name]);
|
||||
}
|
||||
const nextTags = currentSelected.includes(name) ? currentSelected : [...currentSelected, name];
|
||||
props.onTagsChange(
|
||||
nextTags,
|
||||
name.startsWith("@") || name.startsWith("#")
|
||||
? { [name.toLowerCase()]: shareMode() }
|
||||
: undefined
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
setSelectedTagName("");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Component, createMemo, For, Show } from "solid-js";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag, isCurrentUserContextTag, dismissTaskUpdateIndicator, isTaskUpdatedByAnotherUser, collapseTaskUntilUpdated, expandCollapsedTask, isTaskCollapsedUntilUpdated, isTaskCollaborativelyShared } from "@/store";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag, dismissTaskUpdateIndicator, isTaskUpdatedByAnotherUser, collapseTaskUntilUpdated, expandCollapsedTask, isTaskCollapsedUntilUpdated, isTaskCollaborativelyShared, getVisibleTaskTags } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Clock, ArrowUpCircle, Copy, Users2, Star, ChevronRight } from "lucide-solid";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -8,7 +8,6 @@ import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
import { getStatusOptionsForTask } from "@/lib/constants";
|
||||
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||
import { buildShareTag } from "@/lib/tags";
|
||||
import { StatusCircle } from "./StatusCircle";
|
||||
|
||||
export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props) => {
|
||||
@@ -28,18 +27,8 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
// Filter out bucket tags if we are currently INSIDE that bucket view
|
||||
const visibleTags = createMemo(() => {
|
||||
const tags = props.task.tags || [];
|
||||
const ctx = currentTaskContext();
|
||||
|
||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
||||
const bucketTagName = buildShareTag(bucketContext?.displayName || ctx.name).toLowerCase();
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||
}
|
||||
|
||||
return tags.filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||
return getVisibleTaskTags(props.task, currentTaskContext());
|
||||
});
|
||||
|
||||
const isDueWithin12Hours = createMemo(() => {
|
||||
@@ -187,13 +176,13 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
||||
<span class="text-[0.625rem] font-medium">P{props.task.priority}</span>
|
||||
</div>
|
||||
|
||||
{/* Shared Indicator - driven by @tag share contexts */}
|
||||
{/* Shared Indicator - driven by task context tags */}
|
||||
{(() => {
|
||||
if (props.task.shareRefs.length === 0) return null;
|
||||
|
||||
const tooltip = props.task.shareRefs.length === 1
|
||||
? 'Shared via 1 @context'
|
||||
: `Shared via ${props.task.shareRefs.length} @contexts`;
|
||||
? 'Shared via 1 context'
|
||||
: `Shared via ${props.task.shareRefs.length} contexts`;
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-1 shrink-0" title={tooltip}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Component, createEffect, createSignal, For, createMemo, onCleanup, Show } from "solid-js";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now, isCurrentUserContextTag } from "@/store";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now, isCurrentUserContextTag, getVisibleTaskTags, isTaskLockedNoteTag, deriveTaskRelationsFromTags, getTaskShareModeForTag, getTaskSharePolicy } from "@/store";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Star, FileText } from "lucide-solid";
|
||||
@@ -17,7 +17,7 @@ import { lazy, Suspense } from "solid-js";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||
import { buildShareTag } from "@/lib/tags";
|
||||
import { buildNoteTag, buildShareTag } from "@/lib/tags";
|
||||
import { TextBubbleMenu } from "@/components/TextBubbleMenu";
|
||||
import { TableBubbleMenu } from "@/components/TableBubbleMenu";
|
||||
|
||||
@@ -168,7 +168,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateTags = (tags: string[]) => {
|
||||
const updateTags = (tags: string[], shareModeOverrides?: Record<string, "handoff" | "collaborative">) => {
|
||||
const ctx = currentTaskContext();
|
||||
const currentTags = props.task.tags || [];
|
||||
let newTags = [...tags];
|
||||
@@ -191,7 +191,16 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
updateTask(props.task.id, { tags: newTags });
|
||||
props.task.tags
|
||||
.filter(tag => isTaskLockedNoteTag(props.task, tag))
|
||||
.forEach(tag => {
|
||||
if (!newTags.some(existing => existing.toLowerCase() === tag.toLowerCase())) {
|
||||
newTags.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
const { labelTags, shareRefs, noteRefs } = deriveTaskRelationsFromTags(newTags, props.task.shareRefs, shareModeOverrides);
|
||||
updateTask(props.task.id, { tags: newTags, labelTags, shareRefs, noteRefs });
|
||||
};
|
||||
|
||||
const onDateInputChange = (e: Event) => {
|
||||
@@ -207,16 +216,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
|
||||
// Filter out bucket tags if we are currently INSIDE that bucket view
|
||||
const visibleTags = createMemo(() => {
|
||||
const tags = props.task.tags || [];
|
||||
const ctx = currentTaskContext();
|
||||
|
||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
||||
const bucketTagName = buildShareTag(bucketContext?.displayName || ctx.name).toLowerCase();
|
||||
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||
}
|
||||
|
||||
return tags.filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||
return getVisibleTaskTags(props.task, currentTaskContext());
|
||||
});
|
||||
|
||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||
@@ -511,6 +511,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
<div class="flex flex-nowrap overflow-x-auto no-scrollbar items-center gap-1.5 min-w-0 flex-1 pb-1">
|
||||
<TagPicker
|
||||
selectedTags={visibleTags()}
|
||||
getShareModeForTag={(tag) => getTaskShareModeForTag(props.task, tag)}
|
||||
onTagsChange={updateTags}
|
||||
/>
|
||||
<div class="flex flex-nowrap items-center gap-1.5 shrink-0 pr-2">
|
||||
@@ -694,7 +695,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
<div class="px-2 py-1.5 rounded-lg bg-muted/20 border border-border/30 space-y-2">
|
||||
<p class="text-[0.6875rem] text-muted-foreground leading-relaxed">
|
||||
Sharing is driven by the task's <span class="font-semibold text-foreground">@tags</span>. Add or remove
|
||||
<span class="font-semibold text-foreground"> @people</span> and <span class="font-semibold text-foreground">@buckets</span> in the tag list above.
|
||||
<span class="font-semibold text-foreground"> @people</span>, <span class="font-semibold text-foreground">@buckets</span>, and <span class="font-semibold text-foreground">#notes</span> in the tag list above.
|
||||
</p>
|
||||
<Show
|
||||
when={props.task.shareRefs.length > 0}
|
||||
@@ -702,13 +703,25 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={props.task.shareRefs.filter(ref => {
|
||||
if (ref.kind === "note") {
|
||||
const label = buildNoteTag(store.notes.find(note => note.id === ref.contextId || note.key === ref.key)?.key || ref.key);
|
||||
return !isTaskLockedNoteTag(props.task, label);
|
||||
}
|
||||
|
||||
const context = store.contexts.find(existing => existing.id === ref.contextId) || store.contexts.find(existing => existing.key === ref.key);
|
||||
return !isCurrentUserContextTag(`@${context?.displayName || ref.key}`);
|
||||
})}>
|
||||
{(ref) => {
|
||||
const context = store.contexts.find(existing => existing.id === ref.contextId) || store.contexts.find(existing => existing.key === ref.key);
|
||||
const label = `@${context?.displayName || ref.key}`;
|
||||
const policy = context?.policy || (ref.kind === "bucket" ? "handoff" : "collaborative");
|
||||
const note = ref.kind === "note"
|
||||
? store.notes.find(existing => existing.id === ref.contextId || existing.key === ref.key)
|
||||
: null;
|
||||
const context = ref.kind === "note"
|
||||
? null
|
||||
: store.contexts.find(existing => existing.id === ref.contextId) || store.contexts.find(existing => existing.key === ref.key);
|
||||
const label = ref.kind === "note"
|
||||
? buildNoteTag(note?.key || ref.key)
|
||||
: `@${context?.displayName || ref.key}`;
|
||||
const policy = getTaskSharePolicy(ref);
|
||||
return (
|
||||
<Badge variant="secondary" class="h-6 px-2 text-[0.625rem] gap-1 bg-muted/40 border-border/40">
|
||||
<span>{label}</span>
|
||||
|
||||
Reference in New Issue
Block a user