@morphemecms/cms 0.11.0 → 0.13.0-next-20261106203613
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/astro.mjs +2 -1
- package/defaults/blockTexts.ts +9 -9
- package/defaults/collections.ts +257 -257
- package/defaults/colors.ts +74 -74
- package/defaults/company.ts +9 -9
- package/defaults/editor.ts +65 -65
- package/defaults/i18n/da-DK/blockTexts.ts +12 -0
- package/defaults/i18n/da-DK/collections.ts +220 -0
- package/defaults/i18n/da-DK/colors.ts +102 -0
- package/defaults/i18n/da-DK/company.ts +7 -0
- package/defaults/i18n/da-DK/editor.ts +13 -0
- package/defaults/i18n/da-DK/index.ts +31 -0
- package/defaults/i18n/da-DK/internalLinks.ts +12 -0
- package/defaults/i18n/da-DK/opens.ts +7 -0
- package/defaults/i18n/da-DK/popup.ts +9 -0
- package/defaults/i18n/da-DK/privacy.ts +38 -0
- package/defaults/i18n/da-DK/rules.ts +56 -0
- package/defaults/i18n/da-DK/sitedata.ts +8 -0
- package/defaults/i18n/da-DK/standard.ts +8 -0
- package/defaults/i18n/da-DK/standardPages.ts +17 -0
- package/defaults/internalLinks.ts +8 -8
- package/defaults/opens.ts +2 -2
- package/defaults/popup.tsx +7 -7
- package/defaults/privacy.ts +37 -37
- package/defaults/rules.ts +68 -66
- package/defaults/schema.ts +1 -1
- package/defaults/sitedata.ts +4 -4
- package/defaults/standard.ts +9 -6
- package/defaults/standardPages.ts +14 -14
- package/defaults/texts.ts +7 -510
- package/lib/appId.d.mts +1 -0
- package/lib/appId.mjs +6 -0
- package/lib/appRoute.d.mts +2 -2
- package/lib/appRoute.mjs +3 -3
- package/lib/appRoute.test.mjs +8 -8
- package/lib/apps.mjs +13 -3
- package/lib/apps.test.mjs +10 -1
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/folderChanges.d.mts +29 -0
- package/lib/folderChanges.mjs +34 -0
- package/lib/licenses.mjs +2 -2
- package/lib/licenses.owner.test.mjs +9 -0
- package/lib/menu.mjs +4 -4
- package/lib/menu.test.mjs +101 -101
- package/lib/scheduled.mjs +1 -1
- package/lib/section-ranges.d.mts +1 -1
- package/lib/section-ranges.mjs +8 -14
- package/lib/section-ranges.test.mjs +26 -5
- package/lib/storeEntries.d.mts +3 -1
- package/lib/storeEntries.mjs +22 -8
- package/lib/storeEntries.test.mjs +59 -7
- package/lib/text-sections.d.mts +11 -1
- package/lib/text-sections.mjs +18 -8
- package/lib/text-sections.test.mjs +34 -1
- package/lib/translate.d.mts +11 -0
- package/lib/translate.mjs +13 -1
- package/lib/translate.test.mjs +23 -2
- package/lib/video-id.d.mts +6 -2
- package/lib/video-id.mjs +5 -1
- package/lib/video.d.mts +6 -2
- package/lib/video.mjs +5 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +19 -4
- package/lib/without-h1.test.mjs +30 -1
- package/package.json +3 -2
- package/scripts/app-screenshots.mjs +9 -8
- package/scripts/check-content.mjs +12 -2
- package/scripts/check-language.mjs +0 -1
- package/scripts/check-site-types.mjs +26 -0
- package/scripts/editor-screens.mjs +3 -3
- package/scripts/site-types.mjs +14 -0
- package/scripts/site-types.test.mjs +21 -0
- package/site/activeTheme.test.mjs +52 -0
- package/site/appSelf.d.mts +2 -0
- package/site/appSelf.mjs +23 -0
- package/site/appSelf.test.mjs +35 -0
- package/site/content.ts +5 -2
- package/site/defineSite.mjs +2 -2
- package/site/schema.ts +3 -3
- package/site/siteModules.mjs +2 -1
- package/site/theme.mjs +19 -8
- package/src/Advanced.tsx +2 -2
- package/src/App.tsx +79 -78
- package/src/AppAdds.tsx +1 -1
- package/src/AppList.tsx +39 -24
- package/src/BlockDialog.tsx +39 -18
- package/src/BlockParts.tsx +6 -6
- package/src/BlockSettings.tsx +2 -2
- package/src/BrandForm.tsx +2 -2
- package/src/Colors.tsx +6 -6
- package/src/CompanyForm.tsx +1 -1
- package/src/Consents.tsx +3 -3
- package/src/CreateFab.tsx +8 -0
- package/src/DataForm.tsx +10 -20
- package/src/Deleted.tsx +3 -3
- package/src/EditorFooter.tsx +2 -2
- package/src/EntryForm.tsx +3 -3
- package/src/FooterSettings.tsx +2 -2
- package/src/FormFrame.tsx +4 -3
- package/src/HeaderSettings.tsx +3 -3
- package/src/Keywords.tsx +5 -5
- package/src/Library.tsx +75 -82
- package/src/Licenses.tsx +8 -8
- package/src/ListFab.tsx +13 -0
- package/src/MainDrawerParts.tsx +3 -3
- package/src/MainMenu.tsx +1 -1
- package/src/MarkdownEditor.tsx +88 -47
- package/src/Media.tsx +11 -20
- package/src/MediaTags.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Parts.tsx +17 -17
- package/src/Redirects.tsx +14 -22
- package/src/Screen.tsx +16 -6
- package/src/SeoScores.tsx +6 -6
- package/src/SideColumn.tsx +2 -2
- package/src/StartFab.tsx +6 -8
- package/src/ThemeStore.tsx +37 -16
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +7 -8
- package/src/checkSite.ts +86 -20
- package/src/drawerYield.d.mts +3 -0
- package/src/drawerYield.mjs +10 -0
- package/src/drawerYield.test.mjs +22 -0
- package/src/editor.css +59 -38
- package/src/examples.ts +1 -1
- package/src/fabScreens.test.mjs +28 -0
- package/src/fabScreens.ts +16 -0
- package/src/i18n/da-DK/library.ts +2 -2
- package/src/i18n/da-DK/text.ts +1 -1
- package/src/i18n/en-US/apps.ts +1 -1
- package/src/i18n/en-US/common.ts +8 -8
- package/src/i18n/en-US/library.ts +5 -5
- package/src/i18n/en-US/parts.ts +3 -3
- package/src/i18n/en-US/text.ts +1 -1
- package/src/i18n/index.ts +4 -8
- package/src/images.ts +2 -2
- package/src/legacyMove.test.mjs +137 -0
- package/src/legacyMove.ts +131 -0
- package/src/licenseData.ts +3 -3
- package/src/markdown.mjs +10 -7
- package/src/markdown.test.mjs +16 -2
- package/src/menuChildren.ts +5 -5
- package/src/menuOpen.ts +20 -4
- package/src/movedRoutes.d.mts +8 -1
- package/src/movedRoutes.mjs +102 -21
- package/src/movedRoutes.test.mjs +128 -6
- package/src/nav.ts +1 -1
- package/src/notificationData.ts +1 -1
- package/src/pageH1Panel.d.mts +4 -0
- package/src/pageH1Panel.mjs +31 -0
- package/src/pageH1Panel.test.mjs +56 -0
- package/src/pagePreview.tsx +22 -12
- package/src/schema.ts +2 -2
- package/src/sectionFrame.ts +3 -3
- package/src/sectionState.d.mts +3 -3
- package/src/sectionState.mjs +6 -6
- package/src/sectionState.test.mjs +14 -2
- package/src/sheetPlacement.d.mts +6 -4
- package/src/sheetPlacement.mjs +15 -10
- package/src/sheetPlacement.test.mjs +16 -9
- package/src/sidePanel.ts +38 -0
- package/src/site.ts +37 -13
- package/src/standard/accordion/index.tsx +1 -1
- package/src/standard/accordion/shared.ts +1 -1
- package/src/standard/buttons/index.tsx +1 -1
- package/src/standard/cards/index.tsx +1 -1
- package/src/standard/cards/shared.ts +1 -1
- package/src/standard/cookies/index.tsx +1 -1
- package/src/standard/posts/index.tsx +1 -1
- package/src/standard/testimonials/index.tsx +1 -1
- package/src/standard/testimonials/list.ts +1 -1
- package/src/store.ts +36 -31
- package/src/tagUses.d.mts +1 -1
- package/src/tagUses.mjs +2 -2
- package/src/tagUses.test.mjs +1 -1
- package/src/topBarSlot.tsx +7 -15
- package/src/ui.tsx +33 -22
- package/src/virtual.d.ts +6 -0
- package/starter/package.json +1 -1
- package/theme/components/PageHeading.astro +22 -0
- package/theme/layouts/Base.astro +2 -2
- package/theme/layouts/PageLayout.astro +37 -20
- package/theme/styles/global.css +2 -1
- package/theme/styles/ui.css +40 -10
package/src/App.tsx
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
1
|
// The editor at /editor/ (docs/editor.md): sign in with GitHub, pick a part of the site, edit and
|
|
2
2
|
// save. The layout: the full-height navigation rail or drawer (MainMenu.tsx) with, beside it, the top bar (topBarSlot.tsx) over the screen. The address after # says what is open, so reloading keeps it:
|
|
3
3
|
// (empty) the start page
|
|
4
|
-
//
|
|
5
|
-
// (AppDefinition.tabs):
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// list/<id> the list of a part, e.g. list/priser, and list/<id>/<app> a tab an installed app adds to it
|
|
5
|
+
// (AppDefinition.tabs): list/cookies/cookiescanner
|
|
6
|
+
// lists the short lists (the testimonials, the apps' lists)
|
|
7
|
+
// settings/appstore the app store, its apps, and settings/appstore/themes its themes (AppList.tsx StorePage); the
|
|
8
8
|
// other settings are screens of their own, in the drawer's last section (MainMenu.tsx)
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// new/<id> a new entry, new/<id>/<kind> one of a kind (a pop-up: standard, or an app's)
|
|
10
|
+
// company the business, and company/<part> its parts: contact, payment; and
|
|
11
|
+
// company/payment/<part> its own parts: mobilepay, bank
|
|
12
12
|
// <screen> a screen with parts of the site's own (partScreens), and <screen>/<part> its parts:
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// its category:
|
|
16
|
-
//
|
|
17
|
-
// seo "Søgemaskineoptimering" (Library.tsx Seo): seo/
|
|
18
|
-
// (Redirects.tsx): seo/
|
|
19
|
-
// the addresses visitors came to that do not exist; seo/
|
|
13
|
+
// privacy/cookie-banner, privacy/privacy-pages, or one of the editor's own screens (Part.opens):
|
|
14
|
+
// privacy/consents the visitors' answers in the cookie banner (Consents.tsx); and the settings of the apps installed with
|
|
15
|
+
// its category: analytics/google-analytics
|
|
16
|
+
// files the uploaded pictures, PDFs, videos and zip files (Media.tsx)
|
|
17
|
+
// seo "Søgemaskineoptimering" (Library.tsx Seo): seo/redirects the redirects' two lists
|
|
18
|
+
// (Redirects.tsx): seo/redirects/all the redirects, and seo/redirects/not-found
|
|
19
|
+
// the addresses visitors came to that do not exist; seo/rating the SEO rating of every page and
|
|
20
20
|
// post (SeoScores.tsx); and seo/<part> the site's own (seoParts)
|
|
21
|
-
//
|
|
22
|
-
// a theme's page, with the button that swaps to it (ThemePage),
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// and icon (BrandForm.tsx),
|
|
29
|
-
// one in every state (Parts.tsx),
|
|
30
|
-
// blocks, the standard ones and the apps', and
|
|
31
|
-
// ("Promovering"), its own and the apps' (BlockParts.tsx), and
|
|
21
|
+
// appearance how the site looks (Library.tsx Theme): appearance/themes the themes installed (Themes) and appearance/themes/<theme>
|
|
22
|
+
// a theme's page, with the button that swaps to it (ThemePage), appearance/icon-packs the icon packs (IconPacks),
|
|
23
|
+
// appearance/motion how it moves (MotionSchemes), appearance/side-column the block beside the pages' text (SideColumn),
|
|
24
|
+
// appearance/header the header's menus and settings with its preview (HeaderSettings.tsx; once at header),
|
|
25
|
+
// appearance/footer the footer's texts and links with its preview (FooterSettings.tsx; once at src/data/footer.json),
|
|
26
|
+
// appearance/colors the colours (Colors.tsx;
|
|
27
|
+
// appearance/colors/fallback-colors the site's own while an app changes the pages'), appearance/logo the logo
|
|
28
|
+
// and icon (BrandForm.tsx), appearance/parts the parts (Library.tsx PartsHome): appearance/parts/editor the editor's, every
|
|
29
|
+
// one in every state (Parts.tsx), appearance/parts/ui the theme's UI components, appearance/parts/blocks the
|
|
30
|
+
// blocks, the standard ones and the apps', and appearance/parts/list/<list> each kind of a list of kinds
|
|
31
|
+
// ("Promovering"), its own and the apps' (BlockParts.tsx), and appearance/parts/values the
|
|
32
32
|
// theme's design tokens, "Værdier" (Tokens.tsx)
|
|
33
|
-
//
|
|
34
|
-
//
|
|
33
|
+
// block-settings the blocks that can be put in, "Blokindstillinger" (BlockSettings.tsx)
|
|
34
|
+
// advanced "Avanceret", under "Indstillinger": the copy of the site's files in R2 (Advanced.tsx)
|
|
35
35
|
// morpheme "Morpheme", under "Indstillinger": the CMS's version, a newer one and its notes, an update's progress (CmsPage.tsx)
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// licenses "Licenser", under "Indstillinger": the licences of what the site and the editor are made with, and
|
|
37
|
+
// licenses/<id> one with its text (Licenses.tsx)
|
|
38
38
|
// apps the apps that come with the site, apps/<app> one with its button to install it and, while it
|
|
39
39
|
// is installed, the tabs of its page (AppList.tsx AppPage): Settings at apps/<app>, with its settings
|
|
40
|
-
// form (also on the screen with its category,
|
|
41
|
-
// Information at apps/<app>/
|
|
42
|
-
// apps/<app>/
|
|
43
|
-
// (Field.page), over its Settings tab, which keeps what is in it: apps/content-colors/
|
|
40
|
+
// form (also on the screen with its category, analytics/<app>, AppList.tsx settingsRoute), and
|
|
41
|
+
// Information at apps/<app>/about (before an app's own screens, AppDefinition.screens)
|
|
42
|
+
// apps/<app>/settings/<field> the page of a field of its settings with its choices on a page of their own
|
|
43
|
+
// (Field.page), over its Settings tab, which keeps what is in it: apps/content-colors/settings/variant
|
|
44
44
|
// <form>/<field> the page of a field of a part's form with its choices on a page of their own (Field.page), over the
|
|
45
45
|
// form, which keeps what is in it (formAt)
|
|
46
|
-
//
|
|
47
|
-
//
|
|
46
|
+
// blocks the blocks, and blocks/<block> the texts all its copies have (blockTexts.ts)
|
|
47
|
+
// deleted/<id> what was deleted from a list, or deleted/files the files (Deleted.tsx)
|
|
48
48
|
// <file path> a file being edited, e.g. src/content/blog/mit-foerste-indlaeg.md
|
|
49
49
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
50
50
|
import {
|
|
@@ -276,9 +276,9 @@ function routeOf(hash: string) {
|
|
|
276
276
|
const route = movedRoute(screenOf(hash), STORE_MOVED);
|
|
277
277
|
// An app's settings: on the screen with its category, when it has one ("Analytics", AppList.tsx settingsRoute), and so
|
|
278
278
|
// the pages of their fields (Field.page) after them. Otherwise they are on the Settings tab of its page: apps/<id>
|
|
279
|
-
// (a field's page, apps/<id>/
|
|
280
|
-
const app = apps.find((a) => route === `apps/${a.id}/
|
|
281
|
-
const rest = app ? route.slice(`apps/${app.id}/
|
|
279
|
+
// (a field's page, apps/<id>/settings/<field>, stays, drawn by the same page).
|
|
280
|
+
const app = apps.find((a) => route === `apps/${a.id}/settings` || route.startsWith(`apps/${a.id}/settings/`));
|
|
281
|
+
const rest = app ? route.slice(`apps/${app.id}/settings`.length) : '';
|
|
282
282
|
const kindScreen = app && settingsHome(app) && settingsRoute(app);
|
|
283
283
|
const moved = !app ? route : kindScreen ? `${kindScreen}${rest}` : rest ? route : `apps/${app.id}`;
|
|
284
284
|
if (moved !== screenOf(hash)) history.replaceState(history.state, '', `#${moved}`);
|
|
@@ -557,7 +557,7 @@ function Editor() {
|
|
|
557
557
|
replace([file, ...(more?.files ?? [])], more?.removed);
|
|
558
558
|
// A new entry, or a post that got a new address: from now on it is edited as the file it
|
|
559
559
|
// became. A form that saves another file than the one at its address (Virksomheden) stays where it is.
|
|
560
|
-
if (route.startsWith('
|
|
560
|
+
if (route.startsWith('new/') || more?.removed.includes(route)) {
|
|
561
561
|
history.replaceState(history.state, '', `#${file.path}`);
|
|
562
562
|
setRoute(file.path);
|
|
563
563
|
}
|
|
@@ -578,7 +578,7 @@ function Editor() {
|
|
|
578
578
|
replace([], [file.path]);
|
|
579
579
|
// Back to the list, as with the back arrow: the deleted entry is not left behind it in the history.
|
|
580
580
|
setUnsaved(false);
|
|
581
|
-
up(`#
|
|
581
|
+
up(`#list/${collection.id}`);
|
|
582
582
|
notify({ text: savedText(change), kind: 'saved' });
|
|
583
583
|
} catch (e) {
|
|
584
584
|
notify({ text: t.app.notDeleted((e as Error).message), error: true, kind: 'failed' });
|
|
@@ -668,7 +668,7 @@ function Editor() {
|
|
|
668
668
|
}
|
|
669
669
|
>
|
|
670
670
|
<>
|
|
671
|
-
<div className="editor-content min-w-0 flex-1">
|
|
671
|
+
<div className="editor-content relative min-w-0 flex-1">
|
|
672
672
|
{files ? (
|
|
673
673
|
<Page key={visit} route={route} files={files} env={env} user={user} builtAt={builtAt} onLogout={signOut} />
|
|
674
674
|
) : (
|
|
@@ -689,15 +689,15 @@ function Editor() {
|
|
|
689
689
|
/** The part whose form is at an address (CompanyForm.tsx): one of "Virksomheden"'s (or of its parts', with the settings of the apps of their kinds), "Søgemaskineoptimering"'s, "Udseende"'s, or one on a screen of parts with
|
|
690
690
|
* the settings of the apps of its kinds ("Analytics"). */
|
|
691
691
|
function partAt(route: string, files: RepoFile[]): Part | null {
|
|
692
|
-
//
|
|
692
|
+
// company/<part>, or company/<part>/<its part> (Part.parts).
|
|
693
693
|
const [, companyId, ownId, ...more] = route.split('/');
|
|
694
|
-
const company = route.startsWith('
|
|
694
|
+
const company = route.startsWith('company/') && !more.length && companyParts.find((p) => p.id === companyId);
|
|
695
695
|
return (
|
|
696
696
|
(company &&
|
|
697
697
|
(ownId ? [...(company.parts ?? []), ...appPartsIn(company, files)].find((p) => p.id === ownId) : company.file && company)) ||
|
|
698
|
-
(route.startsWith('
|
|
698
|
+
(route.startsWith('blocks/') && blockParts.find((p) => p.id === route.slice(7))) ||
|
|
699
699
|
(route.startsWith('seo/') && seoParts.find((p) => p.id === route.slice(4))) ||
|
|
700
|
-
(route.startsWith('
|
|
700
|
+
(route.startsWith('appearance/') && appearance.find((p) => p.id === route.slice('appearance/'.length))) ||
|
|
701
701
|
partScreens.flatMap((s) => [...s.parts, ...appPartsIn(s, files)].filter((p) => route === `${s.id}/${p.id}`))[0] ||
|
|
702
702
|
null
|
|
703
703
|
);
|
|
@@ -718,13 +718,13 @@ type PageProps = { route: string; files: RepoFile[]; env: Env; user: Viewer | nu
|
|
|
718
718
|
|
|
719
719
|
function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
720
720
|
if (!route) return <Start builtAt={builtAt} />;
|
|
721
|
-
if (route === '
|
|
722
|
-
if (route === '
|
|
723
|
-
if (route === '
|
|
724
|
-
if (route === '
|
|
725
|
-
if (route === '
|
|
726
|
-
if (route === '
|
|
727
|
-
return <Media key={route} files={files} env={env} open={route === '
|
|
721
|
+
if (route === 'account') return <Account user={user} files={files} onLogout={onLogout} />;
|
|
722
|
+
if (route === 'notifications') return <Notifications />;
|
|
723
|
+
if (route === 'lists') return <Lists files={files} />;
|
|
724
|
+
if (route === 'company') return <Company files={files} />;
|
|
725
|
+
if (route === 'blocks') return <Blocks files={files} />;
|
|
726
|
+
if (route === 'files' || route.startsWith('files/'))
|
|
727
|
+
return <Media key={route} files={files} env={env} open={route === 'files' ? undefined : '/' + route.slice('files/'.length)} />;
|
|
728
728
|
if (route === 'seo') return <Seo />;
|
|
729
729
|
if (route === REDIRECTS_ROUTE) return <Redirects env={env} />;
|
|
730
730
|
if (route === SEO_SCORES_ROUTE) return <SeoScores files={files} env={env} />;
|
|
@@ -735,22 +735,23 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
|
735
735
|
// A part of a screen of parts that is one of the editor's own screens (Part.opens), going back to that screen.
|
|
736
736
|
const opening = partScreens.flatMap((s) => s.parts.filter((p) => p.opens && route === `${s.id}/${p.id}`).map((p) => ({ s, p })))[0];
|
|
737
737
|
if (opening?.p.opens === 'consents') return <Consents env={env} back={{ href: `#${opening.s.id}`, label: opening.s.label }} />;
|
|
738
|
-
if (route === '
|
|
739
|
-
if (route === '
|
|
740
|
-
if (route === '
|
|
741
|
-
if (route === '
|
|
742
|
-
if (route === '
|
|
743
|
-
if (route === '
|
|
744
|
-
if (route === '
|
|
738
|
+
if (route === 'appearance') return <Theme files={files} />;
|
|
739
|
+
if (route === 'appearance/themes') return <Themes env={env} files={files} />;
|
|
740
|
+
if (route === 'appearance/icon-packs') return <IconPacks env={env} files={files} />;
|
|
741
|
+
if (route === 'appearance/motion') return <MotionSchemes env={env} files={files} />;
|
|
742
|
+
if (route === 'appearance/side-column') return <SideColumn env={env} files={files} />;
|
|
743
|
+
if (route === 'appearance/header') return <HeaderSettings env={env} />;
|
|
744
|
+
if (route === 'appearance/footer') return <FooterSettings env={env} />;
|
|
745
745
|
// A theme installed has its page; one from the store that is not (or not yet built), its page in the store.
|
|
746
|
-
const themeShown = route.startsWith('
|
|
746
|
+
const themeShown = route.startsWith('appearance/themes/') && themes.find((x) => x.id === route.slice('appearance/themes/'.length));
|
|
747
747
|
if (themeShown) return <ThemePage key={themeShown.id} of={themeShown} env={env} files={files} />;
|
|
748
|
-
if (/^
|
|
748
|
+
if (/^appearance\/themes\/[^/]+$/.test(route))
|
|
749
|
+
return <StoreThemePage key={route} id={route.slice('appearance/themes/'.length)} env={env} />;
|
|
749
750
|
// The colours, or the site's own on a page of the same form while an app changes the pages' (FALLBACK_ROUTE).
|
|
750
|
-
if (route === '
|
|
751
|
+
if (route === 'appearance/colors' || route === FALLBACK_ROUTE)
|
|
751
752
|
return <Colors env={env} files={files} fallback={route === FALLBACK_ROUTE} />;
|
|
752
|
-
if (route === '
|
|
753
|
-
if (route === '
|
|
753
|
+
if (route === 'appearance/logo') return <BrandForm env={env} />;
|
|
754
|
+
if (route === 'block-settings') return <BlockSettings env={env} files={files} />;
|
|
754
755
|
if (route === ADVANCED_ROUTE) return <Advanced env={env} />;
|
|
755
756
|
if (route === CMS_ROUTE) return <CmsPage />;
|
|
756
757
|
if (route === LICENSES_ROUTE) return <Licenses />;
|
|
@@ -761,7 +762,7 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
|
761
762
|
if (route.startsWith(`${DEVELOPER_ROUTE}/`))
|
|
762
763
|
return <DeveloperPage key={route} slug={route.slice(DEVELOPER_ROUTE.length + 1)} env={env} files={files} />;
|
|
763
764
|
// An app in the site (its own, or one from the store while it is installed) has its page, with the tabs Settings
|
|
764
|
-
// (apps/<id>, and a settings field's page, apps/<id>/
|
|
765
|
+
// (apps/<id>, and a settings field's page, apps/<id>/settings/<field>) and Information (apps/<id>/about, matched
|
|
765
766
|
// before an app's own screens); one from the store that is not (or no longer) installed, its page in the store.
|
|
766
767
|
const shownApp = route.startsWith('apps/') ? apps.find((a) => appRouteOf(route, a.id)) : undefined;
|
|
767
768
|
const shown = shownApp && appRouteOf(route, shownApp.id);
|
|
@@ -776,29 +777,29 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
|
776
777
|
route.startsWith('apps/') && !deeper.length && apps.find((a) => a.id === screenApp && screenId && a.screens?.[screenId]);
|
|
777
778
|
if (ownScreen && installedApps(files).includes(ownScreen.id))
|
|
778
779
|
return <AppScreenPage key={route} app={ownScreen} id={screenId} env={env} />;
|
|
779
|
-
if (route === '
|
|
780
|
-
if (route === '
|
|
781
|
-
if (route === '
|
|
782
|
-
if (route === '
|
|
783
|
-
if (route.startsWith('
|
|
784
|
-
if (route === '
|
|
785
|
-
if (route.startsWith('
|
|
786
|
-
//
|
|
780
|
+
if (route === 'appearance/parts') return <PartsHome />;
|
|
781
|
+
if (route === 'appearance/parts/editor') return <Parts env={env} />;
|
|
782
|
+
if (route === 'appearance/parts/ui') return <ComponentParts />;
|
|
783
|
+
if (route === 'appearance/parts/blocks') return <BlocksParts />;
|
|
784
|
+
if (route.startsWith('appearance/parts/list/')) return <KindParts key={route} id={route.slice('appearance/parts/list/'.length)} />;
|
|
785
|
+
if (route === 'appearance/parts/values') return <Tokens />;
|
|
786
|
+
if (route.startsWith('deleted/')) return <Deleted id={route.slice('deleted/'.length)} files={files} env={env} />;
|
|
787
|
+
// company/<part>, or company/<part>/<its part> (Part.parts).
|
|
787
788
|
const [, companyId, ownId, ...more] = route.split('/');
|
|
788
|
-
const company = route.startsWith('
|
|
789
|
+
const company = route.startsWith('company/') && !more.length && companyParts.find((p) => p.id === companyId);
|
|
789
790
|
if (company && company.parts && !ownId) return <Company part={company} files={files} />;
|
|
790
791
|
// A part's form, or the page of one of its fields (Field.page) with the form under it: the same form, so it keeps what
|
|
791
792
|
// is in it while the editor goes between them (useFormPages, inForm).
|
|
792
793
|
const form = formAt(route, files);
|
|
793
794
|
if (form) return <CompanyForm part={form.part} at={form.at} page={form.page} env={env} files={files} />;
|
|
794
|
-
// A list, or a tab an installed app adds to it (
|
|
795
|
+
// A list, or a tab an installed app adds to it (list/<id>/<app>).
|
|
795
796
|
const [, listId, listTab, ...below] = route.split('/');
|
|
796
|
-
const listed = route.startsWith('
|
|
797
|
+
const listed = route.startsWith('list/') && !below.length && collections.find((c) => c.id === listId);
|
|
797
798
|
if (listed) return <List key={listed.id} collection={listed} tab={listTab} files={files} env={env} />;
|
|
798
799
|
|
|
799
|
-
//
|
|
800
|
+
// new/<list>, or new/<list>/<kind> for one of its kinds (a pop-up: standard, mailchimp).
|
|
800
801
|
const [, newIn, newKind] = route.split('/');
|
|
801
|
-
const creating = route.startsWith('
|
|
802
|
+
const creating = route.startsWith('new/') && collections.find((c) => c.id === newIn && c.create);
|
|
802
803
|
if (creating) {
|
|
803
804
|
if (creating.kind === 'markdown') return <EntryForm collection={creating} file={newMarkdown(creating, notOffered(files))} env={env} />;
|
|
804
805
|
if (creating.kind === 'folder')
|
package/src/AppAdds.tsx
CHANGED
|
@@ -29,7 +29,7 @@ function addsOf(app: SiteApp, files: RepoFile[]) {
|
|
|
29
29
|
...kindsOf(app).map((k) => ({
|
|
30
30
|
...k,
|
|
31
31
|
block: undefined,
|
|
32
|
-
create: k.create ? { href: `#
|
|
32
|
+
create: k.create ? { href: `#new/${k.key}/${app.id}`, label: k.create } : undefined,
|
|
33
33
|
uses: entriesOf(app, k.key, files),
|
|
34
34
|
})),
|
|
35
35
|
];
|
package/src/AppList.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// with one from the store not yet built as "Installeres…"), each with who made it under its name and, from the store,
|
|
3
3
|
// that it has a newer version, with search (by name, who made it and what it does), the order (A–Å, Å–A, kept in this
|
|
4
4
|
// browser) and headings by the first letter, as the lists under "Indhold" have. "Indstillinger" › "Appstore"
|
|
5
|
-
// (#
|
|
5
|
+
// (#settings/appstore): all apps, those in the app store (store.ts) and the site's own, installed or not, with search (by name, what they
|
|
6
6
|
// do, who made them and the words they are found by), the same order and a funnel by category. An app's page
|
|
7
7
|
// (#apps/<app>) is the same for every app and without examples: its name, who made it, whether it is installed, its
|
|
8
8
|
// version (from the store) and what it does, "Installer" (or "Opdater til …"); installed, it has two tabs: "Indstillinger"
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
// adds (AppAdds.tsx: its blocks, the lists it brings: "Priser", each opening its list, and its kinds of the site's lists:
|
|
12
12
|
// a Mailchimp popup; each with its switch at the end of its row and opening a sheet with where it is used and the same
|
|
13
13
|
// switch, as under "Blokindstillinger", and a kind's button to a new one, while it is on), its notifications with their
|
|
14
|
-
// switches, and "Afinstaller", AppSettingsTab.tsx) and "Information" (#apps/<app>/
|
|
14
|
+
// switches, and "Afinstaller", AppSettingsTab.tsx) and "Information" (#apps/<app>/about, AppInfoTab.tsx: its pictures, what
|
|
15
15
|
// it does more fully ("Om appen"), what it needs of the site ("Rettigheder", needsOf in store.ts: its code on every page,
|
|
16
16
|
// the addresses it loads from and sends forms to, its part of the Worker, a step in the build), the cookies it sets (and
|
|
17
|
-
// whether the cookie banner asks first), who made it (a row to their page, #apps/
|
|
17
|
+
// whether the cookie banner asks first), who made it (a row to their page, #apps/developer/<name>, with all their apps),
|
|
18
18
|
// and where it is used); one not installed has no tabs; installing, updating and uninstalling are each saved at once in its own commit,
|
|
19
19
|
// as a block turned on or off under "Blokindstillinger". The button pressed shows the loading indicator while it runs (busyAction.mjs). One of the site's own is installed or uninstalled in the site's
|
|
20
20
|
// file of apps; one from the store is copied into the site, or deleted from it, with it (appChanges in store.ts), and
|
|
@@ -49,6 +49,11 @@ import { AnyIcon } from './icon';
|
|
|
49
49
|
import {
|
|
50
50
|
appCategories,
|
|
51
51
|
appChanges,
|
|
52
|
+
appliedTo,
|
|
53
|
+
installedHere,
|
|
54
|
+
legacyOf,
|
|
55
|
+
mergeChanges,
|
|
56
|
+
moveChanges,
|
|
52
57
|
appFolder,
|
|
53
58
|
breaking,
|
|
54
59
|
changedFiles,
|
|
@@ -64,7 +69,7 @@ import {
|
|
|
64
69
|
type StoreAdd,
|
|
65
70
|
type StoreRecord,
|
|
66
71
|
} from './store';
|
|
67
|
-
import { apps, siteBlocks } from './registry';
|
|
72
|
+
import { apps, apps as siteApps, siteBlocks } from './registry';
|
|
68
73
|
import { asWritten, backFrom, collections, companyParts, partScreens } from './site';
|
|
69
74
|
import { byText, slugify, type Part } from './schema';
|
|
70
75
|
import { headed, letterOf } from './Sheet';
|
|
@@ -94,10 +99,10 @@ export function settingsHome(app: SiteApp): { route: string; label: string } | u
|
|
|
94
99
|
const screen = partScreens.find((s) => s.apps?.includes(kind));
|
|
95
100
|
if (screen) return { route: screen.id, label: screen.label };
|
|
96
101
|
const company = companyParts.find((p) => p.parts && p.apps?.includes(kind));
|
|
97
|
-
return company && { route: `
|
|
102
|
+
return company && { route: `company/${company.id}`, label: company.label };
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
/** Where an app's settings are: on the screen with its kind (#
|
|
105
|
+
/** Where an app's settings are: on the screen with its kind (#analytics/google-analytics, #company/payment/mobilepay),
|
|
101
106
|
* else on the Settings tab of its page (#apps/<id>); null without settings. */
|
|
102
107
|
export function settingsRoute(app: SiteApp): string | null {
|
|
103
108
|
if (!app.settings) return null;
|
|
@@ -156,7 +161,7 @@ export const listsOf = (app: SiteApp, files: RepoFile[]) =>
|
|
|
156
161
|
key: l.id,
|
|
157
162
|
icon: l.icon,
|
|
158
163
|
label: l.label,
|
|
159
|
-
href: `#
|
|
164
|
+
href: `#list/${l.id}`,
|
|
160
165
|
count: files.filter((f) => f.path.startsWith(`${l.folder}/`) && f.path.endsWith('.json')).length,
|
|
161
166
|
}));
|
|
162
167
|
|
|
@@ -186,7 +191,7 @@ export function kindsOf(app: SiteApp) {
|
|
|
186
191
|
icon: kind.icon,
|
|
187
192
|
label: kind.label,
|
|
188
193
|
hint: kind.hint,
|
|
189
|
-
href: `#
|
|
194
|
+
href: `#list/${c.id}`,
|
|
190
195
|
one: c.singular,
|
|
191
196
|
more: c.label,
|
|
192
197
|
create: c.create,
|
|
@@ -273,7 +278,7 @@ const versionOf = (app: SiteApp) => updated.get(app.id) ?? app.store?.version ??
|
|
|
273
278
|
|
|
274
279
|
/** The newer version of an app from the store, when the store has one. */
|
|
275
280
|
const updateOf = (app: SiteApp, catalog: ShownApp[] | null | 'failed') => {
|
|
276
|
-
const found = app.store && Array.isArray(catalog) ? catalog.find((a) => a.id === app.id) : undefined;
|
|
281
|
+
const found = app.store && Array.isArray(catalog) ? catalog.find((a) => a.id === app.id || a.legacy === app.id) : undefined;
|
|
277
282
|
return found && newer(found.version, versionOf(app)) ? found : undefined;
|
|
278
283
|
};
|
|
279
284
|
|
|
@@ -284,7 +289,7 @@ export function Apps({ env, files }: { env: Env; files: RepoFile[] }) {
|
|
|
284
289
|
const installed = installedApps(files);
|
|
285
290
|
const catalog = useCatalog(env);
|
|
286
291
|
const store = Array.isArray(catalog) ? catalog : [];
|
|
287
|
-
const [query, setQuery] = useKept('apps-
|
|
292
|
+
const [query, setQuery] = useKept('apps-search', '');
|
|
288
293
|
const [sort, pickSort] = useSort();
|
|
289
294
|
|
|
290
295
|
// The apps installed: the site's own and those from the store; and one from the store in the file of apps that the
|
|
@@ -396,7 +401,7 @@ function AppRow({ app, installed }: { app: ReturnType<typeof allApps>[number]; i
|
|
|
396
401
|
);
|
|
397
402
|
}
|
|
398
403
|
|
|
399
|
-
/** A developer's page (#apps/
|
|
404
|
+
/** A developer's page (#apps/developer/<name>): their name, and all their apps and themes (in the store and the site's
|
|
400
405
|
* own), each opening its page. */
|
|
401
406
|
export function DeveloperPage({ slug, env, files }: { slug: string; env: Env; files: RepoFile[] }) {
|
|
402
407
|
const installed = installedApps(files);
|
|
@@ -457,9 +462,9 @@ export function DeveloperRow({ name }: { name: string }) {
|
|
|
457
462
|
);
|
|
458
463
|
}
|
|
459
464
|
|
|
460
|
-
/** "Appstore" (#
|
|
465
|
+
/** "Appstore" (#settings/appstore, under "Indstillinger"): in a tab of its own each in the top app bar, all apps,
|
|
461
466
|
* those in the app store and the site's own, each with what it does and whether it is installed, and all themes
|
|
462
|
-
* (#
|
|
467
|
+
* (#settings/appstore/themes, ThemeStore.tsx), those in the store
|
|
463
468
|
* and those installed, each with its version and who made it; each opening its page. With search (by name, what it
|
|
464
469
|
* does, who made it and the words it is found by, in both languages), the order and the filters by category (apps)
|
|
465
470
|
* and by whether it is installed, each picked one as a chip. */
|
|
@@ -478,10 +483,10 @@ function StoreScreen({ env, files, tab, again }: { env: Env; files: RepoFile[];
|
|
|
478
483
|
const catalog = useCatalog(env);
|
|
479
484
|
const themeCatalog = useThemeCatalog(env);
|
|
480
485
|
const connected = useStoreConnected(env);
|
|
481
|
-
const [query, setQuery] = useKept('
|
|
486
|
+
const [query, setQuery] = useKept('store-search', '');
|
|
482
487
|
const [sort, pickSort] = useSort();
|
|
483
|
-
const [category, setCategory] = useKept<AppCategory | ''>('
|
|
484
|
-
const [state, setState] = useKept<'' | 'installed' | 'notInstalled'>('
|
|
488
|
+
const [category, setCategory] = useKept<AppCategory | ''>('store-category', '');
|
|
489
|
+
const [state, setState] = useKept<'' | 'installed' | 'notInstalled'>('store-status', '');
|
|
485
490
|
const apps = tab === 'apps';
|
|
486
491
|
const read = apps ? catalog : themeCatalog;
|
|
487
492
|
|
|
@@ -491,7 +496,7 @@ function StoreScreen({ env, files, tab, again }: { env: Env; files: RepoFile[];
|
|
|
491
496
|
allApps(catalog)
|
|
492
497
|
.filter((a) => words.every((w) => a.words.includes(w)))
|
|
493
498
|
.filter((a) => !category || a.category === category)
|
|
494
|
-
.filter((a) => !state ||
|
|
499
|
+
.filter((a) => !state || installedHere(a, installed, siteApps) === (state === 'installed'))
|
|
495
500
|
.sort((a, b) => order(a.label, b.label)),
|
|
496
501
|
(a) => letterOf(a.label),
|
|
497
502
|
);
|
|
@@ -692,11 +697,13 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
692
697
|
next: () => Promise<Parameters<typeof appChanges>[2]>,
|
|
693
698
|
// Whether to go on, with the app's files in the site now (an update asks, when some were changed).
|
|
694
699
|
go: (current: RepoFile[]) => Promise<boolean> = async () => true,
|
|
700
|
+
// The id the app is installed under, when it is a legacy copy that moves to `id` (moveChanges).
|
|
701
|
+
legacy: string | null = null,
|
|
695
702
|
): Promise<boolean> {
|
|
696
703
|
setRunning({ id, action, asking: false });
|
|
697
704
|
env.notify({ text: text.busy });
|
|
698
705
|
try {
|
|
699
|
-
const [current, wanted] = await Promise.all([env.folder(appFolder(id)), next()]);
|
|
706
|
+
const [current, wanted] = await Promise.all([env.folder(appFolder(legacy ?? id)), next()]);
|
|
700
707
|
if (!(await go(current))) {
|
|
701
708
|
env.notify(null);
|
|
702
709
|
return false;
|
|
@@ -704,8 +711,11 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
704
711
|
// `go` cleared the message (and its dialog stopped the button being busy): saving goes on.
|
|
705
712
|
setRunning({ id, action, asking: false });
|
|
706
713
|
env.notify({ text: text.busy });
|
|
707
|
-
const
|
|
708
|
-
|
|
714
|
+
const moved = legacy && wanted ? moveChanges({ id }, legacy, wanted, current, files) : null;
|
|
715
|
+
// The cookie list as the move leaves it (the legacy id rewritten), then the new release's cookies on it: one set of changes.
|
|
716
|
+
const cookies = cookieChanges(id, wanted ? cookiesIn(wanted.files) : null, moved ? appliedTo(files, moved) : files);
|
|
717
|
+
const changes = moved ? mergeChanges(moved, cookies) : [...appChanges(id, current, wanted, installed, file), ...cookies];
|
|
718
|
+
await saveApp(env, text.commit, text.change, changes, file);
|
|
709
719
|
env.notify({ text: text.done, kind: text.kind ?? 'saved' });
|
|
710
720
|
return true;
|
|
711
721
|
} catch (e) {
|
|
@@ -731,7 +741,7 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
731
741
|
},
|
|
732
742
|
async () => ({ files: await env.store.files(app), record: recordOf(app) }),
|
|
733
743
|
),
|
|
734
|
-
async update(app: ShownApp, record: StoreRecord) {
|
|
744
|
+
async update(app: ShownApp, record: StoreRecord, legacy: string | null = null) {
|
|
735
745
|
const done = await change(
|
|
736
746
|
app.id,
|
|
737
747
|
'update',
|
|
@@ -751,9 +761,10 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
751
761
|
record.version,
|
|
752
762
|
app.version,
|
|
753
763
|
notesSince(app.releaseNotes, record.version, app.version),
|
|
754
|
-
changedFiles(app.id, record, current),
|
|
764
|
+
changedFiles(app.id, record, current, appFolder(legacy ?? app.id)),
|
|
755
765
|
);
|
|
756
766
|
},
|
|
767
|
+
legacy,
|
|
757
768
|
);
|
|
758
769
|
if (done) updated.set(app.id, app.version);
|
|
759
770
|
return done;
|
|
@@ -937,7 +948,7 @@ type AppProps = { app: SiteApp; env: Env; files: RepoFile[] };
|
|
|
937
948
|
|
|
938
949
|
type AppTab = 'settings' | 'information';
|
|
939
950
|
|
|
940
|
-
/** The tabs of an installed app's page, in the top bar: Settings (#apps/<id>) and Information (#apps/<id>/
|
|
951
|
+
/** The tabs of an installed app's page, in the top bar: Settings (#apps/<id>) and Information (#apps/<id>/about). A form
|
|
941
952
|
* with unsaved changes asks first, as for every address (App.tsx). */
|
|
942
953
|
const tabsOf = (app: SiteApp, tab: AppTab): TabChoice<AppTab> => ({
|
|
943
954
|
label: t.apps.tabs,
|
|
@@ -1047,7 +1058,11 @@ function AppContent({ app, env, files, tab, fields }: AppProps & { tab: AppTab;
|
|
|
1047
1058
|
{(!on || (update && app.store)) && (
|
|
1048
1059
|
<div className="mb-8">
|
|
1049
1060
|
{on ? (
|
|
1050
|
-
<Button
|
|
1061
|
+
<Button
|
|
1062
|
+
disabled={saving}
|
|
1063
|
+
busy={store.busy(update!.id, 'update')}
|
|
1064
|
+
onClick={() => store.update(update!, app.store!, legacyOf(update!, apps))}
|
|
1065
|
+
>
|
|
1051
1066
|
{t.apps.update(update!.version)}
|
|
1052
1067
|
</Button>
|
|
1053
1068
|
) : (
|