feat(studio): clearer store + channel switcher + approvals & welcome polish
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
- store: repair the filter dialog crash (a Spacer lived directly in
AlertDialog.actions, which is an OverflowBar, not a Flex — it threw
and rendered a broken dialog). Buttons now sit in a Row.
- store: promote the module-store manager from a bare icon to a
labelled 'Add store' button, and fully localize the dialog (DE/EN).
- store: add a curated 'Suggested stores' shelf with one-click
add/remove (first entry: Recl∆Im). Each suggestion is probed for
reachability and shows 'not available yet' until its index is
published, instead of failing only on click. Fail-open on network
errors so a transient hiccup never hides a real store.
- sidebar: the channel pill is now a one-click channel switcher
(menu with per-channel running state + active check; switching
writes ~/.chain/current-channel, restarts the daemon, and Studio
repoints to the new channel).
- approvals: lead the card with the human prompt ('what am I
releasing?') and demote the flow/step id to a metadata line; clear
fallback when the step left the prompt empty.
- welcome: tidy the docs grid into equal-height paired rows with a
full-width trailing card for the odd one out.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
62ed3951e5
commit
5220f19ff8
10 changed files with 865 additions and 148 deletions
|
|
@ -527,6 +527,9 @@ class _ApprovalCard extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status row: selection + state + expiry. No flow id here —
|
||||
// the headline below leads with WHAT is being asked, not the
|
||||
// technical flow name.
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
|
|
@ -540,15 +543,7 @@ class _ApprovalCard extends StatelessWidget {
|
|||
tone: ChainPillTone.warning,
|
||||
icon: Icons.pending_outlined,
|
||||
),
|
||||
const SizedBox(width: ChainSpace.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${approval.flowName} › ${approval.stepId}',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (approval.expiresAt != null)
|
||||
ChainPill(
|
||||
label: _expiresInLabel(context, approval.expiresAt!),
|
||||
|
|
@ -558,7 +553,38 @@ class _ApprovalCard extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: ChainSpace.lg),
|
||||
Text(approval.prompt, style: theme.textTheme.bodyLarge),
|
||||
// Headline = the human question the flow author wrote ("what
|
||||
// am I approving"). Falls back to a clear sentence when the
|
||||
// step left the prompt empty, so the card is never reduced to
|
||||
// a cryptic flow id.
|
||||
Text(
|
||||
approval.prompt.trim().isEmpty
|
||||
? l.approvalsRequestFallback
|
||||
: approval.prompt,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Technical context, demoted to a small metadata line.
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_tree_outlined,
|
||||
size: 13,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l.approvalsFlowStepMeta(approval.flowName, approval.stepId),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (approval.payloadPreview != null) ...[
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -234,10 +234,15 @@ class _StorePageState extends State<StorePage> {
|
|||
onPressed: _openFilterDialog,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.storefront_outlined, size: 18),
|
||||
tooltip: 'Module stores',
|
||||
onPressed: () => ChainStoresDialog.show(context),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: ChainSpace.sm),
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.add_business_outlined, size: 16),
|
||||
label: Text(l.storesManagerButton),
|
||||
onPressed: () => ChainStoresDialog.show(context),
|
||||
style:
|
||||
OutlinedButton.styleFrom(visualDensity: VisualDensity.compact),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
|
|
@ -1139,38 +1144,49 @@ class _FilterDialogState extends State<_FilterDialog> {
|
|||
],
|
||||
),
|
||||
),
|
||||
// A single Row is the one action child: AlertDialog wraps
|
||||
// `actions` in an OverflowBar (not a Flex), so a bare
|
||||
// `Spacer()` here throws — it needs a Flex parent. Nesting
|
||||
// the buttons in a Row gives the Spacer a valid Flex
|
||||
// ancestor and keeps Reset left / Cancel+Apply right.
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_status = '';
|
||||
_source = '';
|
||||
_installedOnly = false;
|
||||
// Reset puts `installableOnly` back to its default
|
||||
// (on) — the operator hits Reset to escape a
|
||||
// custom-filter state and expects the default view
|
||||
// back, not an everything-including-planned view.
|
||||
_installableOnly = true;
|
||||
});
|
||||
},
|
||||
child: Text(l.storeFilterReset),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(
|
||||
context,
|
||||
_FilterOutcome(
|
||||
status: _status,
|
||||
source: _source,
|
||||
installedOnly: _installedOnly,
|
||||
installableOnly: _installableOnly,
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_status = '';
|
||||
_source = '';
|
||||
_installedOnly = false;
|
||||
// Reset puts `installableOnly` back to its
|
||||
// default (on) — the operator hits Reset to
|
||||
// escape a custom-filter state and expects the
|
||||
// default view back, not an everything-
|
||||
// including-planned view.
|
||||
_installableOnly = true;
|
||||
});
|
||||
},
|
||||
child: Text(l.storeFilterReset),
|
||||
),
|
||||
),
|
||||
child: Text(l.storeFilterApply),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
const SizedBox(width: ChainSpace.sm),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(
|
||||
context,
|
||||
_FilterOutcome(
|
||||
status: _status,
|
||||
source: _source,
|
||||
installedOnly: _installedOnly,
|
||||
installableOnly: _installableOnly,
|
||||
),
|
||||
),
|
||||
child: Text(l.storeFilterApply),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1003,18 +1003,37 @@ class _DocsRow extends StatelessWidget {
|
|||
],
|
||||
);
|
||||
}
|
||||
final cardWidth = (constraints.maxWidth - ChainSpace.md) / 2;
|
||||
return Wrap(
|
||||
spacing: ChainSpace.md,
|
||||
runSpacing: ChainSpace.md,
|
||||
children: [
|
||||
for (final d in _kDocs)
|
||||
SizedBox(
|
||||
width: cardWidth,
|
||||
child: _DocCard(entry: d),
|
||||
// Pair the cards into equal-height rows: IntrinsicHeight
|
||||
// + stretch makes the two cards in a row match height
|
||||
// instead of the Wrap's ragged tops (their blurbs differ
|
||||
// in length, especially across locales). An odd trailing
|
||||
// card spans the full width so it reads as intentional
|
||||
// rather than a lonely half-box with dead space beside it.
|
||||
final rows = <Widget>[];
|
||||
for (var i = 0; i < _kDocs.length; i += 2) {
|
||||
if (rows.isNotEmpty) {
|
||||
rows.add(const SizedBox(height: ChainSpace.md));
|
||||
}
|
||||
final left = _kDocs[i];
|
||||
final right = i + 1 < _kDocs.length ? _kDocs[i + 1] : null;
|
||||
if (right == null) {
|
||||
rows.add(_DocCard(entry: left));
|
||||
} else {
|
||||
rows.add(
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(child: _DocCard(entry: left)),
|
||||
const SizedBox(width: ChainSpace.md),
|
||||
Expanded(child: _DocCard(entry: right)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
return Column(children: rows);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue