wrolpi/app/src/components/collections/CollectionEditForm.js
Roland 5ce4f2ebf3
ui: a row of buttons is spaced by the row
The archive page's six actions shared edges, and Update sat lower than the
rest.  Both come from the same thing: Semantic's Button carried
`margin: 0 .25em .25em 0`, so a page could list buttons as bare siblings and
they arrived spaced -- and separated from the line below when the row wrapped.
Nothing replaced that margin.

Two of those six answered with `marginTop: 0.5em`, which is the wrong axis: a
Button is `display: inline-flex; vertical-align: middle`, and `vertical-align`
is measured on the MARGIN box, so half of any top margin shows up as the
visible button dropping.  It was covering the wrapped-row case in the only axis
a margin can, and paying for it in the other one.

`.wrolpi-button-row` is a flex row with a gap, so both axes are covered at once
and no button needs a margin of its own -- the same rule as `.wrolpi-stack`,
along the other axis.  Applied to the five rows in the app that had no gap: the
archive page, the shared collection edit form (which three pages hand action
buttons to, and where nine buttons plus Cancel carried `margin-top: 1em` each
while Save carried none), the doc page's two responsive branches, and the
downloads table's edit/restart pair.

`.action-button-spacing` goes with them; it was that same margin, on Cancel.

Guarded four ways.  ui-layout.cy.js measures that the row really spaces what it
holds, wraps rather than overflows, centres its buttons against a taller item
beside them, and counts a confirmed APIButton as one item rather than two --
that one returns a fragment holding a `<Confirm/>`, which would open a hole in
the row if it did not portal.  Common.test.js scans for the two ways this comes
back: a button in a row stamping a vertical margin, and a row of buttons that is
not a row.  Neither throws, which is how the app got here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 18:41:22 -06:00

94 lines
3.9 KiB
JavaScript

import React from 'react';
import {Button, Header, Message, Panel} from '../ui';
import {TagsContext} from '../../Tags';
import {WROLModeMessage} from '../Common';
/**
* Reusable form component for editing collections (Domains, Channels, etc).
*
* @param {Object} form - Form object from useForm hook
* @param {Function} onCancel - Optional callback when cancel is clicked
* @param {Function} onSubmit - Optional custom submit handler (defaults to form.onSubmit)
* @param {String} title - Page title to display in header
* @param {String} wrolModeContent - Content to show in WROL mode message (optional)
* @param {React.ReactNode} actionButtons - Optional additional action buttons to display in the button row
* @param {String} appliedTagName - Optional tag name to display (similar to ChannelEditPage pattern)
* @param {React.ReactNode} children - Form fields to render
*/
export function CollectionEditForm({
form,
onCancel,
onSubmit,
title,
wrolModeContent,
actionButtons,
appliedTagName,
children
}) {
const {SingleTag} = React.useContext(TagsContext);
const handleSubmit = (e) => {
e.preventDefault();
if (onSubmit) {
onSubmit(e);
} else {
form.onSubmit();
}
};
return <Panel>
{title && <Header as="h1">{title}</Header>}
{wrolModeContent && <WROLModeMessage content={wrolModeContent}/>}
{/* Display form-level errors */}
{form.error && <Message kind='error' title='Error'>
<p>{form.error}</p>
</Message>}
<form onSubmit={handleSubmit} autoComplete="off">
{/*
Every caller now passes library components rather than Semantic Grid rows, so
this is a plain flex column instead of the `ui stackable grid` bridge it used
to be. That bridge was the last thing in the app depending on Semantic's
stylesheet.
*/}
<div
className='wrolpi-form-rows'
style={form.loading ? {opacity: 0.6, pointerEvents: 'none'} : undefined}
>
{children}
{appliedTagName && <div><SingleTag name={appliedTagName}/></div>}
{/*
* The shared row. It was already a flex row at this gap, but it did not wrap, so
* the pages that hand it four action buttons ran out of width on a phone -- which
* is why each of those buttons, and the Cancel below, carried `margin-top: 1em`.
* That margin is what made them sit lower than Save, `vertical-align: middle`
* being measured on the margin box. Wrapping plus a row gap covers the case the
* margin was for, in the axis it actually happens in.
*/}
<div className='wrolpi-button-row'>
{actionButtons}
{onCancel && <Button
type='button'
role='cancel'
onClick={onCancel}
disabled={form.disabled}
>
Cancel
</Button>}
<Button
type='submit'
role='save'
size='lg'
style={{marginLeft: 'auto'}}
disabled={form.disabled}
>
Save
</Button>
</div>
</div>
</form>
</Panel>;
}