@simon-says-build/demo-player 0.3.0 → 0.4.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/index.d.ts CHANGED
@@ -29,3 +29,5 @@ export function onDemo(fn: (demo: { id: string; token: string; api: string } | n
29
29
  export const PLAYER_VERSION: string;
30
30
  /** The changes a demo's last card offers to approve or discuss: one per `job:<id>` feature. */
31
31
  export function reviewChangesOf(demo: unknown): { jobId: string; title: string }[];
32
+ /** A release's demo (0.4.0): the session merge its review card releases, or null. */
33
+ export function releaseOf(demo: unknown): { jobId: string; label: string; note: string } | null;
package/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { DemoHost } from './src/DemoHost.js';
2
- export { demoConfig, findTarget, handlerFor, phoneSteps, scalePoint, playDemo, currentDemo, onDemo, reviewChangesOf, PLAYER_VERSION } from './src/core.js';
2
+ export { demoConfig, findTarget, handlerFor, phoneSteps, scalePoint, playDemo, currentDemo, onDemo, reviewChangesOf, releaseOf, PLAYER_VERSION } from './src/core.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simon-says-build/demo-player",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "SimonSays mobile demo player: walks a preview's viewer through a job's change, inside the app. Renders nothing unless the bundle carries a demo.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/DemoHost.js CHANGED
@@ -6,12 +6,13 @@
6
6
  // the viewer through it: a ring on each step's element, a caption card with
7
7
  // Back / Next, "Do it for me" on taps and typing, and a pill to tuck it away.
8
8
  // A demo of CHANGES ends on a review card (0.3.0): ✓ Approve or 💬 Discuss
9
+ // (a RELEASE's demo, 0.4.0: one Approve & release for all of them)
9
10
  // each one, the extension's guided review on the phone (docs/GUIDED-REVIEW.md).
10
11
  //
11
12
  // No JSX, so it needs nothing from the app's Babel setup beyond React Native's.
12
13
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
14
  import { Linking, Pressable, ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions } from 'react-native';
14
- import { cardBox, currentDemo, demoConfig, findTarget, handlerFor, onDemo, phoneSteps, playDemo, reviewChangesOf, scalePoint } from './core.js';
15
+ import { cardBox, currentDemo, demoConfig, findTarget, handlerFor, onDemo, phoneSteps, playDemo, releaseOf, reviewChangesOf, scalePoint } from './core.js';
15
16
  import { demoApi, tourOf } from './api.js';
16
17
 
17
18
  const h = React.createElement;
@@ -68,12 +69,13 @@ function Player({ config, router, insets, onClose, children }) {
68
69
  const [tucked, setTucked] = useState(false);
69
70
  const [done, setDone] = useState(false);
70
71
  const [changes, setChanges] = useState([]); // what the last card reviews (reviewChangesOf)
72
+ const [release, setRelease] = useState(null); // a release's demo ends on releasing it (releaseOf)
71
73
  const [reviewing, setReviewing] = useState(false);
72
74
 
73
75
  useEffect(() => {
74
76
  let live = true;
75
77
  api.load()
76
- .then((d) => { if (live) { setSteps(phoneSteps(tourOf(d, { here: !!config.here })).concat()); setChanges(reviewChangesOf(d)); } })
78
+ .then((d) => { if (live) { setSteps(phoneSteps(tourOf(d, { here: !!config.here })).concat()); setChanges(reviewChangesOf(d)); setRelease(releaseOf(d)); } })
77
79
  .catch((e) => { if (live) setError(e.message); });
78
80
  return () => { live = false; };
79
81
  }, [api]);
@@ -135,7 +137,7 @@ function Player({ config, router, insets, onClose, children }) {
135
137
  const overlay = (() => {
136
138
  if (error) return h(Card, { at: null, screen, insets, card, setCard, title: 'Demo', body: `The demo didn't load: ${error}` });
137
139
  if (!steps) return null;
138
- if (reviewing && !tucked) return h(Review, { api, changes, screen, insets, onBack: steps.length ? () => setReviewing(false) : null, onDone: finish, onHide: () => setTucked(true) });
140
+ if (reviewing && !tucked) return h(Review, { api, changes, release, screen, insets, onBack: steps.length ? () => setReviewing(false) : null, onDone: finish, onHide: () => setTucked(true) });
139
141
  if (tucked || done) {
140
142
  // A demo started at run time closes when it is done (▶ Demo again replays it);
141
143
  // one baked into a preview stays, to be replayed from its pill.
@@ -144,7 +146,7 @@ function Player({ config, router, insets, onClose, children }) {
144
146
  return h(Pressable, { style: [s.pill, { bottom: insets.bottom + 12 }], onPress: () => { setTucked(false); if (done) { setDone(false); setIndex(0); } }, accessibilityRole: 'button', accessibilityLabel: 'Open the demo' },
145
147
  h(Text, { style: s.pillText }, done ? 'Demo done · replay' : reviewing ? 'Review the changes' : `Demo ${index + 1}/${steps.length}`));
146
148
  }
147
- if (!step && changes.length) return h(Review, { api, changes, screen, insets, onBack: null, onDone: finish, onHide: () => setTucked(true) });
149
+ if (!step && changes.length) return h(Review, { api, changes, release, screen, insets, onBack: null, onDone: finish, onHide: () => setTucked(true) });
148
150
  if (!step) return h(Card, { at: null, screen, insets, card, setCard, title: 'Demo', body: 'This demo has no steps a phone can play.' });
149
151
  const canDo = (step.kind === 'click' || step.kind === 'fill') && found && (step.kind === 'click' ? handlerFor(found, 'onPress') : handlerFor(found, 'onChangeText'));
150
152
  return h(React.Fragment, null,
@@ -171,8 +173,15 @@ function Player({ config, router, insets, onClose, children }) {
171
173
  // SSB revises THAT change (functions/demo.mjs reviewRef). Nothing here holds
172
174
  // the tour: Done closes it whatever is left undecided.
173
175
  const ROW = { idle: null, approving: 'Approving…', approved: '✓ Approved', sending: 'Sending…', sent: 'Sent to SSB — it will revise this' };
174
- function Review({ api, changes, screen, insets, onBack, onDone, onHide }) {
176
+ function Review({ api, changes, release, screen, insets, onBack, onDone, onHide }) {
175
177
  const [rows, setRows] = useState({}); // jobId → { state, error, text }
178
+ // A RELEASE (0.4.0): the changes go out together, so they are approved
179
+ // together — one button for the release; each change can still be discussed.
180
+ const [rel, setRel] = useState({ state: 'idle', error: null });
181
+ const doRelease = () => {
182
+ setRel({ state: 'releasing', error: null });
183
+ api.release().then(() => setRel({ state: 'released', error: null })).catch((e) => setRel({ state: 'idle', error: e.message }));
184
+ };
176
185
  const set = (id, patch) => setRows((r) => ({ ...r, [id]: { ...(r[id] || {}), ...patch } }));
177
186
  const approve = (id) => {
178
187
  set(id, { state: 'approving', error: null });
@@ -184,10 +193,12 @@ function Review({ api, changes, screen, insets, onBack, onDone, onHide }) {
184
193
  set(id, { state: 'sending', error: null });
185
194
  api.discuss(id, text).then(() => set(id, { state: 'sent' })).catch((e) => set(id, { state: 'discussing', error: e.message }));
186
195
  };
187
- const left = changes.filter((c) => !['approved', 'sent'].includes(rows[c.jobId]?.state)).length;
196
+ const left = release ? 0 : changes.filter((c) => !['approved', 'sent'].includes(rows[c.jobId]?.state)).length;
188
197
  return h(View, { style: [s.card, s.review, { top: insets.top + 12, maxHeight: screen.height - insets.top - insets.bottom - 24, maxWidth: screen.width - 32, left: Math.max(16, (screen.width - Math.min(360, screen.width - 32)) / 2) }], testID: 'ssb-demo-review' },
189
- h(Text, { style: s.title }, changes.length === 1 ? 'Review the change' : `Review ${changes.length} changes`),
190
- h(Text, { style: s.body }, 'Approve what looks right. Anything that should be different, say so — SSB revises it.'),
198
+ h(Text, { style: s.title }, release ? (changes.length === 1 ? 'The next release: 1 change' : `The next release: ${changes.length} changes`)
199
+ : changes.length === 1 ? 'Review the change' : `Review ${changes.length} changes`),
200
+ h(Text, { style: s.body }, release ? `Happy with it? ${release.label} sends them out together. Anything that should be different, say so — SSB revises it.`
201
+ : 'Approve what looks right. Anything that should be different, say so — SSB revises it.'),
191
202
  h(ScrollView, { style: s.changes, keyboardShouldPersistTaps: 'handled' }, ...changes.map((c) => {
192
203
  const r = rows[c.jobId] || {};
193
204
  const state = r.state || 'idle';
@@ -202,12 +213,16 @@ function Review({ api, changes, screen, insets, onBack, onDone, onHide }) {
202
213
  button('Send', () => send(c.jobId), true))) : null,
203
214
  state === 'idle' ? h(View, { style: s.actions },
204
215
  button('💬 Discuss', () => set(c.jobId, { state: 'discussing' })),
205
- button('✓ Approve', () => approve(c.jobId), true)) : null);
216
+ release ? null : button('✓ Approve', () => approve(c.jobId), true)) : null);
206
217
  })),
218
+ release && rel.state === 'released' ? h(Text, { style: s.changeState, testID: 'ssb-release-done' }, 'Approved — it goes out in 5 minutes. Undo it on the release page in SimonSays if you need to.') : null,
219
+ release && rel.error ? h(Text, { style: s.error }, rel.error) : null,
207
220
  h(View, { style: [s.actions, s.reviewFoot] },
208
221
  button('Hide', onHide),
209
222
  onBack ? button('Back', onBack) : null,
210
- button(left ? `Done (${left} undecided)` : 'Done', onDone, !left)));
223
+ release && rel.state !== 'released'
224
+ ? button(rel.state === 'releasing' ? 'Approving…' : release.label, rel.state === 'releasing' ? () => {} : doRelease, true)
225
+ : button(left ? `Done (${left} undecided)` : 'Done', onDone, !left)));
211
226
  }
212
227
 
213
228
  const button = (label, onPress, primary = false) => h(Pressable, {
package/src/api.js CHANGED
@@ -16,6 +16,9 @@ export function demoApi({ api, id, token }, fetchImpl = globalThis.fetch) {
16
16
  // Guided review (0.3.0): approve one change of this demo, or say something
17
17
  // about it in the conversation it came from — SSB revises the change.
18
18
  approve: (jobId) => call('/review', { method: 'POST', body: JSON.stringify({ jobId }) }),
19
+ // A release's demo (0.4.0): approve and release the session, as the person
20
+ // the demo was made for, after the five-minute cool-off.
21
+ release: () => call('/release', { method: 'POST', body: '{}' }),
19
22
  discuss: (jobId, text) => call('/chat', { method: 'POST', body: JSON.stringify({ text, context: { about: jobId } }) }),
20
23
  };
21
24
  }
package/src/core.js CHANGED
@@ -10,7 +10,7 @@ import { bubbleBox } from './step-place.js';
10
10
  // What this player is, said when the app registers for "Ready to see"
11
11
  // (packages/ssb-overlay sends it; functions/in-app-notify.mjs clientOf). Kept
12
12
  // equal to package.json by test/core.test.mjs.
13
- export const PLAYER_VERSION = '0.3.0';
13
+ export const PLAYER_VERSION = '0.4.0';
14
14
 
15
15
  export const HOST = 5;
16
16
  export const TEXT = 6;
@@ -158,3 +158,14 @@ export function reviewChangesOf(demo) {
158
158
  }
159
159
  return out;
160
160
  }
161
+
162
+ // A RELEASE'S DEMO (0.4.0, docs/SESSION-BRANCHES.md "Told when the release is
163
+ // ready"): a demo of one session's changes carries its merge job
164
+ // (`sessionMerge`), and the review card ends on releasing them together —
165
+ // the button saying what the app's release does ("Approve & release", or
166
+ // "Approve" where the app has a release cycle of its own). Null otherwise.
167
+ export function releaseOf(demo) {
168
+ const sm = demo?.sessionMerge;
169
+ if (!sm || typeof sm.jobId !== 'string' || !sm.jobId) return null;
170
+ return { jobId: sm.jobId, label: String(sm.label || 'Approve & release'), note: String(sm.note || '') };
171
+ }