Merge dev into main (v0.15.1)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 26s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 26s
This commit is contained in:
commit
3cd59b6ffa
6 files changed
+95
-51
No files matched your search
+9
-1
@@ -6,6 +6,13 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.15.1] - 2026-10-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Triage am Handy: Die Warteschlange lässt sich wieder zuklappen („Warteschlange ausblenden“) und fährt beim Auf- und Zuklappen weich auf bzw. zu.
|
||||||
|
- Triage auf dem Desktop: Notizen, die auf einem anderen Gerät einsortiert oder gelöscht wurden, verschwinden jetzt sofort auch hier (bisher wurde nur der Aufgaben-Tab aktualisiert). Kommt der Tab wieder in den Vordergrund, lädt er außerdem neu, falls Änderungen im Hintergrund verpasst wurden.
|
||||||
|
- Aufgaben: Ein Tipp irgendwo auf die Aufgabe öffnet sie jetzt zuverlässig, nicht nur auf den Text. Bisher schluckten die Zeile mit Datum/Tags und in Kanban die Pfeil-Leiste jeden Tipp, und am Handy konnte ein etwas längerer Tipp als Ziehen gewertet werden (Ziehen gibt es nur noch mit Maus oder Stift).
|
||||||
|
|
||||||
## [0.15.0] - 2026-10-31
|
## [0.15.0] - 2026-10-31
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -358,7 +365,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.0...dev
|
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...dev
|
||||||
|
[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
|
||||||
[0.14.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.1...v0.14.2
|
[0.14.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.14.1...v0.14.2
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.15.0",
|
"version": "0.15.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.15.0",
|
"version": "0.15.1",
|
||||||
"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.0",
|
"version": "0.15.1",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+18
-2
@@ -628,8 +628,20 @@ export const App: React.FC = () => {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Refresh stats on task/doc/list real-time events (counters in the navigation)
|
// Refresh stats on task/doc/list real-time events (counters in the navigation). Task events also
|
||||||
|
// reload the triage deck: a note sorted on another device must leave this deck too.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let deckTimer: number | undefined;
|
||||||
|
const reloadDeck = () => {
|
||||||
|
window.clearTimeout(deckTimer);
|
||||||
|
deckTimer = window.setTimeout(() => {
|
||||||
|
api.getTriageDeck().then((res) => setDeck(Array.isArray(res) ? res : [])).catch(() => {});
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
// Events missed while the tab slept (desktop in the background, connection dropped)
|
||||||
|
const onVisible = () => {
|
||||||
|
if (document.visibilityState === 'visible' && getToken()) refreshAll();
|
||||||
|
};
|
||||||
const handleWs = (e: any) => {
|
const handleWs = (e: any) => {
|
||||||
const msg = e.detail;
|
const msg = e.detail;
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
@@ -644,13 +656,17 @@ export const App: React.FC = () => {
|
|||||||
api.getMe().then((res) => {
|
api.getMe().then((res) => {
|
||||||
setStats(res.stats);
|
setStats(res.stats);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
if (msg.event.startsWith('TASK_')) reloadDeck();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('mindsift:ws', handleWs);
|
window.addEventListener('mindsift:ws', handleWs);
|
||||||
|
document.addEventListener('visibilitychange', onVisible);
|
||||||
return () => {
|
return () => {
|
||||||
|
window.clearTimeout(deckTimer);
|
||||||
window.removeEventListener('mindsift:ws', handleWs);
|
window.removeEventListener('mindsift:ws', handleWs);
|
||||||
|
document.removeEventListener('visibilitychange', onVisible);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [refreshAll]);
|
||||||
|
|
||||||
// Close profile dropdown on outside click
|
// Close profile dropdown on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ import { CountBadge } from '../../components/CountBadge';
|
|||||||
import { FormSheet } from '../../components/FormSheet';
|
import { FormSheet } from '../../components/FormSheet';
|
||||||
import { useMobileActions } from '../../context/MobileActionContext';
|
import { useMobileActions } from '../../context/MobileActionContext';
|
||||||
|
|
||||||
|
// Drag & drop needs a mouse or pen; on touch-only devices a draggable row can swallow a slightly
|
||||||
|
// longer tap, so the row would not open
|
||||||
|
const canDragTasks =
|
||||||
|
typeof window !== 'undefined' && !!window.matchMedia?.('(any-pointer: fine)').matches;
|
||||||
|
|
||||||
|
// Rows inside a task card keep taps on their own controls; taps on empty space or plain labels
|
||||||
|
// (due date, webhook) reach the card and open the task
|
||||||
|
const stopOnControl = (e: React.MouseEvent) => {
|
||||||
|
if ((e.target as HTMLElement).closest('button, select, a, input')) e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
interface TasksViewProps {
|
interface TasksViewProps {
|
||||||
currentUser?: User | null;
|
currentUser?: User | null;
|
||||||
onRefreshStats: () => void;
|
onRefreshStats: () => void;
|
||||||
@@ -1093,7 +1104,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={task.id}
|
key={task.id}
|
||||||
draggable={!readOnly}
|
draggable={canDragTasks && !readOnly}
|
||||||
onDragStart={(e) => handleTaskDragStart(e, task)}
|
onDragStart={(e) => handleTaskDragStart(e, task)}
|
||||||
onDragEnd={() => {
|
onDragEnd={() => {
|
||||||
setDraggingTaskId(null);
|
setDraggingTaskId(null);
|
||||||
@@ -1143,13 +1154,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Clickable Title & Metadata */}
|
{/* Clickable Title & Metadata */}
|
||||||
<div
|
<div className="min-w-0 flex-1 cursor-pointer space-y-1">
|
||||||
onClick={() => {
|
|
||||||
setDetailTaskId(task.id);
|
|
||||||
setShowDetailModal(true);
|
|
||||||
}}
|
|
||||||
className="min-w-0 flex-1 cursor-pointer space-y-1"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<p
|
<p
|
||||||
className={`text-sm break-words leading-tight ${
|
className={`text-sm break-words leading-tight ${
|
||||||
@@ -1166,7 +1171,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
{/* Interactive Badges Bar */}
|
{/* Interactive Badges Bar */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-wrap items-center gap-1.5 pt-0.5 empty:hidden"
|
className="flex flex-wrap items-center gap-1.5 pt-0.5 empty:hidden"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={stopOnControl}
|
||||||
>
|
>
|
||||||
{(task.convertedType === 'document_block' || task.docId) && (
|
{(task.convertedType === 'document_block' || task.docId) && (
|
||||||
<button
|
<button
|
||||||
@@ -2163,7 +2168,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={task.id}
|
key={task.id}
|
||||||
draggable={!readOnly}
|
draggable={canDragTasks && !readOnly}
|
||||||
onDragStart={(e) => handleTaskDragStart(e, task)}
|
onDragStart={(e) => handleTaskDragStart(e, task)}
|
||||||
onDragEnd={() => {
|
onDragEnd={() => {
|
||||||
setDraggingTaskId(null);
|
setDraggingTaskId(null);
|
||||||
@@ -2230,7 +2235,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Badges: In Notiz, Priority, Status, Tags */}
|
{/* Badges: In Notiz, Priority, Status, Tags */}
|
||||||
<div className="flex flex-wrap items-center gap-1 pt-0.5" onClick={(e) => e.stopPropagation()}>
|
<div className="flex flex-wrap items-center gap-1 pt-0.5" onClick={stopOnControl}>
|
||||||
{(task.convertedType === 'document_block' || task.docId) && (
|
{(task.convertedType === 'document_block' || task.docId) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2315,7 +2320,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between pt-1 border-t border-border text-[10px]"
|
className="flex items-center justify-between pt-1 border-t border-border text-[10px]"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={stopOnControl}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -916,41 +916,56 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
|||||||
{cards.length > 1 && (
|
{cards.length > 1 && (
|
||||||
<div className="pt-2" ref={queueRef}>
|
<div className="pt-2" ref={queueRef}>
|
||||||
{/* Phones: the queue stays folded so the card and its buttons fit on the screen */}
|
{/* Phones: the queue stays folded so the card and its buttons fit on the screen */}
|
||||||
{!showQueueMobile && (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
aria-expanded={showQueueMobile}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowQueueMobile(true);
|
const opening = !showQueueMobile;
|
||||||
window.setTimeout(() => queueRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
|
setShowQueueMobile(opening);
|
||||||
}}
|
if (opening) {
|
||||||
className="sm:hidden w-full min-h-[48px] px-3 rounded-xl border border-border bg-panel text-sm font-medium text-main flex items-center justify-center gap-2 touch-manipulation cursor-pointer"
|
window.setTimeout(() => queueRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 320);
|
||||||
>
|
}
|
||||||
<Layers className="w-4 h-4 text-accent" />
|
}}
|
||||||
<span>Warteschlange anzeigen ({cards.length - 1})</span>
|
className="sm:hidden w-full min-h-[48px] px-3 rounded-xl border border-border bg-panel text-sm font-medium text-main flex items-center justify-center gap-2 touch-manipulation cursor-pointer"
|
||||||
<ChevronDown className="w-4 h-4 text-muted" />
|
>
|
||||||
</button>
|
<Layers className="w-4 h-4 text-accent" />
|
||||||
)}
|
<span>
|
||||||
<div className={`${showQueueMobile ? 'flex' : 'hidden sm:flex'} items-center justify-between mb-2 px-1`}>
|
Warteschlange {showQueueMobile ? 'ausblenden' : 'anzeigen'} ({cards.length - 1})
|
||||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted flex items-center gap-1.5">
|
|
||||||
<Layers className="w-3.5 h-3.5 text-accent" />
|
|
||||||
Warteschlange ({cards.length - 1})
|
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] text-faint">
|
<ChevronDown
|
||||||
Tippen = Vorholen · 👈 Wegwerfen · 👉 Aufgabe
|
className={`w-4 h-4 text-muted transition-transform duration-300 ${showQueueMobile ? 'rotate-180' : ''}`}
|
||||||
</span>
|
/>
|
||||||
</div>
|
</button>
|
||||||
|
{/* Folds open/closed on phones; always open from sm upwards */}
|
||||||
|
<div
|
||||||
|
className={`grid transition-[grid-template-rows,opacity,visibility] duration-300 ease-out sm:grid-rows-[1fr] sm:opacity-100 sm:visible ${
|
||||||
|
showQueueMobile ? 'grid-rows-[1fr] opacity-100 visible' : 'grid-rows-[0fr] opacity-0 invisible'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden min-h-0">
|
||||||
|
<div className="flex items-center justify-between mb-2 px-1 pt-2 sm:pt-0">
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted flex items-center gap-1.5">
|
||||||
|
<Layers className="w-3.5 h-3.5 text-accent" />
|
||||||
|
Warteschlange ({cards.length - 1})
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-faint">
|
||||||
|
Tippen = Vorholen · 👈 Wegwerfen · 👉 Aufgabe
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`${showQueueMobile ? 'block' : 'hidden sm:block'} panel-surface divide-y divide-border max-h-80 overflow-y-auto overflow-x-hidden`}>
|
<div className="panel-surface divide-y divide-border max-h-80 overflow-y-auto overflow-x-hidden">
|
||||||
{cards.slice(1).map((item) => (
|
{cards.slice(1).map((item) => (
|
||||||
<SwipeableQueueItem
|
<SwipeableQueueItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
lists={writableLists}
|
lists={writableLists}
|
||||||
onBringToFront={handleBringToFront}
|
onBringToFront={handleBringToFront}
|
||||||
onOpenActionSheet={openActionSheetForCard}
|
onOpenActionSheet={openActionSheetForCard}
|
||||||
onAction={handleAction}
|
onAction={handleAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user