fix(workspace): hidden bullet actions on phones, no second extract, notice layout
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m20s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m20s
- The floating bullet actions only react to hover on devices with a real pointer; a tap on a phone made the invisible buttons clickable. - A bullet that is only a note link (plus hashtags) can no longer be extracted: button and /extract are hidden, the server answers 400. - Extract notice on phones sits above the bottom navigation, text on top (two lines max) with the actions below. - Release v0.26.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fdd6e38065
commit
68bc4d279c
12 files changed
+78
-27
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.26.1] - 2026-10-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Notizen am Handy: Ein Tipp rechts neben einen Punkt löste die unsichtbaren Knöpfe aus (In eigene Notiz umwandeln, Fokus, Löschen …). Die Knöpfe reagieren jetzt nur noch, wenn sie zu sehen sind, also beim Bearbeiten des Punkts.
|
||||||
|
- Notizen: Ein Punkt, der schon in eine eigene Notiz umgewandelt wurde (nur noch der Link, ggf. mit Hashtags), lässt sich nicht noch einmal umwandeln; der Knopf und `/extract` fehlen dort. Bisher entstand dabei eine zweite Notiz, die nur den Link enthielt.
|
||||||
|
- Hinweis nach dem Umwandeln am Handy („Öffnen“, „Rückgängig“): sitzt jetzt über der unteren Leiste statt am Bildschirmrand, Text oben (höchstens zwei Zeilen), Knöpfe darunter.
|
||||||
|
|
||||||
## [0.26.0] - 2026-10-31
|
## [0.26.0] - 2026-10-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -527,7 +534,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.26.0...dev
|
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.26.1...dev
|
||||||
|
[0.26.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.26.0...v0.26.1
|
||||||
[0.26.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.25.1...v0.26.0
|
[0.26.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.25.1...v0.26.0
|
||||||
[0.25.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.25.0...v0.25.1
|
[0.25.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.25.0...v0.25.1
|
||||||
[0.25.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.2...v0.25.0
|
[0.25.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.2...v0.25.0
|
||||||
|
|||||||
+1
-1
@@ -127,7 +127,7 @@ Schreibzugriffe von Lesern auf Listen oder deren Aufgaben liefern `403` („Kein
|
|||||||
| POST | `/documents/{id}/blocks/{blockId}/enhanced-task` | Aus einem Block eine verknüpfte Aufgabe machen (`content`, `checked`) |
|
| POST | `/documents/{id}/blocks/{blockId}/enhanced-task` | Aus einem Block eine verknüpfte Aufgabe machen (`content`, `checked`) |
|
||||||
| GET/POST | `/documents/{id}/shares` | Notiz-Freigaben (`userId`/`email`/`teamId`, `permission`: `read`/`write`) |
|
| GET/POST | `/documents/{id}/shares` | Notiz-Freigaben (`userId`/`email`/`teamId`, `permission`: `read`/`write`) |
|
||||||
| DELETE | `/documents/{id}/shares/{userId}` | Notiz-Freigabe entfernen |
|
| DELETE | `/documents/{id}/shares/{userId}` | Notiz-Freigabe entfernen |
|
||||||
| POST | `/documents/{id}/extract` | `{blockId, clientId}`: legt aus den Unterpunkten des Punkts eine eigene Notiz an (Titel = Text ohne Hashtags, gleicher Ordner, Freigaben übernommen, fremder Besitzer bekommt Schreibrecht) → `{document, link}`. Die Ursprungsnotiz ändert der Server nicht; der Editor ersetzt den Ast selbst durch `link` (normale eigene Änderung, Strg+Z möglich). Schreibrecht nötig; `409`, wenn jemand anderes im Ast schreibt |
|
| POST | `/documents/{id}/extract` | `{blockId, clientId}`: legt aus den Unterpunkten des Punkts eine eigene Notiz an (Titel = Text ohne Hashtags, gleicher Ordner, Freigaben übernommen, fremder Besitzer bekommt Schreibrecht) → `{document, link}`. Die Ursprungsnotiz ändert der Server nicht; der Editor ersetzt den Ast selbst durch `link` (normale eigene Änderung, Strg+Z möglich). Schreibrecht nötig; `409`, wenn jemand anderes im Ast schreibt; `400`, wenn der Punkt nur noch ein Link auf eine Notiz ist (ggf. mit Hashtags) |
|
||||||
| GET | `/push/key` | `{enabled, publicKey}` – öffentlicher VAPID-Schlüssel für `pushManager.subscribe` (in der Demo `enabled: false`) |
|
| GET | `/push/key` | `{enabled, publicKey}` – öffentlicher VAPID-Schlüssel für `pushManager.subscribe` (in der Demo `enabled: false`) |
|
||||||
| GET | `/push/devices` | Eigene Geräte mit Push → `{devices: [{id, endpoint, device, createdAt}]}` |
|
| GET | `/push/devices` | Eigene Geräte mit Push → `{devices: [{id, endpoint, device, createdAt}]}` |
|
||||||
| POST | `/push/subscribe` | `{endpoint, keys: {p256dh, auth}, device}` – Gerät anmelden (nur `https`-Endpunkte) |
|
| POST | `/push/subscribe` | `{endpoint, keys: {p256dh, auth}, device}` – Gerät anmelden (nur `https`-Endpunkte) |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](#)
|
[](#)
|
||||||
[](#-schnellstart)
|
[](#-schnellstart)
|
||||||
[](#-deine-daten-gehören-dir)
|
[](#-deine-daten-gehören-dir)
|
||||||
[](CHANGELOG.md)
|
[](CHANGELOG.md)
|
||||||
|
|
||||||
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
|
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.26.0",
|
"version": "0.26.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.26.0",
|
"version": "0.26.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.26.0",
|
"version": "0.26.1",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1372,23 +1372,28 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
{notice && (
|
{notice && (
|
||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 max-w-[90vw] px-3 py-2 rounded-card text-xs text-main bg-panel-elevated border border-border shadow-panel"
|
className="fixed bottom-[calc(5.5rem+env(safe-area-inset-bottom))] sm:bottom-6 left-1/2 -translate-x-1/2 z-50 w-[calc(100vw-1.5rem)] sm:w-auto sm:max-w-[90vw] flex flex-col sm:block pl-4 pr-1 pt-3 pb-1 sm:px-3 sm:py-2 rounded-card text-sm sm:text-xs text-main bg-panel-elevated border border-border shadow-panel"
|
||||||
>
|
>
|
||||||
<span>{notice}</span>
|
{/* Phones: text on top (two lines at most), buttons below; desktop: one line */}
|
||||||
{noticeActions.map((a) => (
|
<span className="max-sm:pr-3 max-sm:line-clamp-2">{notice}</span>
|
||||||
<button
|
{noticeActions.length > 0 && (
|
||||||
key={a.label}
|
<span className="flex justify-end sm:inline">
|
||||||
type="button"
|
{noticeActions.map((a) => (
|
||||||
onClick={() => {
|
<button
|
||||||
setNotice(null);
|
key={a.label}
|
||||||
setNoticeActions([]);
|
type="button"
|
||||||
a.run();
|
onClick={() => {
|
||||||
}}
|
setNotice(null);
|
||||||
className="ml-3 font-semibold text-accent hover:underline min-h-[44px] sm:min-h-0"
|
setNoticeActions([]);
|
||||||
>
|
a.run();
|
||||||
{a.label}
|
}}
|
||||||
</button>
|
className="h-11 px-3 sm:h-auto sm:px-0 sm:ml-3 font-semibold text-accent hover:underline"
|
||||||
))}
|
>
|
||||||
|
{a.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { mediaContent, parseMediaContent } from '../../lib/media';
|
|||||||
import { renderInlineMarkdown } from '../../lib/markdown';
|
import { renderInlineMarkdown } from '../../lib/markdown';
|
||||||
import { api } from '../../lib/api';
|
import { api } from '../../lib/api';
|
||||||
import { safeHref } from '../../lib/safeUrl';
|
import { safeHref } from '../../lib/safeUrl';
|
||||||
|
import { isNoteLinkOnly } from '../../lib/wikiLink';
|
||||||
|
|
||||||
export interface SlashCommand {
|
export interface SlashCommand {
|
||||||
type: BlockType | 'link' | 'dictation' | 'extract';
|
type: BlockType | 'link' | 'dictation' | 'extract';
|
||||||
@@ -459,7 +460,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
|
|
||||||
// /extract only when note has content and is at start/end
|
// /extract only when note has content and is at start/end
|
||||||
if (c.type === 'extract') {
|
if (c.type === 'extract') {
|
||||||
return !isRestEmpty && isAtStartOrEnd && !!onExtractBlock;
|
return !isRestEmpty && isAtStartOrEnd && !!onExtractBlock && !isNoteLinkOnly(restOfBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
// /h1, /h2, /h3, /todo, /callout, /text (type 'bullet') at start or at end
|
// /h1, /h2, /h3, /todo, /callout, /text (type 'bullet') at start or at end
|
||||||
@@ -1579,7 +1580,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
className={`absolute right-1 sm:right-2 top-0.5 sm:top-1 z-20 bg-panel/90 backdrop-blur-sm border border-border shadow-xs rounded-pill px-1 sm:px-1.5 py-0.5 flex items-center gap-0.5 sm:gap-1 transition-opacity ${
|
className={`absolute right-1 sm:right-2 top-0.5 sm:top-1 z-20 bg-panel/90 backdrop-blur-sm border border-border shadow-xs rounded-pill px-1 sm:px-1.5 py-0.5 flex items-center gap-0.5 sm:gap-1 transition-opacity ${
|
||||||
activeFocus
|
activeFocus
|
||||||
? 'opacity-100 pointer-events-auto'
|
? 'opacity-100 pointer-events-auto'
|
||||||
: 'opacity-0 group-hover:opacity-100 focus-within:opacity-100 pointer-events-none group-hover:pointer-events-auto focus-within:pointer-events-auto'
|
: // Hover only with a real pointer: a tap on a phone would make the invisible buttons clickable
|
||||||
|
'opacity-0 [@media(hover:hover)]:group-hover:opacity-100 focus-within:opacity-100 pointer-events-none [@media(hover:hover)]:group-hover:pointer-events-auto focus-within:pointer-events-auto'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Convert back to bullet for headings, callouts, dividers, code and numbered items */}
|
{/* Convert back to bullet for headings, callouts, dividers, code and numbered items */}
|
||||||
@@ -1702,7 +1704,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Branch into its own note */}
|
{/* Branch into its own note */}
|
||||||
{canEdit && onExtractBlock && localContent.trim() !== '' && !mediaPicker && (
|
{canEdit && onExtractBlock && localContent.trim() !== '' && !isNoteLinkOnly(localContent) && !mediaPicker && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onExtractBlock(block.id)}
|
onClick={() => onExtractBlock(block.id)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { buildBlockLink, parseWikiLink, wikiLinkText } from '../wikiLink';
|
import { buildBlockLink, isNoteLinkOnly, parseWikiLink, wikiLinkText } from '../wikiLink';
|
||||||
|
|
||||||
describe('wiki links', () => {
|
describe('wiki links', () => {
|
||||||
it('parses plain, labelled, section and bullet links', () => {
|
it('parses plain, labelled, section and bullet links', () => {
|
||||||
@@ -18,4 +18,12 @@ describe('wiki links', () => {
|
|||||||
expect(parsed.blockId).toBe('blk_x');
|
expect(parsed.blockId).toBe('blk_x');
|
||||||
expect(wikiLinkText(parsed)).toBe('Homelab neu › Backups liefen bisher nur manuell – das…');
|
expect(wikiLinkText(parsed)).toBe('Homelab neu › Backups liefen bisher nur manuell – das…');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recognises a bullet that is only a link to a note', () => {
|
||||||
|
expect(isNoteLinkOnly('[[Konzept: Garten]]')).toBe(true);
|
||||||
|
expect(isNoteLinkOnly('[[Konzept: Garten]] #draußen #idee')).toBe(true);
|
||||||
|
expect(isNoteLinkOnly('Siehe [[Konzept: Garten]]')).toBe(false);
|
||||||
|
expect(isNoteLinkOnly('[[Konzept: Garten]] und mehr')).toBe(false);
|
||||||
|
expect(isNoteLinkOnly('Garten')).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -31,3 +31,8 @@ export function buildBlockLink(title: string, blockId: string, blockContent: str
|
|||||||
const label = excerpt ? `${clean(title)} › ${excerpt}` : clean(title);
|
const label = excerpt ? `${clean(title)} › ${excerpt}` : clean(title);
|
||||||
return `[[${clean(title)}#^${blockId}|${label}]]`;
|
return `[[${clean(title)}#^${blockId}|${label}]]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A bullet that is only a link to a note (plus hashtags), as left behind when a branch became a note. */
|
||||||
|
export function isNoteLinkOnly(content: string): boolean {
|
||||||
|
return /^\s*\[\[[^\]]+\]\](\s+#[\p{L}\p{N}_-]+)*\s*$/u.test(content);
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ func restructureStatus(err error) int {
|
|||||||
return http.StatusConflict
|
return http.StatusConflict
|
||||||
case errors.Is(err, storage.ErrBlockNotFound), errors.Is(err, storage.ErrDocNotFound):
|
case errors.Is(err, storage.ErrBlockNotFound), errors.Is(err, storage.ErrDocNotFound):
|
||||||
return http.StatusNotFound
|
return http.StatusNotFound
|
||||||
case errors.Is(err, storage.ErrMergeSelf), errors.Is(err, storage.ErrDocInTrash):
|
case errors.Is(err, storage.ErrMergeSelf), errors.Is(err, storage.ErrDocInTrash), errors.Is(err, storage.ErrAlreadyNote):
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
case errors.Is(err, storage.ErrMergeNotOwner):
|
case errors.Is(err, storage.ErrMergeNotOwner):
|
||||||
return http.StatusForbidden
|
return http.StatusForbidden
|
||||||
|
|||||||
@@ -84,6 +84,23 @@ func TestExtractBranchIntoOwnNote(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractRejectsBulletThatIsOnlyANoteLink(t *testing.T) {
|
||||||
|
srv, store, token := setupTestServer(t)
|
||||||
|
router := srv.Router()
|
||||||
|
claims, _ := auth.ValidateToken(testJWTSecret, token)
|
||||||
|
src, _ := store.CreateDocument(claims.UserID, "", "Projekt", "", "", []domain.DocumentBlock{
|
||||||
|
{ID: "blk_l", Type: "bullet", Content: "[[Konzept: Garten]] #draußen", Level: 0, Position: 1},
|
||||||
|
{ID: "blk_m", Type: "bullet", Content: "Siehe [[Konzept: Garten]] für Details", Level: 0, Position: 2},
|
||||||
|
}, nil, false)
|
||||||
|
if rec := sessionCall(router, "POST", "/api/v1/documents/"+src.ID+"/extract", token, `{"blockId":"blk_l"}`, ""); rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("link-only bullet: want 400, got %d %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
// A link inside other text is ordinary content
|
||||||
|
if rec := sessionCall(router, "POST", "/api/v1/documents/"+src.ID+"/extract", token, `{"blockId":"blk_m"}`, ""); rec.Code != http.StatusCreated {
|
||||||
|
t.Fatalf("bullet with text and link: want 201, got %d %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractInSharedNoteKeepsAccess(t *testing.T) {
|
func TestExtractInSharedNoteKeepsAccess(t *testing.T) {
|
||||||
srv, store, _ := setupTestServer(t)
|
srv, store, _ := setupTestServer(t)
|
||||||
router := srv.Router()
|
router := srv.Router()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var (
|
|||||||
ErrBlockNotFound = errors.New("Punkt nicht gefunden")
|
ErrBlockNotFound = errors.New("Punkt nicht gefunden")
|
||||||
ErrBranchLocked = errors.New("Jemand anderes bearbeitet gerade einen Punkt in diesem Ast. Bitte kurz warten.")
|
ErrBranchLocked = errors.New("Jemand anderes bearbeitet gerade einen Punkt in diesem Ast. Bitte kurz warten.")
|
||||||
ErrMergeSelf = errors.New("Eine Notiz kann nicht mit sich selbst zusammengeführt werden")
|
ErrMergeSelf = errors.New("Eine Notiz kann nicht mit sich selbst zusammengeführt werden")
|
||||||
|
ErrAlreadyNote = errors.New("Dieser Punkt verweist schon auf eine eigene Notiz")
|
||||||
ErrMergeNotOwner = errors.New("Zusammenführen geht nur mit eigenen Notizen als Quelle")
|
ErrMergeNotOwner = errors.New("Zusammenführen geht nur mit eigenen Notizen als Quelle")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,6 +38,8 @@ func (s *Store) readCollabDoc(docID string) (*collab.Doc, error) {
|
|||||||
var (
|
var (
|
||||||
titlePrefixRe = regexp.MustCompile(`^(\s*(-|\*|#{1,6}|\[[ xX]?\])\s+)+`)
|
titlePrefixRe = regexp.MustCompile(`^(\s*(-|\*|#{1,6}|\[[ xX]?\])\s+)+`)
|
||||||
titleTagRe = regexp.MustCompile(`(^|\s)#[\p{L}0-9_-]+`)
|
titleTagRe = regexp.MustCompile(`(^|\s)#[\p{L}0-9_-]+`)
|
||||||
|
// A bullet that is only a link to a note (with hashtags), as left behind by ExtractBranch
|
||||||
|
noteLinkOnlyRe = regexp.MustCompile(`^\s*\[\[[^\]]+\]\](\s+#[\p{L}0-9_-]+)*\s*$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// branchTitle makes a note title from a bullet's text (without hashtags) and returns the hashtags,
|
// branchTitle makes a note title from a bullet's text (without hashtags) and returns the hashtags,
|
||||||
@@ -116,6 +119,9 @@ func (s *Store) ExtractBranch(userID, docID, blockID string, a collab.Actor, lea
|
|||||||
return nil, "", ErrBlockNotFound
|
return nil, "", ErrBlockNotFound
|
||||||
}
|
}
|
||||||
root := flat[start]
|
root := flat[start]
|
||||||
|
if noteLinkOnlyRe.MatchString(root.Content) {
|
||||||
|
return nil, "", ErrAlreadyNote
|
||||||
|
}
|
||||||
var children []domain.DocumentBlock
|
var children []domain.DocumentBlock
|
||||||
ids := []string{root.ID}
|
ids := []string{root.ID}
|
||||||
for i := start + 1; i < len(flat) && flat[i].Level > root.Level; i++ {
|
for i := start + 1; i < len(flat) && flat[i].Level > root.Level; i++ {
|
||||||
|
|||||||
Reference in new issue
Block a user