added buckets
This commit is contained in:
+219
-12
@@ -1,8 +1,8 @@
|
||||
import { createStore, reconcile } from "solid-js/store";
|
||||
import { createSignal, createEffect, createRoot } from "solid-js";
|
||||
import { pb, TASGRID_COLLECTION, TAGS_COLLECTION } from "@/lib/pocketbase";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { toast } from "solid-sonner";
|
||||
import { URGENCY_HOURS } from "@/lib/constants";
|
||||
import { TASGRID_COLLECTION, TAGS_COLLECTION, SHARE_RULES_COLLECTION, BUCKETS_COLLECTION, URGENCY_HOURS } from "@/lib/constants";
|
||||
|
||||
const getStorageKey = () => {
|
||||
const userId = pb.authStore.model?.id;
|
||||
@@ -10,9 +10,10 @@ const getStorageKey = () => {
|
||||
};
|
||||
|
||||
export const [now, setNow] = createSignal(Date.now());
|
||||
// Context: 'mine' = My Workspace (default), { userId: string } = Oversight View for that user
|
||||
// Context: 'mine' = My Workspace (default), { userId: string } = Oversight View for that user, { bucketId: string, name: string } = Bucket View
|
||||
type TaskContext = 'mine' | { userId: string, name: string } | { bucketId: string, name: string };
|
||||
export const [activeTaskId, setActiveTaskId] = createSignal<string | null>(null);
|
||||
export const [currentTaskContext, setCurrentTaskContext] = createSignal<'mine' | { userId: string, name: string }>('mine');
|
||||
export const [currentTaskContext, setCurrentTaskContext] = createSignal<TaskContext>('mine');
|
||||
|
||||
export type UrgencyLevel = number;
|
||||
|
||||
@@ -159,6 +160,13 @@ export interface ShareRule {
|
||||
tagName?: string;
|
||||
}
|
||||
|
||||
export interface Bucket {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface TaskStore {
|
||||
tasks: Task[];
|
||||
pWeight: number;
|
||||
@@ -170,6 +178,8 @@ interface TaskStore {
|
||||
filter: Filter;
|
||||
shareRules: ShareRule[];
|
||||
filterTemplates: FilterTemplate[];
|
||||
buckets: Bucket[];
|
||||
subscribedBuckets: string[]; // IDs of buckets I'm subscribed to
|
||||
}
|
||||
|
||||
// Initial empty state
|
||||
@@ -190,7 +200,9 @@ export const [store, setStore] = createStore<TaskStore>({
|
||||
editedToday: false
|
||||
},
|
||||
shareRules: [],
|
||||
filterTemplates: []
|
||||
filterTemplates: [],
|
||||
buckets: [],
|
||||
subscribedBuckets: []
|
||||
});
|
||||
|
||||
export const matchesFilter = (task: Task) => {
|
||||
@@ -210,13 +222,34 @@ export const matchesFilter = (task: Task) => {
|
||||
task.tags?.includes(r.tagName || "")
|
||||
);
|
||||
|
||||
// If context is "mine", we hide tasks that only match a "Share All" rule
|
||||
// (i.e., not owner, not explicit, not tag rule)
|
||||
// Bucket visibility logic for "My Workspace":
|
||||
// Users requested: "Any user who adds that bucket to their workspaces list can view the workspace and see tasks in that bucket from all users."
|
||||
// AND "In settings a user should be able to see and edit all buckets... They should be able to add any bucket to their workspaces list."
|
||||
// When in 'mine', do we show bucket tasks?
|
||||
// Typically 'My Workspace' is just MY stuff + stuff shared explicitly with me.
|
||||
// Buckets seem to be separate "Contexts" in the sidebar.
|
||||
// However, if the user wants them mixed in, they didn't explicitly say "mix into my main list",
|
||||
// they said "add that bucket to their workspaces list". This usually implies a separate view.
|
||||
// BUT, if I create a task and tag it "Shop", I expect to see it.
|
||||
|
||||
// If I own the task, I see it.
|
||||
// If it's shared with me, I see it.
|
||||
|
||||
if (!isOwner && !isExplicitlyShared && !isTagRuleShared) return false;
|
||||
} else if (typeof ctx === 'object' && 'bucketId' in ctx) {
|
||||
// Bucket View
|
||||
const bucketProxy = store.buckets.find(b => b.id === (ctx as { bucketId: string }).bucketId);
|
||||
if (!bucketProxy) return false;
|
||||
|
||||
// Show tasks that have the bucket name as a tag
|
||||
// The requirement: "if any user adds a tag that is the name of a bucket to a task that task is then made public and moved to the bucket."
|
||||
if (!task.tags?.includes(bucketProxy.name)) return false;
|
||||
} else {
|
||||
// Context is Oversight for specific user
|
||||
// Show ONLY tasks owned by that user
|
||||
if (task.ownerId !== ctx.userId) return false;
|
||||
// Note: We might want to EXCLUDE bucket tasks from oversight if they are purely bucket tasks,
|
||||
// but typically oversight means "everything that user owns".
|
||||
if (task.ownerId !== (ctx as { userId: string }).userId) return false;
|
||||
}
|
||||
|
||||
// Hide templates from all regular views
|
||||
@@ -396,9 +429,28 @@ const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
||||
return false;
|
||||
});
|
||||
|
||||
// Bucket check
|
||||
if (isTaskInSubscribedBucket(task)) return true;
|
||||
|
||||
return !!matchingRule;
|
||||
};
|
||||
|
||||
// Check if task is visible because it belongs to a subscribed bucket
|
||||
const isTaskInSubscribedBucket = (task: any): boolean => {
|
||||
const taskTags = task.tags || [];
|
||||
if (taskTags.length === 0) return false;
|
||||
|
||||
// Check if any of the task's tags match a bucket we are subscribed to
|
||||
// We need to look up bucket names from our subscribed bucket IDs
|
||||
// This is slightly expensive if we iterate.
|
||||
// Optimization: Pre-calculate subscribed bucket names?
|
||||
// For now, simple find.
|
||||
return store.subscribedBuckets.some(subId => {
|
||||
const bucket = store.buckets.find(b => b.id === subId);
|
||||
return bucket && taskTags.includes(bucket.name);
|
||||
});
|
||||
};
|
||||
|
||||
export const subscribeToRealtime = async () => {
|
||||
// Unsubscribe first to avoid duplicates
|
||||
await pb.collection(TASGRID_COLLECTION).unsubscribe('*');
|
||||
@@ -501,6 +553,32 @@ export const subscribeToRealtime = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to Buckets
|
||||
// We need to know if buckets are added/removed/renamed
|
||||
await pb.collection(BUCKETS_COLLECTION).unsubscribe('*');
|
||||
pb.collection(BUCKETS_COLLECTION).subscribe('*', async (e) => {
|
||||
if (e.action === 'create' || e.action === 'update') {
|
||||
const bucket = {
|
||||
id: e.record.id,
|
||||
name: e.record.name,
|
||||
color: e.record.color,
|
||||
description: e.record.description
|
||||
};
|
||||
|
||||
setStore("buckets", (prev) => {
|
||||
const exists = prev.find(b => b.id === bucket.id);
|
||||
if (exists) return prev.map(b => b.id === bucket.id ? bucket : b);
|
||||
return [...prev, bucket];
|
||||
});
|
||||
}
|
||||
|
||||
if (e.action === 'delete') {
|
||||
setStore("buckets", (prev) => prev.filter(b => b.id !== e.record.id));
|
||||
// Also remove from subscribed if present?
|
||||
// The user record might still have it, but we can filter display.
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to Share Rules
|
||||
await pb.collection(SHARE_RULES_COLLECTION).unsubscribe('*');
|
||||
pb.collection(SHARE_RULES_COLLECTION).subscribe('*', async (e) => {
|
||||
@@ -598,13 +676,27 @@ export const initStore = async () => {
|
||||
pWeight: prefs.pWeight || 1.0,
|
||||
uWeight: prefs.uWeight || 1.0,
|
||||
matrixScaleDays: prefs.matrixScaleDays || 30,
|
||||
prefId: userId
|
||||
prefId: userId,
|
||||
subscribedBuckets: user.subscribedBuckets || []
|
||||
});
|
||||
}
|
||||
} catch (prefErr) {
|
||||
console.warn("Failed to load preferences:", prefErr);
|
||||
}
|
||||
|
||||
// 1.2 Fetch Buckets
|
||||
try {
|
||||
const buckets = await pb.collection(BUCKETS_COLLECTION).getFullList({ sort: 'name' });
|
||||
setStore("buckets", buckets.map((b: any) => ({
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
color: b.color,
|
||||
description: b.description
|
||||
})));
|
||||
} catch (bucketErr) {
|
||||
console.warn("Failed to load buckets (collection might not exist yet):", bucketErr);
|
||||
}
|
||||
|
||||
// 1.5. Fetch Tag Definitions
|
||||
try {
|
||||
const tagRecords = await pb.collection(TAGS_COLLECTION).getFullList({
|
||||
@@ -650,11 +742,26 @@ export const initStore = async () => {
|
||||
}).catch(err => { console.warn("Failed to load share rules:", err); return []; })
|
||||
: Promise.resolve([]);
|
||||
|
||||
// 1.9 Fetch Tasks for Subscribed Buckets
|
||||
const subscribedBucketNames = store.subscribedBuckets
|
||||
.map(id => store.buckets.find(b => b.id === id)?.name)
|
||||
.filter(Boolean);
|
||||
|
||||
const bucketTasksPromise = (subscribedBucketNames.length > 0)
|
||||
? pb.collection(TASGRID_COLLECTION).getFullList({
|
||||
filter: subscribedBucketNames.map(name => `tags ~ "${name}"`).join(' || '),
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
}).catch(err => { console.warn("Failed to load bucket tasks:", err); return []; })
|
||||
: Promise.resolve([]);
|
||||
|
||||
|
||||
// Wait for all initial fetches
|
||||
const [ownRecords, individuallySharedRecords, incomingRules] = await Promise.all([
|
||||
const [ownRecords, individuallySharedRecords, incomingRules, bucketRecords] = await Promise.all([
|
||||
ownTasksPromise,
|
||||
individuallySharedPromise,
|
||||
shareRulesPromise
|
||||
shareRulesPromise,
|
||||
bucketTasksPromise
|
||||
]);
|
||||
|
||||
const allTasks: Task[] = [];
|
||||
@@ -734,6 +841,19 @@ export const initStore = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Process Bucket Tasks
|
||||
if (bucketRecords && bucketRecords.length > 0) {
|
||||
bucketRecords.forEach((r: any) => {
|
||||
if (!seenIds.has(r.id)) {
|
||||
const tags = r.tags || [];
|
||||
if (!tags.includes("__template__")) {
|
||||
allTasks.push(mapRecordToTask(r));
|
||||
seenIds.add(r.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set store ONCE with all tasks and rules
|
||||
setStore("tasks", reconcile(allTasks));
|
||||
setStore("templates", loadedTemplates);
|
||||
@@ -777,6 +897,91 @@ export const initStore = async () => {
|
||||
|
||||
// -- Actions --
|
||||
|
||||
export const toggleBucketSubscription = async (bucketId: string) => {
|
||||
const currentSubscribed = store.subscribedBuckets;
|
||||
const isSubscribed = currentSubscribed.includes(bucketId);
|
||||
|
||||
let newSubscribed = [];
|
||||
if (isSubscribed) {
|
||||
newSubscribed = currentSubscribed.filter(id => id !== bucketId);
|
||||
} else {
|
||||
newSubscribed = [...currentSubscribed, bucketId];
|
||||
}
|
||||
|
||||
setStore("subscribedBuckets", newSubscribed);
|
||||
|
||||
// Persist to user profile
|
||||
const userId = pb.authStore.model?.id;
|
||||
if (userId) {
|
||||
try {
|
||||
await pb.collection('users').update(userId, {
|
||||
subscribedBuckets: newSubscribed
|
||||
});
|
||||
|
||||
// If we just subscribed, we should fetch the tasks for this bucket immediately
|
||||
if (!isSubscribed) {
|
||||
const bucketName = store.buckets.find(b => b.id === bucketId)?.name;
|
||||
if (bucketName) {
|
||||
const records = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||
filter: `tags ~ "${bucketName}"`,
|
||||
sort: '-created'
|
||||
});
|
||||
|
||||
const newTasks: Task[] = [];
|
||||
records.forEach((r: any) => {
|
||||
const exists = store.tasks.find(t => t.id === r.id);
|
||||
if (!exists && !r.tags?.includes("__template__")) {
|
||||
newTasks.push(mapRecordToTask(r));
|
||||
}
|
||||
});
|
||||
|
||||
if (newTasks.length > 0) {
|
||||
setStore("tasks", t => [...newTasks, ...t]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to update subscriptions:", err);
|
||||
// Revert on error
|
||||
setStore("subscribedBuckets", currentSubscribed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createBucket = async (name: string, color: string, description?: string) => {
|
||||
try {
|
||||
await pb.collection(BUCKETS_COLLECTION).create({
|
||||
name,
|
||||
color,
|
||||
description
|
||||
});
|
||||
toast.success("Bucket created");
|
||||
} catch (err) {
|
||||
console.error("Failed to create bucket:", err);
|
||||
toast.error("Failed to create bucket");
|
||||
}
|
||||
};
|
||||
|
||||
export const updateBucket = async (id: string, data: { name?: string, color?: string, description?: string }) => {
|
||||
try {
|
||||
await pb.collection(BUCKETS_COLLECTION).update(id, data);
|
||||
toast.success("Bucket updated");
|
||||
} catch (err) {
|
||||
console.error("Failed to update bucket:", err);
|
||||
toast.error("Failed to update bucket");
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteBucket = async (id: string) => {
|
||||
try {
|
||||
await pb.collection(BUCKETS_COLLECTION).delete(id);
|
||||
toast.success("Bucket deleted");
|
||||
} catch (err) {
|
||||
console.error("Failed to delete bucket:", err);
|
||||
toast.error("Failed to delete bucket");
|
||||
}
|
||||
};
|
||||
|
||||
export const addTask = async (title: string, options?: { dueDate?: Date | string, priority?: number, tags?: string[], content?: string, size?: number }) => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
@@ -1368,7 +1573,7 @@ export const splitTask = async (taskId: string, userId: string) => {
|
||||
|
||||
// -- Share Rules (for tag-based and all-tasks sharing) --
|
||||
|
||||
const SHARE_RULES_COLLECTION = 'TasGrid_ShareRules';
|
||||
|
||||
|
||||
export const loadShareRules = async () => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
@@ -1508,3 +1713,5 @@ export const setupPersistence = () => {
|
||||
// Actually, index.tsx calls this. We can leave it empty or have it check auth.
|
||||
initStore();
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user