@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 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
 
@@ -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
@@ -19,4 +19,6 @@ export interface ActionData {
19
19
  type?: 'submit'
20
20
  subtype?: 'delete'
21
21
  handler?: () => void
22
+ name?: string
23
+ value?: string
22
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"