unstable sharing refactor

This commit is contained in:
2026-03-19 16:15:47 -05:00
parent 4259649fec
commit 37004e4b4a
18 changed files with 1304 additions and 1248 deletions
+40 -40
View File
@@ -5,7 +5,7 @@ import { Button } from "./ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { store, currentTaskContext, setCurrentTaskContext } from "@/store";
import { pb } from "@/lib/pocketbase";
import { createResource, createSignal, createEffect } from "solid-js";
import { createSignal, createEffect } from "solid-js";
interface NavItem {
icon: any;
@@ -121,7 +121,6 @@ export const ContextSwitcher: Component<{
onOpenChange?: (open: boolean) => void;
isMobile?: boolean;
}> = (props) => {
const currentUserId = pb.authStore.model?.id;
const [open, setOpen] = createSignal(false);
// Sync with parent if needed
@@ -136,37 +135,37 @@ export const ContextSwitcher: Component<{
}
});
const [oversightUsers] = createResource(
() => [...new Set(store.shareRules.filter(r => r.targetUserId === currentUserId && r.type === 'all').map(r => r.ownerId))],
async (ownerIds) => {
if (ownerIds.length === 0) return [];
try {
// Fetch user names for these IDs in a single query
const filter = ownerIds.map(id => `id="${id}"`).join(' || ');
const users = await pb.collection('users').getFullList({
filter,
fields: 'id,name,email',
requestKey: null // Disable auto-cancellation
});
return users.map(u => ({ id: u.id, name: u.name || u.email }));
} catch (e: any) {
if (e.isAbort) return []; // Ignore explicit aborts
console.error("Failed to load oversight users", e);
return [];
}
}
);
const ctx = currentTaskContext; // Accessor
const personalContext = () =>
store.contexts.find(context =>
!context.deletedAt &&
context.kind === "user" &&
context.targetUserId === pb.authStore.model?.id
) || null;
const isPersonalContext = () => {
const current = ctx();
const personal = personalContext();
return current === 'mine' || (
typeof current === "object" &&
"bucketId" in current &&
!!personal &&
current.bucketId === personal.id
);
};
const label = () => {
const c = ctx();
return c === 'mine' ? "My Bucket" : c.name;
return isPersonalContext() ? "My Bucket" : (c === 'mine' ? "My Bucket" : c.name);
};
const handleSwitch = (id: string, name: string, type: 'mine' | 'user' | 'bucket' = 'mine') => {
if (type === 'mine') {
setCurrentTaskContext('mine');
const personal = personalContext();
if (personal) {
setCurrentTaskContext({ bucketId: personal.id, name: "My Bucket", isPersonal: true });
} else {
setCurrentTaskContext('mine');
}
} else if (type === 'bucket') {
setCurrentTaskContext({ bucketId: id, name });
} else {
@@ -182,7 +181,7 @@ export const ContextSwitcher: Component<{
}
};
const hasContexts = () => (oversightUsers() && oversightUsers()!.length > 0) || store.subscribedBuckets.length > 0;
const hasContexts = () => !!personalContext() || store.subscribedBuckets.length > 0 || store.supervisedContexts.length > 0;
return (
<Show when={hasContexts()}>
@@ -197,9 +196,9 @@ export const ContextSwitcher: Component<{
<Show when={props.isMobile} fallback={
<>
<div class="flex items-center gap-3 min-w-0">
<div class={cn(
"w-4 h-4 rounded-full flex items-center justify-center shrink-0",
ctx() === 'mine' ? "bg-primary/20 text-primary" : "bg-orange-500/20 text-orange-500"
<div class={cn(
"w-4 h-4 rounded-full flex items-center justify-center shrink-0",
isPersonalContext() ? "bg-primary/20 text-primary" : "bg-orange-500/20 text-orange-500"
)}>
<div class="w-2 h-2 rounded-full bg-current" />
</div>
@@ -209,8 +208,8 @@ export const ContextSwitcher: Component<{
</>
}>
<div class="relative">
<Users size={18} class={cn("transition-transform duration-300", open() && "scale-110", ctx() !== 'mine' && "text-orange-500")} />
{ctx() !== 'mine' && (
<Users size={18} class={cn("transition-transform duration-300", open() && "scale-110", !isPersonalContext() && "text-orange-500")} />
{!isPersonalContext() && (
<div class="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-orange-500 border border-background" />
)}
</div>
@@ -223,7 +222,7 @@ export const ContextSwitcher: Component<{
onClick={() => handleSwitch('mine', '', 'mine')}
class={cn(
"w-full flex items-center gap-2 p-2 rounded-md text-sm transition-colors",
ctx() === 'mine' ? "bg-primary/10 text-primary font-semibold" : "hover:bg-muted text-foreground"
isPersonalContext() ? "bg-primary/10 text-primary font-semibold" : "hover:bg-muted text-foreground"
)}
>
<div class="w-4 h-4 rounded-full bg-primary/20 flex items-center justify-center">
@@ -260,31 +259,32 @@ export const ContextSwitcher: Component<{
</For>
</Show>
<Show when={oversightUsers() && oversightUsers()!.length > 0}>
<Show when={store.supervisedContexts.length > 0}>
<div class="py-1">
<div class="h-px bg-border/50" />
</div>
<div class="px-2 py-1 text-[0.625rem] font-bold uppercase text-muted-foreground tracking-wider flex items-center gap-2">
<span>Shared User Buckets</span>
<span class="ml-auto bg-muted px-1.5 rounded text-[0.5625rem]">{oversightUsers()?.length}</span>
<span>Supervised Contexts</span>
<span class="ml-auto bg-muted px-1.5 rounded text-[0.5625rem]">{store.supervisedContexts.length}</span>
</div>
<For each={oversightUsers()}>
{(user) => (
<For each={store.supervisedContexts}>
{(supervised) => (
<button
onClick={() => handleSwitch(user.id, user.name, 'user')}
onClick={() => handleSwitch(supervised.ownerUserId, supervised.ownerName, 'user')}
class={cn(
"w-full flex items-center gap-2 p-2 rounded-md text-sm transition-colors",
(ctx() as any).userId === user.id ? "bg-orange-500/10 text-orange-600 font-semibold" : "hover:bg-muted text-foreground"
(ctx() as any).userId === supervised.ownerUserId ? "bg-orange-500/10 text-orange-600 font-semibold" : "hover:bg-muted text-foreground"
)}
>
<div class="w-4 h-4 rounded-full bg-orange-500/20 flex items-center justify-center">
<div class="w-2 h-2 rounded-full bg-orange-500" />
</div>
<span class="truncate">{user.name}</span>
<span class="truncate">{supervised.ownerName}</span>
</button>
)}
</For>
</Show>
</div>
</PopoverContent>
</Popover>