desktop editors: edit in place what App Store Connect and Play Console let the owner edit (listings per language for both stores with live counters and the check / scan rules, store details, App info, age rating, App Review, Play store settings, Data safety and App Privacy answers, promotion blocks, catalog names / prices / offers, TestFlight information, scene order); storebound edit (project.edit) saves into the config by path keeping comments and formatting, the listing files and the review contact, dry run with the diff and the checks first; Apply shows each store's dry run in the window; unsaved edits survive tab switches with undo / redo; tests/edit.test.ts, apps/desktop/tests/edits.test.ts, boot check editor case

This commit is contained in:
CRTkafa 2026-10-14 00:35:29 +08:00
parent 714352ec18
commit 3cea2af4e1
37 changed files with 3020 additions and 54 deletions

View file

@ -0,0 +1,48 @@
// TestFlight editor (TestFlight tab): What to Test per locale (### whatToTest in the App Store listing file), the
// Beta App Description per language, notes for Beta App Review, and the beta groups (checked JSON). Saved into the
// files, then applied with the dry run in the window: test information, and creating the missing groups. Sending a
// build to external testers and Beta App Review stay the owner's terminal steps.
import type { EditableResult, Project } from '../../api';
import { t } from '../../i18n';
import { ApplyPanel, EditBar, JsonField, TextField, useEdits } from '../../ui/Editor';
import { Card } from '../../ui/kit';
type O = Record<string, unknown>;
const obj = (v: unknown): O => (v && typeof v === 'object' && !Array.isArray(v) ? (v as O) : {});
export function TestFlightEditor({ project, data, onSaved }: { project: Project; data: EditableResult; onSaved: () => void }) {
const e = useEdits();
const tf = obj(obj(data.config.apple).testflight);
const desc = tf.description;
const unsaved = e.count(project.id, 'testflight') ? t('ed.saveFirst') : null;
if (!data.apple) return null;
return (
<div className="stack" id="testflight-editor" style={{ gap: 16 }}>
<Card title={t('ed.tfTitle')} icon="edit" sub={t('ed.tfSub')}>
<div className="ed-langs">
{data.apple.map(l => (
<div className="ed-lang" key={l.lang}>
<div className="small strong mono-id">{l.lang}</div>
<TextField project={project} area="testflight" op={{ kind: 'listing', store: 'apple', lang: l.lang, field: 'whatToTest' }} saved={l.values.whatToTest ?? ''}
label={t('ed.whatToTest')} limit={4000} multiline rows={3} placeholder={l.values.whatsNew ?? ''} hint={l.values.whatToTest ? undefined : t('ed.whatToTestHint')} />
{typeof desc !== 'string' && (
<TextField project={project} area="testflight" op={{ kind: 'config', path: `apple.testflight.description.${l.language}` }} saved={typeof obj(desc)[l.language] === 'string' ? String(obj(desc)[l.language]) : ''}
label={t('ed.betaDescription')} limit={4000} multiline rows={3} />
)}
</div>
))}
</div>
{typeof desc === 'string' && <TextField project={project} area="testflight" op={{ kind: 'config', path: 'apple.testflight.description' }} saved={desc} label={t('ed.betaDescriptionAll')} limit={4000} multiline rows={3} />}
<TextField project={project} area="testflight" op={{ kind: 'config', path: 'apple.testflight.reviewNotes' }} saved={typeof tf.reviewNotes === 'string' ? tf.reviewNotes : ''} label={t('ed.betaNotes')} limit={4000} multiline rows={3} />
<JsonField project={project} area="testflight" path="apple.testflight.groups" saved={tf.groups ?? null} label={t('ed.tfGroups')} rows={6} hint={t('ed.tfGroupsHint')} />
</Card>
<EditBar project={project} area="testflight" data={data} files={[data.files.apple, data.files.config]} onSaved={onSaved} />
<Card title={t('ed.applyTitle')} icon="upload" sub={t('ed.applyTfSub')}>
<div className="act-grid">
<ApplyPanel project={project} method="apple.testflight_info" label={t('ed.applyTfInfo')} done={t('ai.pushed')} blocked={unsaved} />
<ApplyPanel project={project} method="apple.testflight_groups" label={t('tf.groupsSync')} done={t('tf.groupsSynced')} blocked={unsaved} />
</div>
</Card>
</div>
);
}