Merge dev into main (v0.16.0)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 28s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 28s
This commit is contained in:
commit
351013c7be
5 files changed
+91
-29
No files matched your search
+11
-1
@@ -6,6 +6,15 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.16.0] - 2026-10-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Kanban: Neben „Spalten anpassen“ streckt ein kleiner Knopf (diagonale Pfeile) das Board auf die volle Fensterbreite; alle Spalten stehen dann nebeneinander. Die Einstellung merkt sich der Browser, am Handy gibt es den Knopf nicht.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Aufgaben: Ein neuer Quick-Dump-Eintrag (z. B. per Spracheingabe über das gehaltene Plus) tauchte kurz oben in den Aufgaben auf, obwohl er in die Triage gehört. Live-Änderungen nehmen jetzt nur noch echte Aufgaben auf; eine per „Rückgängig“ zurück in die Triage geholte Aufgabe verschwindet auch sofort aus der Liste.
|
||||||
|
- Kanban: Bei runden Ecken (Theme-Einstellung) ragten die eckigen Spaltenüberschriften oben über den Spaltenrand hinaus.
|
||||||
|
|
||||||
## [0.15.1] - 2026-10-31
|
## [0.15.1] - 2026-10-31
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -365,7 +374,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
|
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
|
||||||
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
|
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
|
||||||
|
|
||||||
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...dev
|
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.0...dev
|
||||||
|
[0.16.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...v0.16.0
|
||||||
[0.15.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.0...v0.15.1
|
[0.15.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.0...v0.15.1
|
||||||
[0.15.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...v0.15.0
|
[0.15.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.3...v0.15.0
|
||||||
[0.14.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.2...v0.14.3
|
[0.14.3]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.2...v0.14.3
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "5.3.0",
|
"@fontsource-variable/inter": "5.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.15.1",
|
"version": "0.16.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ export const App: React.FC = () => {
|
|||||||
const [deck, setDeck] = useState<Thought[]>([]);
|
const [deck, setDeck] = useState<Thought[]>([]);
|
||||||
const [lists, setLists] = useState<SharedList[]>([]);
|
const [lists, setLists] = useState<SharedList[]>([]);
|
||||||
const [activeTab, setActiveTab] = useState<ActiveTab>('triage');
|
const [activeTab, setActiveTab] = useState<ActiveTab>('triage');
|
||||||
|
// Kanban asked for the full window width (reported by the tasks view)
|
||||||
|
const [tasksWide, setTasksWide] = useState(false);
|
||||||
const [previousTab, setPreviousTab] = useState<ActiveTab>('triage');
|
const [previousTab, setPreviousTab] = useState<ActiveTab>('triage');
|
||||||
const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
|
const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
|
||||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
||||||
@@ -1168,7 +1170,9 @@ export const App: React.FC = () => {
|
|||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
{/* The workspace fills the window exactly and scrolls only inside the note, never the page */}
|
{/* The workspace fills the window exactly and scrolls only inside the note, never the page */}
|
||||||
<main
|
<main
|
||||||
className={`flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 ${
|
className={`flex-1 w-full mx-auto px-3 sm:px-4 pt-3 pb-24 ${
|
||||||
|
activeTab === 'tasks' && tasksWide ? 'lg:max-w-none lg:px-6' : 'max-w-6xl'
|
||||||
|
} ${
|
||||||
activeTab === 'workspace' ? 'min-h-0 flex flex-col sm:pb-0' : 'sm:pb-6'
|
activeTab === 'workspace' ? 'min-h-0 flex flex-col sm:pb-0' : 'sm:pb-6'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -1192,6 +1196,7 @@ export const App: React.FC = () => {
|
|||||||
refreshTrigger={refreshRevision}
|
refreshTrigger={refreshRevision}
|
||||||
initialSelectedTaskId={selectedTaskId}
|
initialSelectedTaskId={selectedTaskId}
|
||||||
openTasksCount={stats.tasksOpenCount}
|
openTasksCount={stats.tasksOpenCount}
|
||||||
|
onWideChange={setTasksWide}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
LayoutList,
|
LayoutList,
|
||||||
Columns,
|
Columns,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Edit3,
|
Edit3,
|
||||||
@@ -48,6 +50,10 @@ const stopOnControl = (e: React.MouseEvent) => {
|
|||||||
if ((e.target as HTMLElement).closest('button, select, a, input')) e.stopPropagation();
|
if ((e.target as HTMLElement).closest('button, select, a, input')) e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Same rule as the server's task query: a fresh Quick Dump note (status inbox) is no task yet
|
||||||
|
const isTaskThought = (t: Thought) =>
|
||||||
|
t.status === 'kept' || (t.status === 'converted' && t.convertedType === 'task');
|
||||||
|
|
||||||
interface TasksViewProps {
|
interface TasksViewProps {
|
||||||
currentUser?: User | null;
|
currentUser?: User | null;
|
||||||
onRefreshStats: () => void;
|
onRefreshStats: () => void;
|
||||||
@@ -55,8 +61,11 @@ interface TasksViewProps {
|
|||||||
refreshTrigger?: number;
|
refreshTrigger?: number;
|
||||||
initialSelectedTaskId?: string | null;
|
initialSelectedTaskId?: string | null;
|
||||||
openTasksCount?: number; // all open tasks incl. shared lists (same as the nav bar counter)
|
openTasksCount?: number; // all open tasks incl. shared lists (same as the nav bar counter)
|
||||||
|
onWideChange?: (wide: boolean) => void; // Kanban stretched to the full window width
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const KANBAN_WIDE_KEY = 'mindsift:kanban_wide';
|
||||||
|
|
||||||
export const TasksView: React.FC<TasksViewProps> = ({
|
export const TasksView: React.FC<TasksViewProps> = ({
|
||||||
currentUser,
|
currentUser,
|
||||||
onRefreshStats,
|
onRefreshStats,
|
||||||
@@ -64,8 +73,31 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
refreshTrigger = 0,
|
refreshTrigger = 0,
|
||||||
initialSelectedTaskId,
|
initialSelectedTaskId,
|
||||||
openTasksCount,
|
openTasksCount,
|
||||||
|
onWideChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
|
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
|
||||||
|
// Wide Kanban: the board takes the whole window and shows every column in one row
|
||||||
|
const [kanbanWide, setKanbanWide] = useState(() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(KANBAN_WIDE_KEY) === '1';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const toggleKanbanWide = () => {
|
||||||
|
const next = !kanbanWide;
|
||||||
|
setKanbanWide(next);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(KANBAN_WIDE_KEY, next ? '1' : '0');
|
||||||
|
} catch {
|
||||||
|
// only a convenience
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const isKanbanWide = viewMode === 'kanban' && kanbanWide;
|
||||||
|
useEffect(() => {
|
||||||
|
onWideChange?.(isKanbanWide);
|
||||||
|
}, [isKanbanWide, onWideChange]);
|
||||||
|
useEffect(() => () => onWideChange?.(false), [onWideChange]);
|
||||||
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
|
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
|
||||||
const [tasks, setTasks] = useState<Thought[]>([]);
|
const [tasks, setTasks] = useState<Thought[]>([]);
|
||||||
const [taskLists, setTaskLists] = useState<TaskList[]>([]);
|
const [taskLists, setTaskLists] = useState<TaskList[]>([]);
|
||||||
@@ -186,13 +218,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
|
|
||||||
if (msg.event === 'TASK_CREATED') {
|
if (msg.event === 'TASK_CREATED') {
|
||||||
const created = msg.payload as Thought;
|
const created = msg.payload as Thought;
|
||||||
if (
|
if (created && isTaskThought(created)) {
|
||||||
created &&
|
|
||||||
(created.status === 'converted' ||
|
|
||||||
created.kanbanStatus ||
|
|
||||||
created.taskListId ||
|
|
||||||
created.isCompleted !== undefined)
|
|
||||||
) {
|
|
||||||
setTasks((prev) => {
|
setTasks((prev) => {
|
||||||
if (prev.some((t) => t.id === created.id)) return prev;
|
if (prev.some((t) => t.id === created.id)) return prev;
|
||||||
if (selectedListIdRef.current !== 'all' && created.taskListId !== selectedListIdRef.current) return prev;
|
if (selectedListIdRef.current !== 'all' && created.taskListId !== selectedListIdRef.current) return prev;
|
||||||
@@ -207,17 +233,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
const exists = prev.some((t) => t.id === updated.id);
|
const exists = prev.some((t) => t.id === updated.id);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
if (
|
if (
|
||||||
(updated.status === 'converted' ||
|
isTaskThought(updated) &&
|
||||||
updated.kanbanStatus ||
|
|
||||||
updated.taskListId ||
|
|
||||||
updated.isCompleted !== undefined) &&
|
|
||||||
(selectedListIdRef.current === 'all' || updated.taskListId === selectedListIdRef.current)
|
(selectedListIdRef.current === 'all' || updated.taskListId === selectedListIdRef.current)
|
||||||
) {
|
) {
|
||||||
return [updated, ...prev];
|
return [updated, ...prev];
|
||||||
}
|
}
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
if (selectedListIdRef.current !== 'all' && updated.taskListId !== selectedListIdRef.current) {
|
// Undone back into the Quick Dump, discarded, or moved to another list
|
||||||
|
if (
|
||||||
|
!isTaskThought(updated) ||
|
||||||
|
(selectedListIdRef.current !== 'all' && updated.taskListId !== selectedListIdRef.current)
|
||||||
|
) {
|
||||||
return prev.filter((t) => t.id !== updated.id);
|
return prev.filter((t) => t.id !== updated.id);
|
||||||
}
|
}
|
||||||
return prev.map((t) => (t.id === updated.id ? updated : t));
|
return prev.map((t) => (t.id === updated.id ? updated : t));
|
||||||
@@ -1651,16 +1678,31 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
{/* Group By & Sort By Dropdowns & Manage Columns button */}
|
{/* Group By & Sort By Dropdowns & Manage Columns button */}
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 ml-auto">
|
<div className="flex items-center gap-2 flex-shrink-0 ml-auto">
|
||||||
{viewMode === 'kanban' ? (
|
{viewMode === 'kanban' ? (
|
||||||
!isSelectedListReadOnly && (
|
<>
|
||||||
<button
|
{!isSelectedListReadOnly && (
|
||||||
onClick={() => setShowManageColumnsModal(true)}
|
<button
|
||||||
className="btn-action h-7 px-2.5 text-xs flex items-center gap-1.5"
|
onClick={() => setShowManageColumnsModal(true)}
|
||||||
title="Kanban-Spalten hinzufügen, umbenennen oder neu anordnen"
|
className="btn-action h-7 px-2.5 text-xs flex items-center gap-1.5"
|
||||||
>
|
title="Kanban-Spalten hinzufügen, umbenennen oder neu anordnen"
|
||||||
<Columns className="w-3.5 h-3.5 text-accent" />
|
>
|
||||||
<span>Spalten anpassen</span>
|
<Columns className="w-3.5 h-3.5 text-accent" />
|
||||||
</button>
|
<span>Spalten anpassen</span>
|
||||||
)
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleKanbanWide}
|
||||||
|
aria-pressed={kanbanWide}
|
||||||
|
className="btn-action h-7 w-7 p-0 hidden lg:inline-flex items-center justify-center"
|
||||||
|
title={kanbanWide ? 'Board wieder normal breit' : 'Board auf volle Breite strecken'}
|
||||||
|
>
|
||||||
|
{kanbanWide ? (
|
||||||
|
<Minimize2 className="w-3.5 h-3.5 text-accent" />
|
||||||
|
) : (
|
||||||
|
<Maximize2 className="w-3.5 h-3.5 text-accent" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Group By */}
|
{/* Group By */}
|
||||||
@@ -2113,7 +2155,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
{viewMode === 'kanban' ? (
|
{viewMode === 'kanban' ? (
|
||||||
/* DYNAMIC KANBAN BOARD */
|
/* DYNAMIC KANBAN BOARD */
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 pt-1">
|
<div
|
||||||
|
className={`grid grid-cols-1 md:grid-cols-2 gap-3 pt-1 ${
|
||||||
|
isKanbanWide ? 'lg:grid-cols-[repeat(var(--kanban-cols),minmax(0,1fr))]' : 'lg:grid-cols-4'
|
||||||
|
}`}
|
||||||
|
style={isKanbanWide ? ({ '--kanban-cols': activeColumns.length } as React.CSSProperties) : undefined}
|
||||||
|
>
|
||||||
{activeColumns.map((col, colIdx) => {
|
{activeColumns.map((col, colIdx) => {
|
||||||
const colTasks = kanbanTasks.filter((t) => {
|
const colTasks = kanbanTasks.filter((t) => {
|
||||||
if (col.isDone) {
|
if (col.isDone) {
|
||||||
@@ -2135,7 +2182,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
if (dragOverKanbanCol === col.id) setDragOverKanbanCol(null);
|
if (dragOverKanbanCol === col.id) setDragOverKanbanCol(null);
|
||||||
}}
|
}}
|
||||||
onDrop={(e) => handleDropOnKanbanCol(e, col.id)}
|
onDrop={(e) => handleDropOnKanbanCol(e, col.id)}
|
||||||
className={`bg-canvas border border-border flex flex-col min-h-[420px] transition-all ${
|
className={`bg-canvas border border-border flex flex-col min-h-[420px] overflow-hidden transition-all ${
|
||||||
dragOverKanbanCol === col.id ? 'ring-2 ring-accent/60 bg-accent/5' : ''
|
dragOverKanbanCol === col.id ? 'ring-2 ring-accent/60 bg-accent/5' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in new issue
Block a user