load in optimization and error correction
This commit is contained in:
@@ -7,15 +7,16 @@ interface EmbedLayoutProps {
|
|||||||
export const EmbedLayout: Component<EmbedLayoutProps> = (props) => {
|
export const EmbedLayout: Component<EmbedLayoutProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="flex h-screen w-full min-w-0 bg-background overflow-hidden relative font-sans antialiased text-foreground">
|
<div class="flex h-screen w-full min-w-0 bg-background overflow-hidden relative font-sans antialiased text-foreground">
|
||||||
<main
|
<main class="flex-1 min-w-0 overflow-y-auto overflow-x-hidden relative scroll-smooth bg-background">
|
||||||
class="flex-1 min-w-0 overflow-y-auto overflow-x-hidden relative scroll-smooth bg-background"
|
|
||||||
style={{
|
|
||||||
"padding-bottom": "calc(8px + env(safe-area-inset-bottom, 0px))"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="w-full h-full mx-auto p-4">
|
<div class="w-full h-full mx-auto p-4">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="md:hidden shrink-0"
|
||||||
|
style={{
|
||||||
|
height: "calc(8px + env(safe-area-inset-bottom, 0px))"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+14
-13
@@ -1,6 +1,6 @@
|
|||||||
import { type Component, type JSX, createSignal, createEffect, Show, lazy, Suspense, onMount, onCleanup } from "solid-js";
|
import { type Component, type JSX, createSignal, createEffect, Show, lazy, Suspense, onMount, onCleanup } from "solid-js";
|
||||||
import { Sidebar, BottomNav, ContextSwitcher } from "./Navigation";
|
import { Sidebar, BottomNav, ContextSwitcher } from "./Navigation";
|
||||||
import { store, setStore, activeTaskId, setActiveTaskId } from "@/store";
|
import { store, setStore, activeTaskId, setActiveTaskId, requestImmediateNotesMetadataLoad } from "@/store";
|
||||||
import { PanelLeftOpen, PanelLeftClose } from "lucide-solid";
|
import { PanelLeftOpen, PanelLeftClose } from "lucide-solid";
|
||||||
|
|
||||||
const TaskDetail = lazy(() => import("./TaskDetail").then(m => ({ default: m.TaskDetail })));
|
const TaskDetail = lazy(() => import("./TaskDetail").then(m => ({ default: m.TaskDetail })));
|
||||||
@@ -54,6 +54,12 @@ export const Layout: Component<LayoutProps> = (props) => {
|
|||||||
// Derived active task for the detail view
|
// Derived active task for the detail view
|
||||||
const activeTask = () => store.tasks.find(t => t.id === activeTaskId());
|
const activeTask = () => store.tasks.find(t => t.id === activeTaskId());
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (store.isNotepadMode) {
|
||||||
|
void requestImmediateNotesMetadataLoad();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex h-screen w-full min-w-0 bg-background overflow-hidden relative font-sans antialiased text-foreground">
|
<div class="flex h-screen w-full min-w-0 bg-background overflow-hidden relative font-sans antialiased text-foreground">
|
||||||
{/* Desktop UI Container */}
|
{/* Desktop UI Container */}
|
||||||
@@ -189,25 +195,20 @@ export const Layout: Component<LayoutProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sync Progress Bar Overlay */}
|
{/* Sync Progress Bar Overlay */}
|
||||||
<Show when={store.isInitializing}>
|
<Show when={store.isInitializing || (store.isNotepadMode && store.isNotesLoading)}>
|
||||||
<div class="fixed top-0 left-0 w-full h-0.5 z-[100] overflow-hidden bg-primary/10">
|
<div class="fixed top-0 left-0 w-full h-0.5 z-[100] overflow-hidden bg-primary/10">
|
||||||
<div class="h-full bg-primary animate-progress-fast w-1/3" />
|
<div class="h-full bg-primary animate-progress-fast w-1/3" />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<main
|
<main class={cn(
|
||||||
class={cn(
|
"flex-1 min-w-0 overflow-y-auto overflow-x-hidden relative scroll-smooth",
|
||||||
"flex-1 min-w-0 overflow-y-auto overflow-x-hidden relative scroll-smooth",
|
!store.isNotepadMode ? "sm:pb-6" : ""
|
||||||
!store.isNotepadMode ? "pb-32 sm:pb-6" : ""
|
)}>
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
"padding-bottom": "calc(8px + env(safe-area-inset-bottom, 0px))"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class={cn(
|
<div class={cn(
|
||||||
"w-full h-full mx-auto",
|
"w-full min-h-full mx-auto",
|
||||||
!store.isNotepadMode ? "max-w-screen-2xl px-4 sm:px-8 lg:px-12 pt-16 sm:pt-20 pb-8 space-y-6 sm:space-y-8" : "max-w-screen-2xl md:px-8 lg:px-12 md:pt-20 md:pb-8 pt-14"
|
!store.isNotepadMode ? "max-w-screen-2xl px-4 sm:px-8 lg:px-12 pt-16 sm:pt-20 mobile-footer-clearance space-y-6 sm:space-y-8" : "max-w-screen-2xl md:px-8 lg:px-12 md:pt-20 md:pb-8 pt-14"
|
||||||
)}>
|
)}>
|
||||||
<div class={cn(store.isNotepadMode ? "hidden" : "block")}>
|
<div class={cn(store.isNotepadMode ? "hidden" : "block")}>
|
||||||
<Show when={props.currentView.toLowerCase() !== "settings"}>
|
<Show when={props.currentView.toLowerCase() !== "settings"}>
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ export const BottomNav: Component<{ currentView: string; setView: (v: string) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav class="fixed bottom-0 left-0 right-0 bg-background border-t border-border flex justify-around items-center h-16 md:hidden z-50">
|
<nav
|
||||||
|
class="fixed bottom-0 left-0 right-0 bg-background border-t border-border flex justify-around items-center md:hidden z-50"
|
||||||
|
style={{
|
||||||
|
"padding-bottom": "env(safe-area-inset-bottom, 0px)",
|
||||||
|
height: "calc(4rem + env(safe-area-inset-bottom, 0px))"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<For each={mobileNavGroups}>
|
<For each={mobileNavGroups}>
|
||||||
{(group) => (
|
{(group) => (
|
||||||
<Show when={group.items} fallback={
|
<Show when={group.items} fallback={
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ export const NotesSidebar: Component<{
|
|||||||
|
|
||||||
{/* List */}
|
{/* List */}
|
||||||
<div class="flex-1 overflow-y-auto p-2 space-y-1 pb-20 md:pb-2">
|
<div class="flex-1 overflow-y-auto p-2 space-y-1 pb-20 md:pb-2">
|
||||||
<For each={filteredNotes()} fallback={<div class="p-4 text-center text-xs text-muted-foreground">No notes found.</div>}>
|
<For each={filteredNotes()} fallback={<div class="p-4 text-center text-xs text-muted-foreground">{store.isNotesLoading ? "Loading notes..." : "No notes found."}</div>}>
|
||||||
{(note) => (
|
{(note) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => props.setSelectedNoteId(note.id)}
|
onClick={() => props.setSelectedNoteId(note.id)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type Component, createSignal, createEffect, onMount, For, Show, Suspense, lazy } from "solid-js";
|
import { type Component, createSignal, createEffect, onMount, For, Show, Suspense, lazy } from "solid-js";
|
||||||
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition, type TaskTemplate, currentTaskContext } from "@/store";
|
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition, type TaskTemplate, currentTaskContext, isCurrentUserContextTag } from "@/store";
|
||||||
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";
|
||||||
@@ -414,7 +414,7 @@ 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 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">
|
<div class="flex-1 flex flex-wrap gap-2 items-center">
|
||||||
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
|
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
|
||||||
<For each={tags().filter(t => !t.endsWith("_favorite__"))}>
|
<For each={tags().filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t))}>
|
||||||
{(tag) => (
|
{(tag) => (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type Component, createSignal, For, createEffect } from "solid-js";
|
import { type Component, createSignal, For, createEffect } from "solid-js";
|
||||||
import { store, upsertTagDefinition } from "@/store";
|
import { store, upsertTagDefinition, isCurrentUserContextTag } from "@/store";
|
||||||
import { useTheme } from "./ThemeProvider";
|
import { useTheme } from "./ThemeProvider";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -20,7 +20,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
|||||||
|
|
||||||
// Filter tags for the "Name" part
|
// Filter tags for the "Name" part
|
||||||
const localTagNames = () => {
|
const localTagNames = () => {
|
||||||
const localDef = store.tagDefinitions.filter(d => !d.isUser && !d.isBucket).map(d => d.name);
|
const localDef = store.tagDefinitions.filter(d => !d.isUser && !d.isBucket).map(d => d.name).filter(tag => !isCurrentUserContextTag(tag));
|
||||||
return [...new Set(localDef)].sort();
|
return [...new Set(localDef)].sort();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
|||||||
const noteTags = store.notes
|
const noteTags = store.notes
|
||||||
.filter(n => !(n.tags || []).some(t => t.startsWith("child-of-")))
|
.filter(n => !(n.tags || []).some(t => t.startsWith("child-of-")))
|
||||||
.map(n => buildNoteTag(n.key || n.title));
|
.map(n => buildNoteTag(n.key || n.title));
|
||||||
return [...new Set([...definedTags, ...contextTags, ...noteTags])].sort();
|
return [...new Set([...definedTags, ...contextTags, ...noteTags])].filter(tag => !isCurrentUserContextTag(tag)).sort();
|
||||||
};
|
};
|
||||||
|
|
||||||
const suggestedTags = () => {
|
const suggestedTags = () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type Component, createMemo, For, Show } from "solid-js";
|
import { type Component, createMemo, For, Show } from "solid-js";
|
||||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag } from "@/store";
|
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag, isCurrentUserContextTag } from "@/store";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Clock, ArrowUpCircle, Copy, Users2, Star } from "lucide-solid";
|
import { Clock, ArrowUpCircle, Copy, Users2, Star } from "lucide-solid";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -36,10 +36,10 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
|||||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||||
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
||||||
const bucketTagName = buildShareTag(bucketContext?.displayName || ctx.name).toLowerCase();
|
const bucketTagName = buildShareTag(bucketContext?.displayName || ctx.name).toLowerCase();
|
||||||
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__"));
|
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags.filter(t => !t.endsWith("_favorite__"));
|
return tags.filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDueWithin12Hours = createMemo(() => {
|
const isDueWithin12Hours = createMemo(() => {
|
||||||
@@ -59,13 +59,15 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
|||||||
if (props.task.status !== 10) return null;
|
if (props.task.status !== 10) return null;
|
||||||
return getRecurrenceProgress(props.task.recurrence, props.task.updated, now());
|
return getRecurrenceProgress(props.task.recurrence, props.task.updated, now());
|
||||||
});
|
});
|
||||||
|
const shouldQuickloadFade = createMemo(() => store.quickloadFadeTaskIds.includes(props.task.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
"group relative flex items-center p-3 sm:p-4 bg-card border border-border rounded-2xl transition-[background-color,border-color,box-shadow,opacity] duration-500 hover:duration-100 hover:shadow-xl hover:shadow-primary/5 hover:-translate-y-0.5 cursor-pointer w-full min-w-0 overflow-hidden",
|
"group relative flex items-center p-3 sm:p-4 bg-card border border-border rounded-2xl transition-[background-color,border-color,box-shadow,opacity] duration-500 hover:duration-100 hover:shadow-xl hover:shadow-primary/5 hover:-translate-y-0.5 cursor-pointer w-full min-w-0 overflow-hidden",
|
||||||
props.task.completed && "opacity-60",
|
props.task.completed && "opacity-60",
|
||||||
props.isShaking && "animate-task-shake bg-accent/20"
|
props.isShaking && "animate-task-shake bg-accent/20",
|
||||||
|
shouldQuickloadFade() && "animate-task-quickload-fade"
|
||||||
)}
|
)}
|
||||||
onClick={() => setActiveTaskId(props.task.id)}
|
onClick={() => setActiveTaskId(props.task.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type Component, createEffect, createSignal, For, createMemo, onCleanup, Show } from "solid-js";
|
import { type Component, createEffect, createSignal, For, createMemo, onCleanup, Show } from "solid-js";
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now } from "@/store";
|
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now, isCurrentUserContextTag } from "@/store";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Star, FileText } from "lucide-solid";
|
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Star, FileText } from "lucide-solid";
|
||||||
@@ -170,8 +170,17 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
|
|
||||||
const updateTags = (tags: string[]) => {
|
const updateTags = (tags: string[]) => {
|
||||||
const ctx = currentTaskContext();
|
const ctx = currentTaskContext();
|
||||||
|
const currentTags = props.task.tags || [];
|
||||||
let newTags = [...tags];
|
let newTags = [...tags];
|
||||||
|
|
||||||
|
currentTags
|
||||||
|
.filter(tag => isCurrentUserContextTag(tag))
|
||||||
|
.forEach(tag => {
|
||||||
|
if (!newTags.some(existing => existing.toLowerCase() === tag.toLowerCase())) {
|
||||||
|
newTags.push(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// If in a bucket view, ensure the bucket tag is preserved (it was hidden from the UI so it's missing from 'tags')
|
// 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) {
|
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||||
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
||||||
@@ -204,10 +213,10 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||||
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
const bucketContext = store.contexts.find(context => context.id === ctx.bucketId);
|
||||||
const bucketTagName = buildShareTag(bucketContext?.displayName || ctx.name).toLowerCase();
|
const bucketTagName = buildShareTag(bucketContext?.displayName || ctx.name).toLowerCase();
|
||||||
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__"));
|
return tags.filter(t => t.toLowerCase() !== bucketTagName && !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags.filter(t => !t.endsWith("_favorite__"));
|
return tags.filter(t => !t.endsWith("_favorite__") && !isCurrentUserContextTag(t));
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||||
@@ -692,7 +701,10 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
fallback={<p class="text-[0.6875rem] text-muted-foreground/70">No shared contexts on this task.</p>}
|
fallback={<p class="text-[0.6875rem] text-muted-foreground/70">No shared contexts on this task.</p>}
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
<For each={props.task.shareRefs}>
|
<For each={props.task.shareRefs.filter(ref => {
|
||||||
|
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) => {
|
{(ref) => {
|
||||||
const context = store.contexts.find(existing => existing.id === ref.contextId) || store.contexts.find(existing => existing.key === ref.key);
|
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 label = `@${context?.displayName || ref.key}`;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSignal, createEffect, onCleanup, untrack } from "solid-js";
|
import { createSignal, createEffect, onCleanup, untrack } from "solid-js";
|
||||||
import type { Task } from "@/store";
|
import { store, type Task } from "@/store";
|
||||||
|
|
||||||
export function useDelayedSort(
|
export function useDelayedSort(
|
||||||
sourceTasks: () => Task[],
|
sourceTasks: () => Task[],
|
||||||
@@ -16,6 +16,20 @@ export function useDelayedSort(
|
|||||||
const sorted = [...sourceTasks()].sort(sortFn);
|
const sorted = [...sourceTasks()].sort(sortFn);
|
||||||
const currentDisplayed = untrack(() => displayedTasks());
|
const currentDisplayed = untrack(() => displayedTasks());
|
||||||
|
|
||||||
|
// Startup hydration loads tasks in stages; animating each reorder causes
|
||||||
|
// the entire visible list to flash even when only later-stage tasks changed.
|
||||||
|
if (store.isInitializing) {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = undefined;
|
||||||
|
}
|
||||||
|
if (shakingTaskIds().length > 0) {
|
||||||
|
setShakingTaskIds([]);
|
||||||
|
}
|
||||||
|
setDisplayedTasks(sorted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Initial load (though now handled by initial state, kept for safety if it resets)
|
// Initial load (though now handled by initial state, kept for safety if it resets)
|
||||||
if (currentDisplayed.length === 0 && sorted.length > 0) {
|
if (currentDisplayed.length === 0 && sorted.length > 0) {
|
||||||
setDisplayedTasks(sorted);
|
setDisplayedTasks(sorted);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import autoAnimate from "@formkit/auto-animate";
|
||||||
|
import { createEffect } from "solid-js";
|
||||||
|
import { store } from "@/store";
|
||||||
|
|
||||||
|
export function useTaskListAutoAnimate(getElement: () => HTMLElement | undefined) {
|
||||||
|
let hasInitialized = false;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (hasInitialized || store.isInitializing) return;
|
||||||
|
|
||||||
|
const element = getElement();
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
autoAnimate(element, { duration: 300, easing: "ease-out" });
|
||||||
|
hasInitialized = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
+22
-1
@@ -57,6 +57,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
--animate-task-shake: task-shake 0.3s ease-in-out;
|
--animate-task-shake: task-shake 0.3s ease-in-out;
|
||||||
|
--animate-task-quickload-fade: task-quickload-fade 0.2s ease-out forwards;
|
||||||
|
|
||||||
@keyframes task-shake {
|
@keyframes task-shake {
|
||||||
|
|
||||||
@@ -78,6 +79,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes task-quickload-fade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
--animate-progress-fast: loading-progress 2s infinite linear;
|
--animate-progress-fast: loading-progress 2s infinite linear;
|
||||||
|
|
||||||
@keyframes loading-progress {
|
@keyframes loading-progress {
|
||||||
@@ -186,6 +197,16 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-footer-clearance {
|
||||||
|
padding-bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.mobile-footer-clearance {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Tiptap Table Selection --- */
|
/* --- Tiptap Table Selection --- */
|
||||||
.ProseMirror td.selectedCell,
|
.ProseMirror td.selectedCell,
|
||||||
.ProseMirror th.selectedCell {
|
.ProseMirror th.selectedCell {
|
||||||
@@ -201,4 +222,4 @@
|
|||||||
border: 2px solid var(--primary);
|
border: 2px solid var(--primary);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { store } from "@/store";
|
import { store, isCurrentUserContextTag } from "@/store";
|
||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
import tippy, { type Instance as TippyInstance } from "tippy.js";
|
import tippy, { type Instance as TippyInstance } from "tippy.js";
|
||||||
import { MentionList } from "@/components/MentionList";
|
import { MentionList } from "@/components/MentionList";
|
||||||
@@ -8,6 +8,7 @@ export const userSuggestion = {
|
|||||||
items: ({ query }: { query: string }) => {
|
items: ({ query }: { query: string }) => {
|
||||||
return store.contexts
|
return store.contexts
|
||||||
.filter(context => context.kind === "user" && !context.deletedAt)
|
.filter(context => context.kind === "user" && !context.deletedAt)
|
||||||
|
.filter(context => !isCurrentUserContextTag(buildShareTag(context.displayName)))
|
||||||
.filter(context => context.displayName.toLowerCase().startsWith(query.toLowerCase()))
|
.filter(context => context.displayName.toLowerCase().startsWith(query.toLowerCase()))
|
||||||
.map(context => ({
|
.map(context => ({
|
||||||
id: buildShareTag(context.displayName),
|
id: buildShareTag(context.displayName),
|
||||||
|
|||||||
+177
-20
@@ -13,12 +13,18 @@ export const getFavoriteTag = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LIST_FIELDS = 'id,collectionId,collectionName,created,updated,title,startDate,dueDate,priority,status,completed,tags,labelTags,shareRefs,noteRefs,createdBy,user,size,recurrence,deletedAt';
|
const LIST_FIELDS = 'id,collectionId,collectionName,created,updated,title,startDate,dueDate,priority,status,completed,tags,labelTags,shareRefs,noteRefs,createdBy,user,size,recurrence,deletedAt';
|
||||||
|
const NOTE_LIST_FIELDS = 'id,created,updated,title,key,tags,isPrivate,user,tasks,deletedAt';
|
||||||
|
|
||||||
const getStorageKey = () => {
|
const getStorageKey = () => {
|
||||||
const userId = pb.authStore.model?.id;
|
const userId = pb.authStore.model?.id;
|
||||||
return userId ? `${STORAGE_KEY_PREFIX}_data_${userId}` : null;
|
return userId ? `${STORAGE_KEY_PREFIX}_data_${userId}` : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPersistedStoreSnapshot = () => {
|
||||||
|
const { isInitializing, quickloadFadeTaskIds, isNotesLoading, hasLoadedNotesMetadata, loadingNoteContentIds, ...persisted } = store;
|
||||||
|
return persisted;
|
||||||
|
};
|
||||||
|
|
||||||
export const [now, setNow] = createSignal(Date.now());
|
export const [now, setNow] = createSignal(Date.now());
|
||||||
// Context: 'mine' is a temporary bootstrap alias for the current user's personal context bucket.
|
// Context: 'mine' is a temporary bootstrap alias for the current user's personal context bucket.
|
||||||
type TaskContext = 'mine' | { userId: string, name: string } | { bucketId: string, name: string, isPersonal?: boolean };
|
type TaskContext = 'mine' | { userId: string, name: string } | { bucketId: string, name: string, isPersonal?: boolean };
|
||||||
@@ -395,6 +401,10 @@ export interface Bucket {
|
|||||||
interface TaskStore {
|
interface TaskStore {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
isInitializing: boolean;
|
isInitializing: boolean;
|
||||||
|
quickloadFadeTaskIds: string[];
|
||||||
|
isNotesLoading: boolean;
|
||||||
|
hasLoadedNotesMetadata: boolean;
|
||||||
|
loadingNoteContentIds: string[];
|
||||||
pWeight: number;
|
pWeight: number;
|
||||||
uWeight: number;
|
uWeight: number;
|
||||||
matrixScaleDays: number;
|
matrixScaleDays: number;
|
||||||
@@ -419,6 +429,10 @@ interface TaskStore {
|
|||||||
export const [store, setStore] = createStore<TaskStore>({
|
export const [store, setStore] = createStore<TaskStore>({
|
||||||
tasks: [],
|
tasks: [],
|
||||||
isInitializing: false,
|
isInitializing: false,
|
||||||
|
quickloadFadeTaskIds: [],
|
||||||
|
isNotesLoading: false,
|
||||||
|
hasLoadedNotesMetadata: false,
|
||||||
|
loadingNoteContentIds: [],
|
||||||
pWeight: 1.0,
|
pWeight: 1.0,
|
||||||
uWeight: 1.0,
|
uWeight: 1.0,
|
||||||
matrixScaleDays: 30,
|
matrixScaleDays: 30,
|
||||||
@@ -639,6 +653,25 @@ const getPersonalContext = (userId = pb.authStore.model?.id) =>
|
|||||||
) || null
|
) || null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
export const isCurrentUserContextTag = (tag: string) => {
|
||||||
|
const normalizedTag = tag.trim().toLowerCase();
|
||||||
|
if (!normalizedTag.startsWith("@")) return false;
|
||||||
|
|
||||||
|
const personalContext = getPersonalContext();
|
||||||
|
const currentUserDisplayName = (pb.authStore.model?.name || pb.authStore.model?.email || "").trim();
|
||||||
|
const hiddenTags = new Set<string>();
|
||||||
|
|
||||||
|
if (personalContext?.displayName) {
|
||||||
|
hiddenTags.add(buildShareTag(personalContext.displayName).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUserDisplayName) {
|
||||||
|
hiddenTags.add(buildShareTag(currentUserDisplayName).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return hiddenTags.has(normalizedTag);
|
||||||
|
};
|
||||||
|
|
||||||
const getSubscribedBucketContexts = () =>
|
const getSubscribedBucketContexts = () =>
|
||||||
store.subscribedBuckets
|
store.subscribedBuckets
|
||||||
.map(id => store.contexts.find(context => context.id === id))
|
.map(id => store.contexts.find(context => context.id === id))
|
||||||
@@ -779,7 +812,7 @@ createRoot(() => {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const key = getStorageKey();
|
const key = getStorageKey();
|
||||||
if (key) {
|
if (key) {
|
||||||
localStorage.setItem(key, JSON.stringify(store));
|
localStorage.setItem(key, JSON.stringify(getPersistedStoreSnapshot()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -896,6 +929,11 @@ export const getCombinedScore = (task: Task): number => {
|
|||||||
// -- Persistence & Sync --
|
// -- Persistence & Sync --
|
||||||
|
|
||||||
let heartbeatInterval: number | undefined;
|
let heartbeatInterval: number | undefined;
|
||||||
|
let quickloadFadeTimer: number | undefined;
|
||||||
|
let notesLoadPromise: Promise<void> | null = null;
|
||||||
|
let deferredNotesLoadTimer: number | undefined;
|
||||||
|
const loadedNoteDetailIds = new Set<string>();
|
||||||
|
const failedNoteDetailIds = new Set<string>();
|
||||||
|
|
||||||
const mapRecordToTask = (r: any): Task => {
|
const mapRecordToTask = (r: any): Task => {
|
||||||
const legacyTags: string[] = r.tags || [];
|
const legacyTags: string[] = r.tags || [];
|
||||||
@@ -1143,12 +1181,12 @@ const mapRecordToContext = (r: any): ShareContext => ({
|
|||||||
deletedBy: unwrapRelationId(r.deletedBy)
|
deletedBy: unwrapRelationId(r.deletedBy)
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapRecordToNote = (r: any): Note => {
|
const mapRecordToNote = (r: any, includeContent = true): Note => {
|
||||||
return {
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
title: r.title,
|
title: r.title,
|
||||||
key: r.key || normalizeNoteKey(r.title),
|
key: r.key || normalizeNoteKey(r.title),
|
||||||
content: r.content,
|
content: includeContent ? r.content : undefined,
|
||||||
tags: Array.isArray(r.tags) && r.tags.length > 0 ? r.tags : [buildNoteTag(r.key || r.title)],
|
tags: Array.isArray(r.tags) && r.tags.length > 0 ? r.tags : [buildNoteTag(r.key || r.title)],
|
||||||
isPrivate: r.isPrivate || false,
|
isPrivate: r.isPrivate || false,
|
||||||
user: unwrapRelationId(r.user) || "",
|
user: unwrapRelationId(r.user) || "",
|
||||||
@@ -1159,6 +1197,22 @@ const mapRecordToNote = (r: any): Note => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reconcileFetchedNotes = (records: any[], includeContent = false) => {
|
||||||
|
const existingById = new Map(store.notes.map(note => [note.id, note]));
|
||||||
|
const nextNotes = records.map((record: any) => {
|
||||||
|
const mapped = mapRecordToNote(record, includeContent);
|
||||||
|
const existing = existingById.get(mapped.id);
|
||||||
|
|
||||||
|
if (!includeContent && existing?.content !== undefined) {
|
||||||
|
mapped.content = existing.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
});
|
||||||
|
|
||||||
|
setStore("notes", nextNotes);
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchNoteById = async (id: string): Promise<Note | null> => {
|
export const fetchNoteById = async (id: string): Promise<Note | null> => {
|
||||||
try {
|
try {
|
||||||
const record = await pb.collection(NOTES_COLLECTION).getOne(id);
|
const record = await pb.collection(NOTES_COLLECTION).getOne(id);
|
||||||
@@ -1171,6 +1225,10 @@ export const fetchNoteById = async (id: string): Promise<Note | null> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const updateStoreWithNote = (note: Note) => {
|
export const updateStoreWithNote = (note: Note) => {
|
||||||
|
failedNoteDetailIds.delete(note.id);
|
||||||
|
if (note.content !== undefined) {
|
||||||
|
loadedNoteDetailIds.add(note.id);
|
||||||
|
}
|
||||||
setStore("notes", (currentNotes) => {
|
setStore("notes", (currentNotes) => {
|
||||||
const index = currentNotes.findIndex(n => n.id === note.id);
|
const index = currentNotes.findIndex(n => n.id === note.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -1182,6 +1240,83 @@ export const updateStoreWithNote = (note: Note) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const loadNotesMetadata = async () => {
|
||||||
|
if (!pb.authStore.isValid) return;
|
||||||
|
if (store.hasLoadedNotesMetadata) return;
|
||||||
|
if (notesLoadPromise) return notesLoadPromise;
|
||||||
|
|
||||||
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
setStore("isNotesLoading", true);
|
||||||
|
|
||||||
|
notesLoadPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const notesRecords = await pb.collection(NOTES_COLLECTION).getFullList({
|
||||||
|
filter: `isPrivate = false || ${relationFilter("user", currentUserId)}`,
|
||||||
|
sort: '-created',
|
||||||
|
fields: NOTE_LIST_FIELDS,
|
||||||
|
requestKey: null
|
||||||
|
});
|
||||||
|
|
||||||
|
reconcileFetchedNotes(notesRecords, false);
|
||||||
|
setStore("hasLoadedNotesMetadata", true);
|
||||||
|
} catch (notesErr) {
|
||||||
|
console.warn("Failed to load notes (collection might not exist yet):", notesErr);
|
||||||
|
} finally {
|
||||||
|
setStore("isNotesLoading", false);
|
||||||
|
notesLoadPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return notesLoadPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scheduleDeferredNotesMetadataLoad = () => {
|
||||||
|
if (!pb.authStore.isValid || store.hasLoadedNotesMetadata || notesLoadPromise || deferredNotesLoadTimer) return;
|
||||||
|
|
||||||
|
deferredNotesLoadTimer = window.setTimeout(() => {
|
||||||
|
deferredNotesLoadTimer = undefined;
|
||||||
|
void loadNotesMetadata();
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestImmediateNotesMetadataLoad = async () => {
|
||||||
|
if (deferredNotesLoadTimer) {
|
||||||
|
clearTimeout(deferredNotesLoadTimer);
|
||||||
|
deferredNotesLoadTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadNotesMetadata();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadNoteContent = async (id: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
if (loadedNoteDetailIds.has(id)) return;
|
||||||
|
if (failedNoteDetailIds.has(id)) return;
|
||||||
|
if (store.loadingNoteContentIds.includes(id)) return;
|
||||||
|
|
||||||
|
setStore("loadingNoteContentIds", ids => ids.includes(id) ? ids : [...ids, id]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
const visibilityFilter = `id = "${id}" && (isPrivate = false || ${relationFilter("user", currentUserId)})`;
|
||||||
|
const record = await pb.collection(NOTES_COLLECTION).getFirstListItem(visibilityFilter, {
|
||||||
|
requestKey: `note-content-${id}`
|
||||||
|
});
|
||||||
|
const note = mapRecordToNote(record, true);
|
||||||
|
updateStoreWithNote(note);
|
||||||
|
loadedNoteDetailIds.add(id);
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err as any)?.status;
|
||||||
|
if (status === 404) {
|
||||||
|
failedNoteDetailIds.add(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("Failed to load note content:", err);
|
||||||
|
} finally {
|
||||||
|
setStore("loadingNoteContentIds", ids => ids.filter(existingId => existingId !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Helper to check if a task should be visible based on ownership, shares, or rules
|
// Helper to check if a task should be visible based on ownership, shares, or rules
|
||||||
const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
||||||
@@ -1314,10 +1449,16 @@ export const subscribeToRealtime = async () => {
|
|||||||
|
|
||||||
if (e.action === 'create' || e.action === 'update') {
|
if (e.action === 'create' || e.action === 'update') {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
const updatedNote = mapRecordToNote(e.record);
|
const shouldSyncContent = loadedNoteDetailIds.has(e.record.id) || activeNoteId() === e.record.id;
|
||||||
|
const updatedNote = mapRecordToNote(e.record, shouldSyncContent);
|
||||||
setStore("notes", prev => {
|
setStore("notes", prev => {
|
||||||
const exists = prev.find(n => n.id === updatedNote.id);
|
const exists = prev.find(n => n.id === updatedNote.id);
|
||||||
if (exists) return prev.map(n => n.id === updatedNote.id ? updatedNote : n);
|
if (exists) {
|
||||||
|
const mergedNote = shouldSyncContent || exists.content === undefined
|
||||||
|
? updatedNote
|
||||||
|
: { ...updatedNote, content: exists.content };
|
||||||
|
return prev.map(n => n.id === updatedNote.id ? mergedNote : n);
|
||||||
|
}
|
||||||
return [updatedNote, ...prev];
|
return [updatedNote, ...prev];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1616,6 +1757,7 @@ export const initStore = async () => {
|
|||||||
setStore("isInitializing", true);
|
setStore("isInitializing", true);
|
||||||
|
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
const refreshedTaskIds = new Set<string>();
|
||||||
|
|
||||||
// STAGE 1: Instant Focus from Local Auth Model (Top 20 + Fallback)
|
// STAGE 1: Instant Focus from Local Auth Model (Top 20 + Fallback)
|
||||||
// Goal: < 100ms First Paint. Doesn't wait for any other network requests.
|
// Goal: < 100ms First Paint. Doesn't wait for any other network requests.
|
||||||
@@ -1649,6 +1791,13 @@ export const initStore = async () => {
|
|||||||
.filter(t => !t.tags?.includes("__template__"));
|
.filter(t => !t.tags?.includes("__template__"));
|
||||||
|
|
||||||
setStore("tasks", focusTasks);
|
setStore("tasks", focusTasks);
|
||||||
|
setStore("quickloadFadeTaskIds", focusTasks.map(task => task.id));
|
||||||
|
focusTasks.forEach(task => refreshedTaskIds.add(task.id));
|
||||||
|
if (quickloadFadeTimer) clearTimeout(quickloadFadeTimer);
|
||||||
|
quickloadFadeTimer = window.setTimeout(() => {
|
||||||
|
setStore("quickloadFadeTaskIds", []);
|
||||||
|
quickloadFadeTimer = undefined;
|
||||||
|
}, 200);
|
||||||
} catch (focusErr) {
|
} catch (focusErr) {
|
||||||
console.warn("Stage 1 load failed:", focusErr);
|
console.warn("Stage 1 load failed:", focusErr);
|
||||||
}
|
}
|
||||||
@@ -1660,7 +1809,17 @@ export const initStore = async () => {
|
|||||||
const saved = localStorage.getItem(key);
|
const saved = localStorage.getItem(key);
|
||||||
if (saved) {
|
if (saved) {
|
||||||
try {
|
try {
|
||||||
setStore(JSON.parse(saved));
|
const parsed = JSON.parse(saved);
|
||||||
|
const cachedTasks = Array.isArray(parsed?.tasks) ? parsed.tasks : [];
|
||||||
|
const mergedCachedTasks = [
|
||||||
|
...store.tasks,
|
||||||
|
...cachedTasks.filter((task: Task) => !refreshedTaskIds.has(task.id))
|
||||||
|
];
|
||||||
|
setStore({
|
||||||
|
...parsed,
|
||||||
|
tasks: mergedCachedTasks,
|
||||||
|
isInitializing: true
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse cached data", e);
|
console.error("Failed to parse cached data", e);
|
||||||
}
|
}
|
||||||
@@ -1739,19 +1898,6 @@ export const initStore = async () => {
|
|||||||
console.warn("Failed to load contexts (collection might not exist yet):", contextErr);
|
console.warn("Failed to load contexts (collection might not exist yet):", contextErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.3 Fetch Notes
|
|
||||||
try {
|
|
||||||
const currentUserId = pb.authStore.model?.id;
|
|
||||||
const notesRecords = await pb.collection(NOTES_COLLECTION).getFullList({
|
|
||||||
filter: `isPrivate = false || ${relationFilter("user", currentUserId)}`,
|
|
||||||
sort: '-created',
|
|
||||||
requestKey: null
|
|
||||||
});
|
|
||||||
setStore("notes", notesRecords.map(mapRecordToNote));
|
|
||||||
} catch (notesErr) {
|
|
||||||
console.warn("Failed to load notes (collection might not exist yet):", notesErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStore("tagDefinitions", defs => {
|
setStore("tagDefinitions", defs => {
|
||||||
const plainDefs = defs.filter(def => !def.name.startsWith("@"));
|
const plainDefs = defs.filter(def => !def.name.startsWith("@"));
|
||||||
return [...plainDefs, ...deriveSystemTagDefinitions(store.contexts)];
|
return [...plainDefs, ...deriveSystemTagDefinitions(store.contexts)];
|
||||||
@@ -1814,6 +1960,7 @@ export const initStore = async () => {
|
|||||||
const taskMap = new Map(currentTasks.map(t => [t.id, t]));
|
const taskMap = new Map(currentTasks.map(t => [t.id, t]));
|
||||||
allIncomplete.forEach((r: any) => {
|
allIncomplete.forEach((r: any) => {
|
||||||
if (r.tags?.includes("__template__")) return;
|
if (r.tags?.includes("__template__")) return;
|
||||||
|
if (refreshedTaskIds.has(r.id)) return;
|
||||||
|
|
||||||
const existing = taskMap.get(r.id);
|
const existing = taskMap.get(r.id);
|
||||||
|
|
||||||
@@ -1828,13 +1975,16 @@ export const initStore = async () => {
|
|||||||
newTask.content = existing.content;
|
newTask.content = existing.content;
|
||||||
}
|
}
|
||||||
taskMap.set(r.id, newTask);
|
taskMap.set(r.id, newTask);
|
||||||
|
refreshedTaskIds.add(r.id);
|
||||||
});
|
});
|
||||||
return Array.from(taskMap.values());
|
return Array.from(taskMap.values());
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2b. Content Backfill for Incomplete Tasks
|
// 2b. Content Backfill for Incomplete Tasks
|
||||||
// Fetch content for tasks that don't have it yet, in chunks
|
// Fetch content for tasks that don't have it yet, in chunks
|
||||||
const tasksNeedingContent = store.tasks.filter(t => !t.completed && t.content === undefined).map(t => t.id);
|
const tasksNeedingContent = store.tasks
|
||||||
|
.filter(t => !t.completed && t.content === undefined && !refreshedTaskIds.has(t.id))
|
||||||
|
.map(t => t.id);
|
||||||
if (tasksNeedingContent.length > 0) {
|
if (tasksNeedingContent.length > 0) {
|
||||||
// Fetch in chunks of 20 to avoid URL length limits
|
// Fetch in chunks of 20 to avoid URL length limits
|
||||||
const chunkSize = 20;
|
const chunkSize = 20;
|
||||||
@@ -1936,6 +2086,9 @@ export const initStore = async () => {
|
|||||||
|
|
||||||
// 2.8 Fetch Filter Templates
|
// 2.8 Fetch Filter Templates
|
||||||
await loadFilterTemplates();
|
await loadFilterTemplates();
|
||||||
|
|
||||||
|
// Defer note metadata sync until task loading is fully settled.
|
||||||
|
scheduleDeferredNotesMetadataLoad();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (store.tasks.length === 0) {
|
if (store.tasks.length === 0) {
|
||||||
console.error("Failed to load data:", err);
|
console.error("Failed to load data:", err);
|
||||||
@@ -2543,6 +2696,10 @@ export const removeTagDefinition = async (name: string) => {
|
|||||||
export const updateNote = async (id: string, updates: Partial<Note>) => {
|
export const updateNote = async (id: string, updates: Partial<Note>) => {
|
||||||
if (!pb.authStore.isValid) return;
|
if (!pb.authStore.isValid) return;
|
||||||
|
|
||||||
|
if (updates.content !== undefined) {
|
||||||
|
loadedNoteDetailIds.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
const optimisticUpdates = {
|
const optimisticUpdates = {
|
||||||
...updates,
|
...updates,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Component, For, createMemo, onMount } from "solid-js";
|
import { type Component, For, createMemo } from "solid-js";
|
||||||
import { store, getCombinedScore, matchesFilter } from "@/store";
|
import { store, getCombinedScore, matchesFilter } from "@/store";
|
||||||
import { TaskCard } from "@/components/TaskCard";
|
import { TaskCard } from "@/components/TaskCard";
|
||||||
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useTaskListAutoAnimate } from "@/hooks/useTaskListAutoAnimate";
|
||||||
|
|
||||||
export const CriticalView: Component = () => {
|
export const CriticalView: Component = () => {
|
||||||
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
||||||
@@ -17,9 +17,7 @@ export const CriticalView: Component = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let listRef: HTMLDivElement | undefined;
|
let listRef: HTMLDivElement | undefined;
|
||||||
onMount(() => {
|
useTaskListAutoAnimate(() => listRef);
|
||||||
if (listRef) autoAnimate(listRef, { duration: 300, easing: 'ease-out' });
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Component, For, createMemo, onMount } from "solid-js";
|
import { type Component, For, createMemo } from "solid-js";
|
||||||
import { store, getCombinedScore, matchesFilter } from "@/store";
|
import { store, getCombinedScore, matchesFilter } from "@/store";
|
||||||
import { TaskCard } from "@/components/TaskCard";
|
import { TaskCard } from "@/components/TaskCard";
|
||||||
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useTaskListAutoAnimate } from "@/hooks/useTaskListAutoAnimate";
|
||||||
|
|
||||||
export const DigInView: Component = () => {
|
export const DigInView: Component = () => {
|
||||||
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
||||||
@@ -22,9 +22,7 @@ export const DigInView: Component = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let listRef: HTMLDivElement | undefined;
|
let listRef: HTMLDivElement | undefined;
|
||||||
onMount(() => {
|
useTaskListAutoAnimate(() => listRef);
|
||||||
if (listRef) autoAnimate(listRef, { duration: 300, easing: 'ease-out' });
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
+26
-23
@@ -1,5 +1,5 @@
|
|||||||
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
||||||
import { store, renameTagDefinition, updateNote, updateTask, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag, fetchNoteById, updateStoreWithNote } from "@/store";
|
import { store, renameTagDefinition, updateNote, updateTask, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag, loadNoteContent, requestImmediateNotesMetadataLoad } from "@/store";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { type Note } from "@/store";
|
import { type Note } from "@/store";
|
||||||
import { Plus, X, Lock, Unlock, Trash2, ChevronDown, Link as LinkIcon, MoreHorizontal, Type, FileText, Search, Star, ChevronRight } from "lucide-solid";
|
import { Plus, X, Lock, Unlock, Trash2, ChevronDown, Link as LinkIcon, MoreHorizontal, Type, FileText, Search, Star, ChevronRight } from "lucide-solid";
|
||||||
@@ -57,29 +57,27 @@ export const NotepadView: Component<{
|
|||||||
// 3. Trigger cleanup after a tiny delay so the updateNote above processes first
|
// 3. Trigger cleanup after a tiny delay so the updateNote above processes first
|
||||||
if (currentId) {
|
if (currentId) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
const existingNote = store.notes.find(note => note.id === currentId && !note.deletedAt);
|
||||||
|
if (!existingNote) return;
|
||||||
cleanupNoteAttachments(currentId).catch(console.error);
|
cleanupNoteAttachments(currentId).catch(console.error);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle fetching note for unauthenticated users (public shares)
|
createEffect(() => {
|
||||||
createEffect(async () => {
|
void requestImmediateNotesMetadataLoad();
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
const id = props.selectedNoteId;
|
const id = props.selectedNoteId;
|
||||||
|
const note = activeNote();
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
// If note is already in store, we are good
|
if (!note && pb.authStore.isValid) return;
|
||||||
const exists = store.notes.find(n => n.id === id);
|
|
||||||
if (exists) return;
|
|
||||||
|
|
||||||
// If not authenticated or note just missing (e.g. direct link), try to fetch it
|
console.log("[NotepadView] Ensuring note content is loaded for:", id);
|
||||||
console.log("[NotepadView] Note not found in store, attempting public fetch for:", id);
|
void loadNoteContent(id);
|
||||||
const note = await fetchNoteById(id);
|
|
||||||
if (note) {
|
|
||||||
updateStoreWithNote(note);
|
|
||||||
} else if (!pb.authStore.isValid) {
|
|
||||||
toast.error("Note not found or you don't have permission to view it.");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derived states
|
// Derived states
|
||||||
@@ -361,7 +359,7 @@ export const NotepadView: Component<{
|
|||||||
<div class="h-16 w-16 rounded-3xl bg-muted flex items-center justify-center mb-2 animate-pulse">
|
<div class="h-16 w-16 rounded-3xl bg-muted flex items-center justify-center mb-2 animate-pulse">
|
||||||
<Search size={32} class="text-muted-foreground/50" />
|
<Search size={32} class="text-muted-foreground/50" />
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.selectedNoteId && store.notes.length === 0} fallback={
|
<Show when={props.selectedNoteId && (store.isNotesLoading || store.loadingNoteContentIds.includes(props.selectedNoteId))} fallback={
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-sm font-bold tracking-widest uppercase">No Note Selected</p>
|
<p class="text-sm font-bold tracking-widest uppercase">No Note Selected</p>
|
||||||
<p class="text-[0.8rem] text-muted-foreground max-w-[200px] leading-relaxed">
|
<p class="text-[0.8rem] text-muted-foreground max-w-[200px] leading-relaxed">
|
||||||
@@ -445,7 +443,7 @@ export const NotepadView: Component<{
|
|||||||
{/* Editor Area */}
|
{/* Editor Area */}
|
||||||
<div class={cn(
|
<div class={cn(
|
||||||
"flex-1 md:flex-initial md:shrink flex flex-col min-w-0 md:w-[960px] lg:w-[1120px] md:h-full overflow-visible md:overflow-y-auto relative scroll-smooth bg-background z-20 pt-0",
|
"flex-1 md:flex-initial md:shrink flex flex-col min-w-0 md:w-[960px] lg:w-[1120px] md:h-full overflow-visible md:overflow-y-auto relative scroll-smooth bg-background z-20 pt-0",
|
||||||
hasTopViewer() ? "p-0 space-y-0" : "px-4 md:px-6 pb-4 md:pb-6 space-y-4 md:space-y-6"
|
hasTopViewer() ? "p-0 space-y-0" : "px-4 md:px-6 pb-4 md:pb-6 space-y-2 md:space-y-6"
|
||||||
)}>
|
)}>
|
||||||
|
|
||||||
<div class={cn(
|
<div class={cn(
|
||||||
@@ -617,13 +615,18 @@ export const NotepadView: Component<{
|
|||||||
"grow shrink-0 flex flex-col",
|
"grow shrink-0 flex flex-col",
|
||||||
hasTopViewer() ? "border-none rounded-none p-0 shadow-none mb-0 min-h-full" : "min-h-[300px] mb-4 px-4 pt-0 pb-4 border border-border/50 rounded-xl bg-background shadow-sm"
|
hasTopViewer() ? "border-none rounded-none p-0 shadow-none mb-0 min-h-full" : "min-h-[300px] mb-4 px-4 pt-0 pb-4 border border-border/50 rounded-xl bg-background shadow-sm"
|
||||||
)}>
|
)}>
|
||||||
<TaskEditor
|
<Show
|
||||||
content={note().content}
|
when={note().content !== undefined || !store.loadingNoteContentIds.includes(note().id)}
|
||||||
onUpdate={(html) => handleUpdateContent(note().id, html)}
|
fallback={<div class="h-32 animate-pulse bg-muted/20 rounded-lg" />}
|
||||||
onEditorReady={setEditorInstance}
|
>
|
||||||
editable={canEdit()}
|
<TaskEditor
|
||||||
class={hasTopViewer() ? "[&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:px-4 md:[&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:px-6 [&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:max-w-3xl [&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:mx-auto [&>.file-viewer-wrapper:first-child]:h-[80vh] [&>.file-viewer-wrapper:first-child]:min-h-0 [&>.file-attachment-wrapper:first-child]:h-[80vh] [&>.file-attachment-wrapper:first-child]:min-h-0 pb-20" : ""}
|
content={note().content}
|
||||||
/>
|
onUpdate={(html) => handleUpdateContent(note().id, html)}
|
||||||
|
onEditorReady={setEditorInstance}
|
||||||
|
editable={canEdit()}
|
||||||
|
class={hasTopViewer() ? "[&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:px-4 md:[&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:px-6 [&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:max-w-3xl [&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:mx-auto [&>.file-viewer-wrapper:first-child]:h-[80vh] [&>.file-viewer-wrapper:first-child]:min-h-0 [&>.file-attachment-wrapper:first-child]:h-[80vh] [&>.file-attachment-wrapper:first-child]:min-h-0 pb-20" : ""}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={!hasTopViewer()}>
|
<Show when={!hasTopViewer()}>
|
||||||
<div class="shrink-0 h-16 md:h-8" />
|
<div class="shrink-0 h-16 md:h-8" />
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Component, For, createMemo, onMount } from "solid-js";
|
import { type Component, For, createMemo } from "solid-js";
|
||||||
import { store, getCombinedScore, matchesFilter } from "@/store";
|
import { store, getCombinedScore, matchesFilter } from "@/store";
|
||||||
import { TaskCard } from "@/components/TaskCard";
|
import { TaskCard } from "@/components/TaskCard";
|
||||||
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useTaskListAutoAnimate } from "@/hooks/useTaskListAutoAnimate";
|
||||||
|
|
||||||
export const PriorityView: Component = () => {
|
export const PriorityView: Component = () => {
|
||||||
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
||||||
@@ -17,9 +17,7 @@ export const PriorityView: Component = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let listRef: HTMLDivElement | undefined;
|
let listRef: HTMLDivElement | undefined;
|
||||||
onMount(() => {
|
useTaskListAutoAnimate(() => listRef);
|
||||||
if (listRef) autoAnimate(listRef, { duration: 300, easing: 'ease-out' });
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Component, For, createMemo, onMount } from "solid-js";
|
import { type Component, For, createMemo } from "solid-js";
|
||||||
import { store, getCombinedScore, matchesFilter } from "@/store";
|
import { store, getCombinedScore, matchesFilter } from "@/store";
|
||||||
import { TaskCard } from "@/components/TaskCard";
|
import { TaskCard } from "@/components/TaskCard";
|
||||||
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useTaskListAutoAnimate } from "@/hooks/useTaskListAutoAnimate";
|
||||||
|
|
||||||
export const ProgressView: Component = () => {
|
export const ProgressView: Component = () => {
|
||||||
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
||||||
@@ -24,9 +24,7 @@ export const ProgressView: Component = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let listRef: HTMLDivElement | undefined;
|
let listRef: HTMLDivElement | undefined;
|
||||||
onMount(() => {
|
useTaskListAutoAnimate(() => listRef);
|
||||||
if (listRef) autoAnimate(listRef, { duration: 300, easing: 'ease-out' });
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Component, For, createMemo, onMount } from "solid-js";
|
import { type Component, For, createMemo } from "solid-js";
|
||||||
import { store, getCombinedScore, matchesFilter } from "@/store";
|
import { store, getCombinedScore, matchesFilter } from "@/store";
|
||||||
import { TaskCard } from "@/components/TaskCard";
|
import { TaskCard } from "@/components/TaskCard";
|
||||||
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useTaskListAutoAnimate } from "@/hooks/useTaskListAutoAnimate";
|
||||||
|
|
||||||
export const SnowballView: Component = () => {
|
export const SnowballView: Component = () => {
|
||||||
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
||||||
@@ -22,9 +22,7 @@ export const SnowballView: Component = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let listRef: HTMLDivElement | undefined;
|
let listRef: HTMLDivElement | undefined;
|
||||||
onMount(() => {
|
useTaskListAutoAnimate(() => listRef);
|
||||||
if (listRef) autoAnimate(listRef, { duration: 300, easing: 'ease-out' });
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Component, For, createMemo, onMount } from "solid-js";
|
import { type Component, For, createMemo } from "solid-js";
|
||||||
import { store, calculateUrgencyScore, getCombinedScore, matchesFilter } from "@/store";
|
import { store, calculateUrgencyScore, getCombinedScore, matchesFilter } from "@/store";
|
||||||
import { TaskCard } from "@/components/TaskCard";
|
import { TaskCard } from "@/components/TaskCard";
|
||||||
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
import { useDelayedSort } from "@/hooks/useDelayedSort";
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useTaskListAutoAnimate } from "@/hooks/useTaskListAutoAnimate";
|
||||||
|
|
||||||
export const UrgencyView: Component = () => {
|
export const UrgencyView: Component = () => {
|
||||||
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
const sourceTasks = createMemo(() => store.tasks.filter(t => !t.deletedAt && matchesFilter(t)));
|
||||||
@@ -21,9 +21,7 @@ export const UrgencyView: Component = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let listRef: HTMLDivElement | undefined;
|
let listRef: HTMLDivElement | undefined;
|
||||||
onMount(() => {
|
useTaskListAutoAnimate(() => listRef);
|
||||||
if (listRef) autoAnimate(listRef, { duration: 300, easing: 'ease-out' });
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|||||||
+1
-20
@@ -13,9 +13,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
const env = loadEnv(mode, process.cwd(), '')
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
const dataMode = (env.VITE_TASGRID_DATA_MODE || 'prod').toLowerCase()
|
const dataMode = (env.VITE_TASGRID_DATA_MODE || 'prod').toLowerCase()
|
||||||
const isDevData = dataMode === 'dev'
|
const isDevData = dataMode === 'dev'
|
||||||
const pocketbaseUrl = env.VITE_POCKETBASE_URL || 'https://pocketbase.ccllc.pro'
|
|
||||||
const pocketbaseOrigin = new URL(pocketbaseUrl).origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
||||||
const cacheSuffix = isDevData ? '-devdata' : '-prod'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
define: {
|
define: {
|
||||||
@@ -58,23 +55,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
workbox: {
|
workbox: {
|
||||||
cleanupOutdatedCaches: true,
|
cleanupOutdatedCaches: true,
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
|
||||||
runtimeCaching: [
|
runtimeCaching: []
|
||||||
{
|
|
||||||
urlPattern: new RegExp(`^${pocketbaseOrigin}/api/.*`, 'i'),
|
|
||||||
method: 'GET',
|
|
||||||
handler: 'NetworkFirst',
|
|
||||||
options: {
|
|
||||||
cacheName: `api-cache${cacheSuffix}`,
|
|
||||||
expiration: {
|
|
||||||
maxEntries: 100,
|
|
||||||
maxAgeSeconds: 60 * 60 * 24 * 7
|
|
||||||
},
|
|
||||||
cacheableResponse: {
|
|
||||||
statuses: [0, 200]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user