@getrefino/react 0.1.0-rc.4 → 0.1.0-rc.6

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.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Updating the site's Refino integration from the editor.
3
+ *
4
+ * The editor knows nothing about where updates come from. A host that can
5
+ * update itself passes an `IntegrationUpdateSource` (Refino-hosted sites get
6
+ * one generated for them) whose two methods resolve with whatever JSON the
7
+ * update service answered. Everything is validated here, in the package,
8
+ * rather than in the generated transport, so the generated code can stay a
9
+ * thin pipe that rarely needs to change, and an answer this version does not
10
+ * understand reads as "nothing to offer" rather than as an error.
11
+ */
12
+ export type UpdateUrgency = "optional" | "recommended" | "required";
13
+ export type IntegrationUpdateStatus = {
14
+ readonly state: "current";
15
+ } | {
16
+ readonly state: "available";
17
+ /** The version the update installs. */
18
+ readonly version: string;
19
+ readonly urgency: UpdateUrgency;
20
+ /** Plain-language highlights of the release, possibly none. */
21
+ readonly summary: readonly string[];
22
+ /** False when the update needs a developer; `message` then says so in plain words. */
23
+ readonly automatic: boolean;
24
+ readonly message: string | null;
25
+ };
26
+ export type IntegrationUpdateResult = {
27
+ readonly ok: true;
28
+ readonly version: string;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly code: string;
32
+ readonly message: string;
33
+ /**
34
+ * Whether the repository was left exactly as it was. False only when
35
+ * the request's outcome is unknown (the connection dropped while it
36
+ * ran), never after a refusal.
37
+ */
38
+ readonly unchanged: boolean;
39
+ };
40
+ export interface IntegrationUpdateSource {
41
+ /** Resolve with the update service's JSON body for "is there an update?". */
42
+ check(): Promise<unknown>;
43
+ /** Resolve with the update service's JSON body for "install `version` now". */
44
+ apply(version: string): Promise<unknown>;
45
+ }
46
+ /** `{ ok: true, update: {...} }` → a status. Anything unexpected is "current": no noise. */
47
+ export declare function readUpdateStatus(body: unknown): IntegrationUpdateStatus;
48
+ /** `{ ok: true, result: { status: "updated", version } }` or `{ ok: false, error }` → a result. */
49
+ export declare function readUpdateResult(body: unknown, requested: string): IntegrationUpdateResult;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Updating the site's Refino integration from the editor.
3
+ *
4
+ * The editor knows nothing about where updates come from. A host that can
5
+ * update itself passes an `IntegrationUpdateSource` (Refino-hosted sites get
6
+ * one generated for them) whose two methods resolve with whatever JSON the
7
+ * update service answered. Everything is validated here, in the package,
8
+ * rather than in the generated transport, so the generated code can stay a
9
+ * thin pipe that rarely needs to change, and an answer this version does not
10
+ * understand reads as "nothing to offer" rather than as an error.
11
+ */
12
+ const CURRENT = { state: "current" };
13
+ const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
14
+ const MAX_TEXT = 300;
15
+ function record(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
17
+ }
18
+ function text(value) {
19
+ return typeof value === "string" && value.trim().length > 0 ? value.trim().slice(0, MAX_TEXT) : null;
20
+ }
21
+ /** `{ ok: true, update: {...} }` → a status. Anything unexpected is "current": no noise. */
22
+ export function readUpdateStatus(body) {
23
+ const update = record(record(body)?.ok === true ? record(body).update : null);
24
+ if (!update || update.state !== "available")
25
+ return CURRENT;
26
+ const version = typeof update.version === "string" && VERSION.test(update.version) ? update.version : null;
27
+ if (!version)
28
+ return CURRENT;
29
+ const urgency = update.urgency === "recommended" || update.urgency === "required" ? update.urgency : "optional";
30
+ const summary = Array.isArray(update.summary) ? update.summary.map(text).filter((line) => line !== null).slice(0, 5) : [];
31
+ const automatic = update.automatic === true;
32
+ return { state: "available", version, urgency, summary, automatic, message: automatic ? null : text(update.message) };
33
+ }
34
+ const GENERIC_FAILURE = "Refino could not update this site.";
35
+ const UNKNOWN_OUTCOME = "Refino could not confirm whether the update finished.";
36
+ /** `{ ok: true, result: { status: "updated", version } }` or `{ ok: false, error }` → a result. */
37
+ export function readUpdateResult(body, requested) {
38
+ const value = record(body);
39
+ const result = record(value?.ok === true ? value.result : null);
40
+ if (result && (result.status === "updated" || result.status === "current")) {
41
+ const version = typeof result.version === "string" && VERSION.test(result.version) ? result.version : requested;
42
+ return { ok: true, version };
43
+ }
44
+ const error = record(value?.error);
45
+ const code = typeof error?.code === "string" ? error.code : "UNEXPECTED";
46
+ // "Nothing was changed" is only ever said when the service says so. A
47
+ // dropped connection, a proxy error page or an answer this version cannot
48
+ // read leaves the outcome unknown, and the owner is told exactly that.
49
+ const unchanged = error?.repositoryUnchanged === true;
50
+ const message = text(error?.message) ?? (unchanged ? GENERIC_FAILURE : UNKNOWN_OUTCOME);
51
+ return { ok: false, code, message, unchanged };
52
+ }
@@ -1,11 +1,18 @@
1
1
  import type { ContentId } from "@getrefino/core";
2
- import type { CopyEditorState } from "./store.js";
2
+ import type { ArticleState, CopyEditorState, EditorPanel } from "./store.js";
3
+ import type { IntegrationUpdateSource } from "./updates.js";
3
4
  export interface CopyEditor {
4
5
  readonly state: CopyEditorState;
5
6
  readonly dirtyIds: readonly ContentId[];
6
7
  readonly isDirty: boolean;
7
8
  /** Login page to offer when the endpoint reports the session ended. */
8
9
  readonly signInHref: string;
10
+ /** The provider's update source, if the host offers integration updates. */
11
+ readonly updates: IntegrationUpdateSource | undefined;
12
+ /** Articles on this page, in the order the host declared them. Empty on a page without one. */
13
+ readonly articles: readonly ArticleState[];
14
+ openPanel(panel: EditorPanel): void;
15
+ closePanel(): void;
9
16
  save(): Promise<void>;
10
17
  revertValue(id: ContentId): void;
11
18
  revertAll(): void;
@@ -7,9 +7,10 @@ import { useCopyEditorState, useCopyStoreOrThrow } from "./context.js";
7
7
  * the default toolbar does not fit the host site.
8
8
  */
9
9
  export function useCopyEditor() {
10
- const { store, onExit, signInHref } = useCopyStoreOrThrow();
10
+ const { store, onExit, signInHref, updates } = useCopyStoreOrThrow();
11
11
  const state = useCopyEditorState(store);
12
12
  const dirtyIds = useMemo(() => getDirtyIds(state.draft), [state.draft]);
13
+ const articles = useMemo(() => state.documentIds.map((id) => state.documents[id]).filter((article) => article !== undefined), [state.documentIds, state.documents]);
13
14
  const exit = useCallback(async () => {
14
15
  const count = getDirtyIds(store.getState().draft).length;
15
16
  if (count > 0 && typeof window !== "undefined") {
@@ -24,11 +25,15 @@ export function useCopyEditor() {
24
25
  dirtyIds,
25
26
  isDirty: dirtyIds.length > 0,
26
27
  signInHref,
28
+ updates,
29
+ articles,
30
+ openPanel: store.openPanel,
31
+ closePanel: store.closePanel,
27
32
  save: store.save,
28
33
  revertValue: store.revertValue,
29
34
  revertAll: store.revertAll,
30
35
  reloadLatest: store.reloadLatest,
31
36
  dismissError: store.dismissError,
32
37
  exit,
33
- }), [state, dirtyIds, store, exit, signInHref]);
38
+ }), [state, dirtyIds, store, exit, signInHref, updates, articles]);
34
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrefino/react",
3
- "version": "0.1.0-rc.4",
3
+ "version": "0.1.0-rc.6",
4
4
  "description": "React components for Refino: edit a website's existing copy in place on the rendered page and commit it to the repository. Click text. Edit text. Save.",
5
5
  "keywords": [
6
6
  "refino",
@@ -37,7 +37,7 @@
37
37
  "./package.json": "./package.json"
38
38
  },
39
39
  "dependencies": {
40
- "@getrefino/core": "0.1.0-rc.4"
40
+ "@getrefino/core": "0.1.0-rc.6"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "react": "^18.2.0 || ^19.0.0",