portal-mojo 0.2.17 → 0.2.18
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.
|
@@ -53,7 +53,12 @@ with raw `mojoList` — `SettingModel.normalizeListParams` strips `key`, so the
|
|
|
53
53
|
model's list hook cannot do this. More than one global row for a key disables
|
|
54
54
|
Save and Reset with a notice naming the ids: `Setting.Meta.ordering = ["key"]`
|
|
55
55
|
leaves the server's `.first()` undefined among duplicates, so editing one could
|
|
56
|
-
change a row the renderer never reads.
|
|
56
|
+
change a row the renderer never reads. Save and Reset also stay disabled until
|
|
57
|
+
the lookup succeeds (pending or failed reads say so beside the buttons), and a
|
|
58
|
+
save with no known row (`saveCategoryValue(category, null, …)`) re-reads first:
|
|
59
|
+
it refuses without writing when that read fails or finds a row, and creates
|
|
60
|
+
only when the list is empty. `resetCategory(category, null)` resolves `null`,
|
|
61
|
+
meaning nothing was reset.
|
|
57
62
|
|
|
58
63
|
**Write:** the whole category as one object — `POST /api/settings` to create
|
|
59
64
|
(`{key, is_secret: false, group: null, value}`) or `POST /api/settings/<id>`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portal-mojo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "React portal toolkit for django-mojo — typed client + params store, mission-control UI, dependency-free SVG charts, admin section bundles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -94,9 +94,10 @@ function RoleField({ category, draft, role, kind, name, error, disabled, onChang
|
|
|
94
94
|
function CategoryPanel({ category, categoryKey }: { category: RenditionCategory; categoryKey: string }) {
|
|
95
95
|
const queryClient = useQueryClient();
|
|
96
96
|
const rowsQuery = useQuery({ queryKey: rowQueryKey(category.key), queryFn: () => findCategoryRows(category.key), retry: false, refetchOnWindowFocus: false });
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
97
|
+
// Rows only from a successful read: a failed or pending read is "unknown", never "no row".
|
|
98
|
+
const rows = rowsQuery.isSuccess ? rowsQuery.data : null;
|
|
99
|
+
const row: SettingRow | null = rows?.[0] ?? null;
|
|
100
|
+
const duplicates = (rows?.length ?? 0) > 1;
|
|
100
101
|
const [draft, setDraft] = useState<CategoryDraft>(() => valueToDraft(category, category.override));
|
|
101
102
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
102
103
|
const [formError, setFormError] = useState('');
|
|
@@ -134,6 +135,7 @@ function CategoryPanel({ category, categoryKey }: { category: RenditionCategory;
|
|
|
134
135
|
setErrors((current) => ({ ...current, [key]: routed.message }));
|
|
135
136
|
};
|
|
136
137
|
const save = async () => {
|
|
138
|
+
if (!rowsQuery.isSuccess || duplicates) return;
|
|
137
139
|
setBusy(true); setFormError(''); setErrors({});
|
|
138
140
|
try {
|
|
139
141
|
await saveCategoryValue(category, row, value);
|
|
@@ -146,6 +148,7 @@ function CategoryPanel({ category, categoryKey }: { category: RenditionCategory;
|
|
|
146
148
|
}
|
|
147
149
|
};
|
|
148
150
|
const reset = async () => {
|
|
151
|
+
if (!rowsQuery.isSuccess || duplicates) return;
|
|
149
152
|
const ok = await modal.confirm({
|
|
150
153
|
title: `Reset ${renditionCategoryLabel(categoryKey).toLowerCase()} rendering to defaults?`,
|
|
151
154
|
message: 'Every role goes back to the framework defaults and the default set of roles runs on upload. Existing files are not touched.',
|
|
@@ -155,8 +158,9 @@ function CategoryPanel({ category, categoryKey }: { category: RenditionCategory;
|
|
|
155
158
|
if (!ok) return;
|
|
156
159
|
setBusy(true); setFormError(''); setErrors({});
|
|
157
160
|
try {
|
|
158
|
-
await resetCategory(category, row);
|
|
159
|
-
toast.success(`${renditionCategoryLabel(categoryKey)} rendering reset to defaults`);
|
|
161
|
+
const resetRow = await resetCategory(category, row);
|
|
162
|
+
if (resetRow) toast.success(`${renditionCategoryLabel(categoryKey)} rendering reset to defaults`);
|
|
163
|
+
else toast.info('There was no stored override to reset.');
|
|
160
164
|
await refresh();
|
|
161
165
|
} catch (reason) {
|
|
162
166
|
fail(reason);
|
|
@@ -165,14 +169,17 @@ function CategoryPanel({ category, categoryKey }: { category: RenditionCategory;
|
|
|
165
169
|
}
|
|
166
170
|
};
|
|
167
171
|
|
|
168
|
-
const disabled = busy || duplicates || rowsQuery.
|
|
172
|
+
const disabled = busy || duplicates || !rowsQuery.isSuccess;
|
|
173
|
+
const status = rowsQuery.isPending ? 'Reading the current override…'
|
|
174
|
+
: rowsQuery.isError ? 'The current override could not be read — Save is disabled until it loads.'
|
|
175
|
+
: row ? `Override row #${row.id}` : 'No override row yet — Save creates one.';
|
|
169
176
|
return <div className="media-rendering-panel">
|
|
170
177
|
{hasH265 && <p className="media-rendering-note">
|
|
171
178
|
<b>H.265 (HEVC)</b> encodes take several times longer than H.264 and do not play in every browser. Keep <code>video_mp4</code> as the H.264 compatibility rendition and enable <code>video_hevc</code> on upload only when the extra encode time is worth it.
|
|
172
179
|
</p>}
|
|
173
180
|
{rowsQuery.error && <div className="form-alert" role="alert">The current override could not be read: {rowsQuery.error.message} <button className="btn btn-compact" onClick={() => void rowsQuery.refetch()}>Retry</button></div>}
|
|
174
181
|
{duplicates && <div className="form-alert" role="alert">
|
|
175
|
-
More than one global row exists for <code>{category.key}</code> (ids {rows
|
|
182
|
+
More than one global row exists for <code>{category.key}</code> (ids {rows?.map((entry) => entry.id).join(', ')}). The server reads an undefined one, so this page will not edit them; remove the duplicates on the server first.
|
|
176
183
|
</div>}
|
|
177
184
|
{formError && <div className="form-alert" role="alert">{formError}</div>}
|
|
178
185
|
<div className="media-rendering-roles">
|
|
@@ -210,7 +217,7 @@ function CategoryPanel({ category, categoryKey }: { category: RenditionCategory;
|
|
|
210
217
|
<div className="media-rendering-actions">
|
|
211
218
|
<button className="btn btn-primary" disabled={disabled || !dirty} onClick={() => void save()}>{busy ? 'Saving…' : 'Save'}</button>
|
|
212
219
|
<button className="btn" disabled={disabled || !row || category.override == null} onClick={() => void reset()}>Reset to defaults…</button>
|
|
213
|
-
<span className="dim">{
|
|
220
|
+
<span className="dim">{status}</span>
|
|
214
221
|
</div>
|
|
215
222
|
</div>;
|
|
216
223
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Everything about roles, fields, choices and limits comes from the
|
|
7
7
|
// descriptor. The ONE rule this module hardcodes is the mp4-only trio
|
|
8
8
|
// (`codec`/`crf`/`preset`), which the server refuses on a non-mp4 role.
|
|
9
|
-
import { mojoCall, mojoList, mojoSave, withFreshAuth } from '../../client/runtime';
|
|
9
|
+
import { isReauthRequired, mojoCall, mojoList, mojoSave, withFreshAuth } from '../../client/runtime';
|
|
10
10
|
import type { SettingRow } from '../settings/model';
|
|
11
11
|
|
|
12
12
|
export type RenditionCategoryKey = 'image' | 'video' | 'document';
|
|
@@ -251,13 +251,36 @@ export function routeRenditionError(message: string, knownRoles: readonly string
|
|
|
251
251
|
return { role: role!, field: field ?? null, message: rest! };
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* Create or update the category's global row. Imperative; a failure rejects.
|
|
256
|
+
* With `row === null` the handler proves the row is absent before creating:
|
|
257
|
+
* it re-reads the global rows and refuses (no write) when that read fails or
|
|
258
|
+
* finds a row, so no caller creates one blind. The read and the create are
|
|
259
|
+
* separate requests: two admins saving at the same moment can still both
|
|
260
|
+
* create (#8200 Challenge row 2); server-side uniqueness is #8201.
|
|
261
|
+
*/
|
|
255
262
|
export async function saveCategoryValue(category: RenditionCategory, row: SettingRow | null, value: Record<string, unknown>): Promise<SettingRow> {
|
|
256
263
|
const changes: Record<string, unknown> = row ? { value } : { key: category.key, is_secret: false, group: null, value };
|
|
257
|
-
return withFreshAuth(() =>
|
|
264
|
+
return withFreshAuth(async () => {
|
|
265
|
+
if (!row) {
|
|
266
|
+
let existing: SettingRow[];
|
|
267
|
+
try {
|
|
268
|
+
existing = await findCategoryRows(category.key);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
// A step-up refusal goes back to withFreshAuth, which re-runs the read after re-login.
|
|
271
|
+
if (isReauthRequired(error)) throw error;
|
|
272
|
+
throw new Error('The current override could not be read, so nothing was saved. Retry the read first.');
|
|
273
|
+
}
|
|
274
|
+
if (existing.length) throw new Error(`A global row for ${category.key} already exists (id ${existing.map((entry) => entry.id).join(', ')}); reload before saving.`);
|
|
275
|
+
}
|
|
276
|
+
return mojoSave<SettingRow>('/api/settings', row?.id ?? null, changes);
|
|
277
|
+
});
|
|
258
278
|
}
|
|
259
279
|
|
|
260
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Reset = write `{}`. The live Setting model refuses DELETE, and `{}` is reported as `override: null`.
|
|
282
|
+
* Resolves `null` when `row` is null: nothing was reset and nothing was sent.
|
|
283
|
+
*/
|
|
261
284
|
export async function resetCategory(category: RenditionCategory, row: SettingRow | null): Promise<SettingRow | null> {
|
|
262
285
|
if (!row) return null;
|
|
263
286
|
return saveCategoryValue(category, row, {});
|