@morphemecms/cms 0.10.1-next-20261105180809 → 0.11.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 CHANGED
@@ -1,6 +1,17 @@
1
1
  # @morphemecms/cms
2
2
 
3
- ## 0.10.1-next-20261105180809
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 999d21e: Save notices name the version: app installs, and an app's settings through `changedTo`.
8
+
9
+ ### Patch Changes
10
+
11
+ - 5a64119: Sites type-check again: the block side sheet's save button narrows to the fields step before it reads the block's type, so `BlockDialog.tsx` no longer fails `tsc` in a site.
12
+ - 4d4d32a: Sites type-check again: the types of `searchedAs` and `pageTitle` make every input optional except `site`, so the share preview no longer fails `tsc` in a site.
13
+
14
+ ## 0.10.1
4
15
 
5
16
  ### Patch Changes
6
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphemecms/cms",
3
- "version": "0.10.1-next-20261105180809",
3
+ "version": "0.11.0",
4
4
  "description": "Morpheme CMS: the editor for a site built with Astro, its screens, its blocks and the rules a site gives it. See README.md.",
5
5
  "license": "MIT",
6
6
  "author": "Lars Adolfsen",
package/src/AppList.tsx CHANGED
@@ -118,6 +118,7 @@ export function appSettingsPart(app: SiteApp): Part | null {
118
118
  fields: app.settings.fields,
119
119
  defaults: app.settings.values,
120
120
  commit: siteT.apps.settingsOf(asWritten(app).label),
121
+ changedTo: app.settings.changedTo,
121
122
  back: home ? { href: `#${home.route}`, label: home.label } : { href: appHref(app), label: app.label },
122
123
  };
123
124
  }
@@ -726,7 +727,7 @@ function useStoreActions(env: Env, files: RepoFile[]) {
726
727
  busy: t.apps.installing,
727
728
  done: t.apps.isInstalledSoon(app.label),
728
729
  commit: siteT.apps.commit.install(app.written),
729
- change: t.app.changes.installed(kindAndName(t.app.changes.app, app.label)),
730
+ change: t.app.changes.installed(kindAndName(t.app.changes.app, app.label), app.version),
730
731
  },
731
732
  async () => ({ files: await env.store.files(app), record: recordOf(app) }),
732
733
  ),
@@ -372,7 +372,7 @@ export function useBlockDialog(
372
372
  className={kind === 'standard' ? 'editor-side-sheet' : undefined}
373
373
  actions={
374
374
  <>
375
- <Button onClick={() => (open.step === 'section' ? saveSection() : save(open.type))}>
375
+ <Button onClick={() => (open.step === 'fields' ? save(open.type) : saveSection())}>
376
376
  {open.step === 'fields' && open.isNew ? t.blocks.insert : t.common.save}
377
377
  </Button>
378
378
  <Button kind="outlined" onClick={() => void leave()}>
@@ -5,6 +5,7 @@
5
5
  // app's settings, AppList.tsx appSettingsPart). A field with its choices on a page of their own (Field.page: an app's
6
6
  // "Variant") has it at the form's address with its name after it (`at`, `page`, App.tsx): the form stays open under it
7
7
  // with what is in it, saved or not (useFieldPages), a choice there goes back to it, and it comes back where it was.
8
+ // A save says what it changed through partChange.ts ("… set to <value>" when the part has `changedTo`).
8
9
  // Exposes CompanyForm and usePartForm; the fields (with the block preview) are PartFields.tsx, a field's page PartFieldPage.tsx,
9
10
  // the providers around them PartFormProviders.tsx.
10
11
  import { useState } from 'react';
@@ -14,6 +15,7 @@ import PartFields, { hasPreview } from './PartFields';
14
15
  import PartFieldPage from './PartFieldPage';
15
16
  import PartFormProviders from './PartFormProviders';
16
17
  import { useFieldPages } from './useFieldPages';
18
+ import { partChange } from './partChange';
17
19
  import type { Part } from './schema';
18
20
  import { clean, problemAt } from './check';
19
21
  import { useWrongField, type FieldContext } from './fields';
@@ -69,10 +71,16 @@ export function usePartForm({ part, env, files }: Props) {
69
71
  if (kept) return notify(kept);
70
72
  const message = siteT.company.commit.edit(part.commit ?? asWritten(part).label.toLowerCase());
71
73
  const changes = [{ path, content: text, oid: base.oid || null }];
72
- const change = t.app.changes.edited(lower(part.label));
73
74
  setSaving(true);
74
75
  notify({ text: t.common.saving });
75
76
  try {
77
+ const change = await partChange(
78
+ lower(part.label),
79
+ cleaned,
80
+ part.changedTo,
81
+ { api: (p, init) => env.appApi(part.id, p, init) },
82
+ t.app.changes,
83
+ );
76
84
  const oids = await env.commit(message, changes, change);
77
85
  const saved = { path, oid: oids[path], text };
78
86
  setBase(saved);
package/src/apps.ts CHANGED
@@ -55,6 +55,9 @@ export type AppSettings = {
55
55
  /** Made-up settings over `values`, which its blocks and kinds are drawn with under "Dele" (exampleData in
56
56
  * examples.ts), where the site's own are never shown (MobilePay's number). */
57
57
  example?: Record<string, unknown>;
58
+ /** The value a save of these settings sets, said in the notice after it ("CMS-version set to 0.10.0"), asked with the
59
+ * saved values and the app's own addresses in the Worker (`api`); undefined, or a failure, says "CMS-version changed". */
60
+ changedTo?: (values: Record<string, unknown>, props: AppPageProps) => Promise<string | undefined>;
58
61
  };
59
62
  export const defineSettings = (settings: AppSettings) => settings;
60
63
 
@@ -1,17 +1,29 @@
1
- /** The page's own title as the site writes it in <title> when it has no Google title. */
1
+ /**
2
+ * The page's own title as the site writes it in <title> when it has no Google title.
3
+ * @param {{ title?: string | null; front?: boolean; siteName?: string | null }} input
4
+ * @returns {string}
5
+ */
2
6
  export function pageTitle({ title, front, siteName }: {
3
- title: any;
4
- front?: boolean | undefined;
5
- siteName: any;
7
+ title?: string | null;
8
+ front?: boolean;
9
+ siteName?: string | null;
6
10
  }): string;
7
- /** The title and description a Google result shows for the page. `site`: { name, description }. */
11
+ /**
12
+ * The title and description a Google result shows for the page. `site`: { name, description }.
13
+ * @param {{ title?: string | null; front?: boolean; seoTitle?: string | null; seoDescription?: string | null;
14
+ * description?: string | null; site: { name?: string | null; description?: string | null } }} input
15
+ * @returns {{ title: string; description: string }}
16
+ */
8
17
  export function searchedAs({ title, front, seoTitle, seoDescription, description, site }: {
9
- title: any;
10
- front?: boolean | undefined;
11
- seoTitle: any;
12
- seoDescription: any;
13
- description: any;
14
- site: any;
18
+ title?: string | null;
19
+ front?: boolean;
20
+ seoTitle?: string | null;
21
+ seoDescription?: string | null;
22
+ description?: string | null;
23
+ site: {
24
+ name?: string | null;
25
+ description?: string | null;
26
+ };
15
27
  }): {
16
28
  title: string;
17
29
  description: string;
@@ -6,7 +6,11 @@ const SEPARATOR = ' › ';
6
6
 
7
7
  const text = (s) => String(s ?? '').trim();
8
8
 
9
- /** The page's own title as the site writes it in <title> when it has no Google title. */
9
+ /**
10
+ * The page's own title as the site writes it in <title> when it has no Google title.
11
+ * @param {{ title?: string | null; front?: boolean; siteName?: string | null }} input
12
+ * @returns {string}
13
+ */
10
14
  export function pageTitle({ title, front = false, siteName }) {
11
15
  const own = text(title);
12
16
  if (!own) return String(siteName ?? '');
@@ -14,7 +18,12 @@ export function pageTitle({ title, front = false, siteName }) {
14
18
  return `${own} | ${siteName ?? ''}`;
15
19
  }
16
20
 
17
- /** The title and description a Google result shows for the page. `site`: { name, description }. */
21
+ /**
22
+ * The title and description a Google result shows for the page. `site`: { name, description }.
23
+ * @param {{ title?: string | null; front?: boolean; seoTitle?: string | null; seoDescription?: string | null;
24
+ * description?: string | null; site: { name?: string | null; description?: string | null } }} input
25
+ * @returns {{ title: string; description: string }}
26
+ */
18
27
  export function searchedAs({ title, front = false, seoTitle, seoDescription, description, site }) {
19
28
  const resultTitle = text(seoTitle) || pageTitle({ title, front, siteName: site.name });
20
29
  const resultDescription = text(seoDescription) || text(description) || String(site.description ?? '');
@@ -11,6 +11,7 @@ export const app: Messages['app'] = {
11
11
  changes: {
12
12
  named: (kind, name) => `${kind} “${name}”`,
13
13
  edited: (what) => `${what} ændret`,
14
+ setTo: (what, value) => `${what} sat til ${value}`,
14
15
  created: (what) => `${what} oprettet`,
15
16
  added: (what) => `${what} tilføjet`,
16
17
  deleted: (what) => `${what} slettet`,
@@ -21,7 +22,7 @@ export const app: Messages['app'] = {
21
22
  turnedOn: (what) => `${what} slået til`,
22
23
  turnedOff: (what) => `${what} slået fra`,
23
24
  chosen: (what) => `${what} valgt`,
24
- installed: (what) => `${what} installeret`,
25
+ installed: (what, version) => (version ? `${what} ${version} installeret` : `${what} installeret`),
25
26
  updated: (what, version) => `${what} opdateret til ${version}`,
26
27
  uninstalled: (what) => `${what} afinstalleret`,
27
28
  more: (count) => (count === 1 ? '1 ændring mere' : `${count} ændringer mere`),
@@ -15,6 +15,7 @@ export const app = {
15
15
  changes: {
16
16
  named: (kind: string, name: string) => `${kind} “${name}”`,
17
17
  edited: (what: string) => `${what} changed`,
18
+ setTo: (what: string, value: string) => `${what} set to ${value}`,
18
19
  created: (what: string) => `${what} created`,
19
20
  added: (what: string) => `${what} added`,
20
21
  deleted: (what: string) => `${what} deleted`,
@@ -25,7 +26,7 @@ export const app = {
25
26
  turnedOn: (what: string) => `${what} turned on`,
26
27
  turnedOff: (what: string) => `${what} turned off`,
27
28
  chosen: (what: string) => `${what} chosen`,
28
- installed: (what: string) => `${what} installed`,
29
+ installed: (what: string, version?: string) => (version ? `${what} ${version} installed` : `${what} installed`),
29
30
  updated: (what: string, version: string) => `${what} updated to ${version}`,
30
31
  uninstalled: (what: string) => `${what} uninstalled`,
31
32
  /** After the first three. */
@@ -0,0 +1,44 @@
1
+ // Tests for src/partChange.ts: the text of a part's save, with or without the value its `changedTo` gives.
2
+ // Run: node --test src/partChange.test.mjs
3
+ import assert from 'node:assert/strict';
4
+ import { test } from 'node:test';
5
+ import { partChange } from './partChange.ts';
6
+
7
+ const texts = { edited: (what) => `${what} changed`, setTo: (what, value) => `${what} set to ${value}` };
8
+ const props = { api: async () => new Response('') };
9
+ const run = (changedTo) => partChange('cms-version', {}, changedTo, props, texts);
10
+
11
+ test('no hook says changed', async () => {
12
+ assert.equal(await run(undefined), 'cms-version changed');
13
+ });
14
+
15
+ test('a value says set to it', async () => {
16
+ assert.equal(await run(async () => '0.10.0'), 'cms-version set to 0.10.0');
17
+ });
18
+
19
+ test('an undefined value says changed', async () => {
20
+ assert.equal(await run(async () => undefined), 'cms-version changed');
21
+ });
22
+
23
+ test('an empty value says changed', async () => {
24
+ assert.equal(await run(async () => ''), 'cms-version changed');
25
+ });
26
+
27
+ test('a throw says changed', async () => {
28
+ assert.equal(
29
+ await run(async () => {
30
+ throw new Error('x');
31
+ }),
32
+ 'cms-version changed',
33
+ );
34
+ });
35
+
36
+ test('the hook gets the values and the api', async () => {
37
+ const calls = [];
38
+ const values = { a: 1 };
39
+ await partChange('x', values, async (...args) => void calls.push(args), props, texts);
40
+ assert.equal(calls.length, 1);
41
+ assert.equal(calls[0][0], values);
42
+ assert.equal(calls[0][1], props);
43
+ assert.equal(typeof calls[0][1].api, 'function');
44
+ });
@@ -0,0 +1,24 @@
1
+ // The text of a part's save (CompanyForm.tsx usePartForm): "<label> set to <value>" or "<label> changed".
2
+ // Exposes partChange. Depends only on types, so it runs under node --test; the texts come in as `texts` (t.app.changes).
3
+ import type { AppPageProps } from './apps';
4
+ import type { Part } from './schema';
5
+
6
+ /** The texts it needs, from `t.app.changes`. */
7
+ export type ChangeTexts = { edited: (what: string) => string; setTo: (what: string, value: string) => string };
8
+
9
+ /** The text of a part's save: "<label> set to <value>" with the value its `changedTo` gives, else "<label> changed".
10
+ * `what` is the part's label as it stands in a sentence (lower). A throw, a rejection, no value or an empty one gives
11
+ * "changed". */
12
+ export async function partChange(
13
+ what: string,
14
+ values: Record<string, unknown>,
15
+ changedTo: Part['changedTo'],
16
+ props: AppPageProps,
17
+ texts: ChangeTexts,
18
+ ): Promise<string> {
19
+ try {
20
+ const value = await changedTo?.(values, props);
21
+ if (value) return texts.setTo(what, value);
22
+ } catch {}
23
+ return texts.edited(what);
24
+ }
package/src/schema.ts CHANGED
@@ -8,6 +8,7 @@ import type { Icon } from '@phosphor-icons/react';
8
8
  import type { ComponentType, ReactNode } from 'react';
9
9
  import type { BlockData } from './blocks';
10
10
  import type { AppCategory } from './store';
11
+ import type { AppPageProps } from './apps';
11
12
  import type { SiteData } from '@site/editor';
12
13
  import { locale } from './i18n';
13
14
 
@@ -385,6 +386,10 @@ export type Part = {
385
386
  /** What the commit says was edited, in the site's language, when not its name in lower case (an
386
387
  * app's settings, AppList.tsx: "indstillinger for Mailchimp"). */
387
388
  commit?: string;
389
+ /** The value a save of its fields sets, said in the notice after it ("CMS-version set to 0.10.0"), asked with the saved
390
+ * values and the app's own addresses in the Worker (`api`); undefined, or a failure, says "CMS-version changed"
391
+ * (partChange.ts). An app's settings copy it from AppSettings.changedTo (AppList.tsx appSettingsPart). */
392
+ changedTo?: (values: Record<string, unknown>, props: AppPageProps) => Promise<string | undefined>;
388
393
  /** Parts of its own, instead of fields: a row that opens a screen with them, each at
389
394
  * #<screen>/<id>/<part> and going back to it ("Virksomheden" › "Betaling" with "Bank"). */
390
395
  parts?: Part[];
@@ -32,7 +32,7 @@
32
32
  "@material-symbols/svg-400": "0.48.2",
33
33
  "@material/material-color-utilities": "^0.3.0",
34
34
  "@milkdown/kit": "^7.22.3",
35
- "@morphemecms/cms": "0.10.0",
35
+ "@morphemecms/cms": "0.11.0",
36
36
  "@phosphor-icons/core": "^2.1.1",
37
37
  "@phosphor-icons/react": "^2.1.10",
38
38
  "@tailwindcss/typography": "^0.5.20",