@morphemecms/cms 0.9.0-next-20261105122349 → 0.9.0
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 +17 -1
- package/defaults/collections.ts +110 -56
- package/defaults/editor.ts +2 -2
- package/defaults/texts.ts +14 -5
- package/demo/src/content/pages/app-store.md +0 -1
- package/demo/src/content/pages/blocks.md +0 -1
- package/icons/lucide/arrows-horizontal.svg +1 -0
- package/icons/lucide/columns-plus-left.svg +1 -0
- package/icons/lucide/columns-plus-right.svg +1 -0
- package/icons/lucide/file.svg +1 -0
- package/icons/material-symbols/arrows-horizontal.svg +1 -0
- package/icons/material-symbols/columns-plus-left.svg +1 -0
- package/icons/material-symbols/columns-plus-right.svg +1 -0
- package/icons/material-symbols/file.svg +1 -0
- package/icons/phosphor/arrows-horizontal.svg +1 -0
- package/icons/phosphor/columns-plus-left.svg +1 -0
- package/icons/phosphor/columns-plus-right.svg +1 -0
- package/icons/phosphor/file.svg +1 -0
- package/lib/fold-open.d.mts +2 -0
- package/lib/fold-open.mjs +22 -0
- package/lib/fold-open.test.mjs +24 -0
- package/lib/menu.test.mjs +16 -3
- package/lib/page-template.d.mts +54 -0
- package/lib/page-template.mjs +126 -0
- package/lib/page-template.test.mjs +129 -0
- package/lib/side-block.d.mts +2 -16
- package/lib/side-block.mjs +12 -30
- package/lib/side-block.test.mjs +13 -30
- package/package.json +1 -1
- package/scripts/check-side.mjs +49 -22
- package/scripts/editor-roundtrip.mjs +15 -6
- package/scripts/fixtures/side/side-full.md +8 -0
- package/scripts/fixtures/side/side-left.md +8 -0
- package/scripts/fixtures/side/side-none.md +7 -0
- package/scripts/fixtures/side/side-right.md +8 -0
- package/scripts/fixtures/side/side-single.md +8 -0
- package/scripts/side-width.mjs +29 -0
- package/scripts/side-width.test.mjs +30 -0
- package/site/content.config.ts +3 -4
- package/site/header.d.mts +21 -0
- package/site/header.mjs +35 -0
- package/site/header.test.mjs +59 -0
- package/site/icons.d.mts +16 -0
- package/site/icons.mjs +8 -0
- package/site/site.ts +8 -3
- package/site/templates.ts +7 -0
- package/site/theme.mjs +3 -0
- package/src/App.tsx +12 -24
- package/src/AppList.tsx +36 -14
- package/src/AppSettingsTab.tsx +5 -3
- package/src/BlockDialog.tsx +50 -5
- package/src/DataFileFields.tsx +35 -0
- package/src/DataForm.tsx +16 -38
- package/src/Deleted.tsx +7 -3
- package/src/EntryForm.tsx +41 -88
- package/src/FooterSettings.tsx +76 -0
- package/src/FormFrame.tsx +2 -1
- package/src/HeaderSettings.tsx +97 -0
- package/src/Library.tsx +26 -14
- package/src/MarkdownEditor.tsx +18 -36
- package/src/Media.tsx +6 -3
- package/src/Redirects.tsx +12 -9
- package/src/SectionSettings.tsx +21 -0
- package/src/SectionSettingsIcon.tsx +10 -0
- package/src/SettingsSections.tsx +57 -0
- package/src/ThemeStore.tsx +28 -9
- package/src/Versions.tsx +3 -15
- package/src/busyAction.d.mts +3 -0
- package/src/busyAction.mjs +8 -0
- package/src/busyAction.test.mjs +16 -0
- package/src/dataFileValues.ts +30 -0
- package/src/editor.css +37 -3
- package/src/fields.tsx +29 -10
- package/src/footerPreview.d.mts +46 -0
- package/src/footerPreview.mjs +84 -0
- package/src/footerPreview.test.mjs +100 -0
- package/src/formSections.ts +100 -0
- package/src/frontmatter.mjs +1 -1
- package/src/frontmatter.test.mjs +11 -11
- package/src/headerPreview.d.mts +48 -0
- package/src/headerPreview.mjs +168 -0
- package/src/headerPreview.test.mjs +183 -0
- package/src/headerTheme.tsx +58 -0
- package/src/i18n/da-DK/common.ts +1 -0
- package/src/i18n/da-DK/library.ts +2 -0
- package/src/i18n/da-DK/preview.ts +2 -0
- package/src/i18n/da-DK/text.ts +1 -0
- package/src/i18n/en-US/common.ts +1 -0
- package/src/i18n/en-US/library.ts +4 -0
- package/src/i18n/en-US/preview.ts +3 -0
- package/src/i18n/en-US/text.ts +2 -1
- package/src/images.ts +7 -2
- package/src/menuChildren.ts +1 -3
- package/src/movedRoutes.d.mts +3 -0
- package/src/movedRoutes.mjs +35 -0
- package/src/movedRoutes.test.mjs +22 -0
- package/src/pagePreview.tsx +40 -29
- package/src/partPreview.tsx +59 -0
- package/src/schema.ts +7 -1
- package/src/sheetPlacement.d.mts +3 -0
- package/src/sheetPlacement.mjs +9 -0
- package/src/sheetPlacement.test.mjs +23 -0
- package/src/standard/views.ts +3 -3
- package/src/templateField.ts +28 -0
- package/src/themes.ts +3 -0
- package/src/ui.tsx +35 -8
- package/src/useDataFiles.ts +94 -0
- package/starter/package.json +1 -1
- package/theme/components/Footer.astro +14 -7
- package/theme/components/Header.astro +58 -33
- package/theme/components/MobileNav.astro +5 -4
- package/theme/components/RelatedPosts.astro +1 -1
- package/theme/components/headerParts.ts +17 -0
- package/theme/layouts/PageLayout.astro +20 -16
- package/theme/styles/base.css +6 -5
- package/theme/styles/blocks.css +8 -12
- package/theme/styles/components.css +8 -1
- package/theme/styles/global.css +5 -9
- package/theme/styles/ui.css +111 -5
- package/theme/theme.json +1 -0
- package/theme/ui/BusyContent.tsx +16 -0
- package/theme/ui/Button.tsx +25 -5
- package/theme/ui/IconButton.tsx +17 -4
- package/scripts/fixtures/side/side-off.md +0 -8
- package/scripts/fixtures/side/side-plain.md +0 -7
- package/scripts/fixtures/side/side-wide.md +0 -8
package/site/header.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The header's site setting (src/data/header.json, optional): a button in place of the phone icon, and the side of the
|
|
2
|
+
// header the mobile menu button sits on; and the main menu's items as the header draws them. Exposes headerOf (pure;
|
|
3
|
+
// missing or wrong data gives the defaults) and sortedNav. Used by site/site.ts and the editor's preview of the header
|
|
4
|
+
// (src/headerPreview.mjs); the Button type is src/standard/buttons/fields.ts.
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('../src/standard/buttons/fields').Button} Button */
|
|
7
|
+
/** @typedef {{ button: Button | null, menuSide: 'start' | 'end' }} Header */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads src/data/header.json's content. Missing or wrong → defaults.
|
|
11
|
+
* @param {unknown} raw
|
|
12
|
+
* @returns {Header}
|
|
13
|
+
*/
|
|
14
|
+
export function headerOf(raw) {
|
|
15
|
+
const data = raw && typeof raw === 'object' ? /** @type {Record<string, any>} */ (raw) : {};
|
|
16
|
+
const { enabled, ...button } = data.button && typeof data.button === 'object' ? data.button : { enabled: false };
|
|
17
|
+
return {
|
|
18
|
+
button: enabled === true ? button : null,
|
|
19
|
+
menuSide: data.menuSide === 'end' ? 'end' : 'start',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @typedef {{ label: string, href: string }} NavLink */
|
|
24
|
+
/** @typedef {NavLink & { children?: NavLink[] }} NavItem */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The main menu's items (menu.json) as the header draws them: the sub-items in alphabetical order (Danish: æ, ø, å last).
|
|
28
|
+
* @template {NavItem} T
|
|
29
|
+
* @param {T[]} items
|
|
30
|
+
* @returns {T[]}
|
|
31
|
+
*/
|
|
32
|
+
export const sortedNav = (items) =>
|
|
33
|
+
items.map((item) =>
|
|
34
|
+
item.children ? { ...item, children: [...item.children].sort((a, b) => a.label.localeCompare(b.label, 'da')) } : item,
|
|
35
|
+
);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { headerOf, sortedNav } from './header.mjs';
|
|
4
|
+
|
|
5
|
+
test('no file', () => {
|
|
6
|
+
assert.deepEqual(headerOf(undefined), { button: null, menuSide: 'start' });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('button off', () => {
|
|
10
|
+
assert.equal(headerOf({ button: { enabled: false, to: 'booking' } }).button, null);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('button on', () => {
|
|
14
|
+
assert.deepEqual(headerOf({ button: { enabled: true, to: 'booking', label: 'Book en tid' } }).button, {
|
|
15
|
+
to: 'booking',
|
|
16
|
+
label: 'Book en tid',
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('menu at the end', () => {
|
|
21
|
+
assert.equal(headerOf({ menuSide: 'end' }).menuSide, 'end');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('wrong menu side', () => {
|
|
25
|
+
assert.equal(headerOf({ menuSide: 'middle' }).menuSide, 'start');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('wrong shapes', () => {
|
|
29
|
+
assert.deepEqual(headerOf('x'), { button: null, menuSide: 'start' });
|
|
30
|
+
assert.equal(headerOf({ button: 'x' }).button, null);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('sortedNav sorts the sub-items A–Å and keeps the items in order', () => {
|
|
34
|
+
const items = [
|
|
35
|
+
{ label: 'Om', href: '/om/' },
|
|
36
|
+
{
|
|
37
|
+
label: 'Behandlinger',
|
|
38
|
+
href: '/b/',
|
|
39
|
+
children: [
|
|
40
|
+
{ label: 'Øre', href: '/b/oe/' },
|
|
41
|
+
{ label: 'Arm', href: '/b/a/' },
|
|
42
|
+
{ label: 'Zone', href: '/b/z/' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const sorted = sortedNav(items);
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
sorted.map((i) => i.label),
|
|
49
|
+
['Om', 'Behandlinger'],
|
|
50
|
+
);
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
sorted[1].children.map((c) => c.label),
|
|
53
|
+
['Arm', 'Zone', 'Øre'],
|
|
54
|
+
);
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
items[1].children.map((c) => c.label),
|
|
57
|
+
['Øre', 'Arm', 'Zone'],
|
|
58
|
+
);
|
|
59
|
+
});
|
package/site/icons.d.mts
CHANGED
|
@@ -107,6 +107,7 @@ export const MATERIAL_SYMBOLS: {
|
|
|
107
107
|
'arrow-up': string;
|
|
108
108
|
'arrow-up-right': string;
|
|
109
109
|
'arrows-down-up': string;
|
|
110
|
+
'arrows-horizontal': string;
|
|
110
111
|
article: string;
|
|
111
112
|
bank: string;
|
|
112
113
|
bell: string;
|
|
@@ -142,6 +143,8 @@ export const MATERIAL_SYMBOLS: {
|
|
|
142
143
|
code: string;
|
|
143
144
|
coffee: string;
|
|
144
145
|
columns: string;
|
|
146
|
+
'columns-plus-left': string;
|
|
147
|
+
'columns-plus-right': string;
|
|
145
148
|
cookie: string;
|
|
146
149
|
copy: string;
|
|
147
150
|
'credit-card': string;
|
|
@@ -155,6 +158,7 @@ export const MATERIAL_SYMBOLS: {
|
|
|
155
158
|
'envelope-simple-fill': string;
|
|
156
159
|
eye: string;
|
|
157
160
|
'eye-slash': string;
|
|
161
|
+
file: string;
|
|
158
162
|
'file-pdf': string;
|
|
159
163
|
'file-text': string;
|
|
160
164
|
'file-text-fill': string;
|
|
@@ -290,6 +294,7 @@ export const LUCIDE: {
|
|
|
290
294
|
'arrow-up': string;
|
|
291
295
|
'arrow-up-right': string;
|
|
292
296
|
'arrows-down-up': string;
|
|
297
|
+
'arrows-horizontal': string;
|
|
293
298
|
'arrows-in-line-horizontal': string;
|
|
294
299
|
'arrows-out-line-horizontal': string;
|
|
295
300
|
article: string;
|
|
@@ -328,6 +333,8 @@ export const LUCIDE: {
|
|
|
328
333
|
code: string;
|
|
329
334
|
coffee: string;
|
|
330
335
|
columns: string;
|
|
336
|
+
'columns-plus-left': string;
|
|
337
|
+
'columns-plus-right': string;
|
|
331
338
|
cookie: string;
|
|
332
339
|
copy: string;
|
|
333
340
|
'credit-card': string;
|
|
@@ -341,6 +348,7 @@ export const LUCIDE: {
|
|
|
341
348
|
'envelope-simple-fill': string;
|
|
342
349
|
eye: string;
|
|
343
350
|
'eye-slash': string;
|
|
351
|
+
file: string;
|
|
344
352
|
'file-pdf': string;
|
|
345
353
|
'file-text': string;
|
|
346
354
|
'file-text-fill': string;
|
|
@@ -479,6 +487,7 @@ export const PACK_MAPS: {
|
|
|
479
487
|
'arrow-up': string;
|
|
480
488
|
'arrow-up-right': string;
|
|
481
489
|
'arrows-down-up': string;
|
|
490
|
+
'arrows-horizontal': string;
|
|
482
491
|
article: string;
|
|
483
492
|
bank: string;
|
|
484
493
|
bell: string;
|
|
@@ -514,6 +523,8 @@ export const PACK_MAPS: {
|
|
|
514
523
|
code: string;
|
|
515
524
|
coffee: string;
|
|
516
525
|
columns: string;
|
|
526
|
+
'columns-plus-left': string;
|
|
527
|
+
'columns-plus-right': string;
|
|
517
528
|
cookie: string;
|
|
518
529
|
copy: string;
|
|
519
530
|
'credit-card': string;
|
|
@@ -527,6 +538,7 @@ export const PACK_MAPS: {
|
|
|
527
538
|
'envelope-simple-fill': string;
|
|
528
539
|
eye: string;
|
|
529
540
|
'eye-slash': string;
|
|
541
|
+
file: string;
|
|
530
542
|
'file-pdf': string;
|
|
531
543
|
'file-text': string;
|
|
532
544
|
'file-text-fill': string;
|
|
@@ -660,6 +672,7 @@ export const PACK_MAPS: {
|
|
|
660
672
|
'arrow-up': string;
|
|
661
673
|
'arrow-up-right': string;
|
|
662
674
|
'arrows-down-up': string;
|
|
675
|
+
'arrows-horizontal': string;
|
|
663
676
|
'arrows-in-line-horizontal': string;
|
|
664
677
|
'arrows-out-line-horizontal': string;
|
|
665
678
|
article: string;
|
|
@@ -698,6 +711,8 @@ export const PACK_MAPS: {
|
|
|
698
711
|
code: string;
|
|
699
712
|
coffee: string;
|
|
700
713
|
columns: string;
|
|
714
|
+
'columns-plus-left': string;
|
|
715
|
+
'columns-plus-right': string;
|
|
701
716
|
cookie: string;
|
|
702
717
|
copy: string;
|
|
703
718
|
'credit-card': string;
|
|
@@ -711,6 +726,7 @@ export const PACK_MAPS: {
|
|
|
711
726
|
'envelope-simple-fill': string;
|
|
712
727
|
eye: string;
|
|
713
728
|
'eye-slash': string;
|
|
729
|
+
file: string;
|
|
714
730
|
'file-pdf': string;
|
|
715
731
|
'file-text': string;
|
|
716
732
|
'file-text-fill': string;
|
package/site/icons.mjs
CHANGED
|
@@ -121,6 +121,7 @@ export const MATERIAL_SYMBOLS = {
|
|
|
121
121
|
'arrow-up': 'arrow_upward',
|
|
122
122
|
'arrow-up-right': 'arrow_outward',
|
|
123
123
|
'arrows-down-up': 'swap_vert',
|
|
124
|
+
'arrows-horizontal': 'width_full',
|
|
124
125
|
article: 'article',
|
|
125
126
|
bank: 'account_balance',
|
|
126
127
|
bell: 'notifications',
|
|
@@ -156,6 +157,8 @@ export const MATERIAL_SYMBOLS = {
|
|
|
156
157
|
code: 'code',
|
|
157
158
|
coffee: 'local_cafe',
|
|
158
159
|
columns: 'view_column_2',
|
|
160
|
+
'columns-plus-left': 'dock_to_right',
|
|
161
|
+
'columns-plus-right': 'dock_to_left',
|
|
159
162
|
cookie: 'cookie',
|
|
160
163
|
copy: 'content_copy',
|
|
161
164
|
'credit-card': 'credit_card',
|
|
@@ -169,6 +172,7 @@ export const MATERIAL_SYMBOLS = {
|
|
|
169
172
|
'envelope-simple-fill': 'mail-fill',
|
|
170
173
|
eye: 'visibility',
|
|
171
174
|
'eye-slash': 'visibility_off',
|
|
175
|
+
file: 'crop_portrait',
|
|
172
176
|
'file-pdf': 'picture_as_pdf',
|
|
173
177
|
'file-text': 'description',
|
|
174
178
|
'file-text-fill': 'description-fill',
|
|
@@ -305,6 +309,7 @@ export const LUCIDE = {
|
|
|
305
309
|
'arrow-up': 'arrow-up',
|
|
306
310
|
'arrow-up-right': 'arrow-up-right',
|
|
307
311
|
'arrows-down-up': 'arrow-down-up',
|
|
312
|
+
'arrows-horizontal': 'rectangle-horizontal',
|
|
308
313
|
'arrows-in-line-horizontal': 'fold-horizontal',
|
|
309
314
|
'arrows-out-line-horizontal': 'unfold-horizontal',
|
|
310
315
|
article: 'file-text',
|
|
@@ -343,6 +348,8 @@ export const LUCIDE = {
|
|
|
343
348
|
code: 'code',
|
|
344
349
|
coffee: 'coffee',
|
|
345
350
|
columns: 'columns-2',
|
|
351
|
+
'columns-plus-left': 'panel-left',
|
|
352
|
+
'columns-plus-right': 'panel-right',
|
|
346
353
|
cookie: 'cookie',
|
|
347
354
|
copy: 'copy',
|
|
348
355
|
'credit-card': 'credit-card',
|
|
@@ -356,6 +363,7 @@ export const LUCIDE = {
|
|
|
356
363
|
'envelope-simple-fill': 'mail-fill',
|
|
357
364
|
eye: 'eye',
|
|
358
365
|
'eye-slash': 'eye-off',
|
|
366
|
+
file: 'rectangle-vertical',
|
|
359
367
|
'file-pdf': 'file-text',
|
|
360
368
|
'file-text': 'file-text',
|
|
361
369
|
'file-text-fill': 'file-text-fill',
|
package/site/site.ts
CHANGED
|
@@ -12,6 +12,7 @@ import blocks from '@data/blocks.json';
|
|
|
12
12
|
import standardPages from '@data/standard-pages.json';
|
|
13
13
|
import { config } from '@site/config';
|
|
14
14
|
import { socialOf } from './social.mjs';
|
|
15
|
+
import { headerOf, sortedNav } from './header.mjs';
|
|
15
16
|
import { sharedTexts, type SharedTexts } from '@morphemecms/cms/src/shared';
|
|
16
17
|
import { standardShared } from '@morphemecms/cms/src/standard/sharedTexts';
|
|
17
18
|
|
|
@@ -24,6 +25,11 @@ const { cvr = '' } = contact as typeof contact & { cvr?: string };
|
|
|
24
25
|
// Optional file: an older site has no social.json, so it is read with a glob, not an import.
|
|
25
26
|
const socialFile = Object.values(import.meta.glob<Record<string, unknown>>('/src/data/social.json', { eager: true, import: 'default' }))[0];
|
|
26
27
|
|
|
28
|
+
// Optional file (header.json, "Header"): a site without it keeps the header as it is.
|
|
29
|
+
const headerFile = Object.values(import.meta.glob<Record<string, unknown>>('/src/data/header.json', { eager: true, import: 'default' }))[0];
|
|
30
|
+
/** The header's button (in place of the phone icon, or null) and the side of the mobile menu button. */
|
|
31
|
+
export const header = headerOf(headerFile);
|
|
32
|
+
|
|
27
33
|
export const site = {
|
|
28
34
|
name: contact.name,
|
|
29
35
|
role: contact.role,
|
|
@@ -65,13 +71,12 @@ export const site = {
|
|
|
65
71
|
|
|
66
72
|
// Primary navigation (menu.json, "Hovedmenu"). Labels, URLs and sub-items started out as the old site's menu.
|
|
67
73
|
export type NavItem = { label: string; href: string; children?: { label: string; href: string }[] };
|
|
68
|
-
|
|
74
|
+
// Sub-items in alphabetical order (Danish: æ, ø, å last), in both the desktop and the mobile menu.
|
|
75
|
+
export const nav: NavItem[] = sortedNav(menu.items);
|
|
69
76
|
/** Whether the header has the menu and its button ("Aktiveret" under "Hovedmenu"). */
|
|
70
77
|
export const navShown = menu.enabled !== false;
|
|
71
78
|
/** The links in the bar under the header on a phone (mobile-menu.json, "Mobilmenu"); none when it is turned off. */
|
|
72
79
|
export const mobileNav: { label: string; href: string }[] = mobileMenu.enabled ? mobileMenu.items : [];
|
|
73
|
-
// Sub-items in alphabetical order (Danish: æ, ø, å last), in both the desktop and the mobile menu.
|
|
74
|
-
for (const item of nav) item.children?.sort((a, b) => a.label.localeCompare(b.label, 'da'));
|
|
75
80
|
|
|
76
81
|
/** The texts every copy of a block has ("Blokke" in the editor), with {…} for what differs, by
|
|
77
82
|
* the block's type: those written in its shared.ts, with the saved ones (blocks.json) over them. The
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The page templates the site's theme offers: the theme in use's ("templates" in its theme.json), else the standard theme's.
|
|
2
|
+
// Exposes offeredTemplates, read by PageLayout.astro and the check in scripts/check-side.mjs.
|
|
3
|
+
import { templatesOf } from '../lib/page-template.mjs';
|
|
4
|
+
import theme from '@theme/theme.json';
|
|
5
|
+
|
|
6
|
+
/** The templates a page can choose, the first the default. */
|
|
7
|
+
export const offeredTemplates: string[] = templatesOf(theme as { templates?: unknown });
|
package/site/theme.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { installedPacks, isIconName, packFolder, packProblems, phosphorFile, svg
|
|
|
14
14
|
import { MATERIAL_ALL, MATERIAL_SSR, materialModule, materialSsrModule } from './materialSymbols.mjs';
|
|
15
15
|
import { ICON_PACK_FILE, ICON_PACK_LICENSE, SYSTEM_PACKS, THEME_PACKS_FOLDER, packInUse, packTheme, themePackId } from './iconPacks.mjs';
|
|
16
16
|
import { MOTION_SCHEMES } from '@morphemecms/cms/lib/motion-schemes.mjs';
|
|
17
|
+
import { templateProblems } from '@morphemecms/cms/lib/page-template.mjs';
|
|
17
18
|
import { storeImportProblems } from './storeTheme.mjs';
|
|
18
19
|
|
|
19
20
|
/** The site's choice of icon pack, `{ "pack": "<id>" }` (config.icons in src/site/config.ts). */
|
|
@@ -400,6 +401,8 @@ export function themeProblems(theme) {
|
|
|
400
401
|
problems.push(
|
|
401
402
|
`${themeFolder(theme)}/theme.json: "motion" er den bevægelse, temaet bruger, når ingen er valgt: ${MOTION_SCHEMES.map((s) => `"${s}"`).join(', ')}.`,
|
|
402
403
|
);
|
|
404
|
+
// The page templates it offers (lib/page-template.mjs); without the key, the standard theme's list.
|
|
405
|
+
for (const p of templateProblems(manifest.templates)) problems.push(`${themeFolder(theme)}/theme.json: ${p}`);
|
|
403
406
|
// Its own licence and the licences of what it brings (fonts, code, pictures), for "Licenser" and the store.
|
|
404
407
|
problems.push(
|
|
405
408
|
...declarationProblems({
|
package/src/App.tsx
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// (empty) the start page
|
|
4
4
|
// liste/<id> the list of a part, e.g. liste/priser, and liste/<id>/<app> a tab an installed app adds to it
|
|
5
5
|
// (AppDefinition.tabs): liste/cookies/cookiescanner
|
|
6
|
-
// header what is in the header on every page (the menu)
|
|
7
6
|
// lister the short lists (the testimonials, the apps' lists)
|
|
8
7
|
// indstillinger/appstore the app store, its apps, and indstillinger/appstore/temaer its themes (AppList.tsx StorePage); the
|
|
9
8
|
// other settings are screens of their own, in the drawer's last section (MainMenu.tsx)
|
|
@@ -22,6 +21,8 @@
|
|
|
22
21
|
// udseende how the site looks (Library.tsx Theme): udseende/temaer the themes installed (Themes) and udseende/temaer/<theme>
|
|
23
22
|
// a theme's page, with the button that swaps to it (ThemePage), udseende/ikonpakker the icon packs (IconPacks),
|
|
24
23
|
// udseende/bevaegelse how it moves (MotionSchemes), udseende/sidekolonne the block beside the pages' text (SideColumn),
|
|
24
|
+
// udseende/header the header's menus and settings with its preview (HeaderSettings.tsx; once at header),
|
|
25
|
+
// udseende/footer the footer's texts and links with its preview (FooterSettings.tsx; once at src/data/footer.json),
|
|
25
26
|
// udseende/farver the colours (Colors.tsx;
|
|
26
27
|
// udseende/farver/reservefarver the site's own while an app changes the pages'), udseende/logo the logo
|
|
27
28
|
// and icon (BrandForm.tsx), udseende/dele the parts (Library.tsx PartsHome): udseende/dele/editor the editor's, every
|
|
@@ -132,12 +133,13 @@ import {
|
|
|
132
133
|
import Colors, { colorsOf, FALLBACK_ROUTE } from './Colors';
|
|
133
134
|
import { applyColors, applyMotion } from './theme';
|
|
134
135
|
import SideColumn from './SideColumn';
|
|
136
|
+
import HeaderSettings from './HeaderSettings';
|
|
137
|
+
import FooterSettings from './FooterSettings';
|
|
135
138
|
import { apps, notOffered } from './registry';
|
|
136
139
|
import {
|
|
137
140
|
Account,
|
|
138
141
|
Blocks,
|
|
139
142
|
Company,
|
|
140
|
-
Header,
|
|
141
143
|
List,
|
|
142
144
|
Lists,
|
|
143
145
|
PartsHome,
|
|
@@ -168,6 +170,7 @@ import { arrived, screenOf, up } from './nav';
|
|
|
168
170
|
import { siteT, siteText, t } from './i18n';
|
|
169
171
|
import { snackbar } from '../lib/snackbar';
|
|
170
172
|
import { appRouteOf } from '../lib/appRoute.mjs';
|
|
173
|
+
import { movedRoute } from './movedRoutes.mjs';
|
|
171
174
|
import { appType, clearNotifications, isOn, remember, signOff, stopServerWait, syncDevice, told, waitOnServer } from './notificationData';
|
|
172
175
|
import Notifications from './Notifications';
|
|
173
176
|
import { EditorOwn, InEditor } from './standard/view';
|
|
@@ -262,32 +265,15 @@ function taken(address: string, relations: Record<Relation, Option[]>, redirects
|
|
|
262
265
|
return { text: t.app.addressRedirects(address, name), redirect: { end, name, via } };
|
|
263
266
|
}
|
|
264
267
|
|
|
265
|
-
//
|
|
266
|
-
const
|
|
267
|
-
farver: 'udseende/farver',
|
|
268
|
-
'virksomheden/logo': 'udseende/logo',
|
|
269
|
-
'tema/blokvalg': 'blokvalg',
|
|
270
|
-
dele: 'udseende/dele',
|
|
271
|
-
tema: 'udseende',
|
|
272
|
-
'tema/farver': 'udseende/farver',
|
|
273
|
-
'tema/logo': 'udseende/logo',
|
|
274
|
-
'tema/dele': 'udseende/dele',
|
|
275
|
-
'tema/dele/editor': 'udseende/dele/editor',
|
|
276
|
-
'virksomheden/bank': 'virksomheden/betaling/bank',
|
|
277
|
-
videresendelser: 'seo/videresendelser',
|
|
278
|
-
'videresendelser/alle': 'seo/videresendelser/alle',
|
|
279
|
-
'videresendelser/ikke-fundet': 'seo/videresendelser/ikke-fundet',
|
|
280
|
-
// The settings of the booking and newsletter apps were on a screen "Promovering"; they are on the apps' own pages now.
|
|
281
|
-
promovering: 'apps',
|
|
282
|
-
'promovering/calendly': 'apps/calendly',
|
|
283
|
-
'promovering/mailchimp': 'apps/mailchimp',
|
|
268
|
+
// The app store's screens that have moved (movedRoutes.mjs has the others).
|
|
269
|
+
const STORE_MOVED: Record<string, string> = {
|
|
284
270
|
[STORE_BEFORE]: STORE_ROUTE,
|
|
285
271
|
[`${STORE_BEFORE}/temaer`]: STORE_THEMES,
|
|
286
272
|
};
|
|
287
273
|
|
|
288
274
|
// The editor's address after the "#" (nav.ts screenOf).
|
|
289
275
|
function routeOf(hash: string) {
|
|
290
|
-
const route =
|
|
276
|
+
const route = movedRoute(screenOf(hash), STORE_MOVED);
|
|
291
277
|
// An app's settings: on the screen with its category, when it has one ("Analytics", AppList.tsx settingsRoute), and so
|
|
292
278
|
// the pages of their fields (Field.page) after them. Otherwise they are on the Settings tab of its page: apps/<id>
|
|
293
279
|
// (a field's page, apps/<id>/indstillinger/<field>, stays, drawn by the same page).
|
|
@@ -734,7 +720,6 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
|
734
720
|
if (!route) return <Start builtAt={builtAt} />;
|
|
735
721
|
if (route === 'konto') return <Account user={user} files={files} onLogout={onLogout} />;
|
|
736
722
|
if (route === 'notifikationer') return <Notifications />;
|
|
737
|
-
if (route === 'header') return <Header files={files} />;
|
|
738
723
|
if (route === 'lister') return <Lists files={files} />;
|
|
739
724
|
if (route === 'virksomheden') return <Company files={files} />;
|
|
740
725
|
if (route === 'blokke') return <Blocks files={files} />;
|
|
@@ -755,6 +740,8 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
|
755
740
|
if (route === 'udseende/ikonpakker') return <IconPacks env={env} files={files} />;
|
|
756
741
|
if (route === 'udseende/bevaegelse') return <MotionSchemes env={env} files={files} />;
|
|
757
742
|
if (route === 'udseende/sidekolonne') return <SideColumn env={env} files={files} />;
|
|
743
|
+
if (route === 'udseende/header') return <HeaderSettings env={env} />;
|
|
744
|
+
if (route === 'udseende/footer') return <FooterSettings env={env} />;
|
|
758
745
|
// A theme installed has its page; one from the store that is not (or not yet built), its page in the store.
|
|
759
746
|
const themeShown = route.startsWith('udseende/temaer/') && themes.find((x) => x.id === route.slice(16));
|
|
760
747
|
if (themeShown) return <ThemePage key={themeShown.id} of={themeShown} env={env} files={files} />;
|
|
@@ -819,7 +806,8 @@ function Page({ route, files, env, user, builtAt, onLogout }: PageProps) {
|
|
|
819
806
|
}
|
|
820
807
|
|
|
821
808
|
const found = collectionOf(collections, route);
|
|
822
|
-
|
|
809
|
+
// An optional file (the header's) that the site has not got yet opens empty; the first save makes it.
|
|
810
|
+
const file = files.find((f) => f.path === route) ?? (found?.dataFile?.optional ? { path: route, oid: '', text: '{}\n' } : undefined);
|
|
823
811
|
if (!found || !file)
|
|
824
812
|
return (
|
|
825
813
|
<Screen title={t.app.notFound} back={{ href: '#', label: siteName }}>
|
package/src/AppList.tsx
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
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
17
|
// whether the cookie banner asks first), who made it (a row to their page, #apps/udvikler/<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
|
-
// as a block turned on or off under "Blokindstillinger". One of the site's own is installed or uninstalled in the site's
|
|
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
|
|
21
21
|
// its page before it is installed is drawn from the store's catalog (StoreAppPage). Installed, its blocks and kinds are
|
|
22
22
|
// under "Blokindstillinger" (under its name), its blocks under "Indsæt" and its kinds under "+" of their list. An app in
|
|
@@ -82,6 +82,7 @@ import AppAdds from './AppAdds';
|
|
|
82
82
|
import AppFormPage from './AppFormPage';
|
|
83
83
|
import AppSettingsTab from './AppSettingsTab';
|
|
84
84
|
import { INFO_KEY } from '../lib/appRoute.mjs';
|
|
85
|
+
import { busyFor } from './busyAction.mjs';
|
|
85
86
|
|
|
86
87
|
export const appHref = (app: SiteApp) => `#apps/${app.id}`;
|
|
87
88
|
|
|
@@ -670,23 +671,28 @@ export async function autoUpdate(env: Env, files: RepoFile[]) {
|
|
|
670
671
|
}
|
|
671
672
|
}
|
|
672
673
|
|
|
674
|
+
type StoreAction = 'install' | 'update' | 'uninstall';
|
|
675
|
+
|
|
673
676
|
/** Installing, updating and uninstalling an app from the store, each saved at once in its own commit: its folder
|
|
674
677
|
* copied from the store, or deleted, with the site's file of apps (appChanges in store.ts). Each says true when it
|
|
675
678
|
* was saved. */
|
|
676
679
|
function useStoreActions(env: Env, files: RepoFile[]) {
|
|
677
|
-
|
|
680
|
+
// Which action runs, on which app; `asking` while a dialog waits for the user (busyAction.mjs shows only the pressed button).
|
|
681
|
+
const [running, setRunning] = useState<{ id: string; action: StoreAction; asking: boolean } | null>(null);
|
|
682
|
+
const saving = !!running;
|
|
678
683
|
const installed = installedApps(files);
|
|
679
684
|
const file = files.find((f) => f.path === config.apps.installed);
|
|
680
685
|
|
|
681
686
|
async function change(
|
|
682
687
|
id: string,
|
|
688
|
+
action: StoreAction,
|
|
683
689
|
// `kind`: the notification it is when it is saved (an update is "Apps opdateret", as one on its own).
|
|
684
690
|
text: { busy: string; done: string; commit: string; change: string; kind?: 'saved' | 'updates' },
|
|
685
691
|
next: () => Promise<Parameters<typeof appChanges>[2]>,
|
|
686
692
|
// Whether to go on, with the app's files in the site now (an update asks, when some were changed).
|
|
687
693
|
go: (current: RepoFile[]) => Promise<boolean> = async () => true,
|
|
688
694
|
): Promise<boolean> {
|
|
689
|
-
|
|
695
|
+
setRunning({ id, action, asking: false });
|
|
690
696
|
env.notify({ text: text.busy });
|
|
691
697
|
try {
|
|
692
698
|
const [current, wanted] = await Promise.all([env.folder(appFolder(id)), next()]);
|
|
@@ -694,6 +700,9 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
694
700
|
env.notify(null);
|
|
695
701
|
return false;
|
|
696
702
|
}
|
|
703
|
+
// `go` cleared the message (and its dialog stopped the button being busy): saving goes on.
|
|
704
|
+
setRunning({ id, action, asking: false });
|
|
705
|
+
env.notify({ text: text.busy });
|
|
697
706
|
const cookies = cookieChanges(id, wanted ? cookiesIn(wanted.files) : null, files);
|
|
698
707
|
await saveApp(env, text.commit, text.change, [...appChanges(id, current, wanted, installed, file), ...cookies], file);
|
|
699
708
|
env.notify({ text: text.done, kind: text.kind ?? 'saved' });
|
|
@@ -702,15 +711,17 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
702
711
|
env.notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
|
|
703
712
|
return false;
|
|
704
713
|
} finally {
|
|
705
|
-
|
|
714
|
+
setRunning(null);
|
|
706
715
|
}
|
|
707
716
|
}
|
|
708
717
|
|
|
709
718
|
return {
|
|
710
719
|
saving,
|
|
720
|
+
busy: (id: string, action: StoreAction) => busyFor(running, id, action),
|
|
711
721
|
install: (app: ShownApp) =>
|
|
712
722
|
change(
|
|
713
723
|
app.id,
|
|
724
|
+
'install',
|
|
714
725
|
{
|
|
715
726
|
busy: t.apps.installing,
|
|
716
727
|
done: t.apps.isInstalledSoon(app.label),
|
|
@@ -722,6 +733,7 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
722
733
|
async update(app: ShownApp, record: StoreRecord) {
|
|
723
734
|
const done = await change(
|
|
724
735
|
app.id,
|
|
736
|
+
'update',
|
|
725
737
|
{
|
|
726
738
|
busy: t.apps.updating,
|
|
727
739
|
done: t.apps.isUpdatedSoon(app.label, app.version),
|
|
@@ -732,6 +744,7 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
732
744
|
async () => ({ files: await env.store.files(app), record: recordOf(app) }),
|
|
733
745
|
async (current) => {
|
|
734
746
|
env.notify(null);
|
|
747
|
+
setRunning({ id: app.id, action: 'update', asking: true });
|
|
735
748
|
return askUpdate(
|
|
736
749
|
app.label,
|
|
737
750
|
record.version,
|
|
@@ -747,6 +760,7 @@ function useStoreActions(env: Env, files: RepoFile[]) {
|
|
|
747
760
|
uninstall: (id: string, label: string, written: string) =>
|
|
748
761
|
change(
|
|
749
762
|
id,
|
|
763
|
+
'uninstall',
|
|
750
764
|
{
|
|
751
765
|
busy: t.apps.uninstalling,
|
|
752
766
|
done: t.apps.isUninstalled(label),
|
|
@@ -839,7 +853,7 @@ function useStorePictures(env: Env, app: ShownApp | undefined) {
|
|
|
839
853
|
* catalog: as an app's page in the site, what it adds without switches, and "Installer" (or "Afinstaller"). */
|
|
840
854
|
export function StoreAppPage({ id, env, files }: { id: string; env: Env; files: RepoFile[] }) {
|
|
841
855
|
const catalog = useCatalog(env);
|
|
842
|
-
const { saving, install, uninstall } = useStoreActions(env, files);
|
|
856
|
+
const { saving, busy, install, uninstall } = useStoreActions(env, files);
|
|
843
857
|
const pending = installedApps(files).includes(id);
|
|
844
858
|
const app = Array.isArray(catalog) ? catalog.find((a) => a.id === id) : undefined;
|
|
845
859
|
const pictures = useStorePictures(env, app);
|
|
@@ -863,7 +877,7 @@ export function StoreAppPage({ id, env, files }: { id: string; env: Env; files:
|
|
|
863
877
|
/>
|
|
864
878
|
{!pending && (
|
|
865
879
|
<div className="mb-8">
|
|
866
|
-
<Button disabled={saving} onClick={() => install(app)}>
|
|
880
|
+
<Button disabled={saving} busy={busy(app.id, 'install')} onClick={() => install(app)}>
|
|
867
881
|
{t.apps.install}
|
|
868
882
|
</Button>
|
|
869
883
|
</div>
|
|
@@ -900,8 +914,8 @@ export function StoreAppPage({ id, env, files }: { id: string; env: Env; files:
|
|
|
900
914
|
<div>
|
|
901
915
|
<Button
|
|
902
916
|
kind="outlined"
|
|
903
|
-
|
|
904
917
|
disabled={saving}
|
|
918
|
+
busy={busy(id, 'uninstall')}
|
|
905
919
|
onClick={async () => {
|
|
906
920
|
const label = app?.label ?? id;
|
|
907
921
|
if (await ask(t.apps.askUninstall(label), t.apps.uninstall)) uninstall(id, label, app?.written ?? id);
|
|
@@ -978,9 +992,9 @@ export function AppScreenPage({ app, id, env }: { app: SiteApp; id: string; env:
|
|
|
978
992
|
* not installed, with the pictures, what it needs, its cookies, who made it and where it is used). */
|
|
979
993
|
function AppContent({ app, env, files, tab, fields }: AppProps & { tab: AppTab; fields?: ReactNode }) {
|
|
980
994
|
const notify = env.notify;
|
|
981
|
-
const [toggling, setToggling] = useState(
|
|
995
|
+
const [toggling, setToggling] = useState<'install' | 'uninstall' | null>(null);
|
|
982
996
|
const store = useStoreActions(env, files);
|
|
983
|
-
const saving = toggling || store.saving;
|
|
997
|
+
const saving = !!toggling || store.saving;
|
|
984
998
|
const catalog = useCatalog(env);
|
|
985
999
|
const update = updateOf(app, catalog);
|
|
986
1000
|
// Its release notes as the store has them: also those of a version the site's build does not have yet.
|
|
@@ -1001,7 +1015,7 @@ function AppContent({ app, env, files, tab, fields }: AppProps & { tab: AppTab;
|
|
|
1001
1015
|
const next = installed.filter((id) => id !== app.id).concat(yes ? [app.id] : []);
|
|
1002
1016
|
const text = `${JSON.stringify({ installed: [...new Set(next)].sort() }, null, 2)}\n`;
|
|
1003
1017
|
if (text === file?.text) return;
|
|
1004
|
-
setToggling(
|
|
1018
|
+
setToggling(yes ? 'install' : 'uninstall');
|
|
1005
1019
|
notify({ text: yes ? t.apps.installing : t.apps.uninstalling });
|
|
1006
1020
|
try {
|
|
1007
1021
|
// With its cookies added to the cookie list, or those it added taken out.
|
|
@@ -1015,7 +1029,7 @@ function AppContent({ app, env, files, tab, fields }: AppProps & { tab: AppTab;
|
|
|
1015
1029
|
} catch (e) {
|
|
1016
1030
|
notify({ text: e instanceof ConflictError ? e.message : t.common.notSaved((e as Error).message), error: true, kind: 'failed' });
|
|
1017
1031
|
} finally {
|
|
1018
|
-
setToggling(
|
|
1032
|
+
setToggling(null);
|
|
1019
1033
|
}
|
|
1020
1034
|
}
|
|
1021
1035
|
|
|
@@ -1032,11 +1046,11 @@ function AppContent({ app, env, files, tab, fields }: AppProps & { tab: AppTab;
|
|
|
1032
1046
|
{(!on || (update && app.store)) && (
|
|
1033
1047
|
<div className="mb-8">
|
|
1034
1048
|
{on ? (
|
|
1035
|
-
<Button disabled={saving} onClick={() => store.update(update!, app.store!)}>
|
|
1049
|
+
<Button disabled={saving} busy={store.busy(app.id, 'update')} onClick={() => store.update(update!, app.store!)}>
|
|
1036
1050
|
{t.apps.update(update!.version)}
|
|
1037
1051
|
</Button>
|
|
1038
1052
|
) : (
|
|
1039
|
-
<Button disabled={saving} onClick={() => install(true)}>
|
|
1053
|
+
<Button disabled={saving} busy={toggling === 'install' || store.busy(app.id, 'install')} onClick={() => install(true)}>
|
|
1040
1054
|
{t.apps.install}
|
|
1041
1055
|
</Button>
|
|
1042
1056
|
)}
|
|
@@ -1045,7 +1059,15 @@ function AppContent({ app, env, files, tab, fields }: AppProps & { tab: AppTab;
|
|
|
1045
1059
|
{on && tab === 'settings' ? (
|
|
1046
1060
|
<>
|
|
1047
1061
|
{fields}
|
|
1048
|
-
<AppSettingsTab
|
|
1062
|
+
<AppSettingsTab
|
|
1063
|
+
app={app}
|
|
1064
|
+
env={env}
|
|
1065
|
+
files={files}
|
|
1066
|
+
busy={saving}
|
|
1067
|
+
uninstalling={toggling === 'uninstall' || store.busy(app.id, 'uninstall')}
|
|
1068
|
+
stuck={stuck}
|
|
1069
|
+
onUninstall={() => install(false)}
|
|
1070
|
+
/>
|
|
1049
1071
|
</>
|
|
1050
1072
|
) : (
|
|
1051
1073
|
<AppInfoTab
|
package/src/AppSettingsTab.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The Settings tab of an installed app's page, below its head and its settings form: what the app shows itself (`page`),
|
|
2
|
-
// what it adds with its switches, its kinds of notifications with their switches, and "Afinstaller" with its text.
|
|
2
|
+
// what it adds with its switches, its kinds of notifications with their switches, and "Afinstaller" with its text (it shows the loading indicator while it uninstalls).
|
|
3
3
|
// Exposes AppSettingsTab (default). Depends on AppAdds.tsx, AppNotificationSwitches.tsx, ./ui and notificationData.ts.
|
|
4
4
|
import { Button, SupportingText } from './ui';
|
|
5
5
|
import AppAdds from './AppAdds';
|
|
@@ -10,12 +10,13 @@ import type { SiteApp } from './apps';
|
|
|
10
10
|
import { appType } from './notificationData';
|
|
11
11
|
import { siteText, t } from './i18n';
|
|
12
12
|
|
|
13
|
-
/** `busy`: something is saving; `stuck`: the app is in use, so it cannot be uninstalled; `onUninstall`: "Afinstaller". */
|
|
13
|
+
/** `busy`: something is saving; `uninstalling`: this app is uninstalled now; `stuck`: the app is in use, so it cannot be uninstalled; `onUninstall`: "Afinstaller". */
|
|
14
14
|
export default function AppSettingsTab({
|
|
15
15
|
app,
|
|
16
16
|
env,
|
|
17
17
|
files,
|
|
18
18
|
busy,
|
|
19
|
+
uninstalling,
|
|
19
20
|
stuck,
|
|
20
21
|
onUninstall,
|
|
21
22
|
}: {
|
|
@@ -23,6 +24,7 @@ export default function AppSettingsTab({
|
|
|
23
24
|
env: Env;
|
|
24
25
|
files: RepoFile[];
|
|
25
26
|
busy: boolean;
|
|
27
|
+
uninstalling: boolean;
|
|
26
28
|
stuck: boolean;
|
|
27
29
|
onUninstall: () => void;
|
|
28
30
|
}) {
|
|
@@ -42,7 +44,7 @@ export default function AppSettingsTab({
|
|
|
42
44
|
{notifies.length > 0 && <AppNotificationSwitches notifies={notifies} />}
|
|
43
45
|
{/* As wide as the fields and the rows above. */}
|
|
44
46
|
<div>
|
|
45
|
-
<Button kind="outlined" disabled={busy || stuck} onClick={onUninstall}>
|
|
47
|
+
<Button kind="outlined" disabled={busy || stuck} busy={uninstalling} onClick={onUninstall}>
|
|
46
48
|
{t.apps.uninstall}
|
|
47
49
|
</Button>
|
|
48
50
|
<SupportingText className="mt-2">{stuck ? t.apps.stuck : t.apps.uninstallHint}</SupportingText>
|