@stamprally/admin-ui 0.12.0 → 0.14.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/README.md CHANGED
@@ -25,9 +25,13 @@ const editor = useAdminRallyEditor(config, { onChange: saveDraft });
25
25
  editor.addSpot({ name: { en: "Museum" }, conditions: [{ type: "qr", secretToken: "" }] });
26
26
  editor.duplicateReward("reward-1");
27
27
  editor.reorderSpots(2, 0);
28
+ editor.reorderRewards(1, 0);
28
29
  editor.undo();
29
30
  ```
30
31
 
31
32
  Use `canUndo`/`canRedo` to control history buttons. `resetConfig(newConfig)` replaces
32
33
  the current draft when an external CMS update arrives. `updateLocalizedField` accepts
33
34
  paths such as `title`, `description`, `spots.0.name`, or `spots.spot-1.name`.
35
+ Updates use functional React state transitions, so consecutive updates are not
36
+ lost. External configuration changes replace a clean draft automatically; call
37
+ `resetConfig(newConfig)` to explicitly accept a conflicting external draft.
package/dist/index.cjs CHANGED
@@ -1,10 +1,10 @@
1
+ "use client";
1
2
  'use strict';
2
3
 
3
4
  var core = require('@stamprally/core');
4
5
  var react = require('react');
5
6
  var jsxRuntime = require('react/jsx-runtime');
6
7
 
7
- // src/index.tsx
8
8
  function nextEntityId(prefix, ids) {
9
9
  let index = ids.size + 1;
10
10
  let candidate = `${prefix}-${index}`;
@@ -26,12 +26,22 @@ function localizedValue(value, locale, nextValue) {
26
26
  }
27
27
  function useAdminRallyEditor(initialConfig, options = {}) {
28
28
  const initialRef = react.useRef(initialConfig);
29
+ const previousInitialRef = react.useRef(initialConfig);
29
30
  const [history, setHistory] = react.useState({
30
31
  past: [],
31
32
  present: initialConfig,
32
33
  future: []
33
34
  });
34
35
  const config = history.present;
36
+ react.useEffect(() => {
37
+ if (previousInitialRef.current === initialConfig) return;
38
+ previousInitialRef.current = initialConfig;
39
+ const isDirty = JSON.stringify(history.present) !== JSON.stringify(initialRef.current);
40
+ if (!isDirty) {
41
+ initialRef.current = initialConfig;
42
+ setHistory({ past: [], present: initialConfig, future: [] });
43
+ }
44
+ }, [history.present, initialConfig]);
35
45
  const commit = react.useCallback(
36
46
  (value) => {
37
47
  setHistory((current) => {
@@ -68,6 +78,7 @@ function useAdminRallyEditor(initialConfig, options = {}) {
68
78
  spots: current.spots.filter((spot) => spot.id !== spotId).map((spot, index) => ({ ...spot, orderIndex: index }))
69
79
  }));
70
80
  const reorderSpots = (fromIndex, toIndex) => commit((current) => ({ ...current, spots: moveTo(current.spots, fromIndex, toIndex) }));
81
+ const reorderRewards = (fromIndex, toIndex) => commit((current) => ({ ...current, rewards: moveTo(current.rewards, fromIndex, toIndex) }));
71
82
  const duplicateSpot = (spotId) => commit((current) => {
72
83
  const source = current.spots.find((spot) => spot.id === spotId);
73
84
  if (source === void 0) return current;
@@ -166,6 +177,7 @@ function useAdminRallyEditor(initialConfig, options = {}) {
166
177
  });
167
178
  const resetConfig = (newConfig) => {
168
179
  initialRef.current = newConfig;
180
+ previousInitialRef.current = newConfig;
169
181
  setHistory({ past: [], present: newConfig, future: [] });
170
182
  options.onChange?.(newConfig);
171
183
  };
@@ -178,6 +190,7 @@ function useAdminRallyEditor(initialConfig, options = {}) {
178
190
  addSpot,
179
191
  removeSpot,
180
192
  reorderSpots: (fromIndex, toIndex) => reorderSpots(fromIndex, toIndex),
193
+ reorderRewards,
181
194
  duplicateSpot,
182
195
  addReward,
183
196
  removeReward,
@@ -198,9 +211,13 @@ function useSpotEditor(spotId, options = {}) {
198
211
  const [config, setConfig] = react.useState(
199
212
  options.config ?? options.initialConfig
200
213
  );
201
- const commit = (next) => {
202
- setConfig(next);
203
- options.onChange?.(next);
214
+ const commit = (updateConfig) => {
215
+ setConfig((current) => {
216
+ if (current === void 0) return current;
217
+ const next = updateConfig(current);
218
+ options.onChange?.(next);
219
+ return next;
220
+ });
204
221
  };
205
222
  const spot = config?.spots.find((item) => item.id === spotId);
206
223
  return {
@@ -209,14 +226,17 @@ function useSpotEditor(spotId, options = {}) {
209
226
  setConfig,
210
227
  update: (patch) => {
211
228
  if (config === void 0 || spot === void 0) return;
212
- commit({
213
- ...config,
214
- spots: config.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
215
- });
229
+ commit((current) => ({
230
+ ...current,
231
+ spots: current.spots.map((item) => item.id === spotId ? { ...item, ...patch } : item)
232
+ }));
216
233
  },
217
234
  remove: () => {
218
235
  if (config === void 0 || spot === void 0) return;
219
- commit({ ...config, spots: config.spots.filter((item) => item.id !== spotId) });
236
+ commit((current) => ({
237
+ ...current,
238
+ spots: current.spots.filter((item) => item.id !== spotId)
239
+ }));
220
240
  }
221
241
  };
222
242
  }
@@ -225,9 +245,13 @@ function useRewardEditor(rewardId, options = {}) {
225
245
  options.config ?? options.initialConfig
226
246
  );
227
247
  const reward = config?.rewards.find((item) => item.id === rewardId);
228
- const commit = (next) => {
229
- setConfig(next);
230
- options.onChange?.(next);
248
+ const commit = (updateConfig) => {
249
+ setConfig((current) => {
250
+ if (current === void 0) return current;
251
+ const next = updateConfig(current);
252
+ options.onChange?.(next);
253
+ return next;
254
+ });
231
255
  };
232
256
  return {
233
257
  config,
@@ -235,16 +259,19 @@ function useRewardEditor(rewardId, options = {}) {
235
259
  setConfig,
236
260
  update: (patch) => {
237
261
  if (config === void 0 || reward === void 0) return;
238
- commit({
239
- ...config,
240
- rewards: config.rewards.map(
262
+ commit((current) => ({
263
+ ...current,
264
+ rewards: current.rewards.map(
241
265
  (item) => item.id === rewardId ? { ...item, ...patch } : item
242
266
  )
243
- });
267
+ }));
244
268
  },
245
269
  remove: () => {
246
270
  if (config === void 0 || reward === void 0) return;
247
- commit({ ...config, rewards: config.rewards.filter((item) => item.id !== rewardId) });
271
+ commit((current) => ({
272
+ ...current,
273
+ rewards: current.rewards.filter((item) => item.id !== rewardId)
274
+ }));
248
275
  }
249
276
  };
250
277
  }
@@ -1114,21 +1141,40 @@ function AdminRallyEditor({ config, onChange, locale, dictionary }) {
1114
1141
  children: field("addReward", "Add reward")
1115
1142
  }
1116
1143
  ),
1117
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.rewards.map((reward) => /* @__PURE__ */ jsxRuntime.jsx(
1118
- RewardItemForm,
1119
- {
1120
- reward,
1121
- locale: activeLocale,
1122
- ...dictionary === void 0 ? {} : { dictionary },
1123
- onChange: (next) => update({
1124
- rewards: config.rewards.map(
1125
- (current) => current.id === reward.id ? next : current
1126
- )
1127
- }),
1128
- onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
1129
- },
1130
- reward.id
1131
- )) })
1144
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: config.rewards.map((reward, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1145
+ /* @__PURE__ */ jsxRuntime.jsx(
1146
+ RewardItemForm,
1147
+ {
1148
+ reward,
1149
+ locale: activeLocale,
1150
+ ...dictionary === void 0 ? {} : { dictionary },
1151
+ onChange: (next) => update({
1152
+ rewards: config.rewards.map(
1153
+ (current) => current.id === reward.id ? next : current
1154
+ )
1155
+ }),
1156
+ onRemove: () => update({ rewards: config.rewards.filter((current) => current.id !== reward.id) })
1157
+ }
1158
+ ),
1159
+ /* @__PURE__ */ jsxRuntime.jsx(
1160
+ "button",
1161
+ {
1162
+ type: "button",
1163
+ disabled: index === 0,
1164
+ onClick: () => update({ rewards: move(config.rewards, index, -1) }),
1165
+ children: field("moveUp", "Move up")
1166
+ }
1167
+ ),
1168
+ /* @__PURE__ */ jsxRuntime.jsx(
1169
+ "button",
1170
+ {
1171
+ type: "button",
1172
+ disabled: index === config.rewards.length - 1,
1173
+ onClick: () => update({ rewards: move(config.rewards, index, 1) }),
1174
+ children: field("moveDown", "Move down")
1175
+ }
1176
+ )
1177
+ ] }, reward.id)) })
1132
1178
  ] });
1133
1179
  }
1134
1180
  var RallyEditor = AdminRallyEditor;
@@ -1139,18 +1185,97 @@ function GeneralSettingsForm({
1139
1185
  dictionary
1140
1186
  }) {
1141
1187
  const activeLocale = locale ?? "en";
1142
- return /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1143
- text(dictionary, activeLocale, "title", "Rally title"),
1144
- /* @__PURE__ */ jsxRuntime.jsx(
1145
- "input",
1146
- {
1147
- value: core.resolveLocalizedText(config.title, activeLocale),
1148
- onChange: (event) => onChange({
1149
- ...config,
1150
- title: core.updateLocalizedField(config.title, activeLocale, event.target.value)
1151
- })
1152
- }
1153
- )
1188
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { "aria-label": text(dictionary, activeLocale, "generalSettings", "General settings"), children: [
1189
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1190
+ text(dictionary, activeLocale, "title", "Rally title"),
1191
+ /* @__PURE__ */ jsxRuntime.jsx(
1192
+ "input",
1193
+ {
1194
+ value: core.resolveLocalizedText(config.title, activeLocale),
1195
+ onChange: (event) => onChange({
1196
+ ...config,
1197
+ title: core.updateLocalizedField(config.title, activeLocale, event.target.value)
1198
+ })
1199
+ }
1200
+ )
1201
+ ] }),
1202
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1203
+ text(dictionary, activeLocale, "inventoryLimit", "Global inventory limit"),
1204
+ /* @__PURE__ */ jsxRuntime.jsx(
1205
+ "input",
1206
+ {
1207
+ type: "number",
1208
+ min: 0,
1209
+ placeholder: text(dictionary, activeLocale, "inventoryLimitPlaceholder", "Unlimited"),
1210
+ title: text(
1211
+ dictionary,
1212
+ activeLocale,
1213
+ "inventoryOverrideHelp",
1214
+ "Individual reward limits override this global limit."
1215
+ ),
1216
+ value: config.inventory?.global ?? "",
1217
+ onChange: (event) => {
1218
+ const value = event.target.value;
1219
+ const inventory = { ...config.inventory ?? {} };
1220
+ if (value === "") delete inventory.global;
1221
+ else inventory.global = Number(value);
1222
+ onChange({ ...config, inventory });
1223
+ }
1224
+ }
1225
+ )
1226
+ ] }),
1227
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1228
+ text(dictionary, activeLocale, "inventoryMode", "Inventory aggregation"),
1229
+ /* @__PURE__ */ jsxRuntime.jsxs(
1230
+ "select",
1231
+ {
1232
+ value: config.inventoryMode ?? "shared",
1233
+ title: text(
1234
+ dictionary,
1235
+ activeLocale,
1236
+ "inventoryModeHelp",
1237
+ "Individual reward limits override this setting."
1238
+ ),
1239
+ onChange: (event) => onChange({
1240
+ ...config,
1241
+ inventoryMode: event.target.value
1242
+ }),
1243
+ children: [
1244
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "shared", children: text(dictionary, activeLocale, "inventoryShared", "Shared") }),
1245
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "per_reward", children: text(dictionary, activeLocale, "inventoryPerReward", "Per reward") })
1246
+ ]
1247
+ }
1248
+ )
1249
+ ] }),
1250
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1251
+ text(dictionary, activeLocale, "staffPasscode", "Global staff passcode"),
1252
+ /* @__PURE__ */ jsxRuntime.jsx(
1253
+ "input",
1254
+ {
1255
+ type: "password",
1256
+ placeholder: text(
1257
+ dictionary,
1258
+ activeLocale,
1259
+ "staffPasscodePlaceholder",
1260
+ "Used unless a reward has its own passcode"
1261
+ ),
1262
+ title: text(
1263
+ dictionary,
1264
+ activeLocale,
1265
+ "staffPasscodeHelp",
1266
+ "Individual spot or reward settings override this global passcode."
1267
+ ),
1268
+ value: config.staffPasscode ?? "",
1269
+ onChange: (event) => {
1270
+ const value = event.target.value;
1271
+ if (value === "") {
1272
+ const { staffPasscode: _staffPasscode, ...withoutPasscode } = config;
1273
+ onChange(withoutPasscode);
1274
+ } else onChange({ ...config, staffPasscode: value });
1275
+ }
1276
+ }
1277
+ )
1278
+ ] })
1154
1279
  ] });
1155
1280
  }
1156
1281
  function JsonConfigIO({ config, onImport, locale, dictionary }) {