feat(tasks): add read-only sharing permission for task lists
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m5s

Task list shares now carry a permission (editor/viewer). The server rejects
writes from viewers with 403, and the UI hides or disables edit controls
for read-only lists.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Sonnet 5.5 committed 2026-10-27 22:30:28 +01:00
1 parent 6896a29285
commit 4e9c4ffa8c
9 files changed
+300 -61

No files matched your search

@@ -15,6 +15,7 @@ import {
ChevronRight,
Folder,
Webhook,
Lock,
} from 'lucide-react';
import { Thought, SubTask, TaskList, KanbanColumn } from '../../lib/types';
import { triggerHaptic } from '../../lib/api';
@@ -39,6 +40,7 @@ interface TaskDetailModalProps {
onConvertToDoc: (task: Thought) => void;
onOpenDocument?: (docId: string, blockId?: string) => void;
onDeleteTask: (taskId: string) => void;
readOnly?: boolean; // Task belongs to a list shared with "viewer" permission
}
export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
@@ -60,6 +62,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
onConvertToDoc,
onOpenDocument,
onDeleteTask,
readOnly = false,
}) => {
if (!isOpen || !task) return null;
@@ -176,6 +179,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-panel">
<div className="flex items-center gap-2 min-w-0">
<button
disabled={readOnly}
onClick={() => {
triggerHaptic(15);
const nextState = !task.isCompleted;
@@ -184,7 +188,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
const nextKanban = nextState ? (doneCol ? doneCol.id : 'done') : (todoCol ? todoCol.id : 'todo');
onUpdateStatus(task, nextKanban);
}}
className={`w-5 h-5 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border ${
className={`w-5 h-5 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border disabled:cursor-default ${
task.isCompleted
? 'bg-status-ok border-status-ok text-on-accent'
: 'border-strong hover:border-accent text-transparent'
@@ -194,8 +198,14 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
<Check className="w-3.5 h-3.5 stroke-[3]" />
</button>
<span className="text-xs font-semibold text-main truncate">
{task.isCompleted ? 'Erledigte Aufgabe' : 'Aufgabe bearbeiten'}
{readOnly ? 'Aufgabe ansehen' : task.isCompleted ? 'Erledigte Aufgabe' : 'Aufgabe bearbeiten'}
</span>
{readOnly && (
<span className="px-1.5 py-0.5 border border-border bg-canvas text-muted rounded-btn text-[10px] font-medium flex items-center gap-1 flex-shrink-0">
<Lock className="w-2.5 h-2.5 text-faint" />
<span>Nur Lesen</span>
</span>
)}
{task.source?.toLowerCase() === 'webhook' && (
<span className="px-1.5 py-0.5 border border-border bg-canvas text-muted rounded-btn text-[10px] font-medium flex items-center gap-1 flex-shrink-0">
<Webhook className="w-2.5 h-2.5 text-accent" />
@@ -220,7 +230,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
<FileText className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
<span>In Notiz öffnen</span>
</button>
) : (
) : !readOnly && (
<button
type="button"
onClick={() => {
@@ -234,6 +244,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
<span className="hidden sm:inline">Als Notiz vertiefen</span>
</button>
)}
{!readOnly && (
<button
type="button"
onClick={() => {
@@ -248,6 +259,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
<Trash2 className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
<span className="sm:hidden font-medium">Löschen</span>
</button>
)}
<button
type="button"
onClick={onClose}
@@ -259,8 +271,8 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
</div>
</div>
{/* Scrollable Body */}
<div className="p-4 overflow-y-auto space-y-4 flex-1">
{/* Scrollable Body (all controls disabled for read-only shares) */}
<fieldset disabled={readOnly} className="p-4 overflow-y-auto space-y-4 flex-1 min-w-0 border-0 m-0">
{/* Task Title */}
<div>
{isEditingTitle ? (
@@ -303,9 +315,11 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
</div>
) : (
<div
onClick={() => setIsEditingTitle(true)}
className="group cursor-pointer p-2 rounded-card hover:bg-panel transition-colors border border-transparent hover:border-border"
title="Klicken zum Bearbeiten des Titels"
onClick={() => !readOnly && setIsEditingTitle(true)}
className={`group p-2 rounded-card transition-colors border border-transparent ${
readOnly ? '' : 'cursor-pointer hover:bg-panel hover:border-border'
}`}
title={readOnly ? undefined : 'Klicken zum Bearbeiten des Titels'}
>
<h2
className={`text-base font-semibold break-words leading-snug ${
@@ -314,9 +328,11 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
>
{task.content}
</h2>
<span className="text-[10px] text-muted opacity-0 group-hover:opacity-100 transition-opacity">
Klicken zum Umbenennen
</span>
{!readOnly && (
<span className="text-[10px] text-muted opacity-0 group-hover:opacity-100 transition-opacity">
Klicken zum Umbenennen
</span>
)}
</div>
)}
</div>
@@ -515,15 +531,18 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-pill bg-panel border border-border text-xs text-main"
>
<span>#{tag}</span>
<button
type="button"
onClick={() => handleRemoveTag(tag)}
className="text-muted hover:text-status-err"
>
<X className="w-3 h-3" />
</button>
{!readOnly && (
<button
type="button"
onClick={() => handleRemoveTag(tag)}
className="text-muted hover:text-status-err"
>
<X className="w-3 h-3" />
</button>
)}
</span>
))}
{!readOnly && (
<form onSubmit={handleAddTagSubmit} className="inline-flex items-center gap-1">
<input
type="text"
@@ -533,6 +552,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
className="h-6 w-24 px-1.5 text-xs input-field"
/>
</form>
)}
</div>
</div>
@@ -587,6 +607,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
{st.title}
</span>
</div>
{!readOnly && (
<button
type="button"
onClick={() => onDeleteSubtask(task, st.id)}
@@ -594,12 +615,14 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
>
<Trash2 className="w-3 h-3" />
</button>
)}
</div>
))}
</div>
)}
{/* Inline add subtask form */}
{!readOnly && (
<form onSubmit={handleAddSubtaskSubmit} className="flex items-center gap-1.5">
<input
type="text"
@@ -617,6 +640,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
<span>Hinzufügen</span>
</button>
</form>
)}
</div>
{/* Notes Section */}
@@ -626,7 +650,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
<FileText className="w-3.5 h-3.5 text-accent" />
<span>Detailnotizen:</span>
</label>
{!isEditingNotes && task.notes && (
{!readOnly && !isEditingNotes && task.notes && (
<button
type="button"
onClick={() => {
@@ -673,14 +697,19 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
) : task.notes ? (
<div
onClick={() => {
if (readOnly) return;
setNotesDraft(task.notes || '');
setIsEditingNotes(true);
}}
className="p-3 bg-canvas border border-border rounded-card text-xs text-muted leading-relaxed cursor-pointer hover:border-strong transition-colors"
title="Klicken zum Bearbeiten"
className={`p-3 bg-canvas border border-border rounded-card text-xs text-muted leading-relaxed transition-colors ${
readOnly ? '' : 'cursor-pointer hover:border-strong'
}`}
title={readOnly ? undefined : 'Klicken zum Bearbeiten'}
>
<MarkdownViewer content={task.notes} />
</div>
) : readOnly ? (
<p className="text-xs text-faint">Keine Detailnotizen.</p>
) : (
<button
type="button"
@@ -695,7 +724,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
</button>
)}
</div>
</div>
</fieldset>
{/* Modal Footer */}
<div className="flex items-center justify-between px-4 py-2.5 border-t border-border bg-panel">