fixed sharing
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { type Component, createMemo } from "solid-js";
|
||||
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId, store, copyTask } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy } from "lucide-solid";
|
||||
import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { For } from "solid-js";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
@@ -78,6 +78,31 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
<span class="text-[10px] font-medium">P{props.task.priority}</span>
|
||||
</div>
|
||||
|
||||
{/* Shared Indicator - shows for individual shares OR tag-based ShareRules */}
|
||||
{(() => {
|
||||
const hasIndividualShares = props.task.sharedWith && props.task.sharedWith.length > 0;
|
||||
// Check if any tag-based ShareRules match this task's tags (exclude 'all' type)
|
||||
const hasTagRuleShares = store.shareRules.some(rule =>
|
||||
rule.type === 'tag' &&
|
||||
rule.tagName &&
|
||||
props.task.tags?.includes(rule.tagName)
|
||||
);
|
||||
const isShared = hasIndividualShares || hasTagRuleShares;
|
||||
|
||||
if (!isShared) return null;
|
||||
|
||||
const shareCount = (props.task.sharedWith?.length || 0);
|
||||
const tooltip = hasIndividualShares
|
||||
? `Shared with ${shareCount} user${shareCount > 1 ? 's' : ''}`
|
||||
: 'Shared via tag rule';
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-1 shrink-0" title={tooltip}>
|
||||
<Users2 size={11} class="text-muted-foreground/50" />
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Tags */}
|
||||
{props.task.tags && props.task.tags.length > 0 && (
|
||||
<div class="flex flex-wrap items-center gap-1 min-w-0">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Component, createEffect, createSignal, For } from "solid-js";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate } from "@/store";
|
||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask } from "@/store";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge } from "lucide-solid";
|
||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings } from "lucide-solid";
|
||||
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
|
||||
import { Button } from "./ui/button";
|
||||
import { TagPicker } from "./TagPicker";
|
||||
@@ -15,6 +15,7 @@ import { toast } from "solid-sonner";
|
||||
import { lazy, Suspense } from "solid-js";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
|
||||
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
|
||||
@@ -468,6 +469,16 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
<span class="text-xs">Create Template from Task</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Sharing Section */}
|
||||
<div class="p-2 border-t border-border/50">
|
||||
<div class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70 px-2 py-1 mb-1">Sharing</div>
|
||||
<UserSharePicker
|
||||
taskId={props.task.id}
|
||||
sharedWith={props.task.sharedWith || []}
|
||||
tags={props.task.tags || []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@@ -536,3 +547,140 @@ const MetadataItem: Component<{
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// User Share Picker Component
|
||||
const UserSharePicker: Component<{
|
||||
taskId: string;
|
||||
sharedWith: Array<{ userId: string; access: 'view' | 'edit' }>;
|
||||
tags: string[];
|
||||
}> = (props) => {
|
||||
const [users, setUsers] = createSignal<Array<{ id: string; name: string }>>([]);
|
||||
const [selectedUserId, setSelectedUserId] = createSignal<string>("");
|
||||
const [loading, setLoading] = createSignal(true);
|
||||
|
||||
// Get matching ShareRules for this task
|
||||
const matchingRules = () => {
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
return store.shareRules.filter(rule => {
|
||||
if (rule.ownerId !== currentUserId) return false;
|
||||
if (rule.type === 'all') return true;
|
||||
if (rule.type === 'tag' && props.tags.includes(rule.tagName!)) return true;
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const getUserName = (userId: string) => {
|
||||
return users().find(u => u.id === userId)?.name || userId;
|
||||
};
|
||||
|
||||
// Fetch users on mount
|
||||
createEffect(async () => {
|
||||
try {
|
||||
const records = await pb.collection('users').getFullList({
|
||||
fields: 'id,name'
|
||||
});
|
||||
setUsers(records.map(r => ({ id: r.id, name: r.name || r.email || r.id })));
|
||||
} catch (err) {
|
||||
console.error("Failed to load users:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const availableUsers = () => {
|
||||
const sharedUserIds = props.sharedWith.map(s => s.userId);
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
return users().filter(u => !sharedUserIds.includes(u.id) && u.id !== currentUserId);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex gap-1 mb-2">
|
||||
<Select<{ id: string; name: string }>
|
||||
value={users().find(u => u.id === selectedUserId()) || null}
|
||||
onChange={(user) => setSelectedUserId(user?.id || "")}
|
||||
options={availableUsers()}
|
||||
optionValue="id"
|
||||
optionTextValue="name"
|
||||
placeholder={loading() ? "Loading..." : "Select user..."}
|
||||
itemComponent={(itemProps: any) => (
|
||||
<SelectItem item={itemProps.item}>{itemProps.item.rawValue.name}</SelectItem>
|
||||
)}
|
||||
>
|
||||
<SelectTrigger class="flex-1 h-8 text-xs">
|
||||
<SelectValue<{ id: string; name: string }>>
|
||||
{(state) => state.selectedOption()?.name || "Select user..."}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
disabled={!selectedUserId()}
|
||||
onClick={async () => {
|
||||
if (selectedUserId()) {
|
||||
await shareTask(props.taskId, selectedUserId(), 'edit');
|
||||
setSelectedUserId("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Share2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<For each={props.sharedWith}>
|
||||
{(share) => (
|
||||
<div class="flex items-center justify-between py-1 px-2 rounded-lg bg-muted/30 mb-1">
|
||||
<span class="text-xs truncate flex-1">{getUserName(share.userId)}</span>
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6 hover:bg-amber-500/10 hover:text-amber-600"
|
||||
title="Split - Give them an independent copy"
|
||||
onClick={() => splitTask(props.taskId, share.userId)}
|
||||
>
|
||||
<GitBranch size={12} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6 hover:bg-destructive/10 hover:text-destructive"
|
||||
title="Revoke access"
|
||||
onClick={() => revokeShare(props.taskId, share.userId)}
|
||||
>
|
||||
<UserMinus size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
{/* ShareRule-based sharing */}
|
||||
{matchingRules().length > 0 && (
|
||||
<div class="mt-3 pt-3 border-t border-border/30">
|
||||
<div class="text-[9px] font-bold uppercase tracking-widest text-muted-foreground/40 mb-2 px-1">Automatic Rule Shares</div>
|
||||
<For each={matchingRules()}>
|
||||
{(rule) => (
|
||||
<div class="flex items-center gap-2 py-1.5 px-2 rounded-lg bg-muted/10 mb-1 border border-border/5">
|
||||
<div class="shrink-0 text-muted-foreground/40">
|
||||
{rule.type === 'tag' ? <Tag size={10} /> : <Settings size={10} />}
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-[11px] font-medium leading-none truncate">
|
||||
{getUserName(rule.targetUserId)}
|
||||
</span>
|
||||
<span class="text-[9px] text-muted-foreground/50 mt-0.5 uppercase tracking-tighter">
|
||||
{rule.type === 'all' ? 'All Tasks Rule' : `Matched Tag: ${rule.tagName}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user