added saved filters
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { type Component, createSignal, For, Show } from "solid-js";
|
import { type Component, createSignal, For, Show } from "solid-js";
|
||||||
import { Search, X, Hash, ArrowUpCircle, Clock, Minus } from "lucide-solid";
|
import { Search, X, Hash, ArrowUpCircle, Clock, Minus, Save, Trash2, Bookmark } from "lucide-solid";
|
||||||
import { store, setFilter, clearFilter } from "@/store";
|
import { store, setFilter, clearFilter, saveFilterTemplate, applyFilterTemplate, removeFilterTemplate } from "@/store";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
@@ -12,6 +12,8 @@ import { getThemeAdjustedColor } from "@/lib/colors";
|
|||||||
export const FilterBar: Component = () => {
|
export const FilterBar: Component = () => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
const [isOpen, setIsOpen] = createSignal(false);
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
|
const [newTemplateName, setNewTemplateName] = createSignal("");
|
||||||
|
const [isSaving, setIsSaving] = createSignal(false);
|
||||||
|
|
||||||
const hasActiveFilters = () => {
|
const hasActiveFilters = () => {
|
||||||
const f = store.filter;
|
const f = store.filter;
|
||||||
@@ -251,6 +253,109 @@ export const FilterBar: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Templates Section */}
|
||||||
|
<div class="px-4 pb-4 pt-4 border-t border-border/50 space-y-4 bg-muted/5">
|
||||||
|
<div class="flex items-center justify-between ml-1">
|
||||||
|
<label class="text-[10px] font-black uppercase tracking-widest text-muted-foreground flex items-center gap-2">
|
||||||
|
<Bookmark size={12} class="text-primary" />
|
||||||
|
Saved Filters
|
||||||
|
</label>
|
||||||
|
<Show when={!isSaving() && hasActiveFilters()}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSaving(true)}
|
||||||
|
class="text-[9px] font-bold uppercase tracking-wider text-primary hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Save size={10} />
|
||||||
|
Save Current
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isSaving()}>
|
||||||
|
<div class="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Template name..."
|
||||||
|
value={newTemplateName()}
|
||||||
|
onInput={(e) => setNewTemplateName(e.currentTarget.value)}
|
||||||
|
class="flex-1 bg-muted/30 border border-primary/20 rounded-lg px-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-primary/30"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (newTemplateName().trim()) {
|
||||||
|
saveFilterTemplate(newTemplateName().trim());
|
||||||
|
setNewTemplateName("");
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
class="h-8 px-3"
|
||||||
|
disabled={!newTemplateName().trim()}
|
||||||
|
onClick={() => {
|
||||||
|
saveFilterTemplate(newTemplateName().trim());
|
||||||
|
setNewTemplateName("");
|
||||||
|
setIsSaving(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
onClick={() => setIsSaving(false)}
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<For each={store.filterTemplates} fallback={
|
||||||
|
<div class="col-span-2 py-4 text-center border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||||
|
<p class="text-[10px] text-muted-foreground uppercase tracking-widest">No saved filters yet</p>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
{(template) => (
|
||||||
|
<div class="group flex items-center justify-between p-2 rounded-xl bg-muted/20 border border-border/30 hover:bg-muted/40 transition-all">
|
||||||
|
<button
|
||||||
|
class="flex-1 text-left px-1 min-w-0"
|
||||||
|
onClick={() => applyFilterTemplate(template.id)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<p class="text-[11px] font-bold text-foreground truncate">{template.name}</p>
|
||||||
|
<div class="flex gap-1 opacity-60 shrink-0">
|
||||||
|
<Show when={template.filter.query}>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-primary" />
|
||||||
|
</Show>
|
||||||
|
<Show when={template.filter.tags.length > 0}>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-indigo-500" />
|
||||||
|
</Show>
|
||||||
|
<Show when={template.filter.priorityMin > 1 || template.filter.priorityMax < 10}>
|
||||||
|
<div class="w-1 h-1 rounded-full bg-amber-500" />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-destructive/10 hover:text-destructive rounded-lg transition-all"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeFilterTemplate(template.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+77
-1
@@ -144,6 +144,13 @@ export interface Filter {
|
|||||||
editedToday: boolean;
|
editedToday: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FilterTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
filter: Filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ShareRule {
|
export interface ShareRule {
|
||||||
id: string;
|
id: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
@@ -162,6 +169,7 @@ interface TaskStore {
|
|||||||
templates: TaskTemplate[];
|
templates: TaskTemplate[];
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
shareRules: ShareRule[];
|
shareRules: ShareRule[];
|
||||||
|
filterTemplates: FilterTemplate[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial empty state
|
// Initial empty state
|
||||||
@@ -181,7 +189,8 @@ export const [store, setStore] = createStore<TaskStore>({
|
|||||||
urgencyMax: 10,
|
urgencyMax: 10,
|
||||||
editedToday: false
|
editedToday: false
|
||||||
},
|
},
|
||||||
shareRules: []
|
shareRules: [],
|
||||||
|
filterTemplates: []
|
||||||
});
|
});
|
||||||
|
|
||||||
export const matchesFilter = (task: Task) => {
|
export const matchesFilter = (task: Task) => {
|
||||||
@@ -737,6 +746,9 @@ export const initStore = async () => {
|
|||||||
// 2.7. Fetch Share Rules (owned by current user)
|
// 2.7. Fetch Share Rules (owned by current user)
|
||||||
await loadShareRules();
|
await loadShareRules();
|
||||||
|
|
||||||
|
// 2.8 Fetch Filter Templates
|
||||||
|
await loadFilterTemplates();
|
||||||
|
|
||||||
// 3. Subscribe to Realtime Updates
|
// 3. Subscribe to Realtime Updates
|
||||||
await subscribeToRealtime();
|
await subscribeToRealtime();
|
||||||
|
|
||||||
@@ -1412,6 +1424,70 @@ export const removeShareRule = async (ruleId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// -- Filter Templates --
|
||||||
|
const FILTER_TEMPLATES_COLLECTION = 'TasGrid_FilterTemplates';
|
||||||
|
|
||||||
|
export const loadFilterTemplates = async () => {
|
||||||
|
if (!pb.authStore.isValid) return;
|
||||||
|
try {
|
||||||
|
const records = await pb.collection(FILTER_TEMPLATES_COLLECTION).getFullList({
|
||||||
|
filter: `user = "${pb.authStore.model?.id}"`,
|
||||||
|
requestKey: null
|
||||||
|
});
|
||||||
|
const templates: FilterTemplate[] = records.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
filter: r.filter as Filter
|
||||||
|
}));
|
||||||
|
setStore("filterTemplates", templates);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load filter templates:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveFilterTemplate = async (name: string) => {
|
||||||
|
if (!pb.authStore.isValid) return;
|
||||||
|
try {
|
||||||
|
const record = await pb.collection(FILTER_TEMPLATES_COLLECTION).create({
|
||||||
|
user: pb.authStore.model?.id,
|
||||||
|
name,
|
||||||
|
filter: store.filter
|
||||||
|
}, { requestKey: null });
|
||||||
|
|
||||||
|
const newTemplate: FilterTemplate = {
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
filter: record.filter as Filter
|
||||||
|
};
|
||||||
|
|
||||||
|
setStore("filterTemplates", prev => [...prev, newTemplate]);
|
||||||
|
toast.success(`Filter template "${name}" saved`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save filter template:", err);
|
||||||
|
toast.error("Failed to save filter template.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyFilterTemplate = (id: string) => {
|
||||||
|
const template = store.filterTemplates.find(t => t.id === id);
|
||||||
|
if (template) {
|
||||||
|
setFilter(template.filter);
|
||||||
|
toast.success(`Applied filter: ${template.name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeFilterTemplate = async (id: string) => {
|
||||||
|
if (!pb.authStore.isValid) return;
|
||||||
|
try {
|
||||||
|
await pb.collection(FILTER_TEMPLATES_COLLECTION).delete(id);
|
||||||
|
setStore("filterTemplates", prev => prev.filter(t => t.id !== id));
|
||||||
|
toast.success("Filter template deleted");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to delete filter template:", err);
|
||||||
|
toast.error("Failed to delete filter template.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const clearFilter = () => {
|
export const clearFilter = () => {
|
||||||
setStore("filter", {
|
setStore("filter", {
|
||||||
query: "",
|
query: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user