@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.
|
|
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.
|
|
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.
|
|
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",
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -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
|
-
) :
|
|
3923
|
-
<div className="flex justify-end gap-2 mb-2">{
|
|
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
|
-
{
|
|
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
|
-
) :
|
|
4006
|
-
<div className="flex justify-end gap-2 mb-2">{
|
|
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
|
-
{
|
|
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
|
+
})
|