Lots of changes to the cmap stuff
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
# Overdracht racket-wiki
|
||||
|
||||
Bijgewerkt: 22 augustus 2026
|
||||
|
||||
## Project in het kort
|
||||
|
||||
`racket-wiki` is een kleine, zelfgehoste wiki met:
|
||||
|
||||
- een Racket-backend op `web-server-lib`;
|
||||
- PostgreSQL als opslag voor gebruikers, sessies, pagina's, versies, bijlagen en concept maps (CMaps);
|
||||
- een frontend in gewone HTML, CSS en JavaScript, zonder npm- of bundlerstap;
|
||||
- EasyMDE/Marked voor Markdown, DOMPurify voor sanitizing, highlight.js voor code en diff2html voor versieverschillen;
|
||||
- lokale, tijdens setup gedownloade frontendbibliotheken, zodat normaal gebruik geen CDN nodig heeft.
|
||||
|
||||
De ontwikkelversie is **0.2.99** (`info.rkt`). De huidige PostgreSQL-schemaversie is **12** (`private/migrations.rkt`).
|
||||
|
||||
## Belangrijk: huidige werkboom
|
||||
|
||||
De branch is `main` en volgt `origin/main`. De laatste commit is:
|
||||
|
||||
```text
|
||||
63b7ca0 cmap functions, email, architecture documentation.
|
||||
```
|
||||
|
||||
Er staan belangrijke, nog niet gecommitte wijzigingen in de werkboom. Die vormen samen de ontwikkeling van 0.2.95 t/m 0.2.99 en moeten worden behouden. Bij de laatste inventarisatie waren gewijzigd:
|
||||
|
||||
```text
|
||||
README.md
|
||||
architecture/pages/data-en-versies.md
|
||||
architecture/pages/gedrag.md
|
||||
architecture/pages/testbaarheid.md
|
||||
info.rkt
|
||||
private/cmap-storage.rkt
|
||||
private/migrations.rkt
|
||||
server.rkt
|
||||
static/cmap/cmap-racket-wiki.js
|
||||
static/cmap/cmap.css
|
||||
static/cmap/cmap.js
|
||||
static/css/wiki.css
|
||||
static/index.html
|
||||
static/js/wiki.js
|
||||
translate.rkt
|
||||
```
|
||||
|
||||
Nieuw en nog untracked:
|
||||
|
||||
```text
|
||||
migrate-cmap-subpages.rkt
|
||||
```
|
||||
|
||||
Voer dus geen reset, checkout of brede formattering uit voordat deze wijzigingen zijn beoordeeld en veilig gecommitte. `wiki-data/` en `compiled/` zijn lokale/gegenereerde directories en staan in `.gitignore`.
|
||||
|
||||
## Snel starten
|
||||
|
||||
Benodigd:
|
||||
|
||||
- Racket met de dependencies uit `info.rkt` (`crypto-lib`, `db-lib`, `net-lib`, `net-cookies-lib`, `openssl-lib` en `web-server-lib`);
|
||||
- PostgreSQL; de database moet vooraf bestaan en de opgegeven rol moet tabellen en indexen mogen maken;
|
||||
- internettoegang tijdens de eerste setup om de vastgepinde browserbibliotheken te downloaden.
|
||||
|
||||
In deze ontwikkelomgeving is Racket 9.1 gebruikt. Start vanuit de projectroot:
|
||||
|
||||
```sh
|
||||
racket main.rkt --data ./wiki-data --port 8080
|
||||
```
|
||||
|
||||
Open daarna `http://127.0.0.1:8080/`. Een incomplete installatie gaat automatisch naar `/setup`. Daar worden de databaseverbinding, migraties, frontendbestanden en eerste administrator geregeld. Luisteren op alle interfaces kan met:
|
||||
|
||||
```sh
|
||||
racket main.rkt --data ./wiki-data --listen '*' --port 8080
|
||||
```
|
||||
|
||||
Gebruik achter HTTPS altijd `--secure-cookie`. Andere relevante opties zijn `--title`, `--language en|nl` en de hersteloptie `--create-admin USER PASSWORD`.
|
||||
|
||||
De database-instellingen, inclusief eventueel het wachtwoord, staan in `wiki-data/database.rktd`. Dit bestand hoort niet in Git of in logs. De applicatie probeert het bestand met mode `0600` aan te maken.
|
||||
|
||||
Als alleen de browserdependencies hersteld moeten worden:
|
||||
|
||||
```sh
|
||||
racket setup-vendor.rkt --data ./wiki-data
|
||||
```
|
||||
|
||||
## Codekaart
|
||||
|
||||
| Pad | Verantwoordelijkheid |
|
||||
| --- | --- |
|
||||
| `main.rkt` | CLI en programmatische entrypoints `start` en `start-wiki` |
|
||||
| `server.rkt` | HTTP-routering, setup/loginpagina's en alle JSON/API-handlers |
|
||||
| `private/config.rkt` | runtimeconfiguratie en paden onder de datadirectory |
|
||||
| `private/setup.rkt` | eerste websetup en herstelcontrole |
|
||||
| `private/database.rkt` | PostgreSQL-configuratie, verbindingen en migratiestart |
|
||||
| `private/migrations.rkt` | geordende, transactionele databaseschemamigraties |
|
||||
| `private/storage.rkt` | pagina's, historie, zoeken, todo's, bookmarks, aliassen en uploads |
|
||||
| `private/cmap-storage.rkt` | opslag, historie, zoeken en gebruikstellingen voor CMaps |
|
||||
| `private/auth.rkt` | wachtwoorden, sessies, CSRF, rollen en password-reset-tokens |
|
||||
| `private/mail.rkt` | SMTP-instellingen en resetmails |
|
||||
| `translate.rkt` | Engelse/Nederlandse server- en frontendvertalingen |
|
||||
| `static/index.html` | skelet van de ingelogde single-page-interface |
|
||||
| `static/js/wiki.js` | applicatiestatus, API-calls, pagina-editor en CMap-hostintegratie |
|
||||
| `static/cmap/` | lokaal onderhouden CMap-renderer, editorlaag en styling |
|
||||
| `architecture/` | importeerbare Nederlandse architectuurpagina's en twee CMaps |
|
||||
| `scrbl/racket-wiki.scrbl` | package/API-documentatie in Scribble |
|
||||
|
||||
De browserfrontend heeft bewust geen buildstap. Wijzig bronbestanden rechtstreeks en test ze in de browser. Externe browserlibraries staan runtime onder `wiki-data/static/vendor/`; wijzig die niet als applicatiebron.
|
||||
|
||||
## Belangrijke invarianten
|
||||
|
||||
- Er is geen anonieme wiki-toegang. `reader` leest, `editor` schrijft en `admin` beheert gebruikers en instellingen.
|
||||
- Schrijvende API-calls vereisen naast een sessie ook de CSRF-token.
|
||||
- Pagina's hebben een aparte `namespace` en stabiele `slug`; de combinatie is uniek. Een titelwijziging verandert de slug niet.
|
||||
- Elke paginasave wijzigt `pages` en schrijft in dezelfde transactie een onveranderlijke `page_versions`-rij.
|
||||
- Pagina-updates en CMap-updates gebruiken optimistic locking en horen bij een verouderde basisversie HTTP 409 te geven.
|
||||
- Pagina's en CMaps worden gearchiveerd (soft delete); histories blijven behouden.
|
||||
- Bijlagen staan als `BYTEA` in PostgreSQL. Alleen vertrouwde rasterformaten worden inline geserveerd; andere bestanden worden downloads.
|
||||
- Markdown gaat vóór invoegen in de DOM door DOMPurify.
|
||||
- Verhoog `current-schema-version` alleen samen met een nieuwe, opeenvolgende migratie. Een database met een nieuwer schema dan de code moet geweigerd blijven.
|
||||
|
||||
## Lopend werk: 0.2.95–0.2.99
|
||||
|
||||
De niet-gecommitte reeks bouwt vooral de persistente CMap-functionaliteit uit:
|
||||
|
||||
- stabiele, wiki-brede `conceptId`-identiteiten en gekoppelde plaatsingen met een eigen layout;
|
||||
- conceptaspecten, beschrijvingspagina's, stijlpresets, afzonderlijke titel-/synopsisopmaak en een bewerkbaar kleurenpalet;
|
||||
- directe relaties met Alt, gelinkte copy/paste en robuust herstel bij relaties met ontbrekende eindpunten;
|
||||
- afgeleide, opgeslagen sub-CMapweergaven die naar de canonieke parentgraph verwijzen;
|
||||
- keuze van een start-CMap en per-view verborgen concepten;
|
||||
- autosave zonder historievervuiling, maximaal vijf handmatige saves en onbeperkte snapshots;
|
||||
- verwijderen van afzonderlijke handmatige CMap-history-items/snapshots;
|
||||
- een paneel op wikipagina's met alle gekoppelde CMap-concepten en plaatsingstellingen.
|
||||
|
||||
Migratie 12 schoont oude CMap-autosavehistorie op en beperkt herkenbare handmatige saves tot vijf. `migrate-cmap-subpages.rkt` is een aparte contentmigratie voor oudere inline sub-CMaps; dit is geen databaseschemamigratie.
|
||||
|
||||
Gebruik die contentmigratie altijd eerst als dry-run:
|
||||
|
||||
```sh
|
||||
racket migrate-cmap-subpages.rkt --data ./wiki-data
|
||||
racket migrate-cmap-subpages.rkt --data ./wiki-data --apply
|
||||
```
|
||||
|
||||
Zonder `--apply` wordt niets gewijzigd. Met `--apply` gebeurt de omzetting in één PostgreSQL-transactie. Maak desondanks eerst een databasebackup en beoordeel de gemelde slugs.
|
||||
|
||||
## Testen en huidige verificatiestatus
|
||||
|
||||
De automatische testdekking is beperkt. Op 22 augustus 2026 zijn de volgende controles succesvol uitgevoerd:
|
||||
|
||||
```sh
|
||||
raco test architecture/import.rkt
|
||||
# 12 tests passed
|
||||
|
||||
raco make main.rkt server.rkt setup-vendor.rkt \
|
||||
migrate-cmap-subpages.rkt architecture/import.rkt
|
||||
|
||||
node --check static/js/wiki.js
|
||||
node --check static/js/combobox.js
|
||||
node --check static/cmap/cmap.js
|
||||
node --check static/cmap/cmap-racket-wiki.js
|
||||
```
|
||||
|
||||
Er is nog geen geautomatiseerde backendintegratietest tegen PostgreSQL en geen browsertestsuite. De huidige 0.2.95–0.2.99-werkboom is dus wel compileerbaar en syntactisch geldig, maar nog niet in deze overdracht end-to-end gevalideerd.
|
||||
|
||||
Voer vóór commit/release minimaal deze smoke tests uit op een kopie of aparte testdatabase:
|
||||
|
||||
1. Verse `/setup`, login/logout en de drie rollen.
|
||||
2. Pagina maken, wijzigen, hernoemen, history bekijken, conflict (409), namespace-link en upload.
|
||||
3. CMap maken, autosave, expliciet opslaan, snapshot maken/verwijderen en historylimiet controleren.
|
||||
4. Een concept meerdere keren en in meerdere CMaps plaatsen; controleer tellingen en het paneel op de gekoppelde wikipagina.
|
||||
5. Een inline sub-CMap met de dry-run en daarna `--apply` migreren; controleer parent, derived view en oude histories.
|
||||
6. Start-CMap, verborgen concepten, linked copy/paste, undo/redo en beschadigde relatie zonder endpoint controleren.
|
||||
7. Zoekresultaten, bookmarks, todo's, aliassen en adminoverzichten nalopen.
|
||||
8. Indien mail relevant is: SMTP-test en de volledige vergeten-wachtwoordflow.
|
||||
|
||||
## Architectuurdocumentatie importeren
|
||||
|
||||
Valideren en wijzigingen vooraf bekijken:
|
||||
|
||||
```sh
|
||||
racket architecture/import.rkt --data ./wiki-data --dry-run
|
||||
```
|
||||
|
||||
Importeren:
|
||||
|
||||
```sh
|
||||
racket architecture/import.rkt --data ./wiki-data --author "Naam ontwikkelaar"
|
||||
```
|
||||
|
||||
De import is herhaalbaar en gebruikt source hashes. Lokaal aangepaste geïmporteerde pagina's/CMaps worden standaard overgeslagen. Gebruik `--overwrite-modified` alleen na controle van hun histories.
|
||||
|
||||
## Mailconfiguratie
|
||||
|
||||
Password-resetmail kan via de admininterface/database of via omgevingsvariabelen worden ingesteld. Ondersteund zijn:
|
||||
|
||||
```text
|
||||
RACKET_WIKI_PUBLIC_URL
|
||||
RACKET_WIKI_SMTP_HOST
|
||||
RACKET_WIKI_SMTP_PORT
|
||||
RACKET_WIKI_SMTP_FROM
|
||||
RACKET_WIKI_SMTP_USER
|
||||
RACKET_WIKI_SMTP_PASSWORD
|
||||
RACKET_WIKI_SMTP_TLS
|
||||
RACKET_WIKI_SMTP_ACCEPT_UNTRUSTED_CERTIFICATES
|
||||
RACKET_WIKI_RESET_LIMIT
|
||||
```
|
||||
|
||||
Accepteer onbetrouwbare certificaten alleen bewust voor een vertrouwde lokale SMTP-server. Zet secrets nooit in documentatie, commits of testoutput.
|
||||
|
||||
## Aanbevolen eerstvolgende stap
|
||||
|
||||
Maak eerst een databasebackup en test de volledige niet-gecommitte 0.2.95–0.2.99-reeks met bovenstaande smoke tests. Beoordeel daarna de diff als één samenhangende CMap-wijziging, werk zo nodig `README.md`, Scribble-documentatie en architectuurpagina's gelijk bij, en commit pas wanneer migratie 12 en `migrate-cmap-subpages.rkt` op representatieve data zijn geverifieerd.
|
||||
|
||||
Voor functionele details en releasehistorie is `README.md` de uitgebreidste bron. De bestanden onder `architecture/pages/` beschrijven ontwerpbeslissingen en kwaliteitsafspraken; `architecture/pages/code-regels.md` is de beste start voor projectconventies.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Version 0.2.31 adds page namespaces as database metadata and extends wiki references to forms such as `RWS:ModelTreeWalker` and `[roadmap](racket:roadmap)`. Todo items and bookmarks are grouped by namespace. The source has also been documented more thoroughly, especially `static/js/wiki.js`.
|
||||
|
||||
Current development version: **0.2.94**.
|
||||
Current development version: **0.2.99**.
|
||||
|
||||
A small self-hosted wiki with a Racket backend and an HTML5/CSS/JavaScript frontend.
|
||||
|
||||
@@ -834,3 +834,110 @@ instead of passing the invalid alphabetic escapes `\\r` and `\\n` to the
|
||||
regexp parser. The CMap-embed expression uses an actual newline character in
|
||||
its exclusion class for the same reason. Regression tests cover LF and CRLF
|
||||
source markers and single-line CMap embeds.
|
||||
|
||||
### 0.2.95
|
||||
|
||||
CMap concepts can link to missing wiki pages by entering a new title or address; opening
|
||||
that link uses the normal missing-page creation flow. Every concept can also have aspects
|
||||
and a Markdown description page, defaulting to the `cmap` namespace. Separate icons open
|
||||
the description and linked target, while double-clicking opens the concept editor.
|
||||
|
||||
The CMap document now separates stable concept identity from diagram-specific placement.
|
||||
Position, size, colours and typography are retained per root/child-map context. Linked
|
||||
copies made with Ctrl/Cmd+C and Ctrl/Cmd+V share their label, synopsis, aspects and link
|
||||
targets while keeping independent presentation. Existing version-1 documents are upgraded
|
||||
in memory when loaded.
|
||||
|
||||
Holding Alt while drawing a relation creates a direct arrow without a linking phrase. The
|
||||
tools menu is moved outside the drawing stacking context so it remains above large maps.
|
||||
The active stored CMap now shows its title and `cmap:slug`; clicking it copies ready-to-use
|
||||
Markdown link syntax.
|
||||
|
||||
Concept descriptions now appear as rendered Markdown previews when the serif `I` is hovered
|
||||
or focused. Its light-grey/white state shows whether a description exists. The compact concept
|
||||
dialog adds local colour palettes, reusable style presets, normal/bold and italic controls, and
|
||||
wrapped synopsis text.
|
||||
|
||||
Switching between stored CMaps now destroys the previous drawing surface before another one is
|
||||
created. Deferred redraws from the old map are ignored, preventing duplicate layers and relations
|
||||
whose visual endpoints no longer match their concepts. CMap content also scrolls underneath the
|
||||
sticky toolbar instead of over it.
|
||||
|
||||
The sticky CMap header now paints through the main view's former top padding, so scrolled concepts
|
||||
are clipped underneath the complete header area and cannot remain visible above its controls.
|
||||
|
||||
Deleting a concept or relation now limits orphan cleanup to linking phrases directly affected by
|
||||
that deletion. Unrelated incomplete relations already present in a document are left untouched,
|
||||
so deleting one relation cannot unexpectedly remove other parts of the map.
|
||||
|
||||
A stored relation whose source or target concept is missing no longer prevents the complete CMap
|
||||
from opening. Such a damaged relation is omitted from the drawing but retained verbatim in the
|
||||
document, including across automatic saves, until its remaining endpoint is explicitly deleted.
|
||||
|
||||
Every ordinary concept placement shows a compact `(n)` usage indicator. It counts all placements
|
||||
sharing that concept identity across every active stored CMap and updates immediately for unsaved
|
||||
changes in the open map. New concepts receive wiki-wide UUID-based identities. Legacy document-local
|
||||
IDs are qualified with their CMap slug so coincidentally equal old IDs are never conflated.
|
||||
|
||||
### 0.2.96
|
||||
|
||||
**Make separate CMap** now creates a stored derived view of a sub-CMap instead of copying or moving
|
||||
its graph. The parent remains the canonical owner of every concept and relation. Its sub-CMap can
|
||||
still expand and collapse in place, while a separate arrow opens the same elements with their own
|
||||
page-specific layout at a normal `cmap:slug` address.
|
||||
|
||||
Elements inside an expanded nested view can be hidden only in the parent context. A toolbar dropdown
|
||||
lists those hidden concepts and restores them individually. The standalone view continues to contain
|
||||
all elements. Relations crossing the submap boundary remain in the canonical graph; on the standalone
|
||||
page they appear from the viewport edge with the external concept name, which navigates back to the
|
||||
source CMap when clicked.
|
||||
|
||||
An expanded sub-CMap has an independently movable group frame. Its concept remains at its anchor
|
||||
position and a quiet grey line connects that anchor to the group. Collapsing removes only the frame
|
||||
and line; expanding restores the group at its last position for that CMap context.
|
||||
|
||||
The tools menu can mark the current map as the browser's start CMap. The sidebar CMaps icon and the
|
||||
plain `#cmaps` route open that preference, falling back to the first available map if it no longer
|
||||
exists.
|
||||
|
||||
For a complete installation, `racket migrate-cmap-subpages.rkt` first reports every legacy internal
|
||||
page and the target slug without changing data. Run `racket migrate-cmap-subpages.rkt --apply` to
|
||||
perform the conversion in one PostgreSQL transaction. The parent retains its complete document and
|
||||
receives only the child-view slugs; each stored child contains a `derivedView` reference to the parent
|
||||
slug and submap root id. Every preceding parent and child version remains available. A target slug is
|
||||
only reused when it belongs to the earlier subpage migration; an unrelated existing map aborts the
|
||||
transaction.
|
||||
|
||||
### 0.2.97
|
||||
|
||||
CMap autosave now updates only the current document and optimistic version number. It no longer
|
||||
creates a history row. Explicit saves from Ctrl/Cmd-S or **Save now** retain at most the five newest
|
||||
versions per CMap, while explicitly named snapshots remain unlimited. The history dialog displays
|
||||
only those manual versions and snapshots and allows an editor to delete either kind after a
|
||||
confirmation; deleting history never changes the current CMap.
|
||||
|
||||
Database migration 12 removes legacy automatic-save history rows, reclassifies recognizable legacy
|
||||
manual saves, and transactionally trims those manual saves to five per CMap. Migration, repair and
|
||||
other internal safety versions remain stored outside the everyday history list.
|
||||
|
||||
### 0.2.98
|
||||
|
||||
Concept headings and synopsis text now have independent text colour, font family, point size, bold
|
||||
and italic controls. These presentation fields belong to the placement and its CMap context; linked
|
||||
concept identity remains shared while each view can keep its own typography. Legacy concepts inherit
|
||||
their existing shared typography for both text parts until an editor changes either section.
|
||||
|
||||
The compact colour palette is now editable. A single click applies a palette colour to the current
|
||||
form, while a double-click on a palette tile opens the browser's native HTML colour picker and
|
||||
replaces that reusable tile. Double-clicking the current-colour swatch opens the same picker for a
|
||||
one-off colour. The palette is stored locally in the browser, whereas every concept placement stores
|
||||
its selected hexadecimal colour directly; changing a palette tile therefore never recolours existing
|
||||
concepts.
|
||||
|
||||
### 0.2.99
|
||||
|
||||
A wiki page now shows a **Connected CMap concepts** panel when one or more current concepts link to
|
||||
that page. Concepts are grouped by stable `conceptId`; each row shows the concept label, total
|
||||
placement count and clickable chips for every CMap containing it. Repeated placements in one CMap
|
||||
show their local count. The panel follows page aliases and uses only current, non-archived CMap
|
||||
documents—not historical versions.
|
||||
|
||||
@@ -16,7 +16,7 @@ De database bevat zowel de huidige toestand als de auditgeschiedenis. De browser
|
||||
| `attachments` | Metadata en volledige binaire inhoud van uploads |
|
||||
| `attachment_references` | Huidige en historische verwijzingen vanuit paginaversies |
|
||||
| `concept_maps` | Actuele titel, JSONB-document en versieteller per CMap |
|
||||
| `concept_map_versions` | Volledige onveranderlijke CMap-snapshots |
|
||||
| `concept_map_versions` | Expliciete CMap-snapshots en maximaal vijf handmatige opslagversies per CMap |
|
||||
| `password_reset_tokens` | Gehashte, tijdelijke en eenmalige herstelcodes |
|
||||
| `wiki_settings` | Beheerinstellingen, momenteel vooral e-mail |
|
||||
| `wiki_schema` | Geïnstalleerde migratieversie |
|
||||
@@ -39,13 +39,13 @@ Een paginaopslag is één atomaire transactie:
|
||||
6. Vervang huidige bijlageverwijzingen en leg historische verwijzingen voor de nieuwe versie vast.
|
||||
7. Commit alles, of niets.
|
||||
|
||||
Een CMap-opslag volgt hetzelfde kernpatroon: rij vergrendelen, versienummer vergelijken, actuele JSONB bijwerken en dezelfde volledige toestand aan `concept_map_versions` toevoegen.
|
||||
Een CMap-opslag volgt voor de actuele projectie hetzelfde kernpatroon: rij vergrendelen, versienummer vergelijken en actuele JSONB bijwerken. Autosave verhoogt wel het optimistische versienummer, maar maakt geen historieregel. Een expliciete opslag voegt een volledige toestand aan `concept_map_versions` toe en verwijdert oudere handmatige versies boven de grens van vijf. Benoemde snapshots vallen niet onder die grens.
|
||||
|
||||
## Waarom volledige snapshots
|
||||
|
||||
Volledige snapshots zijn eenvoudig te begrijpen, herstellen en vergelijken. Er is geen keten van patches nodig om versie 37 te reconstrueren en een defecte diff kan de historie niet onleesbaar maken. Voor de verwachte wikiomvang is deze eenvoud belangrijker dan maximale opslagcompactheid.
|
||||
|
||||
De prijs is lineaire databasegroei met het aantal versies maal de documentgrootte. Vooral autosave van grote CMaps kan daardoor op termijn veel JSONB opslaan. Bewaarbeleid of deduplicatie hoort pas te worden ontworpen nadat echte datagroei is gemeten; zie [Verwachte performance](racket-wiki:performance).
|
||||
De prijs is lineaire databasegroei met het aantal versies maal de documentgrootte. Voor pagina's en benoemde CMap-snapshots is dat bewust; CMap-autosaves zijn daarom uitgesloten van historie en handmatige CMap-opslag is begrensd op vijf versies. Zie [Verwachte performance](racket-wiki:performance).
|
||||
|
||||
## Afgeleide gegevens
|
||||
|
||||
@@ -75,7 +75,8 @@ Een nieuw schema vereist:
|
||||
|
||||
De volgende regels mogen niet alleen in de browser staan:
|
||||
|
||||
- iedere actuele versie heeft een overeenkomstige onveranderlijke snapshot;
|
||||
- iedere actuele paginaversie heeft een overeenkomstige onveranderlijke snapshot;
|
||||
- CMap-autosave wijzigt alleen de actuele projectie; CMap-historie bevat alleen snapshots en maximaal vijf handmatige versies;
|
||||
- versienummers nemen per object strikt toe;
|
||||
- een stale editor overschrijft geen nieuwere toestand;
|
||||
- e-mailadressen zijn hoofdletterongevoelig uniek wanneer ingevuld;
|
||||
|
||||
@@ -40,9 +40,11 @@ Hernoemen of verplaatsen behoudt dezelfde pagina-id en maakt een alias voor het
|
||||
|
||||
## Conceptmap bewerken
|
||||
|
||||
De CMap-editor houdt items, verbindingszinnen, connectors, groepen en sub-CMaps in één document met `schemaVersion: 1`. Bewerkingen gaan eerst door een lokale Undo/Redo-historie. Na een afgeronde handeling plant de browser een autosave; een nieuwe handeling verschuift die timer. Een lopende save kan een volgende save noodzakelijk maken, zodat tussentijdse wijzigingen niet verdwijnen.
|
||||
De CMap-editor houdt conceptidentiteit, plaatsingen, verbindingszinnen, connectors, groepen en sub-CMaps in één document met `schemaVersion: 2`. Positie en vormgeving horen bij de plaatsing en CMap-context. Kop en subtekst hebben daarbinnen elk hun eigen typografie. Het bewerkbare kleurenpalet is alleen browserinstelling; een toegepaste kleur wordt als concrete hexwaarde bij de plaatsing opgeslagen en verandert dus niet wanneer het palette later wijzigt. Bewerkingen gaan eerst door een lokale Undo/Redo-historie. Na een afgeronde handeling plant de browser een autosave; een nieuwe handeling verschuift die timer. Een lopende save kan een volgende save noodzakelijk maken, zodat tussentijdse wijzigingen niet verdwijnen.
|
||||
|
||||
Iedere serveropslag controleert opnieuw het basisversienummer en schrijft een volledige CMap-snapshot. Autosave beschermt de werkstand. **Snapshot maken** schrijft ook zonder inhoudsverschil een herkenbaar historisch moment met een omschrijving. Een historische versie laden verandert alleen de editor; pas een volgende opslag maakt daarvan een nieuwe actuele versie.
|
||||
Iedere serveropslag controleert opnieuw het basisversienummer en schrijft de actuele CMap-toestand. Autosave beschermt de werkstand zonder een historieregel te maken. Ctrl/Cmd-S en **Nu opslaan** maken een handmatige historieversie, waarvan per CMap de vijf nieuwste blijven staan. **Snapshot maken** schrijft ook zonder inhoudsverschil een herkenbaar, onbeperkt bewaard moment met een omschrijving. Een historische versie laden verandert alleen de editor; pas een volgende opslag maakt daarvan een nieuwe actuele versie. Een editor kan een afzonderlijk geschiedenisitem verwijderen zonder de actuele CMap te veranderen.
|
||||
|
||||
Wanneer een actueel CMap-concept een `pageSlug` heeft, toont de gelezen wikipagina onder de inhoud een verwijzing naar dat concept. Plaatsingen worden op `conceptId` gegroepeerd en iedere betrokken CMap is rechtstreeks aanklikbaar. Alleen actuele, niet-gearchiveerde CMap-documenten tellen mee; historische CMap-versies leveren geen navigatielinks op.
|
||||
|
||||
## Zoeken, Recent en navigatiegraaf
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ De eerste volwaardige suite moet deze risico's afdekken:
|
||||
6. sessie-, CSRF- en herstelcodes worden correct gevalideerd en ingetrokken;
|
||||
7. CMap-documenten maken een stabiele load/save-roundtrip;
|
||||
8. slepen op een sub-CMapframe verplaatst alle afstammelingen, slepen op het hoofdconcept niet;
|
||||
9. autosave, handmatige save en snapshot krijgen het juiste historische `action` en `summary`;
|
||||
9. autosave maakt geen historie, handmatige opslag houdt er maximaal vijf en snapshots blijven onbeperkt;
|
||||
10. migratie vanaf iedere ondersteunde schemaversie levert dezelfde actuele structuur.
|
||||
|
||||
## Tijdelijke PostgreSQL-database
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#lang info
|
||||
|
||||
(define pkg-authors '(hnmdijkema))
|
||||
(define version "0.2.94")
|
||||
(define version "0.2.99")
|
||||
(define license 'MIT)
|
||||
(define collection "racket-wiki")
|
||||
(define pkg-desc
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
#lang racket/base
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Migrate document-internal CMap pages to shared stored views.
|
||||
;;
|
||||
;; The parent document remains the canonical graph. A stored child CMap only
|
||||
;; records which submap root it displays, so concepts, relations and layout
|
||||
;; contexts are never copied or removed from the parent.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(require db
|
||||
json
|
||||
racket/cmdline
|
||||
racket/list
|
||||
racket/path
|
||||
racket/string
|
||||
"private/config.rkt"
|
||||
"private/database.rkt"
|
||||
"private/storage.rkt")
|
||||
|
||||
(define migration-author "system:cmap-shared-submap-migration")
|
||||
(define replaceable-authors
|
||||
'("system:cmap-subpage-migration" "system:cmap-subpage-migration-repair"))
|
||||
|
||||
(define (json-list document key)
|
||||
(define value (hash-ref document key '()))
|
||||
(if (list? value) value '()))
|
||||
|
||||
(define (parse-document text)
|
||||
(define first (string->jsexpr text))
|
||||
(define document (if (string? first) (string->jsexpr first) first))
|
||||
(unless (hash? document)
|
||||
(error 'parse-document "stored CMap document is not an object"))
|
||||
document)
|
||||
|
||||
(define (legacy-submap? item)
|
||||
(and (equal? (hash-ref item 'kind "") "submap")
|
||||
(eq? (hash-ref item 'separateMap #f) #t)
|
||||
(let ((slug (hash-ref item 'cmapSlug 'null)))
|
||||
(or (eq? slug 'null)
|
||||
(and (string? slug) (string=? (string-trim slug) ""))))))
|
||||
|
||||
(define (submap-title item)
|
||||
(define child-map (hash-ref item 'childMap ""))
|
||||
(define label (hash-ref item 'label ""))
|
||||
(cond ((and (string? child-map) (not (string=? (string-trim child-map) "")))
|
||||
(string-trim child-map))
|
||||
((and (string? label) (not (string=? (string-trim label) "")))
|
||||
(string-trim label))
|
||||
(else "Sub-CMap")))
|
||||
|
||||
(define (derived-document source-slug root-id)
|
||||
(hash 'schemaVersion 2
|
||||
'derivedView (hash 'sourceCmapSlug source-slug 'rootItemId root-id)
|
||||
'concepts '()
|
||||
'items '()
|
||||
'connectors '()
|
||||
'conceptMaps '()))
|
||||
|
||||
(define (link-parent-document document roots)
|
||||
(define slugs
|
||||
(for/hash ((root (in-list roots)))
|
||||
(values (hash-ref root 'id) (title->slug (submap-title root)))))
|
||||
(hash-set
|
||||
document
|
||||
'items
|
||||
(for/list ((item (in-list (json-list document 'items))))
|
||||
(define slug (hash-ref slugs (hash-ref item 'id #f) #f))
|
||||
(if slug
|
||||
(hash-set (hash-set item 'cmapSlug slug) 'separateMap #t)
|
||||
item))))
|
||||
|
||||
(define (load-parent-rows connection [lock? #f])
|
||||
(query-rows
|
||||
connection
|
||||
(string-append
|
||||
"SELECT id, slug, title, document::text, current_version "
|
||||
"FROM concept_maps WHERE archived=FALSE "
|
||||
"AND EXISTS (SELECT 1 FROM jsonb_array_elements("
|
||||
"CASE WHEN jsonb_typeof(document->'items')='array' "
|
||||
"THEN document->'items' ELSE '[]'::jsonb END) item "
|
||||
"WHERE coalesce(item->>'kind','')='submap' "
|
||||
"AND coalesce((item->>'separateMap')::boolean,FALSE)=TRUE "
|
||||
"AND coalesce(item->>'cmapSlug','')='') "
|
||||
"ORDER BY id"
|
||||
(if lock? " FOR UPDATE" ""))))
|
||||
|
||||
(define (existing-child connection slug [lock? #f])
|
||||
(query-maybe-row
|
||||
connection
|
||||
(string-append
|
||||
"SELECT id, title, current_version, created_by FROM concept_maps "
|
||||
"WHERE slug=$1 AND archived=FALSE"
|
||||
(if lock? " FOR UPDATE" ""))
|
||||
slug))
|
||||
|
||||
(define (replaceable-child? row)
|
||||
(and row (member (vector-ref row 3) replaceable-authors)))
|
||||
|
||||
(define (check-child! connection parent-slug root [lock? #f])
|
||||
(define slug (title->slug (submap-title root)))
|
||||
(define row (existing-child connection slug lock?))
|
||||
(when (and row (not (replaceable-child? row)))
|
||||
(error 'migrate-cmap-subpages
|
||||
"target CMap ~a already exists and was not created by the earlier migration"
|
||||
slug))
|
||||
(hash 'slug slug
|
||||
'title (submap-title root)
|
||||
'rootId (hash-ref root 'id)
|
||||
'sourceSlug parent-slug
|
||||
'existing row))
|
||||
|
||||
(define (plans-for-row connection row [lock? #f])
|
||||
(define document (parse-document (vector-ref row 3)))
|
||||
(define roots (filter legacy-submap? (json-list document 'items)))
|
||||
(define children
|
||||
(for/list ((root (in-list roots)))
|
||||
(check-child! connection (vector-ref row 1) root lock?)))
|
||||
(values children (link-parent-document document roots)))
|
||||
|
||||
(define (report-plan connection)
|
||||
(define total 0)
|
||||
(for ((row (in-list (load-parent-rows connection))))
|
||||
(define-values (children ignored-parent) (plans-for-row connection row))
|
||||
(printf "Parent ~a (~a): ~a shared submap view(s)\n"
|
||||
(vector-ref row 1) (vector-ref row 2) (length children))
|
||||
(for ((child (in-list children)))
|
||||
(set! total (add1 total))
|
||||
(printf " ~a -> ~a (~a)\n"
|
||||
(hash-ref child 'title)
|
||||
(hash-ref child 'slug)
|
||||
(if (hash-ref child 'existing) "replace earlier migration result" "create"))))
|
||||
(printf "Total: ~a shared view(s). No data changed.\n" total)
|
||||
total)
|
||||
|
||||
(define (write-child! connection child now)
|
||||
(define document-text
|
||||
(jsexpr->string
|
||||
(derived-document (hash-ref child 'sourceSlug) (hash-ref child 'rootId))))
|
||||
(define existing (hash-ref child 'existing))
|
||||
(if existing
|
||||
(let* ((map-id (vector-ref existing 0))
|
||||
(title (vector-ref existing 1))
|
||||
(next-version (add1 (vector-ref existing 2))))
|
||||
(query-exec
|
||||
connection
|
||||
#<<SQL
|
||||
UPDATE concept_maps
|
||||
SET document=$1::text::jsonb, current_version=$2, updated_at=$3, updated_by=$4
|
||||
WHERE id=$5
|
||||
SQL
|
||||
document-text next-version now migration-author map-id)
|
||||
(query-exec
|
||||
connection
|
||||
#<<SQL
|
||||
INSERT INTO concept_map_versions
|
||||
(concept_map_id, version, title, document, author, action, summary, created_at)
|
||||
VALUES ($1,$2,$3,$4::text::jsonb,$5,'shared-view-migration',
|
||||
'Converted copied migration result into a canonical shared sub-CMap view',$6)
|
||||
SQL
|
||||
map-id next-version title document-text migration-author now))
|
||||
(let ((map-id
|
||||
(query-value
|
||||
connection
|
||||
#<<SQL
|
||||
INSERT INTO concept_maps
|
||||
(slug,title,document,current_version,created_at,updated_at,created_by,updated_by)
|
||||
VALUES ($1,$2,$3::text::jsonb,1,$4,$4,$5,$5)
|
||||
RETURNING id
|
||||
SQL
|
||||
(hash-ref child 'slug) (hash-ref child 'title)
|
||||
document-text now migration-author)))
|
||||
(query-exec
|
||||
connection
|
||||
#<<SQL
|
||||
INSERT INTO concept_map_versions
|
||||
(concept_map_id,version,title,document,author,action,summary,created_at)
|
||||
VALUES ($1,1,$2,$3::text::jsonb,$4,'shared-view-migration',
|
||||
'Created canonical shared sub-CMap view',$5)
|
||||
SQL
|
||||
map-id (hash-ref child 'title) document-text migration-author now))))
|
||||
|
||||
(define (apply-migration! connection)
|
||||
(call-with-transaction
|
||||
connection
|
||||
(lambda ()
|
||||
(define now (current-seconds))
|
||||
(define total 0)
|
||||
(for ((row (in-list (load-parent-rows connection #t))))
|
||||
(define-values (children parent-document) (plans-for-row connection row #t))
|
||||
(for ((child (in-list children)))
|
||||
(write-child! connection child now)
|
||||
(set! total (add1 total)))
|
||||
(unless (null? children)
|
||||
(define map-id (vector-ref row 0))
|
||||
(define next-version (add1 (vector-ref row 4)))
|
||||
(define parent-text (jsexpr->string parent-document))
|
||||
(query-exec
|
||||
connection
|
||||
#<<SQL
|
||||
UPDATE concept_maps
|
||||
SET document=$1::text::jsonb, current_version=$2, updated_at=$3, updated_by=$4
|
||||
WHERE id=$5
|
||||
SQL
|
||||
parent-text next-version now migration-author map-id)
|
||||
(query-exec
|
||||
connection
|
||||
#<<SQL
|
||||
INSERT INTO concept_map_versions
|
||||
(concept_map_id,version,title,document,author,action,summary,created_at)
|
||||
VALUES ($1,$2,$3,$4::text::jsonb,$5,'shared-view-migration',
|
||||
'Linked internal sub-CMaps to standalone shared views without moving graph data',$6)
|
||||
SQL
|
||||
map-id next-version (vector-ref row 2) parent-text migration-author now)))
|
||||
(printf "Migrated ~a submap(s) to shared stored views in one transaction.\n" total)
|
||||
total)))
|
||||
|
||||
(module+ main
|
||||
(define config (default-wiki-config))
|
||||
(define apply? #f)
|
||||
(command-line
|
||||
#:program "migrate-cmap-subpages.rkt"
|
||||
#:once-each
|
||||
(("--data") directory
|
||||
"Wiki data directory"
|
||||
(set! config
|
||||
(struct-copy wiki-config config
|
||||
(data-dir (path->complete-path directory)))))
|
||||
(("--apply")
|
||||
"Apply the migration; without this flag only a dry-run is performed"
|
||||
(set! apply? #t)))
|
||||
(call-with-wiki-database
|
||||
config
|
||||
(lambda (connection)
|
||||
(if apply? (apply-migration! connection) (report-plan connection)))))
|
||||
+138
-19
@@ -11,11 +11,13 @@
|
||||
"storage.rkt")
|
||||
|
||||
(provide list-concept-maps
|
||||
list-concept-usage
|
||||
list-recent-concept-maps
|
||||
search-concept-maps
|
||||
read-concept-map
|
||||
concept-map-history
|
||||
read-concept-map-version
|
||||
delete-concept-map-version!
|
||||
create-concept-map!
|
||||
rename-concept-map!
|
||||
update-concept-map!
|
||||
@@ -81,6 +83,23 @@ VALUES ($1, $2, $3, $4::text::jsonb, $5, $6, $7, $8)
|
||||
SQL
|
||||
map-id version title document-text author action summary now))
|
||||
|
||||
(define (prune-manual-concept-map-versions! db map-id)
|
||||
(query-exec
|
||||
db
|
||||
#<<SQL
|
||||
DELETE FROM concept_map_versions
|
||||
WHERE concept_map_id = $1
|
||||
AND action = 'manual'
|
||||
AND version NOT IN (
|
||||
SELECT version
|
||||
FROM concept_map_versions
|
||||
WHERE concept_map_id = $1 AND action = 'manual'
|
||||
ORDER BY version DESC
|
||||
LIMIT 5
|
||||
)
|
||||
SQL
|
||||
map-id))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : List current concept-map metadata.
|
||||
; pre : Database schema migration 9 has been installed.
|
||||
@@ -100,6 +119,73 @@ SQL
|
||||
" ORDER BY lower(title), title")))))
|
||||
(row->concept-map row #f)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Count current concept placements across every active CMap.
|
||||
; pre : Concept-map documents use an items array when placements exist.
|
||||
; post : No database state is changed.
|
||||
; result : Rows containing global concept identity, linked page, concept/map
|
||||
; labels, CMap slug and placement count.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (list-concept-usage config)
|
||||
(call-with-wiki-database
|
||||
config
|
||||
(λ (db)
|
||||
(for/list ((row (in-list
|
||||
(query-rows
|
||||
db
|
||||
#<<SQL
|
||||
WITH placements AS (
|
||||
SELECT cm.slug,
|
||||
cm.title AS cmap_title,
|
||||
CASE
|
||||
WHEN item ->> 'conceptId' ~ '^concept-[0-9]+$'
|
||||
THEN concat('legacy:', cm.slug, ':', item ->> 'conceptId')
|
||||
ELSE item ->> 'conceptId'
|
||||
END AS concept_id,
|
||||
coalesce(nullif(repository.document ->> 'label', ''),
|
||||
nullif(item ->> 'label', ''),
|
||||
'Concept') AS concept_label,
|
||||
coalesce(nullif(repository.document ->> 'pageSlug', ''),
|
||||
nullif(item ->> 'pageSlug', '')) AS page_slug
|
||||
FROM concept_maps cm
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(cm.document -> 'items') = 'array'
|
||||
THEN cm.document -> 'items'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS item
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT concept_value AS document
|
||||
FROM jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(cm.document -> 'concepts') = 'array'
|
||||
THEN cm.document -> 'concepts'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS concept_entry(concept_value)
|
||||
WHERE concept_value ->> 'id' = item ->> 'conceptId'
|
||||
LIMIT 1
|
||||
) repository ON TRUE
|
||||
WHERE cm.archived = FALSE
|
||||
AND nullif(item ->> 'conceptId', '') IS NOT NULL
|
||||
AND coalesce(item ->> 'kind', 'concept') NOT IN ('phrase', 'submap')
|
||||
)
|
||||
SELECT concept_id, slug, cmap_title, concept_label, page_slug, count(*)
|
||||
FROM placements
|
||||
GROUP BY concept_id, slug, cmap_title, concept_label, page_slug
|
||||
ORDER BY concept_id, lower(cmap_title), cmap_title
|
||||
SQL
|
||||
))))
|
||||
(hash 'conceptId (vector-ref row 0)
|
||||
'cmapSlug (vector-ref row 1)
|
||||
'cmapTitle (vector-ref row 2)
|
||||
'label (vector-ref row 3)
|
||||
'pageSlug (if (sql-null? (vector-ref row 4))
|
||||
'null
|
||||
(vector-ref row 4))
|
||||
'count (vector-ref row 5))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : List recently edited current concept maps.
|
||||
; pre : Database schema migration 9 has been installed.
|
||||
@@ -207,7 +293,8 @@ SQL
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Create a persistent concept map.
|
||||
; pre : slug is unused, title is non-empty and document is a JSON object.
|
||||
; post : One concept_maps row exists at version 1.
|
||||
; post : One concept_maps row exists at version 1; history starts only when
|
||||
; the user explicitly saves or creates a snapshot.
|
||||
; result : The newly stored concept-map hash.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (create-concept-map! config slug title document author)
|
||||
@@ -222,24 +309,22 @@ SQL
|
||||
(call-with-transaction
|
||||
db
|
||||
(λ ()
|
||||
(define map-id
|
||||
(query-value
|
||||
db
|
||||
#<<SQL
|
||||
(query-value
|
||||
db
|
||||
#<<SQL
|
||||
INSERT INTO concept_maps
|
||||
(slug, title, document, current_version, created_at, updated_at, created_by, updated_by)
|
||||
VALUES ($1, $2, $3::text::jsonb, 1, $4, $4, $5, $5)
|
||||
RETURNING id
|
||||
SQL
|
||||
clean-slug clean-title document-text now author))
|
||||
(insert-concept-map-version!
|
||||
db map-id 1 clean-title document-text author "create" "Created CMap" now)))))
|
||||
clean-slug clean-title document-text now author)))))
|
||||
(read-concept-map config clean-slug))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Rename a concept map without storing unsaved editor content.
|
||||
; pre : slug identifies a current map and base-version is current.
|
||||
; post : Title, version and audit fields are updated atomically.
|
||||
; post : Title, version and audit fields are updated atomically without
|
||||
; adding a user-facing history item.
|
||||
; result : The renamed concept-map hash with its unchanged document.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (rename-concept-map! config slug title author base-version)
|
||||
@@ -283,19 +368,20 @@ SQL
|
||||
now
|
||||
author
|
||||
(vector-ref row 0))
|
||||
(insert-concept-map-version!
|
||||
db (vector-ref row 0) next-version clean-title (vector-ref row 2)
|
||||
author "rename" "Renamed CMap" now)))))
|
||||
(void)))))
|
||||
(read-concept-map config slug))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Store a new version of an existing concept map.
|
||||
; goal : Store the current state of an existing concept map.
|
||||
; pre : base-version equals the current database version.
|
||||
; post : Title, document, version and audit fields are updated atomically.
|
||||
; post : Current state and version are updated atomically. Autosaves create
|
||||
; no history row; snapshots are unlimited; only five manual saves remain.
|
||||
; result : The updated concept-map hash.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (update-concept-map! config slug title document author base-version
|
||||
[summary "Edited CMap"] [action "edit"])
|
||||
[summary "Edited CMap"] [action "manual"])
|
||||
(unless (member action '("autosave" "manual" "snapshot"))
|
||||
(error 'update-concept-map! "invalid save action: ~a" action))
|
||||
(define clean-title (string-trim title))
|
||||
(validate-concept-map-input 'update-concept-map! slug clean-title document)
|
||||
(define document-text (document->text document))
|
||||
@@ -338,9 +424,12 @@ SQL
|
||||
now
|
||||
author
|
||||
(vector-ref row 0))
|
||||
(insert-concept-map-version!
|
||||
db (vector-ref row 0) next-version clean-title document-text
|
||||
author action summary now)))))
|
||||
(unless (string=? action "autosave")
|
||||
(insert-concept-map-version!
|
||||
db (vector-ref row 0) next-version clean-title document-text
|
||||
author action summary now))
|
||||
(when (string=? action "manual")
|
||||
(prune-manual-concept-map-versions! db (vector-ref row 0)))))))
|
||||
(read-concept-map config slug))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -365,7 +454,7 @@ SQL
|
||||
#<<SQL
|
||||
SELECT version, title, author, action, summary, created_at
|
||||
FROM concept_map_versions
|
||||
WHERE concept_map_id = $1
|
||||
WHERE concept_map_id = $1 AND action IN ('snapshot', 'manual')
|
||||
ORDER BY version DESC
|
||||
SQL
|
||||
map-id))))
|
||||
@@ -408,6 +497,36 @@ SQL
|
||||
'summary (vector-ref row 5)
|
||||
'createdAt (vector-ref row 6)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Delete one user-facing concept-map history item.
|
||||
; pre : slug identifies a current map and version is numeric.
|
||||
; post : Only the selected snapshot or manual-save row is removed; the
|
||||
; current concept_maps document and version are unchanged.
|
||||
; result : #t when an item was deleted, #f when it did not exist.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (delete-concept-map-version! config slug version)
|
||||
(define version-number
|
||||
(if (number? version) version (string->number version)))
|
||||
(and version-number
|
||||
(call-with-wiki-database
|
||||
config
|
||||
(lambda (db)
|
||||
(and
|
||||
(query-maybe-value
|
||||
db
|
||||
#<<SQL
|
||||
DELETE FROM concept_map_versions cmv
|
||||
USING concept_maps cm
|
||||
WHERE cm.id = cmv.concept_map_id
|
||||
AND cm.slug = $1
|
||||
AND cm.archived = FALSE
|
||||
AND cmv.version = $2
|
||||
AND cmv.action IN ('snapshot', 'manual')
|
||||
RETURNING cmv.version
|
||||
SQL
|
||||
slug version-number)
|
||||
#t)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Soft-delete one concept map.
|
||||
; pre : slug identifies a current map.
|
||||
|
||||
+41
-1
@@ -20,7 +20,7 @@
|
||||
;; Supporting functions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(define current-schema-version 11)
|
||||
(define current-schema-version 12)
|
||||
|
||||
(define schema-1-statements
|
||||
(list
|
||||
@@ -384,6 +384,43 @@ SQL
|
||||
)
|
||||
(record-schema-version! db 11))
|
||||
|
||||
(define (migrate-11->12! db)
|
||||
;; Older clients stored both automatic and explicit saves as `edit`. Preserve
|
||||
;; the explicit saves, discard redundant autosave copies, and enforce the new
|
||||
;; five-item manual history limit per CMap.
|
||||
(query-exec db
|
||||
#<<SQL
|
||||
UPDATE concept_map_versions
|
||||
SET action = 'manual'
|
||||
WHERE action = 'edit'
|
||||
AND summary IN ('Manual save', 'Handmatig opgeslagen')
|
||||
SQL
|
||||
)
|
||||
(query-exec db
|
||||
#<<SQL
|
||||
DELETE FROM concept_map_versions
|
||||
WHERE action = 'edit'
|
||||
AND summary IN ('Automatic save', 'Automatisch opgeslagen')
|
||||
SQL
|
||||
)
|
||||
(query-exec db
|
||||
#<<SQL
|
||||
WITH ranked AS (
|
||||
SELECT id,
|
||||
row_number() OVER (
|
||||
PARTITION BY concept_map_id
|
||||
ORDER BY version DESC
|
||||
) AS position
|
||||
FROM concept_map_versions
|
||||
WHERE action = 'manual'
|
||||
)
|
||||
DELETE FROM concept_map_versions cmv
|
||||
USING ranked
|
||||
WHERE cmv.id = ranked.id AND ranked.position > 5
|
||||
SQL
|
||||
)
|
||||
(record-schema-version! db 12))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Bring a racket-wiki PostgreSQL database to the current schema.
|
||||
; pre : db is a writable PostgreSQL connection and config identifies the
|
||||
@@ -428,6 +465,9 @@ SQL
|
||||
(define after-user-profiles (database-schema-version db))
|
||||
(when (= after-user-profiles 10)
|
||||
(migrate-10->11! db))
|
||||
(define after-concept-map-history (database-schema-version db))
|
||||
(when (= after-concept-map-history 11)
|
||||
(migrate-11->12! db))
|
||||
(define resulting-version (database-schema-version db))
|
||||
(when (> resulting-version current-schema-version)
|
||||
(error 'migrate-database!
|
||||
|
||||
+32
-1
@@ -342,6 +342,12 @@ CSS
|
||||
(λ (_session)
|
||||
(json-response (hash 'conceptMaps (list-concept-maps config))))))
|
||||
|
||||
(define (concept-usage-handler config req)
|
||||
(require-role
|
||||
config req 'reader
|
||||
(λ (_session)
|
||||
(json-response (hash 'placements (list-concept-usage config))))))
|
||||
|
||||
(define (request-concept-map-document body)
|
||||
(define document (hash-ref body 'document #f))
|
||||
(unless (hash? document)
|
||||
@@ -400,6 +406,14 @@ CSS
|
||||
(json-response result)
|
||||
(json-error 404 "Concept map version not found")))))
|
||||
|
||||
(define (concept-map-version-delete-handler config req slug version)
|
||||
(require-write-role
|
||||
config req 'editor
|
||||
(lambda (_session)
|
||||
(if (delete-concept-map-version! config slug version)
|
||||
(json-response (hash 'ok #t))
|
||||
(json-error 404 "Concept map history item not found")))))
|
||||
|
||||
(define (concept-map-update-handler config req slug)
|
||||
(require-write-role
|
||||
config req 'editor
|
||||
@@ -414,12 +428,25 @@ CSS
|
||||
(define document (request-concept-map-document body))
|
||||
(define base-version (hash-ref body 'baseVersion ""))
|
||||
(define snapshot? (eq? (hash-ref body 'snapshot #f) #t))
|
||||
(define supplied-save-kind (hash-ref body 'saveKind #f))
|
||||
(define supplied-summary (hash-ref body 'summary "Edited CMap"))
|
||||
(define summary
|
||||
(if (and (string? supplied-summary)
|
||||
(not (string=? (string-trim supplied-summary) "")))
|
||||
(substring supplied-summary 0 (min 500 (string-length supplied-summary)))
|
||||
"Edited CMap"))
|
||||
(define action
|
||||
(cond
|
||||
(snapshot? "snapshot")
|
||||
((member supplied-save-kind '("autosave" "manual" "snapshot"))
|
||||
supplied-save-kind)
|
||||
((not supplied-save-kind)
|
||||
;; Compatibility with the client version from before saveKind was
|
||||
;; explicit. Known automatic summaries must not enter history.
|
||||
(if (member summary '("Automatic save" "Automatisch opgeslagen"))
|
||||
"autosave"
|
||||
"manual"))
|
||||
(else (error 'concept-map-update-handler "invalid save kind"))))
|
||||
(json-response
|
||||
(update-concept-map! config
|
||||
slug
|
||||
@@ -428,7 +455,7 @@ CSS
|
||||
(wiki-user-username (wiki-session-user session))
|
||||
base-version
|
||||
summary
|
||||
(if snapshot? "snapshot" "edit")))))))
|
||||
action))))))
|
||||
|
||||
(define (concept-map-rename-handler config req slug)
|
||||
(require-write-role
|
||||
@@ -998,12 +1025,16 @@ CSS
|
||||
(λ (req) (concept-map-list-handler config req))]
|
||||
[("api" "cmaps") #:method "post"
|
||||
(λ (req) (concept-map-create-handler config req))]
|
||||
[("api" "cmaps" "concept-usage") #:method "get"
|
||||
(λ (req) (concept-usage-handler config req))]
|
||||
[("api" "cmaps" (string-arg)) #:method "get"
|
||||
(λ (req slug) (concept-map-get-handler config req slug))]
|
||||
[("api" "cmaps" (string-arg) "history") #:method "get"
|
||||
(λ (req slug) (concept-map-history-handler config req slug))]
|
||||
[("api" "cmaps" (string-arg) "versions" (string-arg)) #:method "get"
|
||||
(λ (req slug version) (concept-map-version-handler config req slug version))]
|
||||
[("api" "cmaps" (string-arg) "versions" (string-arg)) #:method "delete"
|
||||
(λ (req slug version) (concept-map-version-delete-handler config req slug version))]
|
||||
[("api" "cmaps" (string-arg)) #:method "put"
|
||||
(λ (req slug) (concept-map-update-handler config req slug))]
|
||||
[("api" "cmaps" (string-arg) "rename") #:method "post"
|
||||
|
||||
+912
-51
File diff suppressed because it is too large
Load Diff
+377
-25
@@ -13,6 +13,7 @@ body.cmap-mode #main {
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#cmap-view {
|
||||
@@ -24,7 +25,7 @@ body.cmap-mode #main {
|
||||
z-index: 160;
|
||||
top: 0;
|
||||
margin: 0 0 10px;
|
||||
padding: 6px 0;
|
||||
padding: 34px 0 6px;
|
||||
background: rgb(255 255 255 / 96%);
|
||||
box-shadow: 0 1px 0 rgb(128 139 150 / 22%);
|
||||
backdrop-filter: blur(4px);
|
||||
@@ -51,6 +52,45 @@ body.cmap-mode #main {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.cmap-hidden-items {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.cmap-hidden-items > summary {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cmap-hidden-item-options {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
z-index: 10020;
|
||||
display: grid;
|
||||
min-width: 220px;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
border: 1px solid #c8ced5;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
||||
.cmap-hidden-item-options button {
|
||||
padding: 7px 9px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cmap-hidden-item-options button:hover,
|
||||
.cmap-hidden-item-options button:focus-visible {
|
||||
background: #eef2f6;
|
||||
}
|
||||
|
||||
.cmap-storage-controls input[type="search"] {
|
||||
box-sizing: border-box;
|
||||
min-width: 220px;
|
||||
@@ -60,6 +100,20 @@ body.cmap-mode #main {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.cmap-wiki-reference {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #c3cad2;
|
||||
border-radius: 4px;
|
||||
background: #f7f9fa;
|
||||
color: #34495e;
|
||||
cursor: copy;
|
||||
font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wiki-combobox {
|
||||
position: relative;
|
||||
min-width: 220px;
|
||||
@@ -225,6 +279,8 @@ body.cmap-mode #main {
|
||||
|
||||
.cmap-workspace {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
isolation: isolate;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -337,7 +393,7 @@ body.cmap-mode #main {
|
||||
|
||||
.cmap-context-menu {
|
||||
position: fixed;
|
||||
z-index: 500;
|
||||
z-index: 100000;
|
||||
display: grid;
|
||||
min-width: 210px;
|
||||
padding: 5px;
|
||||
@@ -406,23 +462,131 @@ body.cmap-mode #main {
|
||||
}
|
||||
|
||||
.cmap-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 11px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rw-cmap-view-description {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
z-index: 46;
|
||||
display: grid;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 1px solid rgb(70 82 94 / 45%);
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
color: #34495e;
|
||||
cursor: pointer;
|
||||
font: 700 14px/1 Georgia, "Times New Roman", serif;
|
||||
}
|
||||
|
||||
.rw-cmap-view-description.is-empty {
|
||||
border-color: #c8cdd2;
|
||||
background: #e5e7e9;
|
||||
color: #7a828a;
|
||||
}
|
||||
|
||||
.rw-cmap-view-description.is-filled {
|
||||
border-color: #87919b;
|
||||
background: #fff;
|
||||
color: #263746;
|
||||
}
|
||||
|
||||
.cmap-description-tooltip {
|
||||
position: fixed;
|
||||
z-index: 110000;
|
||||
width: min(420px, calc(100vw - 16px));
|
||||
max-height: min(440px, calc(100vh - 24px));
|
||||
overflow: auto;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #9ca6b0;
|
||||
border-radius: 7px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 32px rgb(0 0 0 / 24%);
|
||||
color: #222;
|
||||
font: 14px/1.45 system-ui, sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cmap-description-tooltip .markdown-body > :first-child { margin-top: 0; }
|
||||
.cmap-description-tooltip .markdown-body > :last-child { margin-bottom: 0; }
|
||||
|
||||
.rw-cmap-open-linked {
|
||||
position: absolute;
|
||||
z-index: 46;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
display: grid;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 1px solid rgb(70 82 94 / 45%);
|
||||
border-radius: 50%;
|
||||
background: rgb(255 255 255 / 86%);
|
||||
color: #34495e;
|
||||
cursor: pointer;
|
||||
font: 700 12px/1 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.rw-cmap-view-description + .cmap-card-image,
|
||||
.rw-cmap-view-description ~ .cmap-card-title {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.rw-cmap-open-linked ~ .cmap-card-title {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.cmap-card-title {
|
||||
margin-bottom: .3rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-size: 1em;
|
||||
font-weight: inherit;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
.cmap-card-usage {
|
||||
opacity: .62;
|
||||
font-size: .72em;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cmap-card-aspects {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px;
|
||||
margin: 0 0 .35rem;
|
||||
}
|
||||
|
||||
.cmap-card-aspect {
|
||||
padding: 1px 5px;
|
||||
border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 58%);
|
||||
font-size: .68em;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.cmap-card-synopsis {
|
||||
color: inherit;
|
||||
opacity: .78;
|
||||
font-size: .84rem;
|
||||
font-size: .84em;
|
||||
line-height: 1.25;
|
||||
font-weight: inherit;
|
||||
font-style: inherit;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cmap-card-image {
|
||||
@@ -434,18 +598,6 @@ body.cmap-mode #main {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.cmap-card-page .cmap-card-title::after {
|
||||
content: " ↗";
|
||||
color: #4479a1;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cmap-card-linked-cmap .cmap-card-title::after {
|
||||
content: " ↗";
|
||||
color: #477b3e;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.rw-cmap-item {
|
||||
overflow: visible !important;
|
||||
}
|
||||
@@ -466,10 +618,30 @@ body.cmap-mode #main {
|
||||
font: 700 17px/18px Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.rw-cmap-submap-open {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 34px;
|
||||
z-index: 45;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: 1px solid #57834a;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
color: #35672c;
|
||||
cursor: pointer;
|
||||
font: 700 14px/18px Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.cmap-card-submap {
|
||||
padding-right: 38px;
|
||||
}
|
||||
|
||||
.rw-cmap-item:has(> .rw-cmap-submap-open) .cmap-card-submap {
|
||||
padding-right: 64px;
|
||||
}
|
||||
|
||||
.rw-cmap-submap-frame {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
@@ -487,6 +659,23 @@ body.cmap-mode #main {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.rw-cmap-submap-anchor-line {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rw-cmap-submap-anchor-line path {
|
||||
fill: none;
|
||||
stroke: #a6adb4;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.rw-cmap-submap-frame-selected {
|
||||
outline: 3px solid #9994ff;
|
||||
outline-offset: 2px;
|
||||
@@ -510,6 +699,43 @@ body.cmap-mode #main {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.rw-cmap-boundary-layer,
|
||||
.rw-cmap-boundary-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 16;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rw-cmap-boundary-reference {
|
||||
position: absolute;
|
||||
z-index: 17;
|
||||
min-height: 30px;
|
||||
overflow: hidden;
|
||||
padding: 5px 9px;
|
||||
border: 1px solid #77838e;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #25313c;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 2px 6px rgb(0 0 0 / 14%);
|
||||
}
|
||||
|
||||
.rw-cmap-boundary-reference-left::before {
|
||||
content: "← ";
|
||||
}
|
||||
|
||||
.rw-cmap-boundary-reference-right::after {
|
||||
content: " →";
|
||||
}
|
||||
|
||||
.rw-cmap-selected {
|
||||
z-index: 20 !important;
|
||||
outline: 3px solid #9994ff !important;
|
||||
@@ -660,6 +886,11 @@ body.cmap-mode #main {
|
||||
box-shadow: 0 18px 60px rgb(0 0 0 / 24%);
|
||||
}
|
||||
|
||||
#cmap-concept-dialog {
|
||||
width: min(780px, calc(100vw - 32px));
|
||||
max-height: calc(100vh - 32px);
|
||||
}
|
||||
|
||||
.cmap-concept-dialog::backdrop {
|
||||
background: rgb(22 30 44 / 38%);
|
||||
}
|
||||
@@ -695,6 +926,8 @@ body.cmap-mode #main {
|
||||
#cmap-history-list { max-height: min(520px, 60vh); overflow: auto; }
|
||||
.cmap-history-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; padding: 11px 0; border-bottom: 1px solid #dfe3e8; }
|
||||
.cmap-history-row:last-child { border-bottom: 0; }
|
||||
.cmap-history-actions { display: flex; gap: 8px; align-items: center; }
|
||||
.cmap-history-empty { margin: 18px 0 4px; }
|
||||
|
||||
.cmap-unsaved-dialog form {
|
||||
display: grid;
|
||||
@@ -713,6 +946,21 @@ body.cmap-mode #main {
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
#cmap-concept-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 11px 14px;
|
||||
max-height: calc(100vh - 34px);
|
||||
overflow: auto;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
#cmap-concept-form > h2,
|
||||
#cmap-concept-form > .cmap-dialog-wide,
|
||||
#cmap-concept-form > .cmap-submap-style-fields,
|
||||
#cmap-concept-form > .cmap-concept-dialog-actions {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.cmap-concept-dialog h2 {
|
||||
margin: 0 0 2px;
|
||||
}
|
||||
@@ -737,18 +985,72 @@ body.cmap-mode #main {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cmap-concept-dialog input[type="color"] {
|
||||
width: 72px;
|
||||
height: 36px;
|
||||
padding: 2px;
|
||||
border: 1px solid #aab2bd;
|
||||
.cmap-color-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cmap-concept-dialog .cmap-color-input {
|
||||
width: 96px;
|
||||
min-width: 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.cmap-color-swatch {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 1px solid #8f99a4;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cmap-color-palette {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 28px);
|
||||
gap: 5px;
|
||||
padding: 7px;
|
||||
border: 1px solid #929ca6;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
box-shadow: 0 7px 22px rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.cmap-color-palette button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid #727b84;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cmap-color-palette button:hover,
|
||||
.cmap-color-palette button:focus-visible {
|
||||
outline: 2px solid #315f91;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.cmap-native-color-picker {
|
||||
position: fixed;
|
||||
left: -100px;
|
||||
top: -100px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cmap-concept-image-fields,
|
||||
.cmap-submap-style-fields {
|
||||
.cmap-submap-style-fields,
|
||||
.cmap-appearance-fields,
|
||||
.cmap-typography-fields {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
margin: 0;
|
||||
@@ -758,7 +1060,9 @@ body.cmap-mode #main {
|
||||
}
|
||||
|
||||
.cmap-concept-image-fields legend,
|
||||
.cmap-submap-style-fields legend {
|
||||
.cmap-submap-style-fields legend,
|
||||
.cmap-appearance-fields legend,
|
||||
.cmap-typography-fields legend {
|
||||
padding: 0 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -767,6 +1071,40 @@ body.cmap-mode #main {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.cmap-appearance-fields {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cmap-typography-fields {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
min-width: 0;
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
|
||||
.cmap-appearance-fields legend,
|
||||
.cmap-style-preset-row {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.cmap-inline-control {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.cmap-inline-control select { flex: 1 1 auto; }
|
||||
|
||||
#cmap-concept-form .cmap-check-label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
align-self: end;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.cmap-check-label input { margin: 0; }
|
||||
|
||||
.cmap-submap-style-fields legend {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -825,6 +1163,20 @@ body.cmap-mode #main {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#cmap-concept-form,
|
||||
.cmap-appearance-fields,
|
||||
.cmap-typography-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#cmap-concept-form > *,
|
||||
.cmap-appearance-fields > *,
|
||||
.cmap-typography-fields > * {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.cmap-storage-controls {
|
||||
flex-wrap: wrap;
|
||||
|
||||
+38
-4
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* racket-wiki cmap component 0.2.94
|
||||
* racket-wiki cmap component 0.2.95
|
||||
* Based on cmap v0.1.3.
|
||||
* (c) 2015 iOnStage
|
||||
* Released under the MIT License.
|
||||
@@ -370,6 +370,12 @@
|
||||
|
||||
var Component = function() {};
|
||||
|
||||
Component.prototype.disposed = false;
|
||||
|
||||
Component.prototype.dispose = function() {
|
||||
this.disposed = true;
|
||||
};
|
||||
|
||||
Component.prototype.prop = function(initialValue, defaultValue, converter) {
|
||||
if (typeof converter !== 'function')
|
||||
converter = helper.identity;
|
||||
@@ -407,8 +413,11 @@
|
||||
var updateRelations = function(index) {
|
||||
for (var i = index, len = dirtyComponents.length; i < len; i++) {
|
||||
var component = dirtyComponents[i];
|
||||
if (component.disposed)
|
||||
continue;
|
||||
component.relations().forEach(function(relation) {
|
||||
relation.update(component);
|
||||
if (!relation.disposed)
|
||||
relation.update(component);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -421,7 +430,8 @@
|
||||
updateRelations(0);
|
||||
|
||||
dirtyComponents.forEach(function(component) {
|
||||
component.redraw();
|
||||
if (!component.disposed)
|
||||
component.redraw();
|
||||
});
|
||||
|
||||
dirtyComponents = [];
|
||||
@@ -429,7 +439,7 @@
|
||||
};
|
||||
|
||||
return function() {
|
||||
if (dom.disabled())
|
||||
if (dom.disabled() || this.disposed)
|
||||
return;
|
||||
|
||||
if (dirtyComponents.indexOf(this) === -1)
|
||||
@@ -1808,6 +1818,9 @@
|
||||
};
|
||||
|
||||
Cmap.prototype.redraw = function() {
|
||||
if (this.disposed)
|
||||
return;
|
||||
|
||||
var rootElement = this.rootElement();
|
||||
|
||||
if (!rootElement) {
|
||||
@@ -1820,6 +1833,7 @@
|
||||
this.rootElement(rootElement);
|
||||
}
|
||||
|
||||
var previousElement = this.element();
|
||||
var element = dom.el('<div>');
|
||||
element.className = 'rw-cmap-surface';
|
||||
dom.draggable(element, function(x, y, event) {
|
||||
@@ -1844,6 +1858,8 @@
|
||||
this.unfixScrollSize();
|
||||
|
||||
dom.css(element, this.style());
|
||||
if (previousElement && previousElement.parentNode)
|
||||
previousElement.parentNode.removeChild(previousElement);
|
||||
dom.append(rootElement, element);
|
||||
};
|
||||
|
||||
@@ -2190,6 +2206,24 @@
|
||||
this.activationHandler = handler;
|
||||
};
|
||||
|
||||
CmapModule.prototype.destroy = function() {
|
||||
var component = this.component;
|
||||
var element = component.element();
|
||||
|
||||
this.selectionHandler = null;
|
||||
this.activationHandler = null;
|
||||
component.selectionHandler = null;
|
||||
component.activationHandler = null;
|
||||
component.componentList().toArray().forEach(function(child) {
|
||||
child.dispose();
|
||||
child.parentElement(null);
|
||||
});
|
||||
component.dispose();
|
||||
|
||||
if (element && element.parentNode)
|
||||
element.parentNode.removeChild(element);
|
||||
};
|
||||
|
||||
CmapModule.prototype.zoom = function(value) {
|
||||
if (typeof value === 'undefined')
|
||||
return this.component.zoomFactor;
|
||||
|
||||
@@ -410,6 +410,65 @@ body {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.page-cmap-connections {
|
||||
box-sizing: border-box;
|
||||
max-width: 900px;
|
||||
margin-top: 2rem;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #c8d8c3;
|
||||
border-radius: 7px;
|
||||
background: #f5faf3;
|
||||
}
|
||||
|
||||
.page-cmap-connections h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.page-cmap-concept-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1fr) minmax(220px, 2fr);
|
||||
gap: 12px 18px;
|
||||
align-items: center;
|
||||
padding: 9px 0;
|
||||
border-top: 1px solid #d9e5d5;
|
||||
}
|
||||
|
||||
.page-cmap-concept-row:first-child { border-top: 0; }
|
||||
|
||||
.page-cmap-concept-description {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.page-cmap-concept-description .muted { font-size: .82rem; }
|
||||
|
||||
.page-cmap-concept-maps {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.page-cmap-concept-maps a {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #96b48d;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #315f2b;
|
||||
font-size: .86rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-cmap-concept-maps a:hover,
|
||||
.page-cmap-concept-maps a:focus-visible {
|
||||
background: #e6f3e2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.page-cmap-concept-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.EasyMDEContainer .editor-preview h1,
|
||||
.EasyMDEContainer .editor-preview-side h1 {
|
||||
|
||||
+101
-31
@@ -81,6 +81,10 @@
|
||||
<svg id="context-dock-graph" class="wiki-graph context-graph-small" viewBox="0 0 1000 700" role="img" aria-label="Context graph"></svg>
|
||||
</aside>
|
||||
</div>
|
||||
<section id="page-cmap-connections" class="page-cmap-connections hidden" aria-labelledby="page-cmap-connections-title">
|
||||
<h2 id="page-cmap-connections-title" data-tr="connected-cmap-concepts">Connected CMap concepts</h2>
|
||||
<div id="page-cmap-connections-list"></div>
|
||||
</section>
|
||||
<footer id="page-details" class="page-details"></footer>
|
||||
</section>
|
||||
|
||||
@@ -175,9 +179,14 @@
|
||||
<button type="button" class="wiki-combobox-toggle" tabindex="-1" aria-label="Show concept maps">⌄</button>
|
||||
<div id="cmap-map-options" class="wiki-combobox-list hidden" role="listbox"></div>
|
||||
</div>
|
||||
<button id="cmap-wiki-reference" class="cmap-wiki-reference hidden" type="button" data-tr-title="copy-wiki-link" title="Copy wiki link"></button>
|
||||
<span class="cmap-toolbar-separator" aria-hidden="true"></span>
|
||||
<button id="cmap-tools-menu" class="editor-only" type="button" aria-label="Concept map tools" aria-haspopup="menu">⋮</button>
|
||||
<span id="cmap-save-status" class="muted" role="status"></span>
|
||||
<details id="cmap-hidden-items" class="cmap-hidden-items hidden">
|
||||
<summary data-tr="hidden-concepts">Hidden concepts</summary>
|
||||
<div id="cmap-hidden-item-options" class="cmap-hidden-item-options"></div>
|
||||
</details>
|
||||
<span class="cmap-toolbar-spacer" aria-hidden="true"></span>
|
||||
<div class="cmap-zoom-controls" aria-label="Zoom controls">
|
||||
<button id="cmap-zoom-out" type="button" aria-label="Zoom out">−</button>
|
||||
@@ -189,10 +198,11 @@
|
||||
<details class="cmap-help-menu">
|
||||
<summary aria-label="Concept map help">?</summary>
|
||||
<div class="cmap-help-popover muted">
|
||||
<p data-tr="concept-map-help-context">Select with Ctrl/Cmd/Shift-click or by dragging over empty space. Drag a selected element to move the complete selection. Grouping creates a new sub-CMap around the selection. Double-click a page or sub-CMap to open it; right-click for editing tools.</p>
|
||||
<p data-tr="concept-map-help-context">Select with Ctrl/Cmd/Shift-click or by dragging over empty space. Drag a selected element to move the complete selection. Hold Alt while drawing a relation to create a direct line without a linking phrase. Grouping creates a new sub-CMap around the selection. Double-click a concept to edit it or a sub-CMap to open it; right-click for more tools.</p>
|
||||
<dl class="cmap-shortcut-list">
|
||||
<div><dt><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>/<kbd>Shift</kbd> + <span data-tr="click">click</span></dt><dd data-tr="cmap-help-add-selection">Add to or remove from the selection</dd></div>
|
||||
<div><dt><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>A</kbd></dt><dd data-tr="select-all">Select all</dd></div>
|
||||
<div><dt><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>C</kbd>/<kbd>V</kbd></dt><dd data-tr="copy-paste-linked-concepts">Copy/paste linked concepts</dd></div>
|
||||
<div><dt><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>G</kbd></dt><dd data-tr="group-selected">Group selection as sub-CMap</dd></div>
|
||||
<div><dt><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd></dt><dd data-tr="ungroup-selected">Detach from sub-CMap</dd></div>
|
||||
<div><dt><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd></dt><dd data-tr="undo">Undo</dd></div>
|
||||
@@ -213,6 +223,7 @@
|
||||
<button id="cmap-save-map" class="editor-only" type="button" role="menuitem" data-tr="save-now">Save now</button>
|
||||
<div class="cmap-context-separator" role="separator"></div>
|
||||
<button id="cmap-rename-map" type="button" role="menuitem" data-tr="rename-concept-map" disabled>Rename CMap</button>
|
||||
<button id="cmap-set-start-map" type="button" role="menuitem" data-tr="set-start-concept-map" disabled>Use as start CMap</button>
|
||||
<button id="cmap-delete-map" class="danger" type="button" role="menuitem" data-tr="delete-concept-map" disabled>Delete CMap</button>
|
||||
<button id="cmap-create-snapshot" type="button" role="menuitem" data-tr="create-concept-map-snapshot" disabled>Make snapshot…</button>
|
||||
<button id="cmap-history" type="button" role="menuitem" data-tr="concept-map-history" disabled>CMap history</button>
|
||||
@@ -222,6 +233,8 @@
|
||||
<button id="cmap-add-submap" type="button" role="menuitem" data-tr="add-submap">Add sub-CMap</button>
|
||||
<button id="cmap-promote-submap" type="button" role="menuitem" data-tr="promote-submap" disabled>Make separate CMap</button>
|
||||
<button id="cmap-edit-selected" type="button" role="menuitem" data-tr="edit-selected-concept">Edit selected</button>
|
||||
<button id="cmap-copy-selected" type="button" role="menuitem" data-tr="copy-linked-concepts">Copy concept reference</button>
|
||||
<button id="cmap-paste-concepts" type="button" role="menuitem" data-tr="paste-linked-concepts" disabled>Paste linked concept</button>
|
||||
<div class="cmap-context-separator" role="separator"></div>
|
||||
<button id="cmap-undo" type="button" role="menuitem" data-tr="undo" disabled>Undo</button>
|
||||
<button id="cmap-redo" type="button" role="menuitem" data-tr="redo-action" disabled>Redo</button>
|
||||
@@ -229,6 +242,7 @@
|
||||
<button id="cmap-select-all" type="button" role="menuitem" data-tr="select-all">Select all</button>
|
||||
<button id="cmap-group-selected" type="button" role="menuitem" data-tr="group-selected" disabled>Group selection as sub-CMap</button>
|
||||
<button id="cmap-ungroup-selected" type="button" role="menuitem" data-tr="ungroup-selected" disabled>Detach from sub-CMap</button>
|
||||
<button id="cmap-hide-selected" type="button" role="menuitem" data-tr="hide-in-this-view" disabled>Hide in this view</button>
|
||||
<button id="cmap-delete-selected" class="danger" type="button" role="menuitem" data-tr="delete-selected" disabled>Delete selection</button>
|
||||
<div class="cmap-context-separator" role="separator"></div>
|
||||
<button id="cmap-toggle-page-guides" type="button" role="menuitemcheckbox" aria-checked="true" data-tr="a4-page-boundaries">A4 page boundaries</button>
|
||||
@@ -405,29 +419,102 @@
|
||||
<input id="cmap-concept-label" type="text" required>
|
||||
</label>
|
||||
<label>
|
||||
<span data-tr="aspects">Aspects</span>
|
||||
<input id="cmap-concept-aspects" type="text" data-tr-placeholder="aspects-placeholder" placeholder="Security, performance">
|
||||
</label>
|
||||
<label class="cmap-dialog-wide">
|
||||
<span data-tr="concept-description-page">Concept description page</span>
|
||||
<input id="cmap-concept-description-page" type="text" data-tr-placeholder="concept-description-page-placeholder" placeholder="cmap:concept-name">
|
||||
<a id="cmap-concept-description-link" class="hidden" href="#" data-tr="view-description-page">View description page</a>
|
||||
</label>
|
||||
<label class="cmap-dialog-wide">
|
||||
<span data-tr="synopsis">Synopsis</span>
|
||||
<textarea id="cmap-concept-synopsis" rows="4"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span id="cmap-concept-background-label" data-tr="background-color">Background color</span>
|
||||
<input id="cmap-concept-background" type="color" value="#f3f6f8">
|
||||
<textarea id="cmap-concept-synopsis" rows="3"></textarea>
|
||||
</label>
|
||||
<fieldset class="cmap-appearance-fields cmap-dialog-wide">
|
||||
<legend data-tr="appearance">Appearance</legend>
|
||||
<label class="cmap-style-preset-row">
|
||||
<span data-tr="style">Style</span>
|
||||
<span class="cmap-inline-control">
|
||||
<select id="cmap-concept-style-preset">
|
||||
<option value="default" data-tr="style-default">Default</option>
|
||||
<option value="subtle" data-tr="style-subtle">Subtle</option>
|
||||
<option value="emphasis" data-tr="style-emphasis">Emphasis</option>
|
||||
<option value="warning" data-tr="style-warning">Warning</option>
|
||||
<option value="success" data-tr="style-success">Success</option>
|
||||
</select>
|
||||
<button id="cmap-apply-style" type="button" data-tr="apply-style">Apply</button>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span id="cmap-concept-background-label" data-tr="background-color">Background color</span>
|
||||
<span class="cmap-color-control"><input id="cmap-concept-background" class="cmap-color-input" type="text" value="#f3f6f8"><button type="button" class="cmap-color-swatch" aria-label="Choose background color"></button></span>
|
||||
</label>
|
||||
<fieldset class="cmap-typography-fields">
|
||||
<legend data-tr="concept-heading-appearance">Heading</legend>
|
||||
<label>
|
||||
<span data-tr="text-color">Text color</span>
|
||||
<span class="cmap-color-control"><input id="cmap-concept-text-color" class="cmap-color-input" type="text" value="#222222"><button type="button" class="cmap-color-swatch" aria-label="Choose heading text color"></button></span>
|
||||
</label>
|
||||
<label>
|
||||
<span data-tr="font-family">Font family</span>
|
||||
<select id="cmap-concept-font-family">
|
||||
<option value="Arial, Helvetica, sans-serif">Arial</option>
|
||||
<option value="Verdana, Geneva, sans-serif">Verdana</option>
|
||||
<option value="Tahoma, Geneva, sans-serif">Tahoma</option>
|
||||
<option value="Trebuchet MS, Arial, sans-serif">Trebuchet MS</option>
|
||||
<option value="Georgia, Times New Roman, serif">Georgia</option>
|
||||
<option value="Times New Roman, Times, serif">Times New Roman</option>
|
||||
<option value="Courier New, Courier, monospace">Courier New</option>
|
||||
<option value="system-ui, sans-serif">System UI</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span data-tr="font-size">Font size</span>
|
||||
<span class="cmap-font-size-input"><input id="cmap-concept-font-size" type="number" min="6" max="54" step="0.01"><span>pt</span></span>
|
||||
</label>
|
||||
<label class="cmap-check-label"><input id="cmap-concept-bold" type="checkbox" checked><span data-tr="bold">Bold</span></label>
|
||||
<label class="cmap-check-label"><input id="cmap-concept-italic" type="checkbox"><span data-tr="italic">Italic</span></label>
|
||||
</fieldset>
|
||||
<fieldset class="cmap-typography-fields">
|
||||
<legend data-tr="concept-subtext-appearance">Subtext</legend>
|
||||
<label>
|
||||
<span data-tr="text-color">Text color</span>
|
||||
<span class="cmap-color-control"><input id="cmap-concept-synopsis-text-color" class="cmap-color-input" type="text" value="#222222"><button type="button" class="cmap-color-swatch" aria-label="Choose subtext color"></button></span>
|
||||
</label>
|
||||
<label>
|
||||
<span data-tr="font-family">Font family</span>
|
||||
<select id="cmap-concept-synopsis-font-family">
|
||||
<option value="Arial, Helvetica, sans-serif">Arial</option>
|
||||
<option value="Verdana, Geneva, sans-serif">Verdana</option>
|
||||
<option value="Tahoma, Geneva, sans-serif">Tahoma</option>
|
||||
<option value="Trebuchet MS, Arial, sans-serif">Trebuchet MS</option>
|
||||
<option value="Georgia, Times New Roman, serif">Georgia</option>
|
||||
<option value="Times New Roman, Times, serif">Times New Roman</option>
|
||||
<option value="Courier New, Courier, monospace">Courier New</option>
|
||||
<option value="system-ui, sans-serif">System UI</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span data-tr="font-size">Font size</span>
|
||||
<span class="cmap-font-size-input"><input id="cmap-concept-synopsis-font-size" type="number" min="6" max="54" step="0.01"><span>pt</span></span>
|
||||
</label>
|
||||
<label class="cmap-check-label"><input id="cmap-concept-synopsis-bold" type="checkbox"><span data-tr="bold">Bold</span></label>
|
||||
<label class="cmap-check-label"><input id="cmap-concept-synopsis-italic" type="checkbox"><span data-tr="italic">Italic</span></label>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
<fieldset id="cmap-submap-style-fields" class="cmap-submap-style-fields hidden">
|
||||
<legend data-tr="submap-appearance">Sub-CMap appearance</legend>
|
||||
<label>
|
||||
<span data-tr="submap-background-color">Sub-CMap background color</span>
|
||||
<input id="cmap-submap-background" type="color" value="#edf7e8">
|
||||
<span class="cmap-color-control"><input id="cmap-submap-background" class="cmap-color-input" type="text" value="#edf7e8"><button type="button" class="cmap-color-swatch" aria-label="Choose submap background color"></button></span>
|
||||
</label>
|
||||
<label>
|
||||
<span data-tr="submap-border-color">Sub-CMap border color</span>
|
||||
<input id="cmap-submap-border" type="color" value="#57834a">
|
||||
<span class="cmap-color-control"><input id="cmap-submap-border" class="cmap-color-input" type="text" value="#57834a"><button type="button" class="cmap-color-swatch" aria-label="Choose submap border color"></button></span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<label>
|
||||
<span data-tr="text-color">Text color</span>
|
||||
<input id="cmap-concept-text-color" type="color" value="#222222">
|
||||
</label>
|
||||
<fieldset class="cmap-concept-image-fields">
|
||||
<fieldset class="cmap-concept-image-fields cmap-dialog-wide">
|
||||
<legend data-tr="concept-image">Concept image</legend>
|
||||
<input id="cmap-concept-image" type="file" accept="image/*">
|
||||
<div id="cmap-concept-image-preview-row" class="cmap-concept-image-preview-row hidden">
|
||||
@@ -435,23 +522,6 @@
|
||||
<button id="cmap-concept-image-remove" type="button" data-tr="remove-image">Remove image</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<label>
|
||||
<span data-tr="font-family">Font family</span>
|
||||
<select id="cmap-concept-font-family">
|
||||
<option value="Arial, Helvetica, sans-serif">Arial</option>
|
||||
<option value="Verdana, Geneva, sans-serif">Verdana</option>
|
||||
<option value="Tahoma, Geneva, sans-serif">Tahoma</option>
|
||||
<option value="Trebuchet MS, Arial, sans-serif">Trebuchet MS</option>
|
||||
<option value="Georgia, Times New Roman, serif">Georgia</option>
|
||||
<option value="Times New Roman, Times, serif">Times New Roman</option>
|
||||
<option value="Courier New, Courier, monospace">Courier New</option>
|
||||
<option value="system-ui, sans-serif">System UI</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span data-tr="font-size">Font size</span>
|
||||
<span class="cmap-font-size-input"><input id="cmap-concept-font-size" type="number" min="6" max="54" step="1"><span>pt</span></span>
|
||||
</label>
|
||||
<div class="cmap-concept-dialog-actions">
|
||||
<button id="cmap-concept-cancel" type="button" data-tr="cancel">Cancel</button>
|
||||
<button type="submit" class="primary" data-tr="save">Save</button>
|
||||
@@ -498,7 +568,7 @@
|
||||
<h2 id="cmap-history-title" data-tr="concept-map-history">CMap history</h2>
|
||||
<button id="cmap-history-close" type="button" data-tr="close">Close</button>
|
||||
</header>
|
||||
<p class="muted" data-tr="concept-map-history-help">Load a historical version into the editor to inspect it. Saving it creates a new current version.</p>
|
||||
<p class="muted" data-tr="concept-map-history-help">Snapshots are kept indefinitely. Up to five explicit saves are kept; automatic saves are not added to history.</p>
|
||||
<div id="cmap-history-list"></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
+797
-55
File diff suppressed because it is too large
Load Diff
+46
-8
@@ -31,24 +31,43 @@
|
||||
'grouped-submap-synopsis "Grouped sub-concept map."
|
||||
'navigation-graph "Navigation graph" 'click "click"
|
||||
'cmap-help-add-selection "Add to or remove from the selection"
|
||||
'copy-paste-linked-concepts "Copy/paste linked concepts"
|
||||
'copy-linked-concepts "Copy concept reference" 'paste-linked-concepts "Paste linked concept"
|
||||
'concept-usage-count "{count} placements across all concept maps"
|
||||
'connected-cmap-concepts "Connected CMap concepts" 'concept-map-locations "Concept map locations"
|
||||
'copy-wiki-link "Copy wiki link" 'wiki-link-copied "Wiki link copied"
|
||||
'open-linked-item "Open linked page or CMap"
|
||||
'appearance "Appearance" 'style "Style" 'apply-style "Apply"
|
||||
'concept-heading-appearance "Heading" 'concept-subtext-appearance "Subtext"
|
||||
'change-palette-color "double-click to change"
|
||||
'color-swatch-help "Click for the palette; double-click for a custom color"
|
||||
'style-default "Default" 'style-subtle "Subtle" 'style-emphasis "Emphasis" 'style-warning "Warning" 'style-success "Success"
|
||||
'bold "Bold" 'italic "Italic"
|
||||
'cmap-help-rename "Edit the selected concept"
|
||||
'cmap-help-clear-selection "Clear selection"
|
||||
'concept-map-help-expanded "Select a concept to show its handles. Drag a relation to a concept or empty space. Double-click a page concept to open it; double-click a sub-CMap to expand or collapse it."
|
||||
'concept-map-help-context "Select with Ctrl/Cmd/Shift-click or by dragging over empty space. Drag a selected element to move the complete selection. Grouping creates a new sub-CMap around the selection. Double-click a page or sub-CMap to open it; right-click for editing tools."
|
||||
'concept-map-help-expanded "Select a concept to show its handles. Drag a relation to a concept or empty space. Hold Alt while dropping it on a concept to create a direct line. Double-click a concept to edit it; double-click a sub-CMap to expand or collapse it."
|
||||
'concept-map-help-context "Select with Ctrl/Cmd/Shift-click or by dragging over empty space. Drag a selected element to move the complete selection. Hold Alt while drawing a relation to create a direct line without a linking phrase. Grouping creates a new sub-CMap around the selection. Double-click a concept to edit it or a sub-CMap to open it; right-click for more tools."
|
||||
'detach-submap-confirm "Place \"{concept}\" outside submap \"{submap}\"?"
|
||||
'promote-submap "Make separate CMap" 'submap-name "Name of the new concept map"
|
||||
'submap-derived "Standalone view created for sub-CMap"
|
||||
'hidden-concepts "Hidden concepts" 'hide-in-this-view "Hide in this view"
|
||||
'show-hidden-concept "Show this concept again"
|
||||
'before-submap-extraction "Before creating shared sub-CMap view" 'submap-extracted "Sub-CMap moved to a separate CMap" 'empty-submap "This sub-CMap has no concepts to move." 'submap-created-parent-unsaved "The new CMap was created, but the parent CMap still needs to be saved."
|
||||
'rename-concept-map "Rename CMap" 'delete-concept-map "Delete CMap" 'redo-action "Redo" 'save-now "Save now"
|
||||
'set-start-concept-map "Use as start CMap" 'start-concept-map "Start CMap" 'start-concept-map-set "Start CMap set"
|
||||
'autosave-pending "Changes waiting to be saved" 'autosaving "Saving automatically…" 'concept-map-autosaved "CMap saved automatically"
|
||||
'concept-map-renamed "CMap renamed" 'concept-map-deleted "CMap deleted"
|
||||
'concept-map-history "CMap history" 'concept-map-history-help "Load a historical version into the editor to inspect it. Saving it creates a new current version."
|
||||
'concept-map-history "CMap history" 'concept-map-history-help "Snapshots are kept indefinitely. Up to five explicit saves are kept; automatic saves are not added to history."
|
||||
'create-concept-map-snapshot "Make snapshot…" 'snapshot-description "Snapshot description" 'snapshot "Snapshot"
|
||||
'creating-snapshot "Creating snapshot…" 'snapshot-created "Snapshot created"
|
||||
'concept-map-version-loaded "Version {version} loaded; save to make it current." 'load-version "Load version" 'current-version "Current"
|
||||
'no-concept-map-history "No snapshots or manual saves yet." 'delete-history-item "Delete"
|
||||
'delete-concept-map-history-confirm "Delete CMap history version {version}? The current CMap will not be changed." 'concept-map-history-deleted "History item deleted"
|
||||
'automatic-save "Automatic save" 'manual-save "Manual save" 'concept-map-created-version "CMap created"
|
||||
'concept-map-renamed-version "CMap renamed" 'concept-map-initial-version "Initial available version"
|
||||
'delete-concept-map-confirm "Delete concept map \"{title}\"? Links to it will remain but will no longer open the map."
|
||||
'reload-concept-map "Reload CMap"
|
||||
'edit-concept "Edit concept" 'linked-page "Linked wiki page" 'no-linked-page "No linked page" 'filter-pages "Filter pages" 'linked-concept-map "Linked concept map" 'no-linked-concept-map "No linked concept map" 'filter-concept-maps "Filter concept maps" 'parent-concept-map "Parent concept map" 'select-concept-map "Select a CMap" 'concept-map-loaded "CMap loaded" 'select-listed-page "Select a wiki page from the list or clear the field." 'select-listed-concept-map "Select a CMap from the list or clear the field." 'unsaved-concept-map "Unsaved concept map" 'save-concept-map-before-leaving "Save the changes to this concept map before leaving?" 'dont-save "Don't save"
|
||||
'edit-concept "Edit concept" 'aspects "Aspects" 'aspects-placeholder "Security, performance" 'concept-description-page "Concept description page" 'concept-description-page-placeholder "cmap:concept-name" 'view-concept-description "View concept description" 'view-description-page "View description page" 'invalid-description-page "Enter a valid description page address." 'linked-page "Linked wiki page" 'no-linked-page "No linked page" 'filter-pages "Select or enter a new page" 'linked-concept-map "Linked concept map" 'no-linked-concept-map "No linked concept map" 'filter-concept-maps "Filter concept maps" 'parent-concept-map "Parent concept map" 'select-concept-map "Select a CMap" 'concept-map-loaded "CMap loaded" 'select-listed-page "Select a wiki page from the list or clear the field." 'invalid-new-page "Enter a page title or valid wiki address." 'select-listed-concept-map "Select a CMap from the list or clear the field." 'unsaved-concept-map "Unsaved concept map" 'save-concept-map-before-leaving "Save the changes to this concept map before leaving?" 'dont-save "Don't save"
|
||||
'concept-image "Concept image" 'remove-image "Remove image" 'text-color "Text color" 'main-concept-background-color "Main concept background color" 'submap-appearance "Sub-CMap appearance" 'submap-background-color "Sub-CMap background color" 'submap-border-color "Sub-CMap border color" 'link-concept-map "Link to CMap" 'insert-concept-map-link "Insert CMap link" 'concept-map-link-help "Insert the selected CMap as a link or as a read-only view." 'concept-map-not-found "CMap not found" 'insert "Insert" 'insert-link "Insert link" 'embed-concept-map "Embed CMap" 'embedded-concept-map-help "Double-click to open this CMap." 'a4-page-boundaries "A4 page boundaries"
|
||||
'search-wiki "Search pages and concept maps" 'contents "Contents" 'pages "Pages" 'namespace "Namespace" 'namespace-placeholder "RWS" 'root-namespace "Root" 'wiki-page "Wiki page" 'concept-map "Concept map" 'no-matching-search-results "No matching pages or concept maps."
|
||||
'edit "Edit" 'rename "Rename" 'rename-page "Rename page" 'slug "Slug" 'rename-summary "Renamed page" 'edit-section "Edit section" 'template "Template" 'no-template "No template" 'replace-with-template "Replace the current page content with template {template}?" 'history "History" 'delete "Delete" 'page-not-found "Page not found"
|
||||
@@ -105,24 +124,43 @@
|
||||
'grouped-submap-synopsis "Gegroepeerde sub-conceptmap."
|
||||
'navigation-graph "Navigatiegraaf" 'click "klik"
|
||||
'cmap-help-add-selection "Aan de selectie toevoegen of eruit verwijderen"
|
||||
'copy-paste-linked-concepts "Gekoppelde concepten kopiëren/plakken"
|
||||
'copy-linked-concepts "Conceptverwijzing kopiëren" 'paste-linked-concepts "Gekoppeld concept plakken"
|
||||
'concept-usage-count "{count} plaatsingen in alle conceptmaps"
|
||||
'connected-cmap-concepts "Verbonden CMap-concepten" 'concept-map-locations "Locaties in conceptmaps"
|
||||
'copy-wiki-link "Wikilink kopiëren" 'wiki-link-copied "Wikilink gekopieerd"
|
||||
'open-linked-item "Gekoppelde pagina of CMap openen"
|
||||
'appearance "Vormgeving" 'style "Stijl" 'apply-style "Toepassen"
|
||||
'concept-heading-appearance "Kop" 'concept-subtext-appearance "Subtekst"
|
||||
'change-palette-color "dubbelklik om te wijzigen"
|
||||
'color-swatch-help "Klik voor het palet; dubbelklik voor een eigen kleur"
|
||||
'style-default "Standaard" 'style-subtle "Subtiel" 'style-emphasis "Nadruk" 'style-warning "Waarschuwing" 'style-success "Succes"
|
||||
'bold "Vet" 'italic "Cursief"
|
||||
'cmap-help-rename "Het geselecteerde concept bewerken"
|
||||
'cmap-help-clear-selection "Selectie opheffen"
|
||||
'concept-map-help-expanded "Selecteer een concept om de grepen te tonen. Sleep een relatie naar een concept of naar lege ruimte. Dubbelklik op een paginaconcept om het te openen; dubbelklik op een sub-CMap om die uit of in te klappen."
|
||||
'concept-map-help-context "Selecteer met Ctrl/Cmd/Shift-klik of door over lege ruimte te slepen. Sleep een geselecteerd element om de volledige selectie te verplaatsen. Groeperen maakt een nieuwe sub-CMap rond de selectie. Dubbelklik op een pagina of sub-CMap om die te openen; klik met rechts voor de bewerkingsfuncties."
|
||||
'concept-map-help-expanded "Selecteer een concept om de grepen te tonen. Sleep een relatie naar een concept of naar lege ruimte. Houd Alt ingedrukt wanneer u hem op een concept loslaat om een directe lijn te maken. Dubbelklik op een concept om het te bewerken; dubbelklik op een sub-CMap om die uit of in te klappen."
|
||||
'concept-map-help-context "Selecteer met Ctrl/Cmd/Shift-klik of door over lege ruimte te slepen. Sleep een geselecteerd element om de volledige selectie te verplaatsen. Houd Alt ingedrukt tijdens het tekenen van een relatie om een directe lijn zonder relatietekst te maken. Groeperen maakt een nieuwe sub-CMap rond de selectie. Dubbelklik op een concept om het te bewerken of op een sub-CMap om die te openen; klik met rechts voor meer functies."
|
||||
'detach-submap-confirm "\"{concept}\" buiten submap \"{submap}\" plaatsen?"
|
||||
'promote-submap "Aparte CMap maken" 'submap-name "Naam van de nieuwe conceptmap"
|
||||
'submap-derived "Zelfstandige weergave voor sub-CMap aangemaakt"
|
||||
'hidden-concepts "Verborgen concepten" 'hide-in-this-view "In deze weergave verbergen"
|
||||
'show-hidden-concept "Dit concept weer tonen"
|
||||
'before-submap-extraction "Voor het maken van de gedeelde sub-CMap-weergave" 'submap-extracted "Sub-CMap verplaatst naar een aparte CMap" 'empty-submap "Deze sub-CMap bevat geen concepten om te verplaatsen." 'submap-created-parent-unsaved "De nieuwe CMap is aangemaakt, maar de bovenliggende CMap moet nog worden opgeslagen."
|
||||
'rename-concept-map "CMap hernoemen" 'delete-concept-map "CMap verwijderen" 'redo-action "Opnieuw uitvoeren" 'save-now "Nu opslaan"
|
||||
'set-start-concept-map "Als start-CMap gebruiken" 'start-concept-map "Start-CMap" 'start-concept-map-set "Start-CMap ingesteld"
|
||||
'autosave-pending "Wijzigingen wachten op opslaan" 'autosaving "Automatisch opslaan…" 'concept-map-autosaved "CMap automatisch opgeslagen"
|
||||
'concept-map-renamed "CMap hernoemd" 'concept-map-deleted "CMap verwijderd"
|
||||
'concept-map-history "CMap-geschiedenis" 'concept-map-history-help "Laad een historische versie in de editor om hem te bekijken. Opslaan maakt hiervan een nieuwe actuele versie."
|
||||
'concept-map-history "CMap-geschiedenis" 'concept-map-history-help "Snapshots blijven onbeperkt bewaard. Van expliciete opslag worden maximaal vijf versies bewaard; automatische opslag komt niet in de geschiedenis."
|
||||
'create-concept-map-snapshot "Snapshot maken…" 'snapshot-description "Omschrijving van snapshot" 'snapshot "Snapshot"
|
||||
'creating-snapshot "Snapshot maken…" 'snapshot-created "Snapshot gemaakt"
|
||||
'concept-map-version-loaded "Versie {version} geladen; sla op om hem actueel te maken." 'load-version "Versie laden" 'current-version "Actueel"
|
||||
'no-concept-map-history "Nog geen snapshots of handmatig opgeslagen versies." 'delete-history-item "Verwijderen"
|
||||
'delete-concept-map-history-confirm "CMap-geschiedenisversie {version} verwijderen? De actuele CMap verandert niet." 'concept-map-history-deleted "Geschiedenisitem verwijderd"
|
||||
'automatic-save "Automatisch opgeslagen" 'manual-save "Handmatig opgeslagen" 'concept-map-created-version "CMap aangemaakt"
|
||||
'concept-map-renamed-version "CMap hernoemd" 'concept-map-initial-version "Eerste beschikbare versie"
|
||||
'delete-concept-map-confirm "Conceptmap \"{title}\" verwijderen? Links ernaartoe blijven bestaan, maar kunnen de conceptmap niet meer openen."
|
||||
'reload-concept-map "CMap opnieuw laden"
|
||||
'edit-concept "Concept bewerken" 'linked-page "Gekoppelde wikipagina" 'no-linked-page "Geen gekoppelde pagina" 'filter-pages "Filter pagina's" 'linked-concept-map "Gekoppelde conceptmap" 'no-linked-concept-map "Geen gekoppelde conceptmap" 'filter-concept-maps "Filter conceptmaps" 'parent-concept-map "Bovenliggende conceptmap" 'select-concept-map "Selecteer een CMap" 'concept-map-loaded "CMap geladen" 'select-listed-page "Selecteer een wikipagina uit de lijst of maak het veld leeg." 'select-listed-concept-map "Selecteer een CMap uit de lijst of maak het veld leeg." 'unsaved-concept-map "Niet-opgeslagen conceptmap" 'save-concept-map-before-leaving "De wijzigingen aan deze conceptmap opslaan voordat u hem verlaat?" 'dont-save "Niet opslaan"
|
||||
'edit-concept "Concept bewerken" 'aspects "Aspecten" 'aspects-placeholder "Beveiliging, performance" 'concept-description-page "Beschrijvingspagina van concept" 'concept-description-page-placeholder "cmap:conceptnaam" 'view-concept-description "Conceptbeschrijving bekijken" 'view-description-page "Beschrijvingspagina bekijken" 'invalid-description-page "Voer een geldig adres voor de beschrijvingspagina in." 'linked-page "Gekoppelde wikipagina" 'no-linked-page "Geen gekoppelde pagina" 'filter-pages "Selecteer of typ een nieuwe pagina" 'linked-concept-map "Gekoppelde conceptmap" 'no-linked-concept-map "Geen gekoppelde conceptmap" 'filter-concept-maps "Filter conceptmaps" 'parent-concept-map "Bovenliggende conceptmap" 'select-concept-map "Selecteer een CMap" 'concept-map-loaded "CMap geladen" 'select-listed-page "Selecteer een wikipagina uit de lijst of maak het veld leeg." 'invalid-new-page "Voer een paginatitel of geldig wiki-adres in." 'select-listed-concept-map "Selecteer een CMap uit de lijst of maak het veld leeg." 'unsaved-concept-map "Niet-opgeslagen conceptmap" 'save-concept-map-before-leaving "De wijzigingen aan deze conceptmap opslaan voordat u hem verlaat?" 'dont-save "Niet opslaan"
|
||||
'concept-image "Afbeelding bij concept" 'remove-image "Afbeelding verwijderen" 'text-color "Tekstkleur" 'main-concept-background-color "Achtergrondkleur hoofdconcept" 'submap-appearance "Vormgeving sub-CMap" 'submap-background-color "Achtergrondkleur sub-CMap" 'submap-border-color "Randkleur sub-CMap" 'link-concept-map "Link naar CMap" 'insert-concept-map-link "CMap invoegen" 'concept-map-link-help "Voeg de geselecteerde CMap in als link of als alleen-lezen weergave." 'concept-map-not-found "CMap niet gevonden" 'insert "Invoegen" 'insert-link "Link invoegen" 'embed-concept-map "CMap insluiten" 'embedded-concept-map-help "Dubbelklik om deze CMap te openen." 'a4-page-boundaries "A4-paginagrenzen"
|
||||
'search-wiki "Pagina's en conceptmaps doorzoeken" 'contents "Inhoud" 'pages "Pagina's" 'namespace "Namespace" 'namespace-placeholder "RWS" 'root-namespace "Hoofdnamespace" 'wiki-page "Wikipagina" 'concept-map "Conceptmap" 'no-matching-search-results "Geen overeenkomende wikipagina's of conceptmaps."
|
||||
'edit "Bewerken" 'rename "Hernoemen" 'rename-page "Pagina hernoemen" 'slug "Slug" 'rename-summary "Pagina hernoemd" 'edit-section "Sectie bewerken" 'template "Sjabloon" 'no-template "Geen sjabloon" 'replace-with-template "De huidige pagina-inhoud vervangen door sjabloon {template}?" 'history "Geschiedenis" 'delete "Verwijderen" 'page-not-found "Pagina niet gevonden"
|
||||
|
||||
Reference in New Issue
Block a user