Desktop v2 screens: releases (store cards, stage stepper, rollout ring), submit checklist, TestFlight cards; listings as store pages with completeness, framed screenshots with issue badges, product cards, price matrix with gap shading; health tiles and issue cards, store-style review feed with rating distribution, growth funnel, readiness score with fix-first, tools grid, store-info sheet; settings, accounts, folders, dependencies and onboarding in the v2 language
This commit is contained in:
parent
ce939ee063
commit
f131de9df2
47 changed files with 2309 additions and 927 deletions
|
|
@ -6,7 +6,8 @@
|
|||
// versionCodes on Play to pick by number and Send to production (`play release --version-code n --production ...`),
|
||||
// the managed-publishing warning, and the console steps Play has no API for (production countries, "Send changes for
|
||||
// review" in Publishing overview) as console plans with a deep link.
|
||||
import { useState } from 'react';
|
||||
// v2: each store is a numbered checklist (ready / blocked / to do per step) that ends in its one primary button.
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { api, type Project } from '../../api';
|
||||
import { type Key, t } from '../../i18n';
|
||||
import { ago, date } from '../../lib/format';
|
||||
|
|
@ -15,15 +16,57 @@ import type { UnifiedApp } from '../../lib/identity';
|
|||
import { useData, useRead } from '../../store';
|
||||
import { ConsolePlanButton } from '../../ui/ConsolePlan';
|
||||
import { Icon, StoreGlyph } from '../../ui/icons';
|
||||
import { Card, Chip, Empty, ErrorNotice, Notice, ReadControl, Seg, Skeleton } from '../../ui/kit';
|
||||
import { Chip, ErrorNotice, Notice, ReadControl, Seg, Skeleton } from '../../ui/kit';
|
||||
import { EmptyState, StatusPill } from '../../ui/ds';
|
||||
import { OwnerAction } from '../../ui/OwnerAction';
|
||||
import { ascUrl, PLAY_CONSOLE } from '../AppView';
|
||||
import '../../styles/submit.css';
|
||||
|
||||
const SUB_KIND: Record<SubmissionKind, ['ok' | 'warn' | 'bad' | 'info' | 'neutral', Key]> = {
|
||||
waiting: ['info', 'sub.kWaiting'], inReview: ['info', 'sub.kInReview'], unresolved: ['bad', 'sub.kUnresolved'], draft: ['neutral', 'sub.kDraft'],
|
||||
emptyDraft: ['neutral', 'sub.kEmptyDraft'], done: ['ok', 'sub.kDone'], other: ['neutral', 'sub.kOther'],
|
||||
};
|
||||
|
||||
type StepState = 'ready' | 'blocked' | 'todo' | 'warn';
|
||||
const STEP_PILL: Record<StepState, ['ok' | 'bad' | 'neutral' | 'warn', Key]> = {
|
||||
ready: ['ok', 'v2rel.ready'], blocked: ['bad', 'v2rel.blocked'], todo: ['neutral', 'v2rel.todo'], warn: ['warn', 'v2rel.attention'],
|
||||
};
|
||||
|
||||
/** One numbered step of a send-for-review checklist: node (number, check or alert), title, state pill, body. */
|
||||
function Step({ n, state: st, title, children }: { n: number; state: StepState | undefined; title: string; children?: ReactNode }) {
|
||||
const state = st ?? 'todo';
|
||||
const [tone, key] = STEP_PILL[state];
|
||||
return (
|
||||
<li className={`sub-step ${state}`} data-step-state={state}>
|
||||
<span className="sub-node" aria-hidden="true">{state === 'ready' ? <Icon name="check" /> : state === 'blocked' ? <Icon name="alertCircle" /> : <span className="num">{n}</span>}</span>
|
||||
<div className="sub-step-b">
|
||||
<div className="sub-step-h"><span className="sub-step-t">{title}</span><StatusPill tone={tone}>{t(key)}</StatusPill></div>
|
||||
{children}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/** The head of a store's send card: store mark, title, read state line, ready count, read / console buttons. */
|
||||
function SubHead({ store, title, sub, actions, done, total }: { store: 'ios' | 'android'; title: string; sub: ReactNode; actions: ReactNode; done: number | null; total: number }) {
|
||||
return (
|
||||
<header className="sub-head">
|
||||
<span className="sub-mark"><StoreGlyph store={store} /></span>
|
||||
<div className="sub-head-t">
|
||||
<h3>{title}</h3>
|
||||
<div className="sub-head-s">{sub}</div>
|
||||
</div>
|
||||
{done !== null && (
|
||||
<span className={`sub-count${done === total ? ' all' : ''}`} title={t('v2rel.readyCount', { done, total })}>
|
||||
<span aria-hidden="true"><span className="num">{done}</span><span className="of">/{total}</span></span>
|
||||
<span className="sr">{t('v2rel.readyCount', { done, total })}</span>
|
||||
</span>
|
||||
)}
|
||||
<div className="sub-head-a actions">{actions}</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/** The App Store card: submission state, build picker, Submit / Resubmit / in-app purchases. */
|
||||
export function AppleSubmitCard({ app, project }: { app: UnifiedApp; project: Project }) {
|
||||
const d = useData();
|
||||
|
|
@ -39,92 +82,112 @@ export function AppleSubmitCard({ app, project }: { app: UnifiedApp; project: Pr
|
|||
const rejectedIap = s?.unresolved?.items.some(i => i.rejected && i.kind !== 'appStoreVersion') ?? false;
|
||||
const args = { build, ...(version ? { version: version.version, platform: platformFlag(version.platform) } : {}), ...(resolveIaps ? { resolve_iaps: true } : {}) };
|
||||
const refresh = () => { r.refresh(); };
|
||||
const open = s ? s.submissions.filter(x => x.kind !== 'done') : [];
|
||||
const states: StepState[] = s ? [
|
||||
'ready',
|
||||
s.busy ? 'blocked' : s.unresolved ? 'warn' : 'ready',
|
||||
picked ? 'ready' : s.builds.length ? 'todo' : 'blocked',
|
||||
version ? 'ready' : 'blocked',
|
||||
picked && version && !s.busy ? 'ready' : 'todo',
|
||||
] : [];
|
||||
const done = s ? states.filter(x => x === 'ready').length : null;
|
||||
return (
|
||||
<Card title={t('sub.appleTitle')} icon="rocket" sub={<span className="row" style={{ gap: 6 }}><StoreGlyph store="ios" className="ic13" />{r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.appleSub')}</span>}
|
||||
actions={<>
|
||||
<ReadControl r={{ ...r, refresh: async () => refresh() }} label={t('sub.read')} />
|
||||
<button className="btn btn-sm btn-ghost" onClick={() => api.openExternal(ascUrl(app.ios?.app.id ?? summary?.apple?.appId)).catch(() => {})}><Icon name="external" />App Store Connect</button>
|
||||
</>}>
|
||||
{r.error && <ErrorNotice error={r.error} keep={!!r.data} />}
|
||||
{!s ? (r.loading && r.data === null && !r.error ? <Skeleton lines={3} /> : <Empty compact illo="cloud" title={t('sub.notRead')}>{t('sub.notReadText')}</Empty>) : (
|
||||
<div className="stack">
|
||||
{s.submissions.length > 0 && (
|
||||
<div className="list sub-list" aria-label={t('sub.submissions')}>
|
||||
{s.submissions.filter(x => x.kind !== 'done').slice(0, 6).map(x => {
|
||||
const [tone, key] = SUB_KIND[x.kind];
|
||||
return (
|
||||
<div className="li" key={x.id} data-sub-kind={x.kind}>
|
||||
<span className={`li-icon tone-bg-${tone === 'neutral' ? 'info' : tone}`}><Icon name={x.kind === 'unresolved' ? 'alertCircle' : x.kind === 'emptyDraft' ? 'info' : 'clock'} /></span>
|
||||
<div className="li-main">
|
||||
<span className="li-title">{t(key)}</span>
|
||||
<span className="li-sub"><code className="selectable">{x.id}</code> · {x.state}{x.date ? ` · ${date(x.date)}` : ''}{x.kind === 'emptyDraft' ? ` · ${t('sub.harmless')}` : ''}</span>
|
||||
{x.items.length > 0 && (
|
||||
<ul className="note-list">
|
||||
{x.items.map(i => (
|
||||
<li key={i.id || i.label}><b>{i.label}</b>: {i.state}{i.rejected ? ` · ${i.kind === 'appStoreVersion' ? t('sub.markVersion') : t('sub.markIap')}` : ''}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<article className="card sub-card" data-store="ios">
|
||||
<SubHead store="ios" title={t('sub.appleTitle')} done={done} total={5}
|
||||
sub={r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.appleSub')}
|
||||
actions={<>
|
||||
<ReadControl r={{ ...r, refresh: async () => refresh() }} label={t('sub.read')} />
|
||||
<button className="btn btn-sm btn-ghost" onClick={() => api.openExternal(ascUrl(app.ios?.app.id ?? summary?.apple?.appId)).catch(() => {})}><Icon name="external" />App Store Connect</button>
|
||||
</>} />
|
||||
<div className="sub-body">
|
||||
{r.error && <ErrorNotice error={r.error} keep={!!r.data} />}
|
||||
{!s ? (r.loading && r.data === null && !r.error ? <Skeleton lines={3} /> : <EmptyState icon="cloud"><b>{t('sub.notRead')}</b> {t('sub.notReadText')}</EmptyState>) : (
|
||||
<ol className="sub-steps" aria-label={t('v2rel.steps')}>
|
||||
<Step n={1} state={states[0]} title={t('v2rel.stepRead')}>
|
||||
{open.length > 0 ? (
|
||||
<div className="sub-list" aria-label={t('sub.submissions')}>
|
||||
{open.slice(0, 6).map(x => {
|
||||
const [tone, key] = SUB_KIND[x.kind];
|
||||
return (
|
||||
<div className={`sub-item tone-${tone}`} key={x.id} data-sub-kind={x.kind}>
|
||||
<span className="sub-item-ic"><Icon name={x.kind === 'unresolved' ? 'alertCircle' : x.kind === 'emptyDraft' ? 'info' : 'clock'} /></span>
|
||||
<div className="sub-item-m">
|
||||
<span className="sub-item-t">{t(key)}</span>
|
||||
<span className="sub-item-s"><code className="selectable">{x.id}</code> · {x.state}{x.date ? ` · ${date(x.date)}` : ''}{x.kind === 'emptyDraft' ? ` · ${t('sub.harmless')}` : ''}</span>
|
||||
{x.items.length > 0 && (
|
||||
<ul className="note-list">
|
||||
{x.items.map(i => (
|
||||
<li key={i.id || i.label}><b>{i.label}</b>: {i.state}{i.rejected ? ` · ${i.kind === 'appStoreVersion' ? t('sub.markVersion') : t('sub.markIap')}` : ''}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<Chip tone={tone}>{x.state}</Chip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : <span className="sub-hint">{t('sub.noOpen')}</span>}
|
||||
</Step>
|
||||
<Step n={2} state={states[1]} title={t('v2rel.stepNoOpen')}>
|
||||
{s.busy && <Notice tone="info" icon="clock">{t('sub.busy', { state: s.busy.state })}</Notice>}
|
||||
{!s.busy && s.unresolved && <Notice tone="bad" icon="alertCircle">{t('sub.unresolvedText')}</Notice>}
|
||||
</Step>
|
||||
{!s.busy && (
|
||||
<>
|
||||
<Step n={3} state={states[2]} title={t('v2rel.stepBuild')}>
|
||||
<label className="sub-field">
|
||||
<span className="sr">{t('sub.build')}</span>
|
||||
<select className="input" value={build} onChange={e => setBuild(e.target.value)} aria-describedby="sub-build-hint">
|
||||
<option value="">{t('sub.pickBuild')}</option>
|
||||
{s.builds.map(b => (
|
||||
<option key={b.id} value={b.number} disabled={!b.valid}>
|
||||
{`${b.number}${b.version ? ` · ${b.version}` : ''} · ${b.state}${b.uploaded ? ` · ${date(b.uploaded)}` : ''}${b.attachedTo ? ` · ${t('sub.attachedTo', { version: b.attachedTo })}` : ''}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<span id="sub-build-hint" className="sub-hint">{s.builds.length ? t('sub.buildHint') : t('sub.noBuilds')}</span>
|
||||
</Step>
|
||||
<Step n={4} state={states[3]} title={t('v2rel.stepVersion')}>
|
||||
<div className="sub-version">
|
||||
<span className="sr">{t('sub.version')}</span>
|
||||
{version ? <><span className="sub-version-v num">{version.version}</span><span className="sub-hint">{platformLabel(version.platform)} · {version.state}</span></> : <Chip tone="warn" icon="question">{t('sub.noVersion')}</Chip>}
|
||||
</div>
|
||||
</Step>
|
||||
<Step n={5} state={states[4]} title={s.unresolved ? t('sub.resubmit') : t('v2rel.stepSend')}>
|
||||
{s.unresolved ? (
|
||||
<>
|
||||
<label className="row small sub-check"><input type="checkbox" checked={resolveIaps} onChange={e => setResolveIaps(e.target.checked)} />{t('sub.resolveIaps')}{rejectedIap ? ` ${t('sub.resolveIapsNeeded')}` : ''}</label>
|
||||
<div className="actions sub-go">
|
||||
<OwnerAction project={project} method="owner.apple_resubmit" args={args} label={t('sub.resubmit')} icon="send" primary disabled={!picked || !version}
|
||||
hint={!picked ? t('sub.pickFirst') : undefined} done={t('sub.resubmitted')} lead={t('sub.resubmitLead')} after={refresh} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="actions sub-go">
|
||||
<OwnerAction project={project} method="owner.apple_submit" args={args} label={t('sub.submit')} icon="send" primary disabled={!picked || !version}
|
||||
hint={!picked ? t('sub.pickFirst') : undefined} done={t('sub.submitted')} lead={t('sub.submitLead')} after={refresh} />
|
||||
</div>
|
||||
<Chip tone={tone}>{x.state}</Chip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
</Step>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
)}
|
||||
{s && !s.busy && iaps > 0 && (
|
||||
<div className="sub-extra">
|
||||
<div className="sub-extra-h"><Icon name="tag" /><span className="strong">{t('sub.iapTitle', { count: iaps })}</span></div>
|
||||
<span className="sub-hint">{t('sub.iapText')}</span>
|
||||
<div className="row" style={{ gap: 10, flexWrap: 'wrap' }}>
|
||||
<label className="row small" style={{ gap: 8 }}><input type="checkbox" checked={withVersion} onChange={e => setWithVersion(e.target.checked)} />{t('sub.withVersion')}</label>
|
||||
<OwnerAction project={project} method="owner.apple_iap_submit" args={withVersion ? { with_version: true } : {}} label={t('sub.iapSubmit')} icon="tag"
|
||||
done={t('sub.submitted')} lead={t('sub.iapLead')} after={refresh} />
|
||||
</div>
|
||||
)}
|
||||
{!s.submissions.some(x => x.kind !== 'done') && <span className="small muted">{t('sub.noOpen')}</span>}
|
||||
{s.busy && <Notice tone="info" icon="clock">{t('sub.busy', { state: s.busy.state })}</Notice>}
|
||||
{!s.busy && (
|
||||
<>
|
||||
<div className="row" style={{ gap: 10, flexWrap: 'wrap', alignItems: 'end' }}>
|
||||
<label className="stack" style={{ gap: 4 }}>
|
||||
<span className="small strong">{t('sub.build')}</span>
|
||||
<select className="input input-sm" value={build} onChange={e => setBuild(e.target.value)} aria-describedby="sub-build-hint">
|
||||
<option value="">{t('sub.pickBuild')}</option>
|
||||
{s.builds.map(b => (
|
||||
<option key={b.id} value={b.number} disabled={!b.valid}>
|
||||
{`${b.number}${b.version ? ` · ${b.version}` : ''} · ${b.state}${b.uploaded ? ` · ${date(b.uploaded)}` : ''}${b.attachedTo ? ` · ${t('sub.attachedTo', { version: b.attachedTo })}` : ''}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="stack small" style={{ gap: 4 }}>
|
||||
<span className="strong">{t('sub.version')}</span>
|
||||
<span>{version ? `${version.version} · ${platformLabel(version.platform)} · ${version.state}` : <Chip tone="warn" icon="question">{t('sub.noVersion')}</Chip>}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span id="sub-build-hint" className="small muted">{s.builds.length ? t('sub.buildHint') : t('sub.noBuilds')}</span>
|
||||
{s.unresolved ? (
|
||||
<>
|
||||
<Notice tone="bad" icon="alertCircle">{t('sub.unresolvedText')}</Notice>
|
||||
<label className="row small" style={{ gap: 8 }}><input type="checkbox" checked={resolveIaps} onChange={e => setResolveIaps(e.target.checked)} />{t('sub.resolveIaps')}{rejectedIap ? ` ${t('sub.resolveIapsNeeded')}` : ''}</label>
|
||||
<div className="actions">
|
||||
<OwnerAction project={project} method="owner.apple_resubmit" args={args} label={t('sub.resubmit')} icon="send" primary disabled={!picked || !version}
|
||||
hint={!picked ? t('sub.pickFirst') : undefined} done={t('sub.resubmitted')} lead={t('sub.resubmitLead')} after={refresh} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="actions">
|
||||
<OwnerAction project={project} method="owner.apple_submit" args={args} label={t('sub.submit')} icon="send" primary disabled={!picked || !version}
|
||||
hint={!picked ? t('sub.pickFirst') : undefined} done={t('sub.submitted')} lead={t('sub.submitLead')} after={refresh} />
|
||||
</div>
|
||||
)}
|
||||
{iaps > 0 && (
|
||||
<div className="sub-iaps stack" style={{ gap: 6 }}>
|
||||
<span className="small strong">{t('sub.iapTitle', { count: iaps })}</span>
|
||||
<span className="small muted">{t('sub.iapText')}</span>
|
||||
<div className="row" style={{ gap: 10, flexWrap: 'wrap' }}>
|
||||
<label className="row small" style={{ gap: 8 }}><input type="checkbox" checked={withVersion} onChange={e => setWithVersion(e.target.checked)} />{t('sub.withVersion')}</label>
|
||||
<OwnerAction project={project} method="owner.apple_iap_submit" args={withVersion ? { with_version: true } : {}} label={t('sub.iapSubmit')} icon="tag"
|
||||
done={t('sub.submitted')} lead={t('sub.iapLead')} after={refresh} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -141,57 +204,76 @@ export function PlayProductionCard({ app, project }: { app: UnifiedApp; project:
|
|||
const [share, setShare] = useState('10');
|
||||
const picked = s?.codes.find(c => c.code === code) ?? null;
|
||||
const args = { version_code: code, status, ...(status === 'inProgress' ? { percent: Number(share) } : {}) };
|
||||
const managed = pr && pr.managedPublishing !== true ? <Notice tone="warn" icon="alert">{pr.managedPublishing === false ? t('sub.managedOff') : t('sub.managedUnset')}</Notice>
|
||||
: pr && pr.managedPublishing === true ? <Notice tone="info" icon="info">{t('sub.managedOn')}</Notice> : null;
|
||||
const states: StepState[] = s ? [
|
||||
pr && pr.managedPublishing !== true ? 'warn' : 'ready',
|
||||
picked ? 'ready' : s.codes.length ? 'todo' : 'blocked',
|
||||
'ready',
|
||||
picked ? 'ready' : 'todo',
|
||||
] : [];
|
||||
const done = s ? states.filter(x => x === 'ready').length : null;
|
||||
return (
|
||||
<Card title={t('sub.playTitle')} icon="rocket" sub={<span className="row" style={{ gap: 6 }}><StoreGlyph store="android" className="ic13" />{r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.playSub')}</span>}
|
||||
actions={<>
|
||||
<ReadControl r={r} label={t('sub.read')} />
|
||||
<button className="btn btn-sm btn-ghost" onClick={() => api.openExternal(PLAY_CONSOLE).catch(() => {})}><Icon name="external" />Play Console</button>
|
||||
</>}>
|
||||
{r.error && <ErrorNotice error={r.error} keep={!!r.data} />}
|
||||
<div className="stack">
|
||||
{pr && pr.managedPublishing !== true && <Notice tone="warn" icon="alert">{pr.managedPublishing === false ? t('sub.managedOff') : t('sub.managedUnset')}</Notice>}
|
||||
{pr && pr.managedPublishing === true && <Notice tone="info" icon="info">{t('sub.managedOn')}</Notice>}
|
||||
{!s ? (r.loading && r.data === null && !r.error ? <Skeleton lines={3} /> : <Empty compact illo="cloud" title={t('sub.notRead')}>{t('sub.playNotReadText')}</Empty>) : (
|
||||
<article className="card sub-card" data-store="android">
|
||||
<SubHead store="android" title={t('sub.playTitle')} done={done} total={4}
|
||||
sub={r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.playSub')}
|
||||
actions={<>
|
||||
<ReadControl r={r} label={t('sub.read')} />
|
||||
<button className="btn btn-sm btn-ghost" onClick={() => api.openExternal(PLAY_CONSOLE).catch(() => {})}><Icon name="external" />Play Console</button>
|
||||
</>} />
|
||||
<div className="sub-body">
|
||||
{r.error && <ErrorNotice error={r.error} keep={!!r.data} />}
|
||||
{!s ? (
|
||||
<>
|
||||
{s.production.length > 0 && (
|
||||
<span className="small">{t('sub.prodNow')}: {s.production.map(p => `${p.name ?? p.codes.join(', ')} · ${p.status}${p.fraction !== null ? ` · ${Math.round(p.fraction * 100)} %` : ''}`).join('; ')}</span>
|
||||
)}
|
||||
<div className="row" style={{ gap: 10, flexWrap: 'wrap', alignItems: 'end' }}>
|
||||
<label className="stack" style={{ gap: 4 }}>
|
||||
<span className="small strong">{t('sub.versionCode')}</span>
|
||||
<select className="input input-sm" value={code} onChange={e => setCode(e.target.value)} aria-describedby="sub-code-hint">
|
||||
{managed}
|
||||
{r.loading && r.data === null && !r.error ? <Skeleton lines={3} /> : <EmptyState icon="cloud"><b>{t('sub.notRead')}</b> {t('sub.playNotReadText')}</EmptyState>}
|
||||
</>
|
||||
) : (
|
||||
<ol className="sub-steps" aria-label={t('v2rel.steps')}>
|
||||
<Step n={1} state={states[0]} title={t('v2rel.stepRead')}>
|
||||
{managed}
|
||||
{s.production.length > 0 && (
|
||||
<span className="sub-hint">{t('sub.prodNow')}: {s.production.map(p => `${p.name ?? p.codes.join(', ')} · ${p.status}${p.fraction !== null ? ` · ${Math.round(p.fraction * 100)} %` : ''}`).join('; ')}</span>
|
||||
)}
|
||||
</Step>
|
||||
<Step n={2} state={states[1]} title={t('v2rel.stepCode')}>
|
||||
<label className="sub-field">
|
||||
<span className="sr">{t('sub.versionCode')}</span>
|
||||
<select className="input" value={code} onChange={e => setCode(e.target.value)} aria-describedby="sub-code-hint">
|
||||
<option value="">{t('sub.pickCode')}</option>
|
||||
{s.codes.map(c => <option key={c.code} value={c.code} disabled={c.inProduction}>{`${c.code}${c.name ? ` · ${c.name}` : ''} · ${c.tracks.join(', ')}${c.inProduction ? ` · ${t('sub.inProduction')}` : ''}`}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<div className="stack" style={{ gap: 4 }}>
|
||||
<span className="small strong">{t('sub.as')}</span>
|
||||
<span id="sub-code-hint" className="sub-hint">{s.codes.length ? t('sub.codeHint') : t('sub.noCodes')}</span>
|
||||
</Step>
|
||||
<Step n={3} state={states[2]} title={t('sub.as')}>
|
||||
<div className="row" style={{ gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Seg value={status} onChange={setStatus} label={t('sub.as')} options={[['completed', t('sub.sCompleted')], ['inProgress', t('sub.sStaged')], ['draft', t('sub.sDraft')]]} />
|
||||
{status === 'inProgress' && (
|
||||
<label className="row small" style={{ gap: 8 }}>
|
||||
<span className="strong">{t('roll.share')}</span>
|
||||
<select className="input input-sm" value={share} onChange={e => setShare(e.target.value)}>{SHARES.map(x => <option key={x} value={x}>{x} %</option>)}</select>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{status === 'inProgress' && (
|
||||
<label className="stack" style={{ gap: 4 }}>
|
||||
<span className="small strong">{t('roll.share')}</span>
|
||||
<select className="input input-sm" value={share} onChange={e => setShare(e.target.value)}>{SHARES.map(x => <option key={x} value={x}>{x} %</option>)}</select>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<span id="sub-code-hint" className="small muted">{s.codes.length ? t('sub.codeHint') : t('sub.noCodes')}</span>
|
||||
<div className="actions">
|
||||
<OwnerAction project={project} method="owner.play_production" args={args} label={status === 'draft' ? t('sub.playDraft') : t('sub.playSend')} icon="send" primary={status !== 'draft'}
|
||||
disabled={!picked} hint={!picked ? t('sub.pickCodeFirst') : undefined} done={status === 'draft' ? t('sub.playDrafted') : t('sub.playSent')} lead={status === 'draft' ? t('sub.playDraftLead') : t('sub.playLead')} after={() => { r.refresh(); }} />
|
||||
</div>
|
||||
</>
|
||||
</Step>
|
||||
<Step n={4} state={states[3]} title={status === 'draft' ? t('sub.playDraft') : t('sub.playSend')}>
|
||||
<div className="actions sub-go">
|
||||
<OwnerAction project={project} method="owner.play_production" args={args} label={status === 'draft' ? t('sub.playDraft') : t('sub.playSend')} icon="send" primary={status !== 'draft'}
|
||||
disabled={!picked} hint={!picked ? t('sub.pickCodeFirst') : undefined} done={status === 'draft' ? t('sub.playDrafted') : t('sub.playSent')} lead={status === 'draft' ? t('sub.playDraftLead') : t('sub.playLead')} after={() => { r.refresh(); }} />
|
||||
</div>
|
||||
</Step>
|
||||
</ol>
|
||||
)}
|
||||
<Notice tone="info" icon="clipboard" action={<div className="actions">
|
||||
<ConsolePlanButton project={project} form="play-production-countries" label={t('cp.playCountries')} />
|
||||
<ConsolePlanButton project={project} form="play-production-release" label={t('sub.sendChanges')} />
|
||||
</div>}>
|
||||
<div className="stack" style={{ gap: 2 }}>
|
||||
<b>{t('sub.consoleStepTitle')}</b>
|
||||
<span className="small">{t('sub.consoleStepText', { countries: pr?.countries ? t('sub.countriesN', { count: pr.countries }) : t('sub.countriesAll') })}</span>
|
||||
<div className="sub-extra">
|
||||
<div className="sub-extra-h"><Icon name="clipboard" /><span className="strong">{t('sub.consoleStepTitle')}</span></div>
|
||||
<span className="sub-hint">{t('sub.consoleStepText', { countries: pr?.countries ? t('sub.countriesN', { count: pr.countries }) : t('sub.countriesAll') })}</span>
|
||||
<div className="actions">
|
||||
<ConsolePlanButton project={project} form="play-production-countries" label={t('cp.playCountries')} />
|
||||
<ConsolePlanButton project={project} form="play-production-release" label={t('sub.sendChanges')} />
|
||||
</div>
|
||||
</Notice>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue