fixed shared to viewing in settings and added workspace navigation in mobile view
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { type Component, type JSX, createSignal, Show, lazy, Suspense, onMount } from "solid-js";
|
||||
import { Sidebar, BottomNav } from "./Navigation";
|
||||
import { Sidebar, BottomNav, ContextSwitcher } from "./Navigation";
|
||||
import { store, activeTaskId, setActiveTaskId } from "@/store";
|
||||
import { PanelLeftOpen } from "lucide-solid";
|
||||
|
||||
@@ -62,6 +62,11 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Context Switcher */}
|
||||
<div class="fixed top-4 left-4 z-[60] md:hidden">
|
||||
<ContextSwitcher isMobile={true} />
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main class="flex-1 min-w-0 overflow-y-auto overflow-x-hidden pb-32 sm:pb-6 relative scroll-smooth">
|
||||
<div class="w-full max-w-screen-2xl mx-auto px-4 sm:px-8 lg:px-12 pt-16 sm:pt-20 pb-8 space-y-6 sm:space-y-8">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Component, For, Show } from "solid-js";
|
||||
import { LayoutDashboard, ListTodo, Settings, Clock, ArrowUpCircle, PanelLeftClose, Snowflake, Pickaxe, ChevronDown, Gauge, TrendingUp } from "lucide-solid";
|
||||
import { LayoutDashboard, ListTodo, Settings, Clock, ArrowUpCircle, PanelLeftClose, Snowflake, Pickaxe, ChevronDown, Gauge, TrendingUp, Users } from "lucide-solid";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -112,11 +112,12 @@ export const BottomNav: Component<{ currentView: string; setView: (v: string) =>
|
||||
);
|
||||
};
|
||||
|
||||
const ContextSwitcher: Component<{
|
||||
isLocked: boolean;
|
||||
isPeeking: boolean;
|
||||
setIsPeeking: (v: boolean) => void;
|
||||
onOpenChange?: (open: boolean) => void
|
||||
export const ContextSwitcher: Component<{
|
||||
isLocked?: boolean;
|
||||
isPeeking?: boolean;
|
||||
setIsPeeking?: (v: boolean) => void;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
isMobile?: boolean;
|
||||
}> = (props) => {
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
const [open, setOpen] = createSignal(false);
|
||||
@@ -126,23 +127,28 @@ const ContextSwitcher: Component<{
|
||||
props.onOpenChange?.(open());
|
||||
});
|
||||
|
||||
// Close popover when sidebar disappears
|
||||
// Close popover when sidebar disappears (Desktop only)
|
||||
createEffect(() => {
|
||||
if (!props.isLocked && !props.isPeeking) {
|
||||
if (!props.isMobile && !props.isLocked && !props.isPeeking) {
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
const [oversightUsers] = createResource(
|
||||
() => store.shareRules.filter(r => r.targetUserId === currentUserId && r.type === 'all').map(r => r.ownerId),
|
||||
() => [...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
|
||||
// Optimization: Could check if we already have them or just fetch
|
||||
const users = await Promise.all(ownerIds.map(id => pb.collection('users').getOne(id, { fields: 'id,name,email' })));
|
||||
// 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) {
|
||||
} catch (e: any) {
|
||||
if (e.isAbort) return []; // Ignore explicit aborts
|
||||
console.error("Failed to load oversight users", e);
|
||||
return [];
|
||||
}
|
||||
@@ -166,8 +172,8 @@ const ContextSwitcher: Component<{
|
||||
// Close popover
|
||||
setOpen(false);
|
||||
|
||||
// Collapse sidebar if peaking
|
||||
if (!props.isLocked && props.isPeeking) {
|
||||
// Collapse sidebar if peaking (Desktop only)
|
||||
if (!props.isMobile && props.setIsPeeking && !props.isLocked && props.isPeeking) {
|
||||
props.setIsPeeking(false);
|
||||
}
|
||||
};
|
||||
@@ -175,21 +181,34 @@ const ContextSwitcher: Component<{
|
||||
return (
|
||||
<Show when={oversightUsers() && oversightUsers()!.length > 0}>
|
||||
<Popover open={open()} onOpenChange={setOpen}>
|
||||
<PopoverTrigger class="w-full">
|
||||
<PopoverTrigger class={cn(props.isMobile ? "w-auto" : "w-full")}>
|
||||
<div class={cn(
|
||||
"flex items-center justify-between w-full px-3 py-2.5 rounded-lg transition-all duration-200 group text-sm",
|
||||
"text-muted-foreground hover:bg-muted hover:text-foreground border border-transparent hover:border-border/50"
|
||||
"flex items-center justify-between transition-all duration-200 group",
|
||||
props.isMobile
|
||||
? "h-10 w-10 flex items-center justify-center bg-background/80 backdrop-blur-sm border border-border hover:bg-muted shadow-sm rounded-xl"
|
||||
: "px-3 py-2.5 rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground border border-transparent hover:border-border/50 w-full text-sm",
|
||||
)}>
|
||||
<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="w-2 h-2 rounded-full bg-current" />
|
||||
<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="w-2 h-2 rounded-full bg-current" />
|
||||
</div>
|
||||
<span class="font-medium truncate">{label()}</span>
|
||||
</div>
|
||||
<ChevronDown size={14} class="opacity-50 group-hover:opacity-100 transition-opacity ml-2" />
|
||||
</>
|
||||
}>
|
||||
<div class="relative">
|
||||
<Users size={18} class={cn("transition-transform duration-300", open() && "scale-110", ctx() !== 'mine' && "text-orange-500")} />
|
||||
{ctx() !== 'mine' && (
|
||||
<div class="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-orange-500 border border-background" />
|
||||
)}
|
||||
</div>
|
||||
<span class="font-medium truncate">{label()}</span>
|
||||
</div>
|
||||
<ChevronDown size={14} class="opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
</Show>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-1 bg-card border-border shadow-xl">
|
||||
|
||||
+53
-11
@@ -179,32 +179,39 @@ export const SettingsView: Component = () => {
|
||||
</div>
|
||||
|
||||
{/* Active share rules */}
|
||||
{/* Active share rules - Custom */}
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-xs font-bold uppercase tracking-wider text-muted-foreground pb-1">Custom Rules</h4>
|
||||
{/* Active share rules - Outgoing */}
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-[10px] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
|
||||
<div class="w-1 h-1 rounded-full bg-primary" />
|
||||
Sharing With Others
|
||||
</h4>
|
||||
<For each={store.shareRules.filter(r => {
|
||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
||||
return !isSystem;
|
||||
const isOwner = r.ownerId === pb.authStore.model?.id;
|
||||
return !isSystem && isOwner;
|
||||
})} fallback={
|
||||
<div class="text-center py-4 text-muted-foreground text-xs italic border border-dashed border-border rounded-xl">
|
||||
No custom share rules.
|
||||
<div class="text-center py-4 text-muted-foreground text-[10px] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||
You aren't sharing with anyone yet.
|
||||
</div>
|
||||
}>
|
||||
{(rule) => (
|
||||
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3">
|
||||
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3 group/rule hover:bg-muted/30 transition-all">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
{rule.type === 'all' ? <Users size={14} class="text-primary shrink-0" /> : <Tag size={14} class="text-primary shrink-0" />}
|
||||
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0 transition-transform group-hover/rule:scale-110">
|
||||
{rule.type === 'all' ? <Users size={14} /> : <Tag size={14} />}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium truncate">{getUserName(rule.targetUserId)}</p>
|
||||
<p class="text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
<p class="text-xs font-bold truncate">To: {getUserName(rule.targetUserId)}</p>
|
||||
<p class="text-[9px] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
||||
<ArrowLeftRight size={8} class="rotate-45" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:bg-destructive/10 hover:text-destructive"
|
||||
class="h-8 w-8 hover:bg-destructive/10 hover:text-destructive rounded-lg transition-all opacity-0 group-hover/rule:opacity-100"
|
||||
onClick={() => removeShareRule(rule.id)}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
@@ -214,6 +221,41 @@ export const SettingsView: Component = () => {
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* Active share rules - Incoming */}
|
||||
<div class="space-y-3 pt-4 border-t border-border/20">
|
||||
<h4 class="text-[10px] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
|
||||
<div class="w-1 h-1 rounded-full bg-indigo-500" />
|
||||
Shared With You
|
||||
</h4>
|
||||
<For each={store.shareRules.filter(r => {
|
||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
||||
const isTarget = r.targetUserId === pb.authStore.model?.id;
|
||||
return !isSystem && isTarget;
|
||||
})} fallback={
|
||||
<div class="text-center py-4 text-muted-foreground text-[10px] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||
No one has shared with you yet.
|
||||
</div>
|
||||
}>
|
||||
{(rule) => (
|
||||
<div class="flex items-center justify-between p-3 rounded-xl bg-muted/20 border border-border/40 gap-3 group/rule hover:bg-muted/30 transition-all opacity-80">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center text-indigo-500 shrink-0">
|
||||
{rule.type === 'all' ? <Users size={14} /> : <Tag size={14} />}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-bold truncate">From: {getUserName(rule.ownerId)}</p>
|
||||
<p class="text-[9px] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
||||
<ArrowLeftRight size={8} class="-rotate-45" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* No delete button for incoming rules unless we add leave logic later */}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* Active share rules - System */}
|
||||
<details class="group/details">
|
||||
<summary class="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground cursor-pointer hover:text-foreground transition-colors py-2 select-none">
|
||||
|
||||
Reference in New Issue
Block a user