pre-migration prepped
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_TASGRID_ENABLE_PROD_MIGRATION=true
|
||||||
|
VITE_TASGRID_MIGRATION_ADMIN_EMAILS=tcardoza@cardoza.construction
|
||||||
@@ -2,10 +2,17 @@ const rawMode = (import.meta.env.VITE_TASGRID_DATA_MODE || "prod").toLowerCase()
|
|||||||
|
|
||||||
export const TASGRID_DATA_MODE = rawMode === "dev" ? "dev" : "prod";
|
export const TASGRID_DATA_MODE = rawMode === "dev" ? "dev" : "prod";
|
||||||
export const TASGRID_IS_DEV_DATA = TASGRID_DATA_MODE === "dev";
|
export const TASGRID_IS_DEV_DATA = TASGRID_DATA_MODE === "dev";
|
||||||
|
const rawMigrationAdmins = import.meta.env.VITE_TASGRID_MIGRATION_ADMIN_EMAILS || "";
|
||||||
|
const rawMigrationEnabled = (import.meta.env.VITE_TASGRID_ENABLE_PROD_MIGRATION || "").toLowerCase();
|
||||||
|
|
||||||
export const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL || "https://pocketbase.ccllc.pro";
|
export const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL || "https://pocketbase.ccllc.pro";
|
||||||
export const AUTH_STORE_KEY = TASGRID_IS_DEV_DATA ? "tasgrid_dev_auth" : "tasgrid_auth";
|
export const AUTH_STORE_KEY = TASGRID_IS_DEV_DATA ? "tasgrid_dev_auth" : "tasgrid_auth";
|
||||||
export const STORAGE_KEY_PREFIX = TASGRID_IS_DEV_DATA ? "tasgrid_dev" : "tasgrid";
|
export const STORAGE_KEY_PREFIX = TASGRID_IS_DEV_DATA ? "tasgrid_dev" : "tasgrid";
|
||||||
export const PWA_CACHE_SUFFIX = TASGRID_IS_DEV_DATA ? "dev" : "prod";
|
export const PWA_CACHE_SUFFIX = TASGRID_IS_DEV_DATA ? "dev" : "prod";
|
||||||
|
export const PROD_MIGRATION_ENABLED = rawMigrationEnabled === "true";
|
||||||
|
export const PROD_MIGRATION_ADMIN_EMAILS = rawMigrationAdmins
|
||||||
|
.split(",")
|
||||||
|
.map((email: string) => email.trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
export const withDataSuffix = (base: string) => TASGRID_IS_DEV_DATA ? `${base}_Dev` : base;
|
export const withDataSuffix = (base: string) => TASGRID_IS_DEV_DATA ? `${base}_Dev` : base;
|
||||||
|
|||||||
+204
-56
@@ -3,7 +3,7 @@ import { createSignal, createEffect, createRoot } from "solid-js";
|
|||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { toast } from "solid-sonner";
|
import { toast } from "solid-sonner";
|
||||||
import { CONTEXTS_COLLECTION, NOTES_COLLECTION, TASGRID_COLLECTION, URGENCY_HOURS, SIZE_HOURS } from "@/lib/constants";
|
import { CONTEXTS_COLLECTION, NOTES_COLLECTION, TASGRID_COLLECTION, URGENCY_HOURS, SIZE_HOURS } from "@/lib/constants";
|
||||||
import { STORAGE_KEY_PREFIX, TASGRID_DATA_MODE } from "@/lib/app-config";
|
import { STORAGE_KEY_PREFIX, TASGRID_DATA_MODE, TASGRID_IS_DEV_DATA, PROD_MIGRATION_ADMIN_EMAILS, PROD_MIGRATION_ENABLED } from "@/lib/app-config";
|
||||||
import { buildNoteTag, buildShareTag, normalizeContextKey, normalizeNoteKey, parseTags } from "@/lib/tags";
|
import { buildNoteTag, buildShareTag, normalizeContextKey, normalizeNoteKey, parseTags } from "@/lib/tags";
|
||||||
|
|
||||||
// Helper to get the current user's personalized favorite tag
|
// Helper to get the current user's personalized favorite tag
|
||||||
@@ -536,6 +536,26 @@ const buildTaskTags = (
|
|||||||
return [...new Set(tags.filter(Boolean))];
|
return [...new Set(tags.filter(Boolean))];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dedupeShareRefs = (refs: TaskShareRef[]) => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return refs.filter(ref => {
|
||||||
|
const key = `${ref.kind}:${ref.contextId}:${ref.key}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dedupeNoteRefs = (refs: NoteRef[]) => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return refs.filter(ref => {
|
||||||
|
const key = `${ref.noteId}:${ref.key}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const withActiveContextTags = (rawTags: string[]) => {
|
const withActiveContextTags = (rawTags: string[]) => {
|
||||||
const nextTags = [...rawTags];
|
const nextTags = [...rawTags];
|
||||||
const ctx = currentTaskContext();
|
const ctx = currentTaskContext();
|
||||||
@@ -586,6 +606,17 @@ const setContexts = (contexts: ShareContext[]) => {
|
|||||||
setStore("tagDefinitions", reconcile(mergedDefs));
|
setStore("tagDefinitions", reconcile(mergedDefs));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadContextsIntoStore = async () => {
|
||||||
|
const contextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({
|
||||||
|
sort: 'displayName,key',
|
||||||
|
requestKey: null
|
||||||
|
}).catch(() => []);
|
||||||
|
const nextContexts = contextRecords.map(mapRecordToContext);
|
||||||
|
setContexts(nextContexts);
|
||||||
|
setStore("shareRules", nextContexts.map(mapContextToShareRule));
|
||||||
|
return nextContexts;
|
||||||
|
};
|
||||||
|
|
||||||
const getPersonalContext = (userId = pb.authStore.model?.id) =>
|
const getPersonalContext = (userId = pb.authStore.model?.id) =>
|
||||||
userId
|
userId
|
||||||
? store.contexts.find(context =>
|
? store.contexts.find(context =>
|
||||||
@@ -631,7 +662,11 @@ export const matchesFilter = (task: Task) => {
|
|||||||
);
|
);
|
||||||
const inPersonalContext = !!personalContext && task.shareRefs.some(ref =>
|
const inPersonalContext = !!personalContext && task.shareRefs.some(ref =>
|
||||||
ref.kind === "user" &&
|
ref.kind === "user" &&
|
||||||
(ref.contextId === personalContext.id || ref.key === personalContext.key)
|
(
|
||||||
|
ref.contextId === personalContext.id ||
|
||||||
|
ref.key === personalContext.key ||
|
||||||
|
normalizeContextKey(personalContext.displayName) === ref.key
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 0. Context Filter
|
// 0. Context Filter
|
||||||
@@ -905,6 +940,66 @@ const mapRecordToTask = (r: any): Task => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildNoteLinkMap = (notes: Note[]) => {
|
||||||
|
const links = new Map<string, NoteRef[]>();
|
||||||
|
for (const note of notes) {
|
||||||
|
for (const taskId of note.tasks || []) {
|
||||||
|
const existing = links.get(taskId) || [];
|
||||||
|
existing.push({ noteId: note.id, key: note.key });
|
||||||
|
links.set(taskId, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTaskMigrationPayload = (
|
||||||
|
task: Task,
|
||||||
|
ownerContextByUserId: Map<string, ShareContext>,
|
||||||
|
linkedNoteRefs: NoteRef[] = [],
|
||||||
|
fallbackUserId?: string
|
||||||
|
) => {
|
||||||
|
const favoriteTag = task.tags.find(tag => tag.endsWith("_favorite__"));
|
||||||
|
const ownerContext = task.ownerId ? ownerContextByUserId.get(task.ownerId) : undefined;
|
||||||
|
let nextShareRefs = dedupeShareRefs(task.shareRefs);
|
||||||
|
let nextNoteRefs = dedupeNoteRefs([...task.noteRefs, ...linkedNoteRefs]);
|
||||||
|
const hasHandoffBucket = nextShareRefs.some(ref => {
|
||||||
|
const context = getContextByRef(ref);
|
||||||
|
return context?.kind === "bucket" && context.policy === "handoff";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ownerContext) {
|
||||||
|
const matchesOwnerContext = (ref: TaskShareRef) =>
|
||||||
|
ref.kind === "user" && (
|
||||||
|
ref.contextId === ownerContext.id ||
|
||||||
|
ref.key === ownerContext.key ||
|
||||||
|
normalizeContextKey(ownerContext.displayName) === ref.key
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasHandoffBucket) {
|
||||||
|
nextShareRefs = nextShareRefs.filter(ref => !matchesOwnerContext(ref));
|
||||||
|
} else if (!nextShareRefs.some(matchesOwnerContext)) {
|
||||||
|
nextShareRefs = [
|
||||||
|
{
|
||||||
|
contextId: ownerContext.id,
|
||||||
|
key: ownerContext.key,
|
||||||
|
kind: "user" as const
|
||||||
|
},
|
||||||
|
...nextShareRefs
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextShareRefs = dedupeShareRefs(nextShareRefs);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags: buildTaskTags(task.labelTags, nextShareRefs, nextNoteRefs, favoriteTag),
|
||||||
|
labelTags: task.labelTags,
|
||||||
|
shareRefs: nextShareRefs,
|
||||||
|
noteRefs: nextNoteRefs,
|
||||||
|
createdBy: task.createdBy || task.ownerId || fallbackUserId || pb.authStore.model?.id || ""
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const uploadTaskAttachment = async (taskId: string, file: File): Promise<{ url: string, filename: string }> => {
|
export const uploadTaskAttachment = async (taskId: string, file: File): Promise<{ url: string, filename: string }> => {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -1247,7 +1342,7 @@ export const subscribeToRealtime = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const runLegacyTagMigration = async () => {
|
const runLegacyTagMigration = async (force = false) => {
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
if (!currentUserId) return;
|
if (!currentUserId) return;
|
||||||
|
|
||||||
@@ -1256,13 +1351,45 @@ const runLegacyTagMigration = async () => {
|
|||||||
const rawPrefs = userRec.Taskgrid_pref || {};
|
const rawPrefs = userRec.Taskgrid_pref || {};
|
||||||
const prefs = readScopedPrefs(rawPrefs);
|
const prefs = readScopedPrefs(rawPrefs);
|
||||||
|
|
||||||
if (prefs.migratedStructuredTagsV2) {
|
if (!force && prefs.migratedStructuredTagsV2) {
|
||||||
return; // Already migrated
|
return; // Already migrated
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[MIGRATION] Starting structured tag migration...");
|
console.log("[MIGRATION] Starting structured tag migration...");
|
||||||
|
|
||||||
await syncUserContexts();
|
await syncUserContexts();
|
||||||
|
const contexts = await loadContextsIntoStore();
|
||||||
|
const ownerContextByUserId = new Map(
|
||||||
|
contexts
|
||||||
|
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
|
||||||
|
.map(context => [context.targetUserId as string, context])
|
||||||
|
);
|
||||||
|
|
||||||
|
const notes = await pb.collection(NOTES_COLLECTION).getFullList({
|
||||||
|
requestKey: null
|
||||||
|
});
|
||||||
|
const migratedNotes: Note[] = [];
|
||||||
|
for (const noteRecord of notes) {
|
||||||
|
const key = normalizeNoteKey(noteRecord.key || noteRecord.title || noteRecord.id);
|
||||||
|
const existingTags = Array.isArray(noteRecord.tags) ? noteRecord.tags.filter(Boolean) : [];
|
||||||
|
const nextTags = [...new Set([buildNoteTag(key), ...existingTags])];
|
||||||
|
const nextTasks = [...new Set((Array.isArray(noteRecord.tasks) ? noteRecord.tasks : []).filter(Boolean))];
|
||||||
|
|
||||||
|
await pb.collection(NOTES_COLLECTION).update(noteRecord.id, {
|
||||||
|
key,
|
||||||
|
tags: nextTags,
|
||||||
|
tasks: nextTasks
|
||||||
|
}, { requestKey: null });
|
||||||
|
|
||||||
|
migratedNotes.push({
|
||||||
|
...mapRecordToNote(noteRecord),
|
||||||
|
key,
|
||||||
|
tags: nextTags,
|
||||||
|
tasks: nextTasks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setStore("notes", migratedNotes);
|
||||||
|
const noteLinksByTaskId = buildNoteLinkMap(migratedNotes);
|
||||||
|
|
||||||
const tasks = await pb.collection(TASGRID_COLLECTION).getFullList({
|
const tasks = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
requestKey: null
|
requestKey: null
|
||||||
@@ -1270,24 +1397,13 @@ const runLegacyTagMigration = async () => {
|
|||||||
for (const taskRecord of tasks) {
|
for (const taskRecord of tasks) {
|
||||||
if (taskRecord.tags?.includes("__template__")) continue;
|
if (taskRecord.tags?.includes("__template__")) continue;
|
||||||
const task = mapRecordToTask(taskRecord);
|
const task = mapRecordToTask(taskRecord);
|
||||||
await pb.collection(TASGRID_COLLECTION).update(task.id, {
|
const payload = buildTaskMigrationPayload(
|
||||||
tags: task.tags,
|
task,
|
||||||
labelTags: task.labelTags,
|
ownerContextByUserId,
|
||||||
shareRefs: task.shareRefs,
|
noteLinksByTaskId.get(task.id) || [],
|
||||||
noteRefs: task.noteRefs,
|
currentUserId
|
||||||
createdBy: task.createdBy || task.ownerId || currentUserId
|
);
|
||||||
}, { requestKey: null });
|
await pb.collection(TASGRID_COLLECTION).update(task.id, payload, { requestKey: null });
|
||||||
}
|
|
||||||
|
|
||||||
const notes = await pb.collection(NOTES_COLLECTION).getFullList({
|
|
||||||
requestKey: null
|
|
||||||
});
|
|
||||||
for (const noteRecord of notes) {
|
|
||||||
const note = mapRecordToNote(noteRecord);
|
|
||||||
await pb.collection(NOTES_COLLECTION).update(note.id, {
|
|
||||||
key: note.key,
|
|
||||||
tags: note.tags
|
|
||||||
}, { requestKey: null });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPrefs = { ...prefs, migratedStructuredTagsV2: true };
|
const nextPrefs = { ...prefs, migratedStructuredTagsV2: true };
|
||||||
@@ -1331,13 +1447,7 @@ const syncUserContexts = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (createdAny || store.contexts.length === 0) {
|
if (createdAny || store.contexts.length === 0) {
|
||||||
const nextContextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({
|
await loadContextsIntoStore();
|
||||||
sort: 'displayName,key',
|
|
||||||
requestKey: null
|
|
||||||
}).catch(() => []);
|
|
||||||
const nextContexts = nextContextRecords.map(mapRecordToContext);
|
|
||||||
setContexts(nextContexts);
|
|
||||||
setStore("shareRules", nextContexts.map(mapContextToShareRule));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1382,7 +1492,7 @@ const syncSupervisedContexts = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const runPersonalContextMigration = async () => {
|
const runPersonalContextMigration = async (force = false) => {
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
if (!currentUserId) return;
|
if (!currentUserId) return;
|
||||||
|
|
||||||
@@ -1391,18 +1501,19 @@ const runPersonalContextMigration = async () => {
|
|||||||
const rawPrefs = userRec.Taskgrid_pref || {};
|
const rawPrefs = userRec.Taskgrid_pref || {};
|
||||||
const prefs = readScopedPrefs(rawPrefs);
|
const prefs = readScopedPrefs(rawPrefs);
|
||||||
|
|
||||||
if (prefs.migratedPersonalContextRefsV1) {
|
if (!force && prefs.migratedPersonalContextRefsV1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contexts = store.contexts.length > 0
|
const contexts = store.contexts.length > 0
|
||||||
? store.contexts
|
? store.contexts
|
||||||
: (await pb.collection(CONTEXTS_COLLECTION).getFullList({ requestKey: null })).map(mapRecordToContext);
|
: await loadContextsIntoStore();
|
||||||
const ownerContextByUserId = new Map(
|
const ownerContextByUserId = new Map(
|
||||||
contexts
|
contexts
|
||||||
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
|
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
|
||||||
.map(context => [context.targetUserId as string, context])
|
.map(context => [context.targetUserId as string, context])
|
||||||
);
|
);
|
||||||
|
const noteLinksByTaskId = buildNoteLinkMap(store.notes);
|
||||||
|
|
||||||
const taskRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
|
const taskRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
fields: LIST_FIELDS,
|
fields: LIST_FIELDS,
|
||||||
@@ -1414,30 +1525,19 @@ const runPersonalContextMigration = async () => {
|
|||||||
|
|
||||||
const task = mapRecordToTask(taskRecord);
|
const task = mapRecordToTask(taskRecord);
|
||||||
if (!task.ownerId) continue;
|
if (!task.ownerId) continue;
|
||||||
|
const payload = buildTaskMigrationPayload(
|
||||||
const ownerContext = ownerContextByUserId.get(task.ownerId);
|
task,
|
||||||
if (!ownerContext) continue;
|
ownerContextByUserId,
|
||||||
|
noteLinksByTaskId.get(task.id) || [],
|
||||||
const alreadyHasOwnerContext = task.shareRefs.some(ref =>
|
currentUserId
|
||||||
ref.kind === "user" &&
|
|
||||||
(ref.contextId === ownerContext.id || ref.key === ownerContext.key)
|
|
||||||
);
|
);
|
||||||
if (alreadyHasOwnerContext) continue;
|
|
||||||
|
|
||||||
const favoriteTag = task.tags.find(tag => tag.endsWith("_favorite__"));
|
const sameShareRefs = JSON.stringify(payload.shareRefs) === JSON.stringify(task.shareRefs);
|
||||||
const nextShareRefs = [
|
const sameNoteRefs = JSON.stringify(payload.noteRefs) === JSON.stringify(task.noteRefs);
|
||||||
{
|
const sameTags = JSON.stringify(payload.tags) === JSON.stringify(task.tags);
|
||||||
contextId: ownerContext.id,
|
if (sameShareRefs && sameNoteRefs && sameTags && payload.createdBy === task.createdBy) continue;
|
||||||
key: ownerContext.key,
|
|
||||||
kind: "user" as const
|
|
||||||
},
|
|
||||||
...task.shareRefs
|
|
||||||
];
|
|
||||||
|
|
||||||
await pb.collection(TASGRID_COLLECTION).update(task.id, {
|
await pb.collection(TASGRID_COLLECTION).update(task.id, payload, { requestKey: null });
|
||||||
shareRefs: nextShareRefs,
|
|
||||||
tags: buildTaskTags(task.labelTags, nextShareRefs, task.noteRefs, favoriteTag)
|
|
||||||
}, { requestKey: null });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await pb.collection('users').update(currentUserId, {
|
await pb.collection('users').update(currentUserId, {
|
||||||
@@ -1451,6 +1551,52 @@ const runPersonalContextMigration = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const runDevDataMigration = async () => {
|
||||||
|
if (!pb.authStore.isValid) return;
|
||||||
|
if (!TASGRID_IS_DEV_DATA) {
|
||||||
|
toast.error("Dev migration is only available in dev data mode.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await toast.promise((async () => {
|
||||||
|
await runLegacyTagMigration(true);
|
||||||
|
await runPersonalContextMigration(true);
|
||||||
|
await loadContextsIntoStore();
|
||||||
|
await syncSupervisedContexts();
|
||||||
|
return "Dev data migration completed.";
|
||||||
|
})(), {
|
||||||
|
loading: "Running dev data migration...",
|
||||||
|
success: (message) => message,
|
||||||
|
error: "Dev data migration failed."
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canRunProdMigration = () => {
|
||||||
|
if (TASGRID_IS_DEV_DATA || !PROD_MIGRATION_ENABLED) return false;
|
||||||
|
const email = (pb.authStore.model?.email || "").trim().toLowerCase();
|
||||||
|
return !!email && PROD_MIGRATION_ADMIN_EMAILS.includes(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runProdDataMigration = async () => {
|
||||||
|
if (!pb.authStore.isValid) return;
|
||||||
|
if (!canRunProdMigration()) {
|
||||||
|
toast.error("Prod migration is not enabled for this account.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await toast.promise((async () => {
|
||||||
|
await runLegacyTagMigration(true);
|
||||||
|
await runPersonalContextMigration(true);
|
||||||
|
await loadContextsIntoStore();
|
||||||
|
await syncSupervisedContexts();
|
||||||
|
return "Production data migration completed.";
|
||||||
|
})(), {
|
||||||
|
loading: "Running production data migration...",
|
||||||
|
success: (message) => message,
|
||||||
|
error: "Production data migration failed."
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const initStore = async () => {
|
export const initStore = async () => {
|
||||||
if (!pb.authStore.isValid || store.isInitializing) return;
|
if (!pb.authStore.isValid || store.isInitializing) return;
|
||||||
@@ -1744,9 +1890,11 @@ export const initStore = async () => {
|
|||||||
// Fire off background sync
|
// Fire off background sync
|
||||||
await backgroundSync();
|
await backgroundSync();
|
||||||
|
|
||||||
// Run one-time migration for legacy tags
|
// Run one-time migration automatically only in dev data mode.
|
||||||
await runLegacyTagMigration();
|
if (TASGRID_IS_DEV_DATA) {
|
||||||
await runPersonalContextMigration();
|
await runLegacyTagMigration();
|
||||||
|
await runPersonalContextMigration();
|
||||||
|
}
|
||||||
|
|
||||||
// Separate Templates load (less critical)
|
// Separate Templates load (less critical)
|
||||||
try {
|
try {
|
||||||
|
|||||||
+102
-1
@@ -1,6 +1,6 @@
|
|||||||
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
|
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
|
||||||
import { ThemeToggle } from "../components/ThemeToggle";
|
import { ThemeToggle } from "../components/ThemeToggle";
|
||||||
import { store, restoreTask, deleteTaskPermanently, restoreNote, deleteNotePermanently, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription, updateUserContextPolicy, savePersonalContextSupervisors } from "@/store";
|
import { store, restoreTask, deleteTaskPermanently, restoreNote, deleteNotePermanently, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription, updateUserContextPolicy, savePersonalContextSupervisors, runDevDataMigration, runProdDataMigration, canRunProdMigration } from "@/store";
|
||||||
import { useTheme } from "@/components/ThemeProvider";
|
import { useTheme } from "@/components/ThemeProvider";
|
||||||
import { Trash2, Undo2, ArrowLeftRight, Tag, ChevronDown, ChevronRight, Share2, Users, HelpCircle, Copy, Plus, Type, Upload, Box, Edit2, Archive } from "lucide-solid";
|
import { Trash2, Undo2, ArrowLeftRight, Tag, ChevronDown, ChevronRight, Share2, Users, HelpCircle, Copy, Plus, Type, Upload, Box, Edit2, Archive } from "lucide-solid";
|
||||||
import { TagPicker } from "@/components/TagPicker";
|
import { TagPicker } from "@/components/TagPicker";
|
||||||
@@ -10,6 +10,7 @@ import { toast } from "solid-sonner";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
|
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { TASGRID_IS_DEV_DATA } from "@/lib/app-config";
|
||||||
import { createEffect } from "solid-js";
|
import { createEffect } from "solid-js";
|
||||||
|
|
||||||
const TaskEditor = lazy(() => import("../components/TaskEditor").then(m => ({ default: m.TaskEditor })));
|
const TaskEditor = lazy(() => import("../components/TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||||
@@ -23,6 +24,10 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
|||||||
const [isSharingOpen, setIsSharingOpen] = createSignal(false);
|
const [isSharingOpen, setIsSharingOpen] = createSignal(false);
|
||||||
const [isImportOpen, setIsImportOpen] = createSignal(false);
|
const [isImportOpen, setIsImportOpen] = createSignal(false);
|
||||||
const [isBucketsOpen, setIsBucketsOpen] = createSignal(false);
|
const [isBucketsOpen, setIsBucketsOpen] = createSignal(false);
|
||||||
|
const [isDevToolsOpen, setIsDevToolsOpen] = createSignal(false);
|
||||||
|
const [isProdMigrationOpen, setIsProdMigrationOpen] = createSignal(false);
|
||||||
|
const [isRunningDevMigration, setIsRunningDevMigration] = createSignal(false);
|
||||||
|
const [isRunningProdMigration, setIsRunningProdMigration] = createSignal(false);
|
||||||
const [expandedTemplateId, setExpandedTemplateId] = createSignal<string | null>(null);
|
const [expandedTemplateId, setExpandedTemplateId] = createSignal<string | null>(null);
|
||||||
const [shareUserId, setShareUserId] = createSignal("");
|
const [shareUserId, setShareUserId] = createSignal("");
|
||||||
const [shareType, setShareType] = createSignal<'all' | 'tag'>('tag');
|
const [shareType, setShareType] = createSignal<'all' | 'tag'>('tag');
|
||||||
@@ -1053,6 +1058,102 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={TASGRID_IS_DEV_DATA}>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h2 class="text-xs font-bold text-muted-foreground/40 uppercase tracking-widest pl-1">Dev Tools</h2>
|
||||||
|
<section class="p-4 sm:p-5 rounded-2xl border border-amber-500/20 bg-amber-500/5 shadow-sm space-y-4 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between cursor-pointer group"
|
||||||
|
onClick={() => setIsDevToolsOpen(!isDevToolsOpen())}
|
||||||
|
>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||||
|
<Archive size={16} />
|
||||||
|
Dev Data Migration
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Rebuild canonical note keys, task `shareRefs` and `noteRefs`, personal user contexts, and bucket handoff cleanup for dev data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||||
|
{isDevToolsOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isDevToolsOpen()}>
|
||||||
|
<div class="pt-2 animate-in fade-in slide-in-from-top-2 duration-300 space-y-3">
|
||||||
|
<p class="text-[0.6875rem] text-muted-foreground">
|
||||||
|
This is a dev-only repair tool. It updates notes and tasks in the current `*_Dev` collections to the latest key and context model.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="w-full sm:w-auto"
|
||||||
|
disabled={isRunningDevMigration()}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsRunningDevMigration(true);
|
||||||
|
try {
|
||||||
|
await runDevDataMigration();
|
||||||
|
} finally {
|
||||||
|
setIsRunningDevMigration(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isRunningDevMigration() ? "Running Migration..." : "Run Dev Data Migration"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={canRunProdMigration()}>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h2 class="text-xs font-bold text-muted-foreground/40 uppercase tracking-widest pl-1">Production Migration</h2>
|
||||||
|
<section class="p-4 sm:p-5 rounded-2xl border border-red-500/20 bg-red-500/5 shadow-sm space-y-4 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between cursor-pointer group"
|
||||||
|
onClick={() => setIsProdMigrationOpen(!isProdMigrationOpen())}
|
||||||
|
>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||||
|
<Archive size={16} />
|
||||||
|
Prod Data Migration
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Admin-only one-shot migration for production tasks, notes, contexts, and note-task links.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||||
|
{isProdMigrationOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isProdMigrationOpen()}>
|
||||||
|
<div class="pt-2 animate-in fade-in slide-in-from-top-2 duration-300 space-y-3">
|
||||||
|
<p class="text-[0.6875rem] text-muted-foreground">
|
||||||
|
Run this only after the PocketBase production schema is ready and you have a fresh backup. This updates live production data in place.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
class="w-full sm:w-auto"
|
||||||
|
disabled={isRunningProdMigration()}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsRunningProdMigration(true);
|
||||||
|
try {
|
||||||
|
await runProdDataMigration();
|
||||||
|
} finally {
|
||||||
|
setIsRunningProdMigration(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isRunningProdMigration() ? "Running Production Migration..." : "Run Production Migration"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* Organization Category */}
|
{/* Organization Category */}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h2 class="text-xs font-bold text-muted-foreground/40 uppercase tracking-widest pl-1">Organization</h2>
|
<h2 class="text-xs font-bold text-muted-foreground/40 uppercase tracking-widest pl-1">Organization</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user