Desktop: app page no longer overflows (page grid minmax, wrapping tabs); app ignore/unpair actions move to a ... menu; unique keys in App info
This commit is contained in:
parent
dece83c3e8
commit
5e82010ace
9 changed files with 32 additions and 21 deletions
|
|
@ -1,13 +1,13 @@
|
|||
// One app across both stores: header (icon, studio, ids, store state, pairing), then the tabs. A store-only app (no
|
||||
// app folder) shows its store reads (releases, health, reviews, TestFlight) read-only, with "Link a folder" and
|
||||
// "Start an app folder here" to make it a full app.
|
||||
import { useContextMenu } from '../ui/ContextMenu';
|
||||
import { useContextMenu, useMenuButton } from '../ui/ContextMenu';
|
||||
import { appMenu } from '../ui/app-menu';
|
||||
import { useShowIgnored } from '../lib/show-ignored';
|
||||
import { useState } from 'react';
|
||||
import { api, errorText } from '../api';
|
||||
import { type Key, t } from '../i18n';
|
||||
import { pairCandidates, type UnifiedApp, withIgnored, withLink, withPair, withSideIgnored, withStudio, withUnpair } from '../lib/identity';
|
||||
import { pairCandidates, type UnifiedApp, withLink, withPair, withStudio } from '../lib/identity';
|
||||
import { attention, storeNow } from '../lib/model';
|
||||
import { Hero, nextStep, Tabs } from '../ui/ds';
|
||||
import { type AppTab, TAB_ICON, TAB_KEY, tabsFor, useNav } from '../nav';
|
||||
|
|
@ -43,8 +43,8 @@ function AppControls({ app }: { app: UnifiedApp }) {
|
|||
const d = useData();
|
||||
const nav = useNav();
|
||||
const cands = pairCandidates(app, d.apps);
|
||||
const canUnpair = !app.project && !!app.ios && !!app.android;
|
||||
const sides = (['ios', 'android'] as const).filter(k => app[k] || app.ignoredStores.includes(k));
|
||||
const more = useMenuButton();
|
||||
const [showIgnored] = useShowIgnored();
|
||||
return (
|
||||
<div className="app-controls">
|
||||
<span className="small muted row" style={{ gap: 6 }}><Icon name="link" className="ic13" />{t(PAIRING[app.pairing])}</span>
|
||||
|
|
@ -65,16 +65,8 @@ function AppControls({ app }: { app: UnifiedApp }) {
|
|||
{cands.slice(0, 30).map(c => <option key={c.key} value={c.key}>{c.name} · {(c.ios ?? c.android)!.app.bundleId}</option>)}
|
||||
</select></label>
|
||||
)}
|
||||
{canUnpair && <button className="btn btn-sm btn-ghost" onClick={() => d.updateOverrides(o => withUnpair(o, app.ios!.key, app.android!.key)).then(() => nav.go({ screen: 'portfolio' }))}><Icon name="unlink" />{t('app.unpair')}</button>}
|
||||
{sides.length === 2 && sides.map(k => {
|
||||
const off = app.ignored || app.ignoredStores.includes(k);
|
||||
const store = k === 'ios' ? 'App Store' : 'Google Play';
|
||||
return <button key={k} className="btn btn-sm btn-ghost" onClick={() => d.updateOverrides(o => withSideIgnored(o, app, k, !off))}
|
||||
title={off ? t('app.unignoreSideHint', { store }) : t('app.ignoreSideHint', { store })}><Icon name={off ? 'eye' : 'eyeOff'} />{off ? t('app.unignoreSide', { store }) : t('app.ignoreSide', { store })}</button>;
|
||||
})}
|
||||
{app.ignored
|
||||
? <button className="btn btn-sm btn-ghost" onClick={() => d.updateOverrides(o => withIgnored(o, app.ignoreKeys, false))} title={t('app.unignoreHint')}><Icon name="eye" />{t('app.unignore')}</button>
|
||||
: <button className="btn btn-sm btn-ghost" onClick={() => d.updateOverrides(o => withIgnored(o, app.ignoreKeys, true)).then(() => nav.go({ screen: 'portfolio' }))} title={t('app.ignoreHint')}><Icon name="eyeOff" />{t('app.ignore')}</button>}
|
||||
<span className="grow" />
|
||||
<button className="btn btn-sm btn-ghost" {...more(() => appMenu(app, d, nav, { showIgnored }))} aria-label={t('cm.more')} title={t('cm.more')}><Icon name="more" />{app.ignored || app.ignoredStores.length ? t('nav.ignored') : null}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ function DataSafety({ project, d, onDone }: { project: Project; d: DeclarationsR
|
|||
{ds.types.map(x => (
|
||||
<tr key={x.id}>
|
||||
<td><div className="strong">{typeLabel(x.id)}</div><div className="small muted">{x.category}{x.optional ? ` · ${t('ai.optional')}` : ''}{x.ephemeral ? ` · ${t('ai.ephemeral')}` : ''}</div></td>
|
||||
<td><div className="chips">{x.collect.map(p => <Chip key={p} tone="neutral" dot={false}>{t(PURPOSE[p] ?? 'ds.functionality')}</Chip>)}</div></td>
|
||||
<td>{x.shared ? <div className="chips">{x.share.map(p => <Chip key={p} tone="warn" dot={false}>{t(PURPOSE[p] ?? 'ds.functionality')}</Chip>)}</div> : <span className="muted small">{t('ai.notShared')}</span>}</td>
|
||||
<td><div className="chips">{x.collect.map((p, i) => <Chip key={`${p}-${i}`} tone="neutral" dot={false}>{t(PURPOSE[p] ?? 'ds.functionality')}</Chip>)}</div></td>
|
||||
<td>{x.shared ? <div className="chips">{x.share.map((p, i) => <Chip key={`${p}-${i}`} tone="warn" dot={false}>{t(PURPOSE[p] ?? 'ds.functionality')}</Chip>)}</div> : <span className="muted small">{t('ai.notShared')}</span>}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -138,7 +138,7 @@ function AppPrivacy({ project, d, appId }: { project: Project; d: DeclarationsRe
|
|||
{ap.items.map(x => (
|
||||
<tr key={x.apple}>
|
||||
<td><div className="strong">{x.apple}</div><div className="small muted">{x.category} · {x.from.map(typeLabel).join(', ')}</div></td>
|
||||
<td><div className="chips">{x.purposes.map(p => <Chip key={p} tone={/Third-Party/.test(p) ? 'warn' : 'neutral'} dot={false}>{p}</Chip>)}</div></td>
|
||||
<td><div className="chips">{x.purposes.map((p, i) => <Chip key={`${p}-${i}`} tone={/Third-Party/.test(p) ? 'warn' : 'neutral'} dot={false}>{p}</Chip>)}</div></td>
|
||||
<td><Chip tone="warn" icon="question">{t('ai.ask')}</Chip></td>
|
||||
<td><Chip tone={x.thirdParty ? 'warn' : 'outline'} icon="question">{x.thirdParty ? t('ai.askLikely') : t('ai.ask')}</Chip></td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue