@open-mercato/ui 0.8.1-develop.7455.1.8f7e53badb → 0.8.1-develop.7458.1.3847638f67

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.8.1-develop.7455.1.8f7e53badb",
3
+ "version": "0.8.1-develop.7458.1.3847638f67",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,14 +155,14 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.8.1-develop.7455.1.8f7e53badb",
158
+ "@open-mercato/shared": "0.8.1-develop.7458.1.3847638f67",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
164
  "@figma/code-connect": "^1.3.4",
165
- "@open-mercato/shared": "0.8.1-develop.7455.1.8f7e53badb",
165
+ "@open-mercato/shared": "0.8.1-develop.7458.1.3847638f67",
166
166
  "@testing-library/dom": "^10.4.1",
167
167
  "@testing-library/jest-dom": "^7.0.1",
168
168
  "@testing-library/react": "^16.3.3",
@@ -1638,6 +1638,15 @@ export function CrudForm<TValues extends Record<string, unknown>>({
1638
1638
  {extraActions}
1639
1639
  </>
1640
1640
  ) : undefined
1641
+ const renderFooterActions = !hideFooterActions && !formReadOnly
1642
+ const embeddedExtraActions = renderFooterActions ? undefined : extraActions
1643
+ const embeddedHeaderActions = versionHistoryEnabled || headerInjectionAction || embeddedExtraActions ? (
1644
+ <>
1645
+ {versionHistoryEnabled ? versionHistoryAction : null}
1646
+ {headerInjectionAction}
1647
+ {embeddedExtraActions}
1648
+ </>
1649
+ ) : undefined
1641
1650
 
1642
1651
  // Auto-append custom fields for this entityId
1643
1652
  React.useEffect(() => {
@@ -3919,8 +3928,8 @@ export function CrudForm<TValues extends Record<string, unknown>>({
3919
3928
  submit: formReadOnly ? undefined : { formId, pending: pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon },
3920
3929
  }}
3921
3930
  />
3922
- ) : headerExtraActions ? (
3923
- <div className="flex justify-end gap-2 mb-2">{headerExtraActions}</div>
3931
+ ) : embeddedHeaderActions ? (
3932
+ <div className="flex justify-end gap-2 mb-2">{embeddedHeaderActions}</div>
3924
3933
  ) : null}
3925
3934
  {contentHeader}
3926
3935
  <DataLoader
@@ -3958,7 +3967,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
3958
3967
  {hasSecondaryColumn ? <div className={densityStackMd} data-crud-injection-region>{col2Content}</div> : null}
3959
3968
  </div>
3960
3969
  {formError && !Object.keys(errors).length ? <div className="text-sm text-status-error-text">{formError}</div> : null}
3961
- {hideFooterActions || formReadOnly ? null : (
3970
+ {renderFooterActions ? (
3962
3971
  <FormFooter
3963
3972
  embedded={embedded}
3964
3973
  className={dialogFooterClass}
@@ -3972,7 +3981,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
3972
3981
  submit: { pending: pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
3973
3982
  }}
3974
3983
  />
3975
- )}
3984
+ ) : null}
3976
3985
  </form>,
3977
3986
  )}
3978
3987
  </DataLoader>
@@ -4002,8 +4011,8 @@ export function CrudForm<TValues extends Record<string, unknown>>({
4002
4011
  submit: formReadOnly ? undefined : { formId, pending: pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon },
4003
4012
  }}
4004
4013
  />
4005
- ) : headerExtraActions ? (
4006
- <div className="flex justify-end gap-2 mb-2">{headerExtraActions}</div>
4014
+ ) : embeddedHeaderActions ? (
4015
+ <div className="flex justify-end gap-2 mb-2">{embeddedHeaderActions}</div>
4007
4016
  ) : null}
4008
4017
  {contentHeader}
4009
4018
  <DataLoader
@@ -4056,7 +4065,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
4056
4065
  })}
4057
4066
  </div>
4058
4067
  {formError && !Object.keys(errors).length ? <div className="text-sm text-status-error-text">{formError}</div> : null}
4059
- {hideFooterActions || formReadOnly ? null : (
4068
+ {renderFooterActions ? (
4060
4069
  <FormFooter
4061
4070
  embedded={embedded}
4062
4071
  className={dialogFooterClass}
@@ -4070,7 +4079,7 @@ export function CrudForm<TValues extends Record<string, unknown>>({
4070
4079
  submit: { pending: pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon },
4071
4080
  }}
4072
4081
  />
4073
- )}
4082
+ ) : null}
4074
4083
  </form>
4075
4084
  </div>,
4076
4085
  )}
@@ -0,0 +1,114 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ /**
4
+ * An embedded CrudForm is the body of a dialog: its footer is the only action
5
+ * row the user sees, and hosts put the dialog's Cancel button there by passing
6
+ * it through `extraActions`. Rendering that same node again in the embedded
7
+ * header strip produced two Cancel buttons in one dialog (issue #6750).
8
+ *
9
+ * The contract pinned here:
10
+ *
11
+ * 1. embedded + a rendered footer -> `extraActions` appear once, in the footer,
12
+ * 2. embedded + no footer -> they fall back to the header strip rather
13
+ * than disappearing, and
14
+ * 3. not embedded -> the intentional FormHeader/FormFooter
15
+ * mirroring is untouched.
16
+ */
17
+
18
+ jest.mock('next/navigation', () => ({
19
+ useRouter: () => ({ push: jest.fn() }),
20
+ usePathname: () => '/',
21
+ useSearchParams: () => new URLSearchParams(),
22
+ }))
23
+ jest.mock('remark-gfm', () => ({ __esModule: true, default: {} }))
24
+ jest.mock('../confirm-dialog', () => ({
25
+ useConfirmDialog: () => ({ confirm: jest.fn().mockResolvedValue(true), ConfirmDialogElement: null }),
26
+ }))
27
+ jest.mock('../custom-fields/FieldDefinitionsManager', () => {
28
+ const React = require('react')
29
+ return { __esModule: true, FieldDefinitionsManager: React.forwardRef(() => <div />) }
30
+ })
31
+ jest.mock('../utils/customFieldForms', () => ({
32
+ __esModule: true,
33
+ buildFormFieldFromCustomFieldDef: jest.fn(),
34
+ buildFormFieldsFromCustomFields: jest.fn(() => []),
35
+ fetchCustomFieldFormStructure: jest.fn(),
36
+ }))
37
+
38
+ import * as React from 'react'
39
+ import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
40
+ import { Button } from '../../primitives/button'
41
+ import { CrudForm, type CrudField, type CrudFormGroup } from '../CrudForm'
42
+
43
+ const fields: CrudField[] = [{ id: 'name', label: 'Name', type: 'text' }]
44
+ const groups: CrudFormGroup[] = [{ id: 'basic', title: 'Basic', column: 1, fields: ['name'] }]
45
+
46
+ type FormOptions = {
47
+ embedded?: boolean
48
+ hideFooterActions?: boolean
49
+ readOnly?: boolean
50
+ }
51
+
52
+ function renderForm({ embedded = true, hideFooterActions, readOnly }: FormOptions = {}) {
53
+ const { container } = renderWithProviders(
54
+ <CrudForm
55
+ fields={fields}
56
+ groups={groups}
57
+ initialValues={{ name: 'x' }}
58
+ onSubmit={jest.fn()}
59
+ submitLabel="Apply"
60
+ embedded={embedded}
61
+ hideFooterActions={hideFooterActions}
62
+ readOnly={readOnly}
63
+ extraActions={(
64
+ <Button type="button" variant="ghost">Cancel</Button>
65
+ )}
66
+ />,
67
+ )
68
+ return container
69
+ }
70
+
71
+ function cancelButtons(container: HTMLElement): HTMLElement[] {
72
+ return Array.from(container.querySelectorAll<HTMLElement>('button')).filter(
73
+ (button) => button.textContent?.trim() === 'Cancel',
74
+ )
75
+ }
76
+
77
+ describe('CrudForm embedded extraActions', () => {
78
+ it('renders a single Cancel action, in the footer beside the submit button', () => {
79
+ const container = renderForm()
80
+ const cancels = cancelButtons(container)
81
+
82
+ expect(cancels).toHaveLength(1)
83
+
84
+ const form = container.querySelector('form')
85
+ expect(form).not.toBeNull()
86
+ expect(form!.contains(cancels[0])).toBe(true)
87
+
88
+ const submit = container.querySelector<HTMLElement>('button[type="submit"]')
89
+ expect(submit?.textContent).toContain('Apply')
90
+ expect(cancels[0].parentElement).toBe(submit?.parentElement)
91
+ })
92
+
93
+ it('keeps the actions visible in the header strip when the footer is hidden', () => {
94
+ const container = renderForm({ hideFooterActions: true })
95
+ const cancels = cancelButtons(container)
96
+
97
+ expect(cancels).toHaveLength(1)
98
+ expect(container.querySelector('form')?.contains(cancels[0])).toBe(false)
99
+ })
100
+
101
+ it('keeps the actions visible in the header strip when the form is read-only', () => {
102
+ const container = renderForm({ readOnly: true })
103
+ const cancels = cancelButtons(container)
104
+
105
+ expect(cancels).toHaveLength(1)
106
+ expect(container.querySelector('form')?.contains(cancels[0])).toBe(false)
107
+ })
108
+
109
+ it('leaves the non-embedded header/footer mirroring in place', () => {
110
+ const container = renderForm({ embedded: false })
111
+
112
+ expect(cancelButtons(container)).toHaveLength(2)
113
+ })
114
+ })