diff --git a/src/components/NotesSidebar.tsx b/src/components/NotesSidebar.tsx
index 69e9e9d..6e79bc0 100644
--- a/src/components/NotesSidebar.tsx
+++ b/src/components/NotesSidebar.tsx
@@ -81,7 +81,7 @@ export const NotesSidebar: Component<{
{(t) => (
- #{t}
+ {t}
)}
diff --git a/src/components/TagPicker.tsx b/src/components/TagPicker.tsx
index 4a82546..917ecee 100644
--- a/src/components/TagPicker.tsx
+++ b/src/components/TagPicker.tsx
@@ -24,8 +24,8 @@ export const TagPicker: Component = (props) => {
const allTagNames = () => {
const definedTags = store.tagDefinitions.map(d => d.name);
- const bucketTags = store.buckets.map(b => b.name);
- const noteTags = store.notes.map(n => n.title);
+ const bucketTags = store.buckets.map(b => `@${b.name}`);
+ const noteTags = store.notes.map(n => `#${n.title}`);
return [...new Set([...definedTags, ...bucketTags, ...noteTags])].sort();
};
@@ -52,7 +52,7 @@ export const TagPicker: Component = (props) => {
if (!name) return;
// update global definition
- const bucket = store.buckets.find(b => b.name === name);
+ const bucket = store.buckets.find(b => b.name === name || `@${b.name}` === name);
const color = bucket ? bucket.color : undefined;
upsertTagDefinition(name, valueScore(), color, resolvedTheme());
diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx
index 018b5d3..8669566 100644
--- a/src/components/TaskCard.tsx
+++ b/src/components/TaskCard.tsx
@@ -33,8 +33,9 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
if (typeof ctx === 'object' && 'bucketId' in ctx) {
// We are in a bucket view. Hide the tag that matches this bucket's name.
- const bucketName = (ctx as { name: string }).name.toLowerCase();
- return tags.filter(t => t.toLowerCase() !== bucketName);
+ // Buckets are now tagged with '@' + name.
+ const bucketTagName = `@${(ctx as { name: string }).name.toLowerCase()}`;
+ return tags.filter(t => t.toLowerCase() !== bucketTagName);
}
return tags;
diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx
index a932e42..e93f753 100644
--- a/src/components/TaskDetail.tsx
+++ b/src/components/TaskDetail.tsx
@@ -101,10 +101,10 @@ export const TaskDetail: Component = (props) => {
// 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) {
- const bucketName = (ctx as { name: string }).name;
+ const bucketTagName = `@${(ctx as { name: string }).name}`;
// Check case-insensitive existence
- if (!newTags.some(t => t.toLowerCase() === bucketName.toLowerCase())) {
- newTags.push(bucketName);
+ if (!newTags.some(t => t.toLowerCase() === bucketTagName.toLowerCase())) {
+ newTags.push(bucketTagName);
}
}
@@ -129,8 +129,9 @@ export const TaskDetail: Component = (props) => {
if (typeof ctx === 'object' && 'bucketId' in ctx) {
// We are in a bucket view. Hide the tag that matches this bucket's name.
- const bucketName = (ctx as { name: string }).name.toLowerCase();
- return tags.filter(t => t.toLowerCase() !== bucketName);
+ // Buckets are now tagged with '@' + name.
+ const bucketTagName = `@${(ctx as { name: string }).name.toLowerCase()}`;
+ return tags.filter(t => t.toLowerCase() !== bucketTagName);
}
return tags;
diff --git a/src/store/index.ts b/src/store/index.ts
index 77184a4..3fe2be2 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -260,28 +260,16 @@ export const matchesFilter = (task: Task) => {
task.tags?.includes(r.tagName || "");
});
- // Bucket visibility logic for "My Bucket":
- // 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 Bucket' 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.
-
+ // Bucket tasks are now tagged with '@' + name.
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;
+ // Show tasks that have the bucket name (prefixed with @) as a tag
+ const bucketTagName = `@${bucketProxy.name}`;
+ if (!task.tags?.includes(bucketTagName)) return false;
} else {
// Context is Oversight for specific user
// Show ONLY tasks owned by that user
@@ -545,13 +533,9 @@ const isTaskInSubscribedBucket = (task: any): boolean => {
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);
+ return bucket && taskTags.includes(`@${bucket.name}`);
});
};
@@ -1315,17 +1299,18 @@ const checkForHandoffs = (task: Task, updates: Partial): Partial =>
// Check for any tag that triggers a SEND_TASK rule
for (const tag of updates.tags) {
+ // Handle @ prefix in rule matching
const rule = store.shareRules.find(r =>
r.ownerId === currentUserId &&
r.type === 'tag' &&
- r.tagName === tag &&
+ (r.tagName === tag || `@${r.tagName}` === tag) &&
r.share_mode === 'SEND_TASK'
);
if (rule) {
console.log(`Handoff triggered by tag "${tag}" -> Transferring to ${rule.targetUserId}`);
- // Check if it's a bucket tag (target is self, but tag is a bucket)
+ // Check if it's a bucket tag
const isBucketRule = rule.targetUserId === currentUserId && store.tagDefinitions.find(t => t.name === tag)?.isBucket;
if (isBucketRule) {
@@ -1633,12 +1618,13 @@ export const syncSystemTagsAndRules = async () => {
// --- Sync Bucket Tags ---
for (const bucket of store.buckets) {
- const tagExists = tagDefinitions.find(t => t.name === bucket.name);
+ const bucketTagName = `@${bucket.name}`;
+ const tagExists = tagDefinitions.find(t => t.name === bucketTagName);
if (!tagExists) {
try {
const record = await pb.collection(TAGS_COLLECTION).create({
user: currentUserId,
- name: bucket.name,
+ name: bucketTagName,
value: 5,
color: bucket.color || "#64748b",
theme: "dark",
@@ -1653,23 +1639,22 @@ export const syncSystemTagsAndRules = async () => {
isBucket: true
}]);
} catch (e) {
- console.error(`Failed to create bucket tag ${bucket.name}`, e);
+ console.error(`Failed to create bucket tag ${bucketTagName}`, e);
}
} else if (!tagExists.isBucket) {
try {
await pb.collection(TAGS_COLLECTION).update(tagExists.id, { isBucket: true });
setStore("tagDefinitions", d => d.id === tagExists.id, { isBucket: true });
} catch (e) {
- console.error(`Failed to flag tag ${bucket.name} as bucket`, e);
+ console.error(`Failed to flag tag ${bucketTagName} as bucket`, e);
}
}
// --- Sync Bucket System Rule ---
- // Ensure a ShareRule exists for this bucket so it appears in "System Rules"
const bucketRuleExists = normalizedRules.some(r =>
r.ownerId === currentUserId &&
r.targetUserId === currentUserId &&
r.type === 'tag' &&
- r.tagName === bucket.name
+ (r.tagName === bucket.name || r.tagName === bucketTagName)
);
if (!bucketRuleExists) {
@@ -1678,19 +1663,20 @@ export const syncSystemTagsAndRules = async () => {
ownerId: currentUserId,
targetUserId: currentUserId,
type: 'tag',
- tagName: bucket.name,
+ tagName: bucketTagName,
share_mode: 'SEND_TASK'
});
} catch (e) {
- console.error(`Failed to ensure system rule for bucket ${bucket.name}`, e);
+ console.error(`Failed to ensure system rule for bucket ${bucketTagName}`, e);
}
}
}
// --- Sync User Tags ---
for (const user of users) {
- const userName = user.name || user.email;
- if (!userName) continue;
+ const originalName = user.name || user.email;
+ if (!originalName) continue;
+ const userName = `@${originalName}`;
const tagExists = tagDefinitions.find(t => t.name === userName);
if (!tagExists) {
@@ -1723,34 +1709,13 @@ export const syncSystemTagsAndRules = async () => {
}
}
- // --- User Share Rule Sync ---
- // OPTIMIZATION: We no longer create individual share rules for every user-pair.
- // Instead, we rely on the "Global Rule" (Self-Rule) created above for the user's own name.
- // When User A tags a task "Bob", the Global Rule for Bob (Owner=Bob, Target=Bob, Tag=Bob)
- // will pick it up via the updated `shouldTaskBeVisible` logic.
-
- // If we want to support "Collab" (Edit access), we might still need specific rules or specific shares,
- // but for "View" access (Handoff), the Global Rule is sufficient.
-
- // We do NOT delete existing rules here to avoid data loss during transition,
- // unless strictly requested. The user said "I want to optimize share rules... make them ownerless".
- // So we stop creating new ones.
-
- /*
- if (user.id !== currentUserId) {
- // Legacy loop removed to prevent exponential growth
- }
- */
-
// --- Ensure Self-Rule for Current User (Global Subscription) ---
- // This is the critical piece for "Global Sharing".
- // We must ensure the user has a rule: Owner=Me, Target=Me, Tag=Me.
if (user.id === currentUserId) {
const selfRuleExists = normalizedRules.some(r =>
r.ownerId === currentUserId &&
r.targetUserId === currentUserId &&
r.type === 'tag' &&
- r.tagName === userName
+ (r.tagName === originalName || r.tagName === userName)
);
if (!selfRuleExists) {
@@ -1760,10 +1725,9 @@ export const syncSystemTagsAndRules = async () => {
targetUserId: currentUserId,
type: 'tag',
tagName: userName,
- share_mode: 'ADD_USER' // Mode doesn't strictly matter for self-rule visibility, but ADD_USER is safe default
+ share_mode: 'ADD_USER'
});
- // Update local store immediately to ensure loadAllHistory sees it
const newRule: ShareRule = {
id: record.id,
ownerId: currentUserId,
diff --git a/src/views/NotepadView.tsx b/src/views/NotepadView.tsx
index 2a8a3ff..2b05ea8 100644
--- a/src/views/NotepadView.tsx
+++ b/src/views/NotepadView.tsx
@@ -53,7 +53,8 @@ export const NotepadView: Component<{
await handleUpdateNote(id, { title: newTitle });
// 2. Rename the TagDefinition (preserves color) and propagate to all tagged tasks
- await renameTagDefinition(oldTitle, newTitle);
+ // Use '#' prefix for note tags
+ await renameTagDefinition(`#${oldTitle}`, `#${newTitle}`);
};
const handleDeleteNote = async (id: string) => {
try {
@@ -86,11 +87,12 @@ export const NotepadView: Component<{
const linkedTasks = createMemo(() => {
const note = activeNote();
if (!note) return [];
+ const noteTagName = `#${note.title}`;
return store.tasks.filter(t => {
// Explicit relation
if (note.tasks && note.tasks.includes(t.id)) return true;
- // Tag relation (case-insensitive title match)
- if (t.tags && t.tags.some(tag => tag.toLowerCase() === note.title.toLowerCase())) return true;
+ // Tag relation (case-insensitive title match with # prefix)
+ if (t.tags && t.tags.some(tag => tag.toLowerCase() === noteTagName.toLowerCase())) return true;
return false;
});
});
@@ -206,7 +208,6 @@ export const NotepadView: Component<{
{(tag) => (
-
{tag}