Desktop: folders keep claiming their store records before the config read returns; a store app with a folder's id is never listed twice

This commit is contained in:
CRTkafa 2026-10-23 09:18:03 +08:00
parent 5e82010ace
commit 7c769c18f4
3 changed files with 38 additions and 2 deletions

View file

@ -224,11 +224,18 @@ export function DataProvider({ children }: { children: ReactNode }) {
setStoreApps(Object.fromEntries((Array.isArray(s) ? s : []).filter(x => x && typeof x.account === 'string').map(x => [x.account, x.entry ?? null])));
}, []);
// the last config summary seen per folder: a folder whose read has not come back yet (start) or failed this time keeps
// claiming its store records, so its App Store / Play entries never show up a second time as store-only apps
const lastSummary = useRef(new Map<string, NonNullable<PortfolioResult['apps'][number]['summary']>>());
const apps = useMemo(() => buildApps({
projects, accounts, overrides,
summaryOf: p => portfolio?.apps.find(a => a.project === p.path)?.summary ?? liveRef.current?.value.apps.find(a => a.project === p.path)?.summary,
summaryOf: p => {
const fresh = portfolio?.apps.find(a => a.project === p.path)?.summary ?? live?.value.apps.find(a => a.project === p.path)?.summary;
if (fresh) lastSummary.current.set(p.path, fresh);
return fresh ?? lastSummary.current.get(p.path);
},
storeApps: Object.entries(storeApps).filter(([, e]) => !!e).map(([account, e]) => ({ account, apps: e!.value.apps })),
}), [projects, accounts, overrides, portfolio, storeApps]);
}), [projects, accounts, overrides, portfolio, live, storeApps]);
const appsRef = useRef(apps);
appsRef.current = apps;

View file

@ -53,6 +53,21 @@ describe('identity pairing', () => {
const apps2 = buildApps({ projects: [prism], accounts, storeApps: twin, overrides: NO_OVERRIDES, summaryOf: () => summary('com.example.prism', 'com.example.prism') });
expect(apps2[0]!.ios?.account).toBe(A1);
});
test('a folder with a config summary claims its iOS and Android records; ignore flags do not break it', () => {
for (const o of [NO_OVERRIDES, withIgnored(NO_OVERRIDES, ['project:p1:ios'], true), withIgnored(NO_OVERRIDES, ['project:p1:android'], true)]) {
const apps = build(o);
expect(apps.filter(a => a.ios?.app.bundleId === 'com.example.prism' || a.android?.app.id === 'com.example.prism').length).toBeLessThanOrEqual(1);
expect(apps.filter(a => a.name === 'Prism' && !a.project)).toEqual([]);
}
});
test('without a summary the folder claims nothing (the start-up race); a store app of a folder bundle id is never shown separately', () => {
const none = buildApps({ projects: [prism], accounts, storeApps, overrides: NO_OVERRIDES, summaryOf: () => null });
expect(none.filter(a => !a.project && a.name === 'Prism')).toHaveLength(1);
// guard: the folder is linked to other records, its summary's ids still hide the duplicates
const linked = { ...NO_OVERRIDES, links: [{ key: `ios:${A1}:102`, project: 'p1' }] };
const apps = build(linked);
expect(apps.filter(a => !a.project && (a.ios?.app.bundleId === 'com.example.prism' || a.android?.app.id === 'com.example.prism'))).toEqual([]);
});
test('unpair blocks the automatic pair; manual pair beats it', () => {
const o1 = withUnpair(NO_OVERRIDES, `ios:${A1}:102`, `android:${P1}:com.example.orbit`);
const apps1 = build(o1);

View file

@ -134,6 +134,20 @@ export function buildApps<P extends IdProject>(i: BuildInput<P>): UnifiedApp<P>[
});
}
// guard: a store app with the same App Store id / bundle id / package name as a folder is that folder's record, never a
// second entry (the folder could not take it above, e.g. it is linked elsewhere); it is dropped, not shown separately
const folderIds = new Set<string>();
for (const p of i.projects) {
const s = i.summaryOf(p);
if (s?.apple?.appId) folderIds.add(`ios:id:${s.apple.appId}`);
if (s?.apple?.bundleId) folderIds.add(`ios:bundle:${s.apple.bundleId}`);
if (s?.play?.packageName) folderIds.add(`android:${s.play.packageName}`);
}
for (const r of refs) {
if (used.has(r.key)) continue;
if (r.store === 'ios' ? folderIds.has(`ios:id:${r.app.id}`) || folderIds.has(`ios:bundle:${r.app.bundleId}`) : folderIds.has(`android:${r.app.id}`)) used.add(r.key);
}
// 2. pairs among the remaining store apps
const freeIos = () => refs.filter(r => r.store === 'ios' && !used.has(r.key));
const freeAndroid = () => refs.filter(r => r.store === 'android' && !used.has(r.key));