Desktop: account and studio secondary actions move to ... menus; one Import apps button on the home; App folders header keeps one primary action
This commit is contained in:
parent
7c18b3f7dd
commit
43e394ef7d
3 changed files with 16 additions and 13 deletions
|
|
@ -3,7 +3,7 @@
|
|||
// issuer id, .p8). Keys live in the machine's credential profiles (~/.storebound, shared with the CLI);
|
||||
// the app keeps a label, the studio and the profile name only. Per account: a health
|
||||
// check (key works / permission missing / revoked, with the fix) and "Import apps" (every app the key can see).
|
||||
import { useContextMenu } from '../ui/ContextMenu';
|
||||
import { type MenuInput, useContextMenu, useMenuButton } from '../ui/ContextMenu';
|
||||
import { copyToast } from '../ui/app-menu';
|
||||
import { type FormEvent, useEffect, useState } from 'react';
|
||||
import { type Account, type AccountCheck, api, errorText, STUDIO_COLORS, type Studio, type StudioColor } from '../api';
|
||||
|
|
@ -59,8 +59,8 @@ function AccountRow({ a, busy, setBusy, onError }: { a: Account; busy: string |
|
|||
const save = (e: FormEvent) => { e.preventDefault(); run(`ed-${a.id}`, async () => { await api.updateAccount(a.id, label, studio || null); if (a.kind === 'play' && a.profile && dev.trim() !== (a.developerId ?? '')) await api.setPlayDeveloper(a.id, dev.trim() || null); await d.reload(); setEdit(false); }); };
|
||||
const check = h?.check;
|
||||
const cm = useContextMenu();
|
||||
return (
|
||||
<div className={`acct${ignored ? ' is-ignored' : ''}`} {...cm(() => [
|
||||
const more = useMenuButton();
|
||||
const menu = (): MenuInput => [
|
||||
{ label: t('acct.test'), icon: 'shield', disabled: busy ? t('cm.busy') : false, onSelect: () => run(`ck-${a.id}`, () => d.checkAccount(a.id)) },
|
||||
{ label: t('acct.import'), icon: 'download', disabled: busy || d.importBusy ? t('cm.busy') : false, onSelect: () => run(`im-${a.id}`, () => d.importApps(a.id)) },
|
||||
{ label: t('acct.edit'), icon: 'edit', onSelect: () => setEdit(true) },
|
||||
|
|
@ -73,7 +73,9 @@ function AccountRow({ a, busy, setBusy, onError }: { a: Account; busy: string |
|
|||
'sep',
|
||||
{ label: ignored ? t('acct.unignore') : t('acct.ignore'), icon: ignored ? 'eye' : 'eyeOff', onSelect: () => { void d.updateOverrides(o => withIgnored(o, [`account:${a.id}`], !ignored)); } },
|
||||
{ label: t('accounts.remove'), icon: 'trash', danger: true, disabled: busy ? t('cm.busy') : false, onSelect: () => run(`rm-${a.id}`, async () => { await api.removeAccount(a.id); await d.reload(); }) },
|
||||
])}>
|
||||
];
|
||||
return (
|
||||
<div className={`acct${ignored ? ' is-ignored' : ''}`} {...cm(menu)}>
|
||||
<div className="acct-row">
|
||||
<span className={`acct-glyph ${a.kind}`}><StoreGlyph store={a.kind === 'play' ? 'android' : 'ios'} /></span>
|
||||
<div className="acct-main">
|
||||
|
|
@ -91,8 +93,7 @@ function AccountRow({ a, busy, setBusy, onError }: { a: Account; busy: string |
|
|||
<button className="btn btn-sm" onClick={() => run(`ck-${a.id}`, () => d.checkAccount(a.id))} disabled={!!busy}><Icon name="shield" className={busy === `ck-${a.id}` ? 'spin' : undefined} />{t('acct.test')}</button>
|
||||
<button className="btn btn-sm" onClick={() => run(`im-${a.id}`, () => d.importApps(a.id))} disabled={!!busy || d.importBusy}><Icon name="download" />{t('acct.import')}</button>
|
||||
<button className="btn btn-sm btn-ghost btn-icon" onClick={() => setEdit(x => !x)} aria-expanded={edit} aria-label={t('acct.editAria', { label: a.label })} title={t('acct.edit')}><Icon name="edit" /></button>
|
||||
<button className="btn btn-sm btn-ghost btn-icon" onClick={() => d.updateOverrides(o => withIgnored(o, [`account:${a.id}`], !ignored))} aria-label={ignored ? t('acct.unignore') : t('acct.ignore')} title={ignored ? t('acct.unignore') : t('acct.ignoreHint')}><Icon name={ignored ? 'eye' : 'eyeOff'} /></button>
|
||||
<button className="btn btn-sm btn-ghost btn-icon btn-danger" disabled={!!busy} onClick={() => run(`rm-${a.id}`, async () => { await api.removeAccount(a.id); await d.reload(); })} aria-label={t('accounts.removeAria', { label: a.label })} title={t('accounts.remove')}><Icon name="trash" /></button>
|
||||
<button className="btn btn-sm btn-ghost btn-icon" {...more(menu)} aria-label={t('cm.more')} title={t('cm.more')}><Icon name="more" /></button>
|
||||
</div>
|
||||
</div>
|
||||
{edit && (
|
||||
|
|
@ -132,6 +133,7 @@ function StudioCard({ s, accounts, focus }: { s: Studio | null; accounts: Accoun
|
|||
const storeOnly = apps.filter(a => !a.project).length;
|
||||
const act = async (key: string, fn: () => Promise<unknown>) => { setBusy(key); setError(null); try { await fn(); await d.reload(); } catch (e) { setError(errorText(e)); } finally { setBusy(null); } };
|
||||
const id = s ? `studio-${s.id}` : 'studio-none';
|
||||
const more = useMenuButton();
|
||||
useEffect(() => { if (focus) document.getElementById(id)?.scrollIntoView({ block: 'start' }); }, [focus, id]);
|
||||
return (
|
||||
<section className={`ds-sec studio-card${focus ? ' focus' : ''}`} id={id} aria-labelledby={`${id}-h`}>
|
||||
|
|
@ -146,9 +148,12 @@ function StudioCard({ s, accounts, focus }: { s: Studio | null; accounts: Accoun
|
|||
{s && (
|
||||
<div className="actions">
|
||||
<button className="btn btn-sm btn-ghost" onClick={() => setEdit(x => !x)} aria-expanded={edit}><Icon name="edit" />{t('studio.edit')}</button>
|
||||
<button className="btn btn-sm btn-ghost" onClick={() => act('logo', () => api.studioLogo(s.id))} disabled={!!busy}><Icon name="image" />{t('studio.logo')}</button>
|
||||
{s.logo && <button className="btn btn-sm btn-ghost" onClick={() => act('logo', () => api.studioLogo(s.id, true))} disabled={!!busy}>{t('studio.logoClear')}</button>}
|
||||
<button className="btn btn-sm btn-ghost btn-danger" onClick={() => act('rm', () => api.removeStudio(s.id))} disabled={!!busy} title={t('studio.removeHint')}><Icon name="trash" />{t('studio.remove')}</button>
|
||||
<button className="btn btn-sm btn-ghost btn-icon" {...more(() => [
|
||||
{ label: t('studio.logo'), icon: 'image', disabled: busy ? t('cm.busy') : false, onSelect: () => { void act('logo', () => api.studioLogo(s.id)); } },
|
||||
s.logo && { label: t('studio.logoClear'), icon: 'x', disabled: busy ? t('cm.busy') : false, onSelect: () => { void act('logo', () => api.studioLogo(s.id, true)); } },
|
||||
'sep',
|
||||
{ label: t('studio.remove'), icon: 'trash', danger: true, disabled: busy ? t('cm.busy') : false, onSelect: () => { void act('rm', () => api.removeStudio(s.id)); } },
|
||||
])} aria-label={t('cm.more')} title={t('cm.more')}><Icon name="more" /></button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -85,9 +85,7 @@ export function AppFolders() {
|
|||
|
||||
return (
|
||||
<div className="page scr-folders">
|
||||
<PageHeader icon="folder" eyebrow={t('v2cfg.workspace')} title={t('nav.apps')} subtitle={t('folders.lead')} actions={<><button className="btn" onClick={fastlane} disabled={!!busy} title={t('folders.fastlaneHint')}><Icon name="download" />{t('folders.fastlane')}</button>
|
||||
<button className="btn" onClick={init} disabled={!!busy}><Icon name="sparkle" />{t('folders.startShort')}</button>
|
||||
<button className="btn btn-primary" onClick={add} disabled={!!busy}><Icon name="folderPlus" />{t('action.addFolder')}</button></>} />
|
||||
<PageHeader icon="folder" eyebrow={t('v2cfg.workspace')} title={t('nav.apps')} subtitle={t('folders.lead')} actions={<button className="btn btn-primary" onClick={add} disabled={!!busy}><Icon name="folderPlus" />{t('action.addFolder')}</button>} />
|
||||
{error && <Notice tone="bad">{error}</Notice>}
|
||||
|
||||
{!d.projects.length ? (
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ export function Portfolio() {
|
|||
<p>{t('portfolio.subStudios', { count: visible.length, stores, studios: d.studios.length })}{d.live ? ` · ${t('pf.readAgo', { when: ago(d.live.at) })}` : ''}</p>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button className="btn" onClick={() => d.importApps()} disabled={d.importBusy || !d.accounts.length} title={t('action.importAppsHint')}><Icon name="download" className={d.importBusy ? 'spin' : undefined} />{t('action.importApps')}</button>
|
||||
{neverImported === 0 && <button className="btn" onClick={() => d.importApps()} disabled={d.importBusy || !d.accounts.length} title={t('action.importAppsHint')}><Icon name="download" className={d.importBusy ? 'spin' : undefined} />{t('action.importApps')}</button>}
|
||||
<button className="btn btn-primary" onClick={() => d.refreshPortfolio(true)} disabled={!!d.portfolioBusy} title={t('action.readStoresHint')}>
|
||||
<Icon name="refresh" className={d.portfolioBusy === 'live' ? 'spin' : undefined} />{d.portfolioBusy === 'live' ? t('action.reading') : t('action.readStores')}
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue