Skip to content

Commit 5c7277c

Browse files
committed
Only save changed locales on store listing and app details pages
1 parent 1f1c879 commit 5c7277c

3 files changed

Lines changed: 99 additions & 58 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Fix app details allowing locale add/delete when version is locked
77
- Fix submitting a new nomination creating a duplicate draft
88
- Fix ASC rate limit errors when saving many locales at once
9+
- Only save changed locales instead of all locales on store listing and app details
910
- Fix all linting and React compiler errors, make lint failures block CI
1011

1112
## 1.6.1

src/app/dashboard/apps/[appId]/details/page.tsx

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,9 @@ export default function AppDetailsPage() {
188188
);
189189
}
190190

191-
// Track original locale → localization ID mapping for diffing saves
191+
// Track original locale → localization ID mapping and data for diffing saves
192192
const originalLocaleIdsRef = useRef<Record<string, string>>({});
193+
const originalLocaleDataRef = useRef<Record<string, AppInfoLocaleFields>>({});
193194

194195
// Track which localizations have been synced to avoid re-syncing
195196
const [syncedLocalizations, setSyncedLocalizations] = useState(localizations);
@@ -217,6 +218,7 @@ export default function AppDetailsPage() {
217218
ids[loc.attributes.locale] = loc.id;
218219
}
219220
originalLocaleIdsRef.current = ids;
221+
originalLocaleDataRef.current = buildLocaleData(localizations);
220222
}, [localizations]);
221223

222224
// Sync content rights when app data loads
@@ -295,26 +297,47 @@ export default function AppDetailsPage() {
295297
registerSave(async () => {
296298
const promises: Promise<void>[] = [];
297299

298-
// Save localizations
300+
// Only send locales that actually changed (or are new/deleted)
301+
const changedLocales: Record<string, AppInfoLocaleFields> = {};
302+
const changedLocaleIds: Record<string, string> = {};
303+
const orig = originalLocaleDataRef.current;
304+
305+
for (const [locale, fields] of Object.entries(localeData)) {
306+
const origFields = orig[locale];
307+
if (!origFields || Object.keys(fields).some((k) => fields[k as keyof AppInfoLocaleFields] !== origFields[k as keyof AppInfoLocaleFields])) {
308+
changedLocales[locale] = fields;
309+
changedLocaleIds[locale] = originalLocaleIdsRef.current[locale];
310+
}
311+
}
312+
// Detect deleted locales
313+
for (const locale of Object.keys(originalLocaleIdsRef.current)) {
314+
if (!localeData[locale]) {
315+
changedLocaleIds[locale] = originalLocaleIdsRef.current[locale];
316+
}
317+
}
318+
319+
// Save localizations (skip if nothing changed)
299320
let locCreatedIds: Record<string, string> = {};
300321
const syncErrors: SyncError[] = [];
301-
promises.push(
302-
fetch(`/api/apps/${appId}/info/${appInfoId}/localizations`, {
303-
method: "PUT",
304-
headers: { "Content-Type": "application/json" },
305-
body: JSON.stringify({
306-
locales: localeData,
307-
originalLocaleIds: originalLocaleIdsRef.current,
322+
if (Object.keys(changedLocales).length > 0 || Object.keys(changedLocaleIds).length > Object.keys(changedLocales).length) {
323+
promises.push(
324+
fetch(`/api/apps/${appId}/info/${appInfoId}/localizations`, {
325+
method: "PUT",
326+
headers: { "Content-Type": "application/json" },
327+
body: JSON.stringify({
328+
locales: changedLocales,
329+
originalLocaleIds: changedLocaleIds,
330+
}),
331+
}).then(async (res) => {
332+
const data = await res.json();
333+
if (!res.ok && !data.errors) throw new Error(data.error ?? "Save failed");
334+
if (data.errors?.length > 0) {
335+
syncErrors.push(...(data.errors as SyncError[]));
336+
}
337+
locCreatedIds = data.createdIds ?? {};
308338
}),
309-
}).then(async (res) => {
310-
const data = await res.json();
311-
if (!res.ok && !data.errors) throw new Error(data.error ?? "Save failed");
312-
if (data.errors?.length > 0) {
313-
syncErrors.push(...(data.errors as SyncError[]));
314-
}
315-
locCreatedIds = data.createdIds ?? {};
316-
}),
317-
);
339+
);
340+
}
318341

319342
// Save app attributes if changed (content rights + notification URLs)
320343
const appAttrs: Record<string, string | null> = {};
@@ -384,7 +407,7 @@ export default function AppDetailsPage() {
384407

385408
toast.success("App details saved");
386409

387-
// Update original snapshot with real IDs from created locales
410+
// Update original snapshots so subsequent saves only send new diffs
388411
const ids: Record<string, string> = { ...originalLocaleIdsRef.current };
389412
for (const [locale, id] of Object.entries(locCreatedIds)) {
390413
ids[locale] = id;
@@ -393,6 +416,7 @@ export default function AppDetailsPage() {
393416
if (!localeData[locale]) delete ids[locale];
394417
}
395418
originalLocaleIdsRef.current = ids;
419+
originalLocaleDataRef.current = { ...localeData };
396420

397421
// Update app name in context if primary locale name changed
398422
const primaryName = localeData[primaryLocale]?.name;

src/app/dashboard/apps/[appId]/store-listing/page.tsx

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,9 @@ export default function StoreListingPage() {
289289
originalCopyrightRef.current = cr;
290290
}
291291

292-
// Track original locale → localization ID mapping for diffing saves
292+
// Track original locale → localization ID mapping and data for diffing saves
293293
const originalLocaleIdsRef = useRef<Record<string, string>>({});
294+
const originalLocaleDataRef = useRef<Record<string, LocaleFields>>({});
294295

295296
// Reset locale data when localizations change (during render)
296297
const [prevLocalizations, setPrevLocalizations] = useState(localizations);
@@ -311,13 +312,14 @@ export default function StoreListingPage() {
311312
setDirty(false);
312313
}
313314

314-
// Snapshot original locale → ID mapping for save diffing
315+
// Snapshot original locale IDs and data for save diffing
315316
useEffect(() => {
316317
const ids: Record<string, string> = {};
317318
for (const loc of localizations) {
318319
ids[loc.attributes.locale] = loc.id;
319320
}
320321
originalLocaleIdsRef.current = ids;
322+
originalLocaleDataRef.current = buildLocaleData(localizations);
321323
}, [localizations]);
322324

323325
// Validate field limits across all locales
@@ -363,44 +365,57 @@ export default function StoreListingPage() {
363365
const promises: Promise<void>[] = [];
364366
const allSyncErrors: SyncError[] = [];
365367

366-
// When the version is read-only (live), only promotional text is
367-
// editable – send just that field to avoid ASC rejecting locked fields.
368-
// For the first-ever version, strip whatsNew – ASC rejects it.
369-
const locPayload = readOnly
370-
? Object.fromEntries(
371-
Object.entries(localeData).map(([locale, fields]) => [
372-
locale,
373-
{ promotionalText: fields.promotionalText },
374-
]),
375-
)
376-
: isFirstVersion
377-
? Object.fromEntries(
378-
Object.entries(localeData).map(([locale, fields]) => [
379-
locale,
380-
Object.fromEntries(Object.entries(fields).filter(([k]) => k !== "whatsNew")),
381-
]),
382-
)
383-
: localeData;
384-
385-
// Save localizations
368+
// Only send locales that actually changed (or are new/deleted)
369+
const changedLocales: Record<string, LocaleFields | Record<string, string>> = {};
370+
const changedLocaleIds: Record<string, string> = {};
371+
const orig = originalLocaleDataRef.current;
372+
373+
for (const [locale, fields] of Object.entries(localeData)) {
374+
const origFields = orig[locale];
375+
// New locale or any field differs → include it
376+
if (!origFields || Object.keys(fields).some((k) => fields[k as keyof LocaleFields] !== origFields[k as keyof LocaleFields])) {
377+
changedLocaleIds[locale] = originalLocaleIdsRef.current[locale];
378+
// When read-only, only promotional text is editable.
379+
// For the first-ever version, strip whatsNew – ASC rejects it.
380+
if (readOnly) {
381+
changedLocales[locale] = { promotionalText: fields.promotionalText };
382+
} else if (isFirstVersion) {
383+
changedLocales[locale] = Object.fromEntries(
384+
Object.entries(fields).filter(([k]) => k !== "whatsNew"),
385+
);
386+
} else {
387+
changedLocales[locale] = fields;
388+
}
389+
}
390+
}
391+
// Detect deleted locales
392+
for (const locale of Object.keys(originalLocaleIdsRef.current)) {
393+
if (!localeData[locale]) {
394+
changedLocaleIds[locale] = originalLocaleIdsRef.current[locale];
395+
}
396+
}
397+
398+
// Save localizations (skip if nothing changed)
386399
let locCreatedIds: Record<string, string> = {};
387-
promises.push(
388-
fetch(`/api/apps/${appId}/versions/${versionId}/localizations`, {
389-
method: "PUT",
390-
headers: { "Content-Type": "application/json" },
391-
body: JSON.stringify({
392-
locales: locPayload,
393-
originalLocaleIds: originalLocaleIdsRef.current,
400+
if (Object.keys(changedLocales).length > 0 || Object.keys(changedLocaleIds).length > Object.keys(changedLocales).length) {
401+
promises.push(
402+
fetch(`/api/apps/${appId}/versions/${versionId}/localizations`, {
403+
method: "PUT",
404+
headers: { "Content-Type": "application/json" },
405+
body: JSON.stringify({
406+
locales: changedLocales,
407+
originalLocaleIds: changedLocaleIds,
408+
}),
409+
}).then(async (res) => {
410+
const data = await res.json();
411+
if (!res.ok && !data.errors) throw new Error(data.error ?? "Save failed");
412+
if (data.errors?.length > 0) {
413+
allSyncErrors.push(...(data.errors as SyncError[]));
414+
}
415+
locCreatedIds = data.createdIds ?? {};
394416
}),
395-
}).then(async (res) => {
396-
const data = await res.json();
397-
if (!res.ok && !data.errors) throw new Error(data.error ?? "Save failed");
398-
if (data.errors?.length > 0) {
399-
allSyncErrors.push(...(data.errors as SyncError[]));
400-
}
401-
locCreatedIds = data.createdIds ?? {};
402-
}),
403-
);
417+
);
418+
}
404419

405420
// Release settings are locked on live versions
406421
if (!readOnly) {
@@ -478,7 +493,7 @@ export default function StoreListingPage() {
478493

479494
toast.success(readOnly ? "Promotional text saved" : "Store listing saved");
480495

481-
// Update original snapshot with real IDs from created locales
496+
// Update original snapshots so subsequent saves only send new diffs
482497
const ids = { ...originalLocaleIdsRef.current };
483498
for (const [locale, id] of Object.entries(locCreatedIds)) {
484499
ids[locale] = id;
@@ -487,6 +502,7 @@ export default function StoreListingPage() {
487502
if (!localeData[locale]) delete ids[locale];
488503
}
489504
originalLocaleIdsRef.current = ids;
505+
originalLocaleDataRef.current = { ...localeData };
490506

491507
// Update cached version with release settings + build
492508
if (!readOnly && selectedVersion) {

0 commit comments

Comments
 (0)