@autobusal/common 1.8.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Button/Button.tsx +9 -1
- package/CHANGELOG.md +44 -0
- package/Meta.tsx +32 -0
- package/Viewer/Actions.tsx +18 -1
- package/Viewer/Viewer.tsx +19 -3
- package/Viewer/types.ts +2 -0
- package/package.json +1 -1
package/Button/Button.tsx
CHANGED
|
@@ -11,9 +11,15 @@ interface Props {
|
|
|
11
11
|
noMargin?: boolean
|
|
12
12
|
maxWidth?: number
|
|
13
13
|
onClick?: any
|
|
14
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-31
|
|
15
|
+
// A native <button name value> pair - lets a form with more than one
|
|
16
|
+
// submit-type action (e.g. Viewer's 'save' vs 'test') tell them apart via
|
|
17
|
+
// the submit event's `submitter`, without any extra state.
|
|
18
|
+
name?: string
|
|
19
|
+
value?: string
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
const Button = ({ type, size, loading, active, subtype, text, noMargin, maxWidth, onClick }: Props) => (
|
|
22
|
+
const Button = ({ type, size, loading, active, subtype, text, noMargin, maxWidth, onClick, name, value }: Props) => (
|
|
17
23
|
<ButtonStyled
|
|
18
24
|
$size={ size }
|
|
19
25
|
type={ loading ? 'button' : type }
|
|
@@ -22,6 +28,8 @@ const Button = ({ type, size, loading, active, subtype, text, noMargin, maxWidth
|
|
|
22
28
|
$margin={ noMargin ? false : true }
|
|
23
29
|
$maxWidth={ maxWidth }
|
|
24
30
|
onClick={ onClick }
|
|
31
|
+
name={ name }
|
|
32
|
+
value={ value }
|
|
25
33
|
>
|
|
26
34
|
{ loading ? <RiLoader2Fill /> : text }
|
|
27
35
|
</ButtonStyled>
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,50 @@
|
|
|
3
3
|
All notable changes to `@autobusal/common` are documented here. This project follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [1.9.0] - 2026-07-31
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`Viewer`/`Actions`/`Button` support a second submit-type action** via an
|
|
11
|
+
optional `onTest` prop on `Viewer` and a new `'test'` entry in its
|
|
12
|
+
`actions` array. A form with two submit buttons can't otherwise tell
|
|
13
|
+
which one triggered submission - `Button` now forwards a native
|
|
14
|
+
`name`/`value` pair, and `Viewer`'s `onSubmit` reads the submit event's
|
|
15
|
+
`submitter` to route to `onTest` instead of `onSave` when the 'test'
|
|
16
|
+
button was clicked, while keeping the exact same current form values.
|
|
17
|
+
First consumer: `@autobusal/admin-label` 1.10.0's "Test" button on the
|
|
18
|
+
SMTP settings tab.
|
|
19
|
+
|
|
20
|
+
## [1.8.3] - 2026-07-31
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`Meta.tsx`'s `useSoleMetaOwnership` no longer runs on every render.**
|
|
25
|
+
It fired on every render of every `<Meta>` usage (136 call sites, no
|
|
26
|
+
dependency array) and directly removed `<head>` children via raw DOM
|
|
27
|
+
`.remove()` calls each time. The duplicate-tag problem it exists for -
|
|
28
|
+
React's `createRoot` not reconciling against tags baked into the
|
|
29
|
+
prerendered static HTML - can only exist before React's first render;
|
|
30
|
+
every render after that, `<title>`/`<meta>`/`<link>` in `<head>` are
|
|
31
|
+
exclusively React 19's own hoisting mechanism's doing, and it already
|
|
32
|
+
correctly swaps them out as components mount/unmount with no help
|
|
33
|
+
needed. Re-running this raw removal on every navigation instead raced
|
|
34
|
+
that hoisting: it could delete a node mid-transition that React's own
|
|
35
|
+
reconciler still held a reference to, and the next time React tried to
|
|
36
|
+
remove or replace that now-detached node, `parentNode` was null and it
|
|
37
|
+
threw an uncaught `TypeError: Cannot read properties of null (reading
|
|
38
|
+
'removeChild')` - not render-phase, so no `ErrorBoundary` catches it,
|
|
39
|
+
silently aborting whatever navigation was in flight (URL updates,
|
|
40
|
+
page never re-renders). This is the actual root cause of the frozen-
|
|
41
|
+
navigation bug worked around piecemeal in `@autobusal/providers`
|
|
42
|
+
1.6.6-1.6.8, `@autobusal/hooks` 1.3.1, and `@autobusal/auth`
|
|
43
|
+
1.5.2-1.5.4 - all of which targeted `GoogleReCaptchaProvider` as a
|
|
44
|
+
contributing trigger, but the crash reproduced identically with that
|
|
45
|
+
provider removed entirely, on ~any route rendering `<Meta>`. Now
|
|
46
|
+
scoped to a module-level flag so the cleanup runs exactly once, on the
|
|
47
|
+
app's first `<Meta>` mount - it does its one real job (wiping stale
|
|
48
|
+
prerendered tags before first paint) and never touches the DOM again.
|
|
49
|
+
|
|
6
50
|
## [1.8.2] - 2026-07-30
|
|
7
51
|
|
|
8
52
|
### Fixed
|
package/Meta.tsx
CHANGED
|
@@ -52,8 +52,40 @@ const withBrand = (value: string): string => (brand ? `${ value } - ${ brand }`
|
|
|
52
52
|
// them, since nothing here can claim the page has one.
|
|
53
53
|
type OwnedTag = { selector: string, attr: 'text' | string, value?: string };
|
|
54
54
|
|
|
55
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-31
|
|
56
|
+
// Bug: this ran on EVERY render of EVERY <Meta> usage (no dependency array),
|
|
57
|
+
// removing head children via raw DOM .remove() calls each time. But the
|
|
58
|
+
// duplicate-tag problem this exists for is a ONE-TIME artifact of the
|
|
59
|
+
// prerendered static HTML `createRoot` never reconciles against - it can
|
|
60
|
+
// only exist before React's first render. Every render after that, these
|
|
61
|
+
// tags are exclusively React 19's own hoisting mechanism's doing, and
|
|
62
|
+
// React already correctly swaps them out as components mount/unmount with
|
|
63
|
+
// no help needed. Re-running this raw removal on every subsequent
|
|
64
|
+
// navigation raced that hoisting instead: this effect could delete a
|
|
65
|
+
// <title>/<meta> node mid-transition that React's own reconciler still
|
|
66
|
+
// held a reference to (e.g. while the outgoing page's <Meta> was still
|
|
67
|
+
// completing its unmount), leaving React's fiber pointing at a detached
|
|
68
|
+
// node - the next time React tried to remove or replace it, `parentNode`
|
|
69
|
+
// was null and it threw an uncaught `TypeError: Cannot read properties of
|
|
70
|
+
// null (reading 'removeChild')`. That's not render-phase, so no
|
|
71
|
+
// ErrorBoundary catches it, and it silently aborted whatever navigation
|
|
72
|
+
// was in flight: the URL would update but the page never re-rendered -
|
|
73
|
+
// reproduced on ~every route that renders <Meta>, i.e. nearly every page.
|
|
74
|
+
// Scoping the cleanup to the app's first-ever <Meta> mount (module-scope
|
|
75
|
+
// flag, not per-component-instance state, since every route mounts a NEW
|
|
76
|
+
// Meta instance) keeps it doing its one real job - wiping stale prerendered
|
|
77
|
+
// tags before React's first paint - without ever again running once React
|
|
78
|
+
// is the sole owner of this part of the DOM.
|
|
79
|
+
let hasCleanedPrerenderedTags = false;
|
|
80
|
+
|
|
55
81
|
const useSoleMetaOwnership = (tags: OwnedTag[]): void => {
|
|
56
82
|
useEffect(() => {
|
|
83
|
+
if (hasCleanedPrerenderedTags) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
hasCleanedPrerenderedTags = true;
|
|
88
|
+
|
|
57
89
|
tags.forEach(({ selector, attr, value }) => {
|
|
58
90
|
const elements = [...document.querySelectorAll(`head > ${ selector }`)];
|
|
59
91
|
|
package/Viewer/Actions.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
2
|
import { BiEditAlt, BiTrash, BiSend } from 'react-icons/bi';
|
|
3
|
-
import { RiSave2Line, RiCheckFill, RiSearchLine } from 'react-icons/ri';
|
|
3
|
+
import { RiSave2Line, RiCheckFill, RiSearchLine, RiMailSendLine } from 'react-icons/ri';
|
|
4
4
|
import { BsPlusCircleFill } from 'react-icons/bs';
|
|
5
5
|
import Button from '../Button/Button';
|
|
6
6
|
import { ContainerActions } from './styles';
|
|
@@ -41,6 +41,21 @@ const Actions = ({ id, available, pending, t, onDelete }: Props): JSX.Element =>
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-31
|
|
45
|
+
// A second submit-type action alongside 'save' (e.g. "Test SMTP") - the
|
|
46
|
+
// name/value pair lets Viewer's onSubmit tell which button was actually
|
|
47
|
+
// clicked via the submit event's `submitter`, since a form with two
|
|
48
|
+
// submit buttons can't otherwise distinguish them.
|
|
49
|
+
if (available.includes('test')) {
|
|
50
|
+
options.push({
|
|
51
|
+
label: t('table.actions.test', { ns: 'common' }),
|
|
52
|
+
icon: <RiMailSendLine />,
|
|
53
|
+
type: 'submit',
|
|
54
|
+
name: 'intent',
|
|
55
|
+
value: 'test'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
if (available.includes('send')) {
|
|
45
60
|
options.push({
|
|
46
61
|
label: t('table.actions.send', { ns: 'common' }),
|
|
@@ -93,6 +108,8 @@ const Actions = ({ id, available, pending, t, onDelete }: Props): JSX.Element =>
|
|
|
93
108
|
loading={ pending }
|
|
94
109
|
noMargin
|
|
95
110
|
onClick={ item.handler }
|
|
111
|
+
name={ item.name }
|
|
112
|
+
value={ item.value }
|
|
96
113
|
/>
|
|
97
114
|
));
|
|
98
115
|
|
package/Viewer/Viewer.tsx
CHANGED
|
@@ -16,11 +16,16 @@ interface Props {
|
|
|
16
16
|
t: TFunction<'common'>
|
|
17
17
|
onSave?: (data: any) => void
|
|
18
18
|
onDelete?: (data?: any) => void
|
|
19
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-31
|
|
20
|
+
// Optional second submit-type action (actions={['save', 'test']}) - e.g.
|
|
21
|
+
// the SMTP tab's "Test" button, which needs the same current form values
|
|
22
|
+
// as save but must call a different handler instead of persisting.
|
|
23
|
+
onTest?: (data: any) => void
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
const Viewer = ({ id, type, data, fetching, pending, actions, t, onSave, onDelete }: Props): JSX.Element => {
|
|
26
|
+
const Viewer = ({ id, type, data, fetching, pending, actions, t, onSave, onDelete, onTest }: Props): JSX.Element => {
|
|
22
27
|
const { register, handleSubmit, formState: { errors }, setValue } = useForm<any>();
|
|
23
|
-
|
|
28
|
+
|
|
24
29
|
if (fetching) {
|
|
25
30
|
return <Loading t={ t } />;
|
|
26
31
|
}
|
|
@@ -37,7 +42,18 @@ const Viewer = ({ id, type, data, fetching, pending, actions, t, onSave, onDelet
|
|
|
37
42
|
/>
|
|
38
43
|
));
|
|
39
44
|
|
|
40
|
-
const onSubmit = (data: any): void => {
|
|
45
|
+
const onSubmit = (data: any, event?: React.BaseSyntheticEvent): void => {
|
|
46
|
+
// A form with two submit buttons can't tell which one triggered
|
|
47
|
+
// submission except via the native SubmitEvent's `submitter` - the
|
|
48
|
+
// 'test' button is the only one with a name/value pair (see Actions.tsx).
|
|
49
|
+
const submitter = (event?.nativeEvent as SubmitEvent | undefined)?.submitter as (HTMLButtonElement | undefined);
|
|
50
|
+
|
|
51
|
+
if (submitter?.value === 'test' && onTest !== undefined) {
|
|
52
|
+
onTest(data);
|
|
53
|
+
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
if (onSave !== undefined) {
|
|
42
58
|
onSave(data);
|
|
43
59
|
}
|
package/Viewer/types.ts
CHANGED