src/store/index.ts breakup (no drama)
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
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 [];
|
||||
};
|
||||
Reference in New Issue
Block a user