task bucket fixes and task creation clarity
This commit is contained in:
@@ -3,6 +3,7 @@ import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, ups
|
|||||||
import { Search, Command, Clock, ArrowUpCircle, Copy, ChevronDown, Gauge, RotateCcw } from "lucide-solid";
|
import { Search, Command, Clock, ArrowUpCircle, Copy, ChevronDown, Gauge, RotateCcw } from "lucide-solid";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { TextField, TextFieldInput } from "@/components/ui/textfield";
|
import { TextField, TextFieldInput } from "@/components/ui/textfield";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||||
import { TagPicker } from "@/components/TagPicker";
|
import { TagPicker } from "@/components/TagPicker";
|
||||||
@@ -29,6 +30,7 @@ const [tags, setTags] = createSignal<string[]>([]);
|
|||||||
const [description, setDescription] = createSignal("");
|
const [description, setDescription] = createSignal("");
|
||||||
const [size, setSize] = createSignal<string>("3");
|
const [size, setSize] = createSignal<string>("3");
|
||||||
const [dateString, setDateString] = createSignal<string>("");
|
const [dateString, setDateString] = createSignal<string>("");
|
||||||
|
const [showValidation, setShowValidation] = createSignal(false);
|
||||||
|
|
||||||
let lastParsedTags: string[] = [];
|
let lastParsedTags: string[] = [];
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ const resetQuickEntryState = () => {
|
|||||||
setDescription("");
|
setDescription("");
|
||||||
setSize("3");
|
setSize("3");
|
||||||
setDateString("");
|
setDateString("");
|
||||||
|
setShowValidation(false);
|
||||||
lastParsedTags = [];
|
lastParsedTags = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,7 +143,12 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
|||||||
|
|
||||||
const parseAndAdd = async () => {
|
const parseAndAdd = async () => {
|
||||||
let text = input();
|
let text = input();
|
||||||
if (!text || !priority() || !urgency()) return;
|
if (!text || !priority() || !urgency()) {
|
||||||
|
setShowValidation(true);
|
||||||
|
toast.error("Please fill in all required fields (Title, Priority, Urgency)");
|
||||||
|
if (!text) inputRef?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let finalPriority = parseInt(priority());
|
let finalPriority = parseInt(priority());
|
||||||
let finalUrgency = parseInt(urgency());
|
let finalUrgency = parseInt(urgency());
|
||||||
@@ -268,20 +276,33 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full bg-card">
|
<div class="flex flex-col h-full bg-card">
|
||||||
<div class="flex items-center px-4 py-3 border-b border-border sticky top-0 bg-card z-10">
|
<div class={cn("flex flex-col border-b border-border sticky top-0 bg-card z-10 transition-colors", showValidation() && !input() && "bg-destructive/5")}>
|
||||||
<Search class="text-muted-foreground mr-3" size={20} />
|
<div class="flex items-center px-4 py-3">
|
||||||
<TextField class="flex-1">
|
<Search class={cn("text-muted-foreground mr-3 transition-colors", showValidation() && !input() && "text-destructive")} size={20} />
|
||||||
<TextFieldInput
|
<TextField
|
||||||
id="quick-entry-input"
|
class="flex-1"
|
||||||
name="task-title"
|
validationState={showValidation() && !input() ? "invalid" : "valid"}
|
||||||
ref={inputRef}
|
>
|
||||||
value={input()}
|
<TextFieldInput
|
||||||
onInput={(e) => setInput(e.currentTarget.value)}
|
id="quick-entry-input"
|
||||||
onKeyDown={(e) => e.key === "Enter" && parseAndAdd()}
|
name="task-title"
|
||||||
placeholder="Task title... type /p1-10 for priority and /u1-10 for urgency"
|
ref={inputRef}
|
||||||
class="border-none shadow-none focus-visible:ring-0 text-lg"
|
value={input()}
|
||||||
/>
|
onInput={(e) => setInput(e.currentTarget.value)}
|
||||||
</TextField>
|
onKeyDown={(e) => e.key === "Enter" && parseAndAdd()}
|
||||||
|
placeholder="Task title... type /p1-10 for priority and /u1-10 for urgency"
|
||||||
|
class="border-none shadow-none focus-visible:ring-0 text-lg p-0"
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
<Show when={showValidation() && !input()}>
|
||||||
|
<div class="px-4 pb-2 -mt-1 flex items-center gap-1.5 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-destructive" />
|
||||||
|
<span class="text-[0.625rem] font-bold uppercase tracking-widest text-destructive/80">
|
||||||
|
Title is required to create a task
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto overscroll-contain">
|
<div class="flex-1 overflow-y-auto overscroll-contain">
|
||||||
@@ -305,11 +326,17 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
|||||||
{props.item.rawValue.label}
|
{props.item.rawValue.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
|
validationState={showValidation() && !priority() ? "invalid" : "valid"}
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3 overflow-hidden">
|
<SelectTrigger
|
||||||
|
class={cn(
|
||||||
|
"w-full bg-background border shadow-sm h-10 px-3 overflow-hidden transition-colors border-transparent",
|
||||||
|
showValidation() && !priority() && "border-destructive/50 bg-destructive/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2 truncate">
|
<div class="flex items-center gap-2 truncate">
|
||||||
<ArrowUpCircle size={14} class="text-primary shrink-0" />
|
<ArrowUpCircle size={14} class={cn("text-primary shrink-0", showValidation() && !priority() && "text-destructive")} />
|
||||||
<span class="text-sm font-medium truncate">
|
<span class={cn("text-sm font-medium truncate", showValidation() && !priority() && "text-destructive")}>
|
||||||
{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || (priority() === "" ? "Pick Priority" : priority())}
|
{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || (priority() === "" ? "Pick Priority" : priority())}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,11 +391,17 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
|||||||
{props.item.rawValue.label}
|
{props.item.rawValue.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
|
validationState={showValidation() && !urgency() ? "invalid" : "valid"}
|
||||||
>
|
>
|
||||||
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3 overflow-hidden">
|
<SelectTrigger
|
||||||
|
class={cn(
|
||||||
|
"w-full bg-background border shadow-sm h-10 px-3 overflow-hidden transition-colors border-transparent",
|
||||||
|
showValidation() && !urgency() && "border-destructive/50 bg-destructive/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2 truncate">
|
<div class="flex items-center gap-2 truncate">
|
||||||
<Clock size={14} class="text-primary shrink-0" />
|
<Clock size={14} class={cn("text-primary shrink-0", showValidation() && !urgency() && "text-destructive")} />
|
||||||
<span class="text-sm font-medium truncate">
|
<span class={cn("text-sm font-medium truncate", showValidation() && !urgency() && "text-destructive")}>
|
||||||
{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || (urgency() === "" ? "Pick Urgency" : urgency())}
|
{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || (urgency() === "" ? "Pick Urgency" : urgency())}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -505,7 +538,7 @@ export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Button size="sm" onClick={parseAndAdd} disabled={!input() || !priority() || !urgency()}>
|
<Button size="sm" onClick={parseAndAdd}>
|
||||||
Create Task
|
Create Task
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+22
-2
@@ -346,8 +346,7 @@ export const matchesFilter = (task: Task) => {
|
|||||||
|
|
||||||
// EXCLUDE Buckets from the main "Mine" view
|
// EXCLUDE Buckets from the main "Mine" view
|
||||||
// The user wants to see "Direct Shares" (User Tags), not "Bucket Subscriptions" here.
|
// The user wants to see "Direct Shares" (User Tags), not "Bucket Subscriptions" here.
|
||||||
const isBucket = store.buckets.some(b => b.name.toLowerCase() === tagName.toLowerCase());
|
if (isBucketTag(tagName)) return false;
|
||||||
if (isBucket) return false;
|
|
||||||
|
|
||||||
return task.tags?.some(t => t.toLowerCase() === tagName.toLowerCase());
|
return task.tags?.some(t => t.toLowerCase() === tagName.toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -755,6 +754,7 @@ const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
|||||||
// We ignore the rule.ownerId check (which would match the task.user)
|
// We ignore the rule.ownerId check (which would match the task.user)
|
||||||
// and instead match purely on the tag.
|
// and instead match purely on the tag.
|
||||||
if (rule.ownerId === rule.targetUserId && rule.type === 'tag') {
|
if (rule.ownerId === rule.targetUserId && rule.type === 'tag') {
|
||||||
|
if (isBucketTag(rule.tagName || "")) return false;
|
||||||
const tags: string[] = task.tags || [];
|
const tags: string[] = task.tags || [];
|
||||||
return tags.some(t => t.toLowerCase() === (rule.tagName || "").toLowerCase());
|
return tags.some(t => t.toLowerCase() === (rule.tagName || "").toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -787,6 +787,26 @@ const isTaskInSubscribedBucket = (task: any): boolean => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper: detect bucket tags robustly (case-insensitive, with or without '@')
|
||||||
|
const isBucketTag = (tagName: string): boolean => {
|
||||||
|
const raw = (tagName || "").trim().toLowerCase();
|
||||||
|
if (!raw) return false;
|
||||||
|
const normalized = raw.startsWith("@") ? raw.slice(1) : raw;
|
||||||
|
|
||||||
|
const bucketNameMatch = store.buckets.some(b => {
|
||||||
|
const bName = (b.name || "").trim().toLowerCase();
|
||||||
|
return normalized === bName || raw === `@${bName}`;
|
||||||
|
});
|
||||||
|
if (bucketNameMatch) return true;
|
||||||
|
|
||||||
|
const bucketTagDefMatch = store.tagDefinitions.some(d => {
|
||||||
|
if (!d.isBucket) return false;
|
||||||
|
const dName = (d.name || "").trim().toLowerCase();
|
||||||
|
return raw === dName || normalized === dName || raw === `@${dName}`;
|
||||||
|
});
|
||||||
|
return bucketTagDefMatch;
|
||||||
|
};
|
||||||
|
|
||||||
export const subscribeToRealtime = async () => {
|
export const subscribeToRealtime = async () => {
|
||||||
// Unsubscribe first to avoid duplicates
|
// Unsubscribe first to avoid duplicates
|
||||||
await pb.collection(TASGRID_COLLECTION).unsubscribe('*');
|
await pb.collection(TASGRID_COLLECTION).unsubscribe('*');
|
||||||
|
|||||||
Reference in New Issue
Block a user