fix(collab): fix self-lockout, stuck live sessions, block locking propagation and reactive hashtags
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
This commit is contained in:
1 parent
0e06cc5577
commit
996f27133c
6 files changed
+303
-89
No files matched your search
@@ -267,7 +267,29 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const [docs, setDocs] = useState<Document[]>([]);
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
const [sharedFolders, setSharedFolders] = useState<SharedFolder[]>([]);
|
||||
const [tagsSummary, setTagsSummary] = useState<TagSummary[]>([]);
|
||||
const [serverTagsSummary, setServerTagsSummary] = useState<TagSummary[]>([]);
|
||||
|
||||
// Dynamically compute hashtag counts directly from in-memory documents
|
||||
const tagsSummary = useMemo(() => {
|
||||
if (docs.length === 0) return serverTagsSummary;
|
||||
const tagMap = new Map<string, number>();
|
||||
for (const d of docs) {
|
||||
for (const b of d.blocks || []) {
|
||||
const matches = b.content?.match(/#([a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
||||
if (matches) {
|
||||
for (const m of matches) {
|
||||
const clean = m.slice(1).toLowerCase();
|
||||
tagMap.set(clean, (tagMap.get(clean) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const list: TagSummary[] = [];
|
||||
tagMap.forEach((count, tag) => {
|
||||
list.push({ tag, count });
|
||||
});
|
||||
return list.sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
||||
}, [docs, serverTagsSummary]);
|
||||
|
||||
const [selectedDocId, setSelectedDocId] = useState<string | null>(initialDocId || null);
|
||||
const [targetBlockId, setTargetBlockId] = useState<string | null>(null);
|
||||
@@ -356,7 +378,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
setDocs(docsRes);
|
||||
setFolders(foldersRes);
|
||||
setSharedFolders(sharedFoldersRes);
|
||||
setTagsSummary(tagsRes);
|
||||
setServerTagsSummary(tagsRes);
|
||||
|
||||
setSelectedDocId((prevId) => {
|
||||
if (prevId && docsRes.some((d) => d.id === prevId)) {
|
||||
@@ -1196,32 +1218,64 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
{/* Interactive Live Collaboration Button (replaces passive radio icon) */}
|
||||
{docLiveState === 'locked_by_other' ? (
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-warn/15 border border-status-warn/40 text-status-warn select-none"
|
||||
title={`Live-Zusammenarbeit aktiv durch ${docLiveInitiator || 'anderer Nutzer'}`}
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="truncate max-w-[120px]">Live: {docLiveInitiator || 'Gesperrt'}</span>
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-warn/15 border border-status-warn/40 text-status-warn select-none"
|
||||
title={`Live-Zusammenarbeit aktiv durch ${docLiveInitiator || 'anderer Nutzer'}`}
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="truncate max-w-[120px]">Live: {docLiveInitiator || 'Gesperrt'}</span>
|
||||
</div>
|
||||
{(selectedDocObj?.isOwner || selectedDocObj?.permission === 'owner') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => liveToggleRef.current?.()}
|
||||
className="px-2 py-1 rounded-btn text-xs font-semibold bg-status-warn/20 border border-status-warn/40 text-status-warn hover:bg-status-warn/30 transition-colors cursor-pointer"
|
||||
title="Als Notiz-Besitzer Live-Sitzung beenden und Dokument freigeben"
|
||||
>
|
||||
Freigeben
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : docLiveState === 'blocked' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWsBlockedModal(true)}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-err/15 border border-status-err/40 text-status-err hover:bg-status-err/25 transition-colors cursor-pointer"
|
||||
title="WebSockets im Firmennetzwerk blockiert – Hier klicken für Infos"
|
||||
>
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-status-err animate-pulse shrink-0" />
|
||||
<span>Live blockiert</span>
|
||||
</button>
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWsBlockedModal(true)}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-status-err/15 border border-status-err/40 text-status-err hover:bg-status-err/25 transition-colors cursor-pointer"
|
||||
title="WebSockets im Firmennetzwerk blockiert – Hier klicken für Infos"
|
||||
>
|
||||
<AlertTriangle className="w-3.5 h-3.5 text-status-err animate-pulse shrink-0" />
|
||||
<span>Live blockiert</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => liveToggleRef.current?.()}
|
||||
className="px-2 py-1 rounded-btn text-xs font-medium bg-canvas border border-border text-muted hover:text-status-err hover:border-status-err/50 transition-colors cursor-pointer"
|
||||
title="Live-Sitzung beenden"
|
||||
>
|
||||
Beenden
|
||||
</button>
|
||||
</div>
|
||||
) : docLiveState === 'connecting' ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-amber-500/15 border border-amber-500/40 text-amber-500 opacity-80 cursor-wait"
|
||||
>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-amber-500 shrink-0" />
|
||||
<span>Verbinde…</span>
|
||||
</button>
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn text-xs font-medium bg-amber-500/15 border border-amber-500/40 text-amber-500 opacity-80 cursor-wait"
|
||||
>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-amber-500 shrink-0" />
|
||||
<span>Verbinde…</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => liveToggleRef.current?.()}
|
||||
className="px-2 py-1 rounded-btn text-xs font-medium bg-canvas border border-border text-muted hover:text-status-err hover:border-status-err/50 transition-colors cursor-pointer"
|
||||
title="Verbindungsaufbau abbrechen"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
) : docLiveState === 'active' ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -2547,7 +2601,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-2">
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
liveToggleRef.current?.();
|
||||
setShowWsBlockedModal(false);
|
||||
}}
|
||||
className="px-3 py-1.5 text-xs font-medium bg-status-err/15 border border-status-err/40 text-status-err hover:bg-status-err/25 rounded-btn transition-colors cursor-pointer"
|
||||
>
|
||||
Live-Sitzung beenden
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWsBlockedModal(false)}
|
||||
|
||||
Reference in new issue
Block a user