Files
TasGrid/src/store/state.ts
T

353 lines
9.6 KiB
TypeScript

import { createSignal } from "solid-js";
import { createStore } from "solid-js/store";
import { pb } from "@/lib/pocketbase";
import { STORAGE_KEY_PREFIX, TASGRID_DATA_MODE } from "@/lib/app-config";
// Helper to get the current user's personalized favorite tag
export const getFavoriteTag = () => {
const userId = pb.authStore.model?.id;
return userId ? `__${userId}_favorite__` : "__favorite__";
};
export const [now, setNow] = createSignal(Date.now());
// Context: 'mine' is a temporary bootstrap alias for the current user's personal context bucket.
export type TaskContext = "mine" | { userId: string, name: string } | { bucketId: string, name: string, isPersonal?: boolean };
export const [activeTaskId, setActiveTaskId] = createSignal<string | null>(null);
export const [activeNoteId, setActiveNoteId] = createSignal<string | null>(null);
export const [currentTaskContext, setCurrentTaskContext] = createSignal<TaskContext>("mine");
export type UrgencyLevel = number;
export interface TaskShareRef {
contextId: string;
key: string;
kind: "user" | "bucket" | "note";
policy?: "collaborative" | "handoff";
}
export interface NoteRef {
noteId: string;
key: string;
}
export interface Task {
id: string;
title: string;
startDate: string;
dueDate: string;
priority: number;
status: number;
completed: boolean;
tags: string[];
labelTags: string[];
shareRefs: TaskShareRef[];
noteRefs: NoteRef[];
ownerId: string | null;
createdBy: string | null;
updatedBy?: string | null;
content?: string;
deletedAt?: number | null;
created: string;
updated: string;
recurrence?: {
type: "daily" | "weekly" | "monthly";
days?: number[];
dayOfMonth?: number;
lastUncompleted?: string;
};
size?: number;
sharedWith?: Array<{ userId: string; access: "view" | "edit" }>;
attachments?: string[];
}
export interface TaskTemplate {
id: string;
name: string;
title: string;
priority: number;
urgency: number;
tags: string[];
content: string;
}
export interface TagDefinition {
id: string;
name: string;
value: number;
color?: string;
theme?: "light" | "dark";
isUser?: boolean;
isBucket?: boolean;
}
export interface Note {
id: string;
title: string;
key: string;
content?: string;
tags: string[];
isPrivate: boolean;
user: string;
tasks: string[];
created: string;
updated: string;
deletedAt?: number | null;
}
export interface FilterTag {
name: string;
excluded: boolean;
}
export interface Filter {
query: string;
tags: FilterTag[];
priorityMin: number;
priorityMax: number;
urgencyMin: number;
urgencyMax: number;
editedToday: boolean;
ownedByMe: boolean;
starred: boolean;
jobStatus: FilterTag[];
jobDivision: FilterTag[];
}
export interface FilterTemplate {
id: string;
name: string;
filter: Filter;
}
export interface ShareContext {
id: string;
key: string;
displayName: string;
kind: "user" | "bucket" | "note";
policy: "collaborative" | "handoff";
targetUserId?: string | null;
supervisorUserIds?: string[];
color?: string;
description?: string;
deletedAt?: number | null;
deletedBy?: string | null;
}
export interface ShareRule {
id: string;
ownerId: string;
targetUserId: string;
type: "tag" | "all";
tagName?: string;
share_mode?: "ADD_USER" | "SEND_TASK";
}
export interface SupervisedContextAccess {
ownerUserId: string;
ownerName: string;
contextId: string;
contextName: string;
}
export interface Bucket {
id: string;
name: string;
color: string;
description?: string;
policy?: "collaborative" | "handoff";
key?: string;
deletedAt?: number | null;
}
export interface TaskStore {
tasks: Task[];
isInitializing: boolean;
quickloadFadeTaskIds: string[];
dismissedTaskUpdateIndicators: Record<string, string>;
collapsedUntilUpdatedTasks: Record<string, string>;
isNotesLoading: boolean;
hasLoadedNotesMetadata: boolean;
loadingNoteContentIds: string[];
pWeight: number;
uWeight: number;
matrixScaleDays: number;
prefId?: string;
tagDefinitions: TagDefinition[];
templates: TaskTemplate[];
filter: Filter;
shareRules: ShareRule[];
contexts: ShareContext[];
supervisedContexts: SupervisedContextAccess[];
filterTemplates: FilterTemplate[];
buckets: Bucket[];
subscribedBuckets: string[];
personalContextSupervisorIds: string[];
notes: Note[];
isNotepadMode: boolean;
quickloadTasks: string[];
noteFilter: Filter;
lastCompletedTaskCleanupDate: string;
}
export interface ScopedTaskgridPrefs {
pWeight?: number;
uWeight?: number;
matrixScaleDays?: number;
tagDefinitions?: TagDefinition[];
filter?: Filter;
filterTemplates?: FilterTemplate[];
noteFilterTemplates?: FilterTemplate[];
quickloadTasks?: string[];
noteFilter?: Filter;
subscribedBuckets?: string[];
supervisorUserIds?: string[];
dismissedTaskUpdateIndicators?: Record<string, string>;
collapsedUntilUpdatedTasks?: Record<string, string>;
migratedStructuredTagsV2?: boolean;
migratedPersonalContextRefsV1?: boolean;
migratedContextOwnershipV2?: boolean;
lastCompletedTaskCleanupDate?: string;
}
export const createDefaultFilter = (): Filter => ({
query: "",
tags: [],
priorityMin: 1,
priorityMax: 10,
urgencyMin: 1,
urgencyMax: 10,
editedToday: false,
ownedByMe: false,
starred: false,
jobStatus: [],
jobDivision: []
});
export const [store, setStore] = createStore<TaskStore>({
tasks: [],
isInitializing: false,
quickloadFadeTaskIds: [],
dismissedTaskUpdateIndicators: {},
collapsedUntilUpdatedTasks: {},
isNotesLoading: false,
hasLoadedNotesMetadata: false,
loadingNoteContentIds: [],
pWeight: 1.0,
uWeight: 1.0,
matrixScaleDays: 30,
tagDefinitions: [],
templates: [],
filter: createDefaultFilter(),
shareRules: [],
contexts: [],
supervisedContexts: [],
filterTemplates: [],
buckets: [],
subscribedBuckets: [],
personalContextSupervisorIds: [],
notes: [],
isNotepadMode: false,
quickloadTasks: [],
lastCompletedTaskCleanupDate: "",
noteFilter: createDefaultFilter()
});
const PREF_ENVIRONMENTS_KEY = "environments";
export const getStorageKey = () => {
const userId = pb.authStore.model?.id;
return userId ? `${STORAGE_KEY_PREFIX}_data_${userId}` : null;
};
export const getPersistedStoreSnapshot = () => {
const { isInitializing, quickloadFadeTaskIds, isNotesLoading, hasLoadedNotesMetadata, loadingNoteContentIds, ...persisted } = store;
return persisted;
};
export const readScopedPrefs = (rawPrefs: any): ScopedTaskgridPrefs => {
const scoped = rawPrefs?.[PREF_ENVIRONMENTS_KEY]?.[TASGRID_DATA_MODE];
return (scoped || rawPrefs || {}) as ScopedTaskgridPrefs;
};
export const writeScopedPrefs = (rawPrefs: any, scopedPrefs: ScopedTaskgridPrefs) => ({
...(rawPrefs || {}),
[PREF_ENVIRONMENTS_KEY]: {
...(rawPrefs?.[PREF_ENVIRONMENTS_KEY] || {}),
[TASGRID_DATA_MODE]: scopedPrefs
}
});
export const applyScopedPrefsToStore = (prefs: ScopedTaskgridPrefs) => {
const currentUserId = pb.authStore.model?.id;
const personalContext = currentUserId
? store.contexts.find(context =>
!context.deletedAt &&
context.kind === "user" &&
context.targetUserId === currentUserId
) || null
: null;
setStore({
pWeight: prefs.pWeight || 1.0,
uWeight: prefs.uWeight || 1.0,
matrixScaleDays: prefs.matrixScaleDays || 30,
prefId: currentUserId,
subscribedBuckets: prefs.subscribedBuckets || [],
personalContextSupervisorIds: personalContext?.supervisorUserIds || prefs.supervisorUserIds || [],
tagDefinitions: (prefs.tagDefinitions || []).filter((def: TagDefinition) => !def.name.startsWith("@")),
filterTemplates: prefs.filterTemplates || [],
quickloadTasks: prefs.quickloadTasks || [],
dismissedTaskUpdateIndicators: prefs.dismissedTaskUpdateIndicators || {},
collapsedUntilUpdatedTasks: prefs.collapsedUntilUpdatedTasks || {},
noteFilter: store.noteFilter,
filter: store.filter
});
};
export const unwrapRelationId = (value: unknown): string | null => {
if (typeof value === "string") {
return value || null;
}
if (Array.isArray(value)) {
const first = value[0];
return typeof first === "string" ? first : null;
}
return null;
};
export const unwrapRelationIds = (value: unknown): string[] => {
if (Array.isArray(value)) {
return value
.map(item => {
if (typeof item === "string") return item;
if (item && typeof item === "object" && "id" in item && typeof (item as any).id === "string") {
return (item as any).id;
}
return null;
})
.filter((item): item is string => !!item);
}
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) return [];
try {
const parsed = JSON.parse(trimmed);
return unwrapRelationIds(parsed);
} catch {
return trimmed.includes(",")
? trimmed.split(",").map(item => item.trim()).filter(Boolean)
: [trimmed];
}
}
if (value && typeof value === "object" && "id" in value && typeof (value as any).id === "string") {
return [(value as any).id];
}
return [];
};