chore(frontend): real ESLint instead of a placeholder; fix hook order (FLAW-19)
eslint.config.js with the recommended JS/TypeScript rules and the React hooks rules; npm run lint runs ESLint and is part of the image build. Errors fail the build, the 24 exhaustive-deps findings stay warnings. It found two real bugs: TaskDetailModal and ManageColumnsModal called their hooks after 'if (!isOpen) return null', so the hook count changed whenever the dialog opened or closed. Both are now a thin wrapper plus a content component. Also removes unused imports and dead code (old task list share handlers, an unused realtime status, an unused rollup). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b56a33500
commit
4c50d15915
19 files changed
+1360
-140
No files matched your search
@@ -8,7 +8,6 @@ import {
|
||||
RotateCcw,
|
||||
Check,
|
||||
Columns,
|
||||
Layers,
|
||||
} from 'lucide-react';
|
||||
import { KanbanColumn } from '../../lib/types';
|
||||
import { triggerHaptic } from '../../lib/api';
|
||||
@@ -28,15 +27,17 @@ export const DEFAULT_KANBAN_COLUMNS: KanbanColumn[] = [
|
||||
{ id: 'done', name: 'Erledigt', isDone: true, color: 'emerald' },
|
||||
];
|
||||
|
||||
export const ManageColumnsModal: React.FC<ManageColumnsModalProps> = ({
|
||||
// Wrapper and content split so the hooks always run in the same number (see TaskDetailModal)
|
||||
export const ManageColumnsModal: React.FC<ManageColumnsModalProps> = (props) =>
|
||||
props.isOpen ? <ManageColumnsModalContent {...props} /> : null;
|
||||
|
||||
const ManageColumnsModalContent: React.FC<ManageColumnsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
initialColumns,
|
||||
onSave,
|
||||
listTitle,
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const [columns, setColumns] = useState<KanbanColumn[]>(() => {
|
||||
return initialColumns && initialColumns.length > 0 ? initialColumns : DEFAULT_KANBAN_COLUMNS;
|
||||
});
|
||||
|
||||
@@ -7,19 +7,16 @@ import {
|
||||
FileText,
|
||||
Plus,
|
||||
CheckSquare,
|
||||
Clock,
|
||||
Layers,
|
||||
Hash,
|
||||
AlertTriangle,
|
||||
Flame,
|
||||
ChevronRight,
|
||||
Folder,
|
||||
Webhook,
|
||||
Lock,
|
||||
UserPlus,
|
||||
Repeat,
|
||||
} from 'lucide-react';
|
||||
import { Thought, SubTask, TaskList, TaskListMember, KanbanColumn, User, Recurrence } from '../../lib/types';
|
||||
import { Thought, TaskList, TaskListMember, KanbanColumn, User, Recurrence } from '../../lib/types';
|
||||
import { UserAvatar } from '../../components/UserAvatar';
|
||||
import { UserAutocomplete } from '../UserAutocomplete';
|
||||
import { triggerHaptic } from '../../lib/api';
|
||||
@@ -51,9 +48,13 @@ interface TaskDetailModalProps {
|
||||
readOnly?: boolean; // Task belongs to a list shared with "viewer" permission
|
||||
}
|
||||
|
||||
export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
// The wrapper decides whether the dialog is shown; the content holds all hooks, so their number never
|
||||
// changes between renders (hooks after an early return break React when isOpen flips)
|
||||
export const TaskDetailModal: React.FC<TaskDetailModalProps> = (props) =>
|
||||
props.isOpen && props.task ? <TaskDetailModalContent {...props} task={props.task} /> : null;
|
||||
|
||||
const TaskDetailModalContent: React.FC<TaskDetailModalProps & { task: Thought }> = ({
|
||||
task,
|
||||
isOpen,
|
||||
onClose,
|
||||
activeColumns,
|
||||
taskLists,
|
||||
@@ -75,8 +76,6 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
onDeleteTask,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
if (!isOpen || !task) return null;
|
||||
|
||||
const [titleDraft, setTitleDraft] = useState(task.content);
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
|
||||
@@ -144,7 +143,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
|
||||
const handleAddTagSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
let cleaned = newTagDraft.trim().replace(/^#+/, '').toLowerCase();
|
||||
const cleaned = newTagDraft.trim().replace(/^#+/, '').toLowerCase();
|
||||
if (!cleaned) return;
|
||||
const currentTags = Array.isArray(task.tags) ? [...task.tags] : [];
|
||||
if (!currentTags.includes(cleaned)) {
|
||||
@@ -177,11 +176,6 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
||||
onUpdateDueDate(task, joinDue(addDays(daysOffset), splitDue(task.dueDate).time));
|
||||
};
|
||||
|
||||
const currentColumn = activeColumns.find((c) => c.id === task.kanbanStatus) ||
|
||||
(task.isCompleted
|
||||
? activeColumns.find((c) => c.isDone) || activeColumns[activeColumns.length - 1]
|
||||
: activeColumns.find((c) => !c.isDone) || activeColumns[0]);
|
||||
|
||||
const subtasks = Array.isArray(task.subtasks) ? task.subtasks : [];
|
||||
const completedSubtasks = subtasks.filter((s) => s.isCompleted).length;
|
||||
const subtasksPercent = subtasks.length > 0 ? Math.round((completedSubtasks / subtasks.length) * 100) : 0;
|
||||
|
||||
@@ -15,19 +15,18 @@ import {
|
||||
Layers,
|
||||
Users,
|
||||
CheckSquare,
|
||||
User as UserIcon,
|
||||
User as
|
||||
Lock,
|
||||
Hash,
|
||||
AlertTriangle,
|
||||
Flame,
|
||||
Settings,
|
||||
ChevronDown,
|
||||
Folder,
|
||||
Filter,
|
||||
Webhook,
|
||||
Repeat,
|
||||
} from 'lucide-react';
|
||||
import { TaskList, Thought, SubTask, Team, User, TaskListShare, TaskListMember, KanbanColumn, PriorityLevel, Recurrence, RECURRENCE_LABELS } from '../../lib/types';
|
||||
import { TaskList, Thought, SubTask, Team, User, TaskListMember, KanbanColumn, PriorityLevel, Recurrence, RECURRENCE_LABELS } from '../../lib/types';
|
||||
import { formatDue, isDueToday, isOverdue, localDay, splitDue } from '../../lib/due';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||
@@ -92,10 +91,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
// Share TaskList modal state
|
||||
const [showShareListModal, setShowShareListModal] = useState(false);
|
||||
const [sharingTaskList, setSharingTaskList] = useState<TaskList | null>(null);
|
||||
const [addShareType, setAddShareType] = useState<'team' | 'user'>('team');
|
||||
const [selectedShareTargetId, setSelectedShareTargetId] = useState('');
|
||||
const [selectedShareUser, setSelectedShareUser] = useState<User | null>(null);
|
||||
const [shareLoading, setShareLoading] = useState(false);
|
||||
|
||||
// Detail modal state
|
||||
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
|
||||
@@ -854,8 +849,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
|
||||
const handleOpenShareModal = async (tl: TaskList) => {
|
||||
setSharingTaskList(tl);
|
||||
setSelectedShareTargetId('');
|
||||
setSelectedShareUser(null);
|
||||
setShowShareListModal(true);
|
||||
try {
|
||||
const [freshTeams, freshUsers, freshShares] = await Promise.all([
|
||||
@@ -871,60 +864,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddShare = async () => {
|
||||
if (!sharingTaskList) return;
|
||||
const targetId = addShareType === 'team' ? selectedShareTargetId : selectedShareUser?.id;
|
||||
if (!targetId) return;
|
||||
setShareLoading(true);
|
||||
triggerHaptic(15);
|
||||
try {
|
||||
await api.addTaskListShare(sharingTaskList.id, {
|
||||
targetType: addShareType,
|
||||
targetId: targetId,
|
||||
});
|
||||
const [freshLists, freshShares] = await Promise.all([
|
||||
api.getTaskLists(),
|
||||
api.getTaskListShares(sharingTaskList.id).catch(() => []),
|
||||
]);
|
||||
setTaskLists(freshLists);
|
||||
const updated = freshLists.find((l) => l.id === sharingTaskList.id);
|
||||
if (updated) {
|
||||
setSharingTaskList({ ...updated, shares: freshShares });
|
||||
}
|
||||
setSelectedShareTargetId('');
|
||||
setSelectedShareUser(null);
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Fehler beim Hinzufügen der Freigabe');
|
||||
} finally {
|
||||
setShareLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveShare = async (shareId: string) => {
|
||||
if (!sharingTaskList) return;
|
||||
if (!window.confirm('Freigabe wirklich aufheben?')) return;
|
||||
setShareLoading(true);
|
||||
triggerHaptic(15);
|
||||
try {
|
||||
await api.removeTaskListShare(sharingTaskList.id, shareId);
|
||||
const [freshLists, freshShares] = await Promise.all([
|
||||
api.getTaskLists(),
|
||||
api.getTaskListShares(sharingTaskList.id).catch(() => []),
|
||||
]);
|
||||
setTaskLists(freshLists);
|
||||
const updated = freshLists.find((l) => l.id === sharingTaskList.id);
|
||||
if (updated) {
|
||||
setSharingTaskList({ ...updated, shares: freshShares });
|
||||
}
|
||||
await loadData();
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Fehler beim Entfernen der Freigabe');
|
||||
} finally {
|
||||
setShareLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveColumns = async (newColumns: KanbanColumn[]) => {
|
||||
triggerHaptic(15);
|
||||
if (currentSelectedList && selectedListId !== 'all') {
|
||||
|
||||
Reference in new issue
Block a user