Desktop D3 writes in the window: push listing text, upload graphics, screenshots and previews, render jobs, catalog sync and price export, Play rollout share / halt / resume / complete and iOS phased release controls, TestFlight What to Test, internal groups and testers, reply editor with approve and send; every one a dry run, a native dialog, then the real run
This commit is contained in:
parent
f47c00bed0
commit
ba58895d3a
15 changed files with 573 additions and 69 deletions
|
|
@ -1,11 +1,13 @@
|
|||
// Releases: the release timeline (one version on both stores on one track: build -> testing -> review -> live ->
|
||||
// rollout %), then every version read from the stores. Submit and release stay in the consoles.
|
||||
import { api, type ReleaseTrain } from '../../api';
|
||||
import { useState } from 'react';
|
||||
import { api, type ReleaseTrain, type StoreRelease } from '../../api';
|
||||
import type { UnifiedApp } from '../../lib/identity';
|
||||
import { WriteButton } from '../../ui/WriteButton';
|
||||
import { type Key, t } from '../../i18n';
|
||||
import { ago, date, share } from '../../lib/format';
|
||||
import { type Lane, STAGES, type TestFlightStatus, trainLanes, versionHistory } from '../../lib/model';
|
||||
import { useData, useRead } from '../../store';
|
||||
import { useData, useRead, useWrite } from '../../store';
|
||||
import { Icon, StoreGlyph } from '../../ui/icons';
|
||||
import { Bar, Card, Chip, Empty, Notice, PhaseChip, storeName } from '../../ui/kit';
|
||||
import { ascUrl, PLAY_CONSOLE } from '../AppView';
|
||||
|
|
@ -48,7 +50,54 @@ function LaneRow({ lane, version }: { lane: Lane; version: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
function Train({ title, train, tf, tone }: { title: string; train: ReleaseTrain; tf: TestFlightStatus | null; tone: 'accent' | 'ok' }) {
|
||||
const SHARES = [1, 2, 5, 10, 20, 50];
|
||||
|
||||
/** Rollout controls of one store release: Play staged rollout share / halt / resume / complete, iOS phased release
|
||||
* pause / resume / everyone, or turning the phased release on for an upcoming iOS version. Owner actions: each one is
|
||||
* a dry run first, then a system dialog that names it. */
|
||||
function RolloutControls({ app, r }: { app: UnifiedApp; r: StoreRelease }) {
|
||||
const w = useWrite(app.project?.id ?? null);
|
||||
const pct = r.fraction !== null ? Math.round(r.fraction * 100) : null;
|
||||
const [share, setShare] = useState(() => String(SHARES.find(s => pct === null || s > pct) ?? 50));
|
||||
if (!app.project) return null;
|
||||
const v = r.version ?? '';
|
||||
if (r.store === 'android' && (r.phase === 'rollout' || r.phase === 'halted')) {
|
||||
const track = r.track && ['production', 'beta', 'alpha', 'internal'].includes(r.track) ? r.track : 'production';
|
||||
const next = SHARES.filter(s => pct === null || s > pct);
|
||||
return (
|
||||
<div className="roll-ctl">
|
||||
<span className="small muted">{t('roll.playNow', { pct: pct ?? 0 })}</span>
|
||||
{r.phase === 'rollout' && next.length > 0 && <>
|
||||
<select className="input input-sm" value={share} onChange={e => setShare(e.target.value)} aria-label={t('roll.share')}>{next.map(s => <option key={s} value={s}>{s} %</option>)}</select>
|
||||
<WriteButton run={w} icon="arrowRight" w={{ method: 'owner.play_rollout', args: { action: 'percent', percent: Number(share), track }, title: t('roll.toShare', { pct: share }), done: t('roll.done') }}>{t('roll.toShare', { pct: share })}</WriteButton>
|
||||
</>}
|
||||
{r.phase === 'rollout' && <WriteButton run={w} icon="pause" w={{ method: 'owner.play_rollout', args: { action: 'halt', track }, title: t('roll.halt'), done: t('roll.halted') }} hint={t('roll.haltHint')}>{t('roll.halt')}</WriteButton>}
|
||||
{r.phase === 'halted' && <WriteButton run={w} icon="play" w={{ method: 'owner.play_rollout', args: { action: 'resume', track }, title: t('roll.resume'), done: t('roll.done') }}>{t('roll.resume')}</WriteButton>}
|
||||
<WriteButton run={w} icon="users" w={{ method: 'owner.play_rollout', args: { action: 'complete', track }, title: t('roll.complete'), done: t('roll.done') }} hint={t('roll.completeHint')}>{t('roll.complete')}</WriteButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (r.store === 'ios' && r.phased && (r.phased.state === 'ACTIVE' || r.phased.state === 'PAUSED')) {
|
||||
return (
|
||||
<div className="roll-ctl">
|
||||
<span className="small muted">{t('roll.iosNow', { day: r.phased.day ?? 0 })}</span>
|
||||
{r.phased.state === 'ACTIVE' && <WriteButton run={w} icon="pause" w={{ method: 'owner.ios_phased', args: { action: 'pause', version: v }, title: t('roll.pause'), done: t('roll.paused') }} hint={t('roll.pauseHint')}>{t('roll.pause')}</WriteButton>}
|
||||
{r.phased.state === 'PAUSED' && <WriteButton run={w} icon="play" w={{ method: 'owner.ios_phased', args: { action: 'resume', version: v }, title: t('roll.resume'), done: t('roll.done') }}>{t('roll.resume')}</WriteButton>}
|
||||
<WriteButton run={w} icon="users" w={{ method: 'owner.ios_phased', args: { action: 'complete', version: v }, title: t('roll.everyone'), done: t('roll.done') }} hint={t('roll.everyoneHint')}>{t('roll.everyone')}</WriteButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (r.store === 'ios' && !r.phased && ['draft', 'review', 'approved'].includes(r.phase) && /^\d+(\.\d+){0,2}$/.test(v)) {
|
||||
return (
|
||||
<div className="roll-ctl">
|
||||
<WriteButton run={w} icon="layers" w={{ method: 'owner.ios_phased', args: { action: 'on', version: v }, title: t('roll.phasedOn'), done: t('roll.phasedOnDone') }} hint={t('roll.phasedOnHint')}>{t('roll.phasedOn')}</WriteButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function Train({ title, train, tf, tone, app }: { title: string; train: ReleaseTrain; tf: TestFlightStatus | null; tone: 'accent' | 'ok'; app: UnifiedApp }) {
|
||||
const lanes = trainLanes(train, tf);
|
||||
return (
|
||||
<Card pad={false} title={<span className="train-head"><span className="ver">{train.version}</span><Chip tone={tone}>{title}</Chip></span>}
|
||||
|
|
@ -57,6 +106,11 @@ function Train({ title, train, tf, tone }: { title: string; train: ReleaseTrain;
|
|||
<div className="tl-head"><span>{t('tl.store')}</span>{STAGES.map(s => <span key={s}>{t(STAGE_KEY[s])}</span>)}</div>
|
||||
{lanes.map(l => <LaneRow key={l.store} lane={l} version={train.version} />)}
|
||||
</div>
|
||||
{lanes.some(l => l.release) && app.project && (
|
||||
<div className="card-foot roll-foot">
|
||||
{lanes.map(l => l.release && <RolloutControls key={l.store} app={app} r={l.release} />)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -83,8 +137,8 @@ export function ReleasesTab({ app }: { app: UnifiedApp }) {
|
|||
return (
|
||||
<>
|
||||
{errs.map(([s, msg]) => <Notice key={s} tone="warn">{t('releases.storeUnreadable', { store: storeName(s as 'ios'), error: msg })}</Notice>)}
|
||||
{l.release.next && <Train title={t('tl.next')} train={l.release.next} tf={tf.data} tone="accent" />}
|
||||
{l.release.current && <Train title={t('tl.live')} train={l.release.current} tf={tf.data} tone="ok" />}
|
||||
{l.release.next && <Train title={t('tl.next')} train={l.release.next} tf={tf.data} tone="accent" app={app} />}
|
||||
{l.release.current && <Train title={t('tl.live')} train={l.release.current} tf={tf.data} tone="ok" app={app} />}
|
||||
{!l.release.next && !l.release.current && <Card pad={false}><Empty compact illo="unknown" title={t('releases.noVersions')}>{t('releases.noVersionsText')}</Empty></Card>}
|
||||
|
||||
<Card title={t('releases.history')} icon="layers" sub={t('releases.historySub', { when: ago(d.live?.at ?? d.portfolio?.at) })} pad={false}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue