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
+1
-1
@@ -8,7 +8,7 @@ RUN npm install --no-audit --no-fund
|
|||||||
COPY frontend/ ./
|
COPY frontend/ ./
|
||||||
# Version shown in the header: the git tag for releases (set by CI), otherwise "<package version>-dev"
|
# Version shown in the header: the git tag for releases (set by CI), otherwise "<package version>-dev"
|
||||||
ARG APP_VERSION=""
|
ARG APP_VERSION=""
|
||||||
RUN npx tsc --noEmit && npm run test:run && APP_VERSION="${APP_VERSION}" npm run build
|
RUN npx tsc --noEmit && npm run lint && npm run test:run && APP_VERSION="${APP_VERSION}" npm run build
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Stage 2: Go Backend Lint, Unit Tests & Binary Build (Cached Go module & sqlite layer)
|
# Stage 2: Go Backend Lint, Unit Tests & Binary Build (Cached Go module & sqlite layer)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Lint rules for the frontend (FLAW-19): real mistakes are errors, style is left to the code.
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist', 'node_modules', 'coverage'] },
|
||||||
|
{
|
||||||
|
files: ['src/**/*.{ts,tsx}'],
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: { 'react-hooks': reactHooks },
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
// Error payloads and browser APIs without types are handled as any on purpose
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrors: 'none' }],
|
||||||
|
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['public/sw.js'],
|
||||||
|
languageOptions: { globals: globals.serviceworker },
|
||||||
|
extends: [js.configs.recommended],
|
||||||
|
},
|
||||||
|
);
|
||||||
Generated
+1301
-4
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "echo 'Linting passed'",
|
"lint": "eslint .",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:run": "vitest run"
|
"test:run": "vitest run"
|
||||||
},
|
},
|
||||||
@@ -22,17 +22,22 @@
|
|||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "9.17.0",
|
||||||
"@testing-library/dom": "^10.4.2",
|
"@testing-library/dom": "^10.4.2",
|
||||||
"@testing-library/react": "^16.3.3",
|
"@testing-library/react": "^16.3.3",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "9.17.0",
|
||||||
|
"eslint-plugin-react-hooks": "5.1.0",
|
||||||
"fake-indexeddb": "^6.2.6",
|
"fake-indexeddb": "^6.2.6",
|
||||||
|
"globals": "15.14.0",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
|
"typescript-eslint": "8.18.2",
|
||||||
"vite": "^5.4.11",
|
"vite": "^5.4.11",
|
||||||
"vitest": "^2.1.9"
|
"vitest": "^2.1.9"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ export const App: React.FC = () => {
|
|||||||
.getThemes()
|
.getThemes()
|
||||||
.then((themes) => handleCustomThemesChange(themes))
|
.then((themes) => handleCustomThemesChange(themes))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [user?.id]);
|
}, [user?.id]);
|
||||||
|
|
||||||
const refreshAll = useCallback(async () => {
|
const refreshAll = useCallback(async () => {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
X,
|
X,
|
||||||
Loader2,
|
Loader2,
|
||||||
ArrowRight,
|
|
||||||
CornerDownLeft,
|
CornerDownLeft,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { SearchHit } from '../lib/types';
|
import { SearchHit } from '../lib/types';
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
CheckSquare,
|
CheckSquare,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
Pin,
|
Pin,
|
||||||
FolderTree,
|
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Command,
|
Command,
|
||||||
Users,
|
Users,
|
||||||
@@ -19,14 +18,10 @@ import {
|
|||||||
Lock,
|
Lock,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
X,
|
X,
|
||||||
Smartphone,
|
|
||||||
RotateCcw,
|
|
||||||
Heading1,
|
Heading1,
|
||||||
ListTodo,
|
ListTodo,
|
||||||
Code,
|
Code,
|
||||||
Download,
|
Download,
|
||||||
Flame,
|
|
||||||
Check,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AboutModal } from '../../components/AboutModal';
|
import { AboutModal } from '../../components/AboutModal';
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import {
|
|||||||
Radio,
|
Radio,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
X,
|
X,
|
||||||
Hash,
|
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types';
|
import { ListItem, SharedList, Team, User, WSMessage } from '../../lib/types';
|
||||||
import { api, triggerHaptic, createRealtimeConnection, RealtimeTransport } from '../../lib/api';
|
import { api, triggerHaptic, createRealtimeConnection, RealtimeTransport } from '../../lib/api';
|
||||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||||
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
|
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
|
||||||
@@ -502,7 +501,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
if (!sourceItem) return;
|
if (!sourceItem) return;
|
||||||
|
|
||||||
const targetCatNormalized = targetCategory.trim();
|
const targetCatNormalized = targetCategory.trim();
|
||||||
let newCategory = targetCatNormalized;
|
const newCategory = targetCatNormalized;
|
||||||
let newContent = sourceItem.content;
|
let newContent = sourceItem.content;
|
||||||
|
|
||||||
if (!targetCatNormalized) {
|
if (!targetCatNormalized) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
Palette,
|
Palette,
|
||||||
Upload,
|
Upload,
|
||||||
Camera,
|
|
||||||
Pencil,
|
Pencil,
|
||||||
Copy,
|
Copy,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
RotateCcw,
|
RotateCcw,
|
||||||
Check,
|
Check,
|
||||||
Columns,
|
Columns,
|
||||||
Layers,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { KanbanColumn } from '../../lib/types';
|
import { KanbanColumn } from '../../lib/types';
|
||||||
import { triggerHaptic } from '../../lib/api';
|
import { triggerHaptic } from '../../lib/api';
|
||||||
@@ -28,15 +27,17 @@ export const DEFAULT_KANBAN_COLUMNS: KanbanColumn[] = [
|
|||||||
{ id: 'done', name: 'Erledigt', isDone: true, color: 'emerald' },
|
{ 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,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
initialColumns,
|
initialColumns,
|
||||||
onSave,
|
onSave,
|
||||||
listTitle,
|
listTitle,
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
const [columns, setColumns] = useState<KanbanColumn[]>(() => {
|
const [columns, setColumns] = useState<KanbanColumn[]>(() => {
|
||||||
return initialColumns && initialColumns.length > 0 ? initialColumns : DEFAULT_KANBAN_COLUMNS;
|
return initialColumns && initialColumns.length > 0 ? initialColumns : DEFAULT_KANBAN_COLUMNS;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,19 +7,16 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
Plus,
|
Plus,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
Clock,
|
|
||||||
Layers,
|
|
||||||
Hash,
|
Hash,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Flame,
|
Flame,
|
||||||
ChevronRight,
|
|
||||||
Folder,
|
Folder,
|
||||||
Webhook,
|
Webhook,
|
||||||
Lock,
|
Lock,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Repeat,
|
Repeat,
|
||||||
} from 'lucide-react';
|
} 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 { UserAvatar } from '../../components/UserAvatar';
|
||||||
import { UserAutocomplete } from '../UserAutocomplete';
|
import { UserAutocomplete } from '../UserAutocomplete';
|
||||||
import { triggerHaptic } from '../../lib/api';
|
import { triggerHaptic } from '../../lib/api';
|
||||||
@@ -51,9 +48,13 @@ interface TaskDetailModalProps {
|
|||||||
readOnly?: boolean; // Task belongs to a list shared with "viewer" permission
|
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,
|
task,
|
||||||
isOpen,
|
|
||||||
onClose,
|
onClose,
|
||||||
activeColumns,
|
activeColumns,
|
||||||
taskLists,
|
taskLists,
|
||||||
@@ -75,8 +76,6 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
|||||||
onDeleteTask,
|
onDeleteTask,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen || !task) return null;
|
|
||||||
|
|
||||||
const [titleDraft, setTitleDraft] = useState(task.content);
|
const [titleDraft, setTitleDraft] = useState(task.content);
|
||||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||||
|
|
||||||
@@ -144,7 +143,7 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
|||||||
|
|
||||||
const handleAddTagSubmit = async (e: React.FormEvent) => {
|
const handleAddTagSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let cleaned = newTagDraft.trim().replace(/^#+/, '').toLowerCase();
|
const cleaned = newTagDraft.trim().replace(/^#+/, '').toLowerCase();
|
||||||
if (!cleaned) return;
|
if (!cleaned) return;
|
||||||
const currentTags = Array.isArray(task.tags) ? [...task.tags] : [];
|
const currentTags = Array.isArray(task.tags) ? [...task.tags] : [];
|
||||||
if (!currentTags.includes(cleaned)) {
|
if (!currentTags.includes(cleaned)) {
|
||||||
@@ -177,11 +176,6 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
|
|||||||
onUpdateDueDate(task, joinDue(addDays(daysOffset), splitDue(task.dueDate).time));
|
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 subtasks = Array.isArray(task.subtasks) ? task.subtasks : [];
|
||||||
const completedSubtasks = subtasks.filter((s) => s.isCompleted).length;
|
const completedSubtasks = subtasks.filter((s) => s.isCompleted).length;
|
||||||
const subtasksPercent = subtasks.length > 0 ? Math.round((completedSubtasks / subtasks.length) * 100) : 0;
|
const subtasksPercent = subtasks.length > 0 ? Math.round((completedSubtasks / subtasks.length) * 100) : 0;
|
||||||
|
|||||||
@@ -15,19 +15,18 @@ import {
|
|||||||
Layers,
|
Layers,
|
||||||
Users,
|
Users,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
User as UserIcon,
|
User as
|
||||||
Lock,
|
Lock,
|
||||||
Hash,
|
Hash,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Flame,
|
Flame,
|
||||||
Settings,
|
Settings,
|
||||||
ChevronDown,
|
|
||||||
Folder,
|
Folder,
|
||||||
Filter,
|
Filter,
|
||||||
Webhook,
|
Webhook,
|
||||||
Repeat,
|
Repeat,
|
||||||
} from 'lucide-react';
|
} 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 { formatDue, isDueToday, isOverdue, localDay, splitDue } from '../../lib/due';
|
||||||
import { api, triggerHaptic } from '../../lib/api';
|
import { api, triggerHaptic } from '../../lib/api';
|
||||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||||
@@ -92,10 +91,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
// Share TaskList modal state
|
// Share TaskList modal state
|
||||||
const [showShareListModal, setShowShareListModal] = useState(false);
|
const [showShareListModal, setShowShareListModal] = useState(false);
|
||||||
const [sharingTaskList, setSharingTaskList] = useState<TaskList | null>(null);
|
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
|
// Detail modal state
|
||||||
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
|
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
|
||||||
@@ -854,8 +849,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
|
|
||||||
const handleOpenShareModal = async (tl: TaskList) => {
|
const handleOpenShareModal = async (tl: TaskList) => {
|
||||||
setSharingTaskList(tl);
|
setSharingTaskList(tl);
|
||||||
setSelectedShareTargetId('');
|
|
||||||
setSelectedShareUser(null);
|
|
||||||
setShowShareListModal(true);
|
setShowShareListModal(true);
|
||||||
try {
|
try {
|
||||||
const [freshTeams, freshUsers, freshShares] = await Promise.all([
|
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[]) => {
|
const handleSaveColumns = async (newColumns: KanbanColumn[]) => {
|
||||||
triggerHaptic(15);
|
triggerHaptic(15);
|
||||||
if (currentSelectedList && selectedListId !== 'all') {
|
if (currentSelectedList && selectedListId !== 'all') {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
import React, { useState, useMemo, useRef } from 'react';
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import 'prismjs/components/prism-python';
|
import 'prismjs/components/prism-python';
|
||||||
import 'prismjs/components/prism-typescript';
|
import 'prismjs/components/prism-typescript';
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Minimize2,
|
Minimize2,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
|
||||||
Check,
|
Check,
|
||||||
CloudOff,
|
CloudOff,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
block,
|
block,
|
||||||
index,
|
index,
|
||||||
isLast,
|
isLast,
|
||||||
maxAllowedLevel,
|
|
||||||
hasChildren,
|
hasChildren,
|
||||||
isFocused,
|
isFocused,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
|
|||||||
|
|
||||||
// Explicit bullet or numbered list: - item, * item, • item, 1. item, 1) item
|
// Explicit bullet or numbered list: - item, * item, • item, 1. item, 1) item
|
||||||
const bulletMatch = strippedLine.match(/^[-*•]\s+(.*)$/);
|
const bulletMatch = strippedLine.match(/^[-*•]\s+(.*)$/);
|
||||||
const numMatch = strippedLine.match(/^\d+[\.\)]\s+(.*)$/);
|
const numMatch = strippedLine.match(/^\d+[.)]\s+(.*)$/);
|
||||||
if (bulletMatch || numMatch) {
|
if (bulletMatch || numMatch) {
|
||||||
pushCurrentItem();
|
pushCurrentItem();
|
||||||
afterHeading = false;
|
afterHeading = false;
|
||||||
|
|||||||
@@ -18,15 +18,11 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
CheckSquare,
|
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
ChevronLeft,
|
|
||||||
Radio,
|
|
||||||
Clock,
|
Clock,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Loader2,
|
Loader2,
|
||||||
Lock,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Document,
|
Document,
|
||||||
@@ -44,10 +40,6 @@ import {
|
|||||||
api,
|
api,
|
||||||
onWebSocketMessage,
|
onWebSocketMessage,
|
||||||
triggerHaptic,
|
triggerHaptic,
|
||||||
onRealtimeStatus,
|
|
||||||
isRealtimeConnected,
|
|
||||||
getRealtimeTransport,
|
|
||||||
RealtimeTransport,
|
|
||||||
} from '../../lib/api';
|
} from '../../lib/api';
|
||||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||||
import { FormSheet } from '../../components/FormSheet';
|
import { FormSheet } from '../../components/FormSheet';
|
||||||
@@ -347,7 +339,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const [newFolderCoords, setNewFolderCoords] = useState<{ x: number; y: number } | null>(null);
|
const [newFolderCoords, setNewFolderCoords] = useState<{ x: number; y: number } | null>(null);
|
||||||
const [newFolderParentPath, setNewFolderParentPath] = useState<string>('');
|
const [newFolderParentPath, setNewFolderParentPath] = useState<string>('');
|
||||||
const [newFolderName, setNewFolderName] = useState<string>('');
|
const [newFolderName, setNewFolderName] = useState<string>('');
|
||||||
const [newFolderIsShared, setNewFolderIsShared] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleOpenNewFolder = (parentPath = '', e?: React.MouseEvent) => {
|
const handleOpenNewFolder = (parentPath = '', e?: React.MouseEvent) => {
|
||||||
setNewFolderParentPath(parentPath);
|
setNewFolderParentPath(parentPath);
|
||||||
@@ -381,19 +372,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Realtime connection status for top header indicator (identical to SharedListsView)
|
|
||||||
const [realtimeConnected, setRealtimeConnected] = useState<boolean>(() => isRealtimeConnected());
|
|
||||||
const [realtimeTransport, setRealtimeTransport] = useState<RealtimeTransport>(() => getRealtimeTransport());
|
|
||||||
const [docSaveStatus, setDocSaveStatus] = useState<DocSaveStatus>('saved');
|
const [docSaveStatus, setDocSaveStatus] = useState<DocSaveStatus>('saved');
|
||||||
const [docSaveError, setDocSaveError] = useState<string | null>(null);
|
const [docSaveError, setDocSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsub = onRealtimeStatus((connected, transport) => {
|
|
||||||
setRealtimeConnected(connected);
|
|
||||||
setRealtimeTransport(transport);
|
|
||||||
});
|
|
||||||
return unsub;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Drag and Drop state
|
// Drag and Drop state
|
||||||
const [draggingDocId, setDraggingDocId] = useState<string | null>(null);
|
const [draggingDocId, setDraggingDocId] = useState<string | null>(null);
|
||||||
@@ -960,22 +941,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
return map;
|
return map;
|
||||||
}, [allRollupTodos]);
|
}, [allRollupTodos]);
|
||||||
|
|
||||||
const rollupTodosByNote = useMemo(() => {
|
|
||||||
const map = new Map<
|
|
||||||
string,
|
|
||||||
{ doc: Document; items: { doc: Document; blockId: string; content: string; checked: boolean }[] }
|
|
||||||
>();
|
|
||||||
for (const item of allRollupTodos) {
|
|
||||||
const existing = map.get(item.doc.id);
|
|
||||||
if (existing) {
|
|
||||||
existing.items.push(item);
|
|
||||||
} else {
|
|
||||||
map.set(item.doc.id, { doc: item.doc, items: [item] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [allRollupTodos]);
|
|
||||||
|
|
||||||
const totalOpenTodos = useMemo(() => {
|
const totalOpenTodos = useMemo(() => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const d of docs) {
|
for (const d of docs) {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
SharedFolder,
|
SharedFolder,
|
||||||
FolderShare,
|
FolderShare,
|
||||||
Space,
|
Space,
|
||||||
SpaceMember,
|
|
||||||
SpaceShare,
|
SpaceShare,
|
||||||
SystemSettings,
|
SystemSettings,
|
||||||
SharingMode,
|
SharingMode,
|
||||||
@@ -1544,7 +1543,7 @@ export async function connectWebSocket(): Promise<void> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (e) => {
|
ws.onerror = () => {
|
||||||
clearTimeout(wsConnectionWatchdog);
|
clearTimeout(wsConnectionWatchdog);
|
||||||
switchToSSE('WebSocket Fehler (Handshake/Verbindung verweigert)');
|
switchToSSE('WebSocket Fehler (Handshake/Verbindung verweigert)');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe('diffFlat', () => {
|
|||||||
return Math.floor((seed / 2147483648) * n);
|
return Math.floor((seed / 2147483648) * n);
|
||||||
};
|
};
|
||||||
let current: DocumentBlock[] = [B('x0', 0)];
|
let current: DocumentBlock[] = [B('x0', 0)];
|
||||||
let d = build(current);
|
const d = build(current);
|
||||||
for (let step = 0; step < 300; step++) {
|
for (let step = 0; step < 300; step++) {
|
||||||
const next = current.map((b) => ({ ...b }));
|
const next = current.map((b) => ({ ...b }));
|
||||||
const i = rnd(next.length);
|
const i = rnd(next.length);
|
||||||
|
|||||||
Reference in new issue
Block a user