refine-mantine 1.0.0-dev.1

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/index.d.ts +276 -0
  4. package/dist/index.js +1576 -0
  5. package/package.json +89 -0
  6. package/src/App.tsx +33 -0
  7. package/src/Router.tsx +97 -0
  8. package/src/components/ColorSchemeToggle.tsx +13 -0
  9. package/src/components/breadcrumb/Breadcrumb.tsx +80 -0
  10. package/src/components/buttons/CloneButton.story.tsx +22 -0
  11. package/src/components/buttons/CloneButton.tsx +86 -0
  12. package/src/components/buttons/CreateButton.story.tsx +22 -0
  13. package/src/components/buttons/CreateButton.tsx +81 -0
  14. package/src/components/buttons/DeleteButton.story.tsx +22 -0
  15. package/src/components/buttons/DeleteButton.tsx +133 -0
  16. package/src/components/buttons/EditButton.story.tsx +22 -0
  17. package/src/components/buttons/EditButton.tsx +87 -0
  18. package/src/components/buttons/ExportButton.story.tsx +28 -0
  19. package/src/components/buttons/ExportButton.tsx +48 -0
  20. package/src/components/buttons/ImportButton.story.tsx +44 -0
  21. package/src/components/buttons/ImportButton.tsx +61 -0
  22. package/src/components/buttons/ListButton.story.tsx +25 -0
  23. package/src/components/buttons/ListButton.tsx +91 -0
  24. package/src/components/buttons/RefreshButton.story.tsx +22 -0
  25. package/src/components/buttons/RefreshButton.tsx +59 -0
  26. package/src/components/buttons/SaveButton.story.tsx +22 -0
  27. package/src/components/buttons/SaveButton.tsx +51 -0
  28. package/src/components/buttons/ShowButton.story.tsx +22 -0
  29. package/src/components/buttons/ShowButton.tsx +83 -0
  30. package/src/components/crud/Create.story.tsx +83 -0
  31. package/src/components/crud/Create.tsx +146 -0
  32. package/src/components/crud/Edit.story.tsx +173 -0
  33. package/src/components/crud/Edit.tsx +236 -0
  34. package/src/components/crud/List.story.tsx +98 -0
  35. package/src/components/crud/List.tsx +109 -0
  36. package/src/components/crud/Show.tsx +220 -0
  37. package/src/components/layout/Layout.story.tsx +28 -0
  38. package/src/components/layout/Layout.tsx +257 -0
  39. package/src/components/notification/AutoSaveIndicator.story.tsx +41 -0
  40. package/src/components/notification/AutoSaveIndicator.tsx +58 -0
  41. package/src/components/notification/Message.story.tsx +30 -0
  42. package/src/components/notification/Message.tsx +79 -0
  43. package/src/components/table/ColumnFilter.tsx +107 -0
  44. package/src/components/table/ColumnSorter.tsx +26 -0
  45. package/src/components/table/Table.story.tsx +146 -0
  46. package/src/components/table/Table.tsx +64 -0
  47. package/src/favicon.svg +1 -0
  48. package/src/hooks/useForm.ts +271 -0
  49. package/src/hooks/useOtp.ts +45 -0
  50. package/src/index.ts +36 -0
  51. package/src/main.tsx +4 -0
  52. package/src/pages/auth/DefaultTitle.tsx +13 -0
  53. package/src/pages/auth/ForgotPasswordPage.story.tsx +14 -0
  54. package/src/pages/auth/ForgotPasswordPage.tsx +128 -0
  55. package/src/pages/auth/LoginPage.story.tsx +98 -0
  56. package/src/pages/auth/LoginPage.tsx +256 -0
  57. package/src/pages/auth/RegisterPage.story.tsx +18 -0
  58. package/src/pages/auth/RegisterPage.tsx +151 -0
  59. package/src/pages/auth/UpdatePasswordPage.story.tsx +27 -0
  60. package/src/pages/auth/UpdatePasswordPage.tsx +124 -0
  61. package/src/pages/category/CategoryCreate.tsx +1 -0
  62. package/src/pages/category/CategoryEdit.tsx +1 -0
  63. package/src/pages/category/CategoryList.tsx +1 -0
  64. package/src/pages/category/CategoryShow.tsx +1 -0
  65. package/src/pages/product/ProductCreate.tsx +1 -0
  66. package/src/pages/product/ProductEdit.tsx +1 -0
  67. package/src/pages/product/ProductList.tsx +1 -0
  68. package/src/pages/product/ProductShow.tsx +1 -0
  69. package/src/providers/authProvider.ts +105 -0
  70. package/src/providers/i18nProvider.ts +7 -0
  71. package/src/providers/notificationProvider.tsx +122 -0
  72. package/src/resources.tsx +63 -0
  73. package/src/theme.ts +5 -0
  74. package/src/utils/paths.ts +52 -0
  75. package/src/utils/wait.ts +4 -0
  76. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,83 @@
1
+ import { Box, NumberInput, Select, SimpleGrid, Textarea, TextInput } from "@mantine/core";
2
+ import { useSelect } from "@refinedev/core";
3
+ import { IconCurrencyEuro } from "@tabler/icons-react";
4
+ import { useForm } from "@/hooks/useForm";
5
+ import { Create } from "./Create";
6
+
7
+ export default {
8
+ title: 'Crud/Create',
9
+ component: Create,
10
+ };
11
+
12
+ interface Category {
13
+ id: number;
14
+ title: string;
15
+ }
16
+
17
+ export const Default = () => {
18
+ const {
19
+ refineCore: { formLoading },
20
+ getInputProps,
21
+ saveButtonProps,
22
+ } = useForm({
23
+ initialValues: {
24
+ name: "",
25
+ description: "",
26
+ price: 0,
27
+ material: "",
28
+ category: {
29
+ id: null
30
+ },
31
+ },
32
+ });
33
+
34
+ const { options, onSearch } = useSelect<Category>({
35
+ resource: "categories",
36
+ optionLabel: category => category.title,
37
+ optionValue: category => category.id.toString(),
38
+ debounce: 500,
39
+ });
40
+
41
+ return (
42
+ <Create
43
+ saveButtonProps={saveButtonProps}
44
+ isLoading={formLoading}
45
+ resource="products"
46
+ >
47
+ <SimpleGrid cols={2}>
48
+ <Box>
49
+ <TextInput
50
+ label="Name"
51
+ mb="sm"
52
+ {...getInputProps("name")}
53
+ />
54
+ <NumberInput
55
+ label="Price"
56
+ mb="sm"
57
+ rightSection={<IconCurrencyEuro size={18}/>}
58
+ {...getInputProps("price")}
59
+ />
60
+ <TextInput
61
+ label="Material"
62
+ mb="sm"
63
+ {...getInputProps("material")}
64
+ />
65
+ <Select
66
+ label="Category"
67
+ data={options}
68
+ searchable
69
+ onSearchChange={onSearch}
70
+ {...getInputProps("category.id")}
71
+ />
72
+ </Box>
73
+ <Box>
74
+ <Textarea
75
+ label="Description"
76
+ rows={6}
77
+ {...getInputProps("description")}
78
+ />
79
+ </Box>
80
+ </SimpleGrid>
81
+ </Create>
82
+ );
83
+ }
@@ -0,0 +1,146 @@
1
+ import {
2
+ ActionIcon,
3
+ Box,
4
+ type BoxProps,
5
+ Card,
6
+ type CardProps,
7
+ Group,
8
+ type GroupProps,
9
+ LoadingOverlay,
10
+ Stack,
11
+ Title,
12
+ } from "@mantine/core";
13
+ import {
14
+ useBack,
15
+ useRefineContext,
16
+ useResourceParams,
17
+ useTranslate,
18
+ useUserFriendlyName,
19
+ } from "@refinedev/core";
20
+ import {
21
+ type RefineCrudCreateProps,
22
+ RefinePageHeaderClassNames,
23
+ } from "@refinedev/ui-types";
24
+ import { IconArrowLeft } from "@tabler/icons-react";
25
+ import type React from "react";
26
+ import {
27
+ SaveButton,
28
+ type SaveButtonProps,
29
+ } from "@/components/buttons/SaveButton";
30
+ import { Breadcrumb } from "../breadcrumb/Breadcrumb";
31
+
32
+ export type CreateProps = RefineCrudCreateProps<
33
+ SaveButtonProps,
34
+ GroupProps,
35
+ GroupProps,
36
+ CardProps,
37
+ GroupProps,
38
+ BoxProps
39
+ >;
40
+
41
+ export const Create: React.FC<CreateProps> = (props) => {
42
+ const {
43
+ children,
44
+ saveButtonProps: saveButtonPropsFromProps,
45
+ isLoading,
46
+ resource: resourceFromProps,
47
+ footerButtons: footerButtonsFromProps,
48
+ footerButtonProps,
49
+ headerButtons: headerButtonsFromProps,
50
+ headerButtonProps,
51
+ wrapperProps,
52
+ contentProps,
53
+ headerProps,
54
+ goBack: goBackFromProps,
55
+ breadcrumb: breadcrumbFromProps,
56
+ title,
57
+ } = props;
58
+ const translate = useTranslate();
59
+ const { options: { breadcrumb: globalBreadcrumb } = {} } = useRefineContext();
60
+
61
+ const back = useBack();
62
+ const getUserFriendlyName = useUserFriendlyName();
63
+
64
+ const { resource, identifier } = useResourceParams({
65
+ resource: resourceFromProps,
66
+ });
67
+
68
+ const breadcrumb =
69
+ typeof breadcrumbFromProps === "undefined"
70
+ ? globalBreadcrumb
71
+ : breadcrumbFromProps;
72
+
73
+ const breadcrumbComponent =
74
+ typeof breadcrumb !== "undefined" ? breadcrumb : <Breadcrumb />;
75
+
76
+ const saveButtonProps: SaveButtonProps = {
77
+ ...(isLoading ? { disabled: true } : {}),
78
+ ...saveButtonPropsFromProps,
79
+ };
80
+
81
+ const loadingOverlayVisible = isLoading ?? saveButtonProps?.disabled ?? false;
82
+
83
+ const defaultFooterButtons = <SaveButton {...saveButtonProps} />;
84
+
85
+ const buttonBack =
86
+ goBackFromProps === (false || null) ? null : (
87
+ <ActionIcon variant="subtle" onClick={back}>
88
+ {typeof goBackFromProps !== "undefined" ? (
89
+ goBackFromProps
90
+ ) : (
91
+ <IconArrowLeft />
92
+ )}
93
+ </ActionIcon>
94
+ );
95
+
96
+ const headerButtons = headerButtonsFromProps
97
+ ? typeof headerButtonsFromProps === "function"
98
+ ? headerButtonsFromProps({
99
+ defaultButtons: null,
100
+ })
101
+ : headerButtonsFromProps
102
+ : null;
103
+
104
+ const footerButtons = footerButtonsFromProps
105
+ ? typeof footerButtonsFromProps === "function"
106
+ ? footerButtonsFromProps({
107
+ defaultButtons: defaultFooterButtons,
108
+ saveButtonProps,
109
+ })
110
+ : footerButtonsFromProps
111
+ : defaultFooterButtons;
112
+
113
+ return (
114
+ <Card p="md" {...wrapperProps}>
115
+ <LoadingOverlay visible={loadingOverlayVisible} zIndex={1000} />
116
+ <Group justify="space-between" {...headerProps}>
117
+ <Stack gap="xs">
118
+ {breadcrumbComponent}
119
+ <Group gap="xs">
120
+ {buttonBack}
121
+ {title ?? (
122
+ <Title order={3} className={RefinePageHeaderClassNames.Title}>
123
+ {translate(
124
+ `${identifier}.titles.create`,
125
+ `Create ${getUserFriendlyName(
126
+ resource?.meta?.label ?? identifier,
127
+ "singular",
128
+ )}`,
129
+ )}
130
+ </Title>
131
+ )}
132
+ </Group>
133
+ </Stack>
134
+ <Group gap="xs" {...headerButtonProps}>
135
+ {headerButtons}
136
+ </Group>
137
+ </Group>
138
+ <Box pt="sm" {...contentProps}>
139
+ {children}
140
+ </Box>
141
+ <Group justify="right" gap="xs" mt="md" {...footerButtonProps}>
142
+ {footerButtons}
143
+ </Group>
144
+ </Card>
145
+ );
146
+ };
@@ -0,0 +1,173 @@
1
+ import { Box, NumberInput, Select, SimpleGrid, Textarea, TextInput } from "@mantine/core";
2
+ import { useSelect } from "@refinedev/core";
3
+ import { IconCurrencyEuro } from "@tabler/icons-react";
4
+ import { useForm } from "@/hooks/useForm";
5
+ import { Edit } from "./Edit";
6
+
7
+ export default {
8
+ title: 'Crud/Edit',
9
+ component: Edit,
10
+ };
11
+
12
+ interface Category {
13
+ id: number;
14
+ title: string;
15
+ }
16
+
17
+ export const Default = () => {
18
+ const {
19
+ values,
20
+ refineCore: { formLoading },
21
+ getInputProps,
22
+ saveButtonProps,
23
+ } = useForm({
24
+ refineCoreProps: {
25
+ // [WORKAROUND-START]
26
+ // these hardcoded params are only for demonstration purpose, please don't use these in your app
27
+ resource: "products",
28
+ id: 11,
29
+ action: "edit",
30
+ // [WORKAROUND-END]
31
+ },
32
+ initialValues: {
33
+ name: "",
34
+ description: "",
35
+ price: 0,
36
+ material: "",
37
+ category: {
38
+ id: null
39
+ },
40
+ },
41
+ });
42
+
43
+ const { options, onSearch } = useSelect<Category>({
44
+ resource: "categories",
45
+ optionLabel: category => category.title,
46
+ optionValue: category => category.id.toString(),
47
+ debounce: 200,
48
+ });
49
+
50
+ return (
51
+ <Edit
52
+ saveButtonProps={saveButtonProps}
53
+ isLoading={formLoading}
54
+ >
55
+ <SimpleGrid cols={2}>
56
+ <Box>
57
+ <TextInput
58
+ label="Name"
59
+ mb="sm"
60
+ {...getInputProps("name")}
61
+ />
62
+ <NumberInput
63
+ label="Price"
64
+ mb="sm"
65
+ rightSection={<IconCurrencyEuro size={18}/>}
66
+ {...getInputProps("price")}
67
+ />
68
+ <TextInput
69
+ label="Material"
70
+ mb="sm"
71
+ {...getInputProps("material")}
72
+ />
73
+ <Select
74
+ label="Category"
75
+ data={options}
76
+ searchable
77
+ onSearchChange={onSearch}
78
+ {...getInputProps("category.id")}
79
+ value={String(values.category.id)}
80
+ />
81
+ </Box>
82
+ <Box>
83
+ <Textarea
84
+ label="Description"
85
+ rows={6}
86
+ {...getInputProps("description")}
87
+ />
88
+ </Box>
89
+ </SimpleGrid>
90
+ </Edit>
91
+ );
92
+ }
93
+
94
+ export const WithAutoSave = () => {
95
+ const {
96
+ values,
97
+ refineCore: { autoSaveProps },
98
+ getInputProps,
99
+ saveButtonProps,
100
+ } = useForm({
101
+ refineCoreProps: {
102
+ autoSave: {
103
+ enabled: true,
104
+ debounce: 1000,
105
+ },
106
+ // [WORKAROUND-START]
107
+ // these hardcoded params are only for demonstration purpose, please don't use these in your app
108
+ resource: "products",
109
+ id: 11,
110
+ action: "edit",
111
+ // [WORKAROUND-END]
112
+ },
113
+ initialValues: {
114
+ name: "",
115
+ description: "",
116
+ price: 0,
117
+ material: "",
118
+ category: {
119
+ id: null
120
+ },
121
+ },
122
+ });
123
+
124
+ const { options, onSearch } = useSelect<Category>({
125
+ resource: "categories",
126
+ optionLabel: category => category.title,
127
+ optionValue: category => category.id.toString(),
128
+ debounce: 200,
129
+ });
130
+
131
+ return (
132
+ <Edit
133
+ saveButtonProps={saveButtonProps}
134
+ autoSaveProps={autoSaveProps}
135
+ >
136
+ <SimpleGrid cols={2}>
137
+ <Box>
138
+ <TextInput
139
+ label="Name"
140
+ mb="sm"
141
+ {...getInputProps("name")}
142
+ />
143
+ <NumberInput
144
+ label="Price"
145
+ mb="sm"
146
+ rightSection={<IconCurrencyEuro size={18}/>}
147
+ {...getInputProps("price")}
148
+ />
149
+ <TextInput
150
+ label="Material"
151
+ mb="sm"
152
+ {...getInputProps("material")}
153
+ />
154
+ <Select
155
+ label="Category"
156
+ data={options}
157
+ searchable
158
+ onSearchChange={onSearch}
159
+ {...getInputProps("category.id")}
160
+ value={String(values.category.id)}
161
+ />
162
+ </Box>
163
+ <Box>
164
+ <Textarea
165
+ label="Description"
166
+ rows={6}
167
+ {...getInputProps("description")}
168
+ />
169
+ </Box>
170
+ </SimpleGrid>
171
+ </Edit>
172
+ );
173
+ }
@@ -0,0 +1,236 @@
1
+ import {
2
+ ActionIcon,
3
+ Box,
4
+ type BoxProps,
5
+ Card,
6
+ type CardProps,
7
+ Group,
8
+ type GroupProps,
9
+ LoadingOverlay,
10
+ Stack,
11
+ Title,
12
+ } from "@mantine/core";
13
+ import {
14
+ useBack,
15
+ useGo,
16
+ useMutationMode,
17
+ useRefineContext,
18
+ useResourceParams,
19
+ useToPath,
20
+ useTranslate,
21
+ useUserFriendlyName,
22
+ } from "@refinedev/core";
23
+ import {
24
+ type RefineCrudEditProps,
25
+ RefinePageHeaderClassNames,
26
+ } from "@refinedev/ui-types";
27
+ import { IconArrowLeft } from "@tabler/icons-react";
28
+ import type React from "react";
29
+ import { Breadcrumb } from "@/components/breadcrumb/Breadcrumb";
30
+ import {
31
+ DeleteButton,
32
+ type DeleteButtonProps,
33
+ } from "@/components/buttons/DeleteButton";
34
+ import {
35
+ ListButton,
36
+ type ListButtonProps,
37
+ } from "@/components/buttons/ListButton";
38
+ import {
39
+ RefreshButton,
40
+ type RefreshButtonProps,
41
+ } from "@/components/buttons/RefreshButton";
42
+ import {
43
+ SaveButton,
44
+ type SaveButtonProps,
45
+ } from "@/components/buttons/SaveButton";
46
+ import { AutoSaveIndicator } from "@/components/notification/AutoSaveIndicator";
47
+
48
+ export type EditProps = RefineCrudEditProps<
49
+ SaveButtonProps,
50
+ DeleteButtonProps,
51
+ GroupProps,
52
+ GroupProps,
53
+ CardProps,
54
+ GroupProps,
55
+ BoxProps,
56
+ Record<never, never>,
57
+ RefreshButtonProps,
58
+ ListButtonProps
59
+ >;
60
+
61
+ export const Edit: React.FC<EditProps> = (props) => {
62
+ const {
63
+ children,
64
+ resource: resourceFromProps,
65
+ recordItemId,
66
+ deleteButtonProps: deleteButtonPropsFromProps,
67
+ mutationMode: mutationModeFromProps,
68
+ saveButtonProps: saveButtonPropsFromProps,
69
+ canDelete,
70
+ dataProviderName,
71
+ isLoading,
72
+ footerButtons: footerButtonsFromProps,
73
+ footerButtonProps,
74
+ headerButtons: headerButtonsFromProps,
75
+ headerButtonProps,
76
+ wrapperProps,
77
+ contentProps,
78
+ headerProps,
79
+ goBack: goBackFromProps,
80
+ breadcrumb: breadcrumbFromProps,
81
+ title,
82
+ autoSaveProps,
83
+ } = props;
84
+ const translate = useTranslate();
85
+ const { options: { breadcrumb: globalBreadcrumb } = {} } = useRefineContext();
86
+ const { mutationMode: mutationModeContext } = useMutationMode();
87
+ const mutationMode = mutationModeFromProps ?? mutationModeContext;
88
+
89
+ const back = useBack();
90
+ const go = useGo();
91
+ const getUserFriendlyName = useUserFriendlyName();
92
+
93
+ const {
94
+ resource,
95
+ id: idFromParams,
96
+ identifier,
97
+ } = useResourceParams({
98
+ resource: resourceFromProps,
99
+ });
100
+
101
+ const goListPath = useToPath({
102
+ resource,
103
+ action: "list",
104
+ });
105
+
106
+ const id = recordItemId ?? idFromParams;
107
+
108
+ const breadcrumb =
109
+ typeof breadcrumbFromProps === "undefined"
110
+ ? globalBreadcrumb
111
+ : breadcrumbFromProps;
112
+
113
+ const hasList = resource?.list && !recordItemId;
114
+
115
+ const isDeleteButtonVisible =
116
+ canDelete ?? (resource?.meta?.canDelete || deleteButtonPropsFromProps);
117
+
118
+ const breadcrumbComponent =
119
+ typeof breadcrumb !== "undefined" ? breadcrumb : <Breadcrumb />;
120
+
121
+ const loadingOverlayVisible =
122
+ isLoading ?? false;
123
+
124
+ const listButtonProps: ListButtonProps | undefined = hasList
125
+ ? {
126
+ ...(isLoading ? { disabled: true } : {}),
127
+ resource: identifier,
128
+ }
129
+ : undefined;
130
+
131
+ const refreshButtonProps: RefreshButtonProps = {
132
+ ...(isLoading ? { disabled: true } : {}),
133
+ resource: identifier,
134
+ recordItemId: id,
135
+ dataProviderName,
136
+ };
137
+
138
+ const deleteButtonProps: DeleteButtonProps | undefined = isDeleteButtonVisible
139
+ ? ({
140
+ ...(isLoading ? { disabled: true } : {}),
141
+ resource: identifier,
142
+ mutationMode,
143
+ onSuccess: () => {
144
+ go({ to: goListPath });
145
+ },
146
+ recordItemId: id,
147
+ dataProviderName,
148
+ ...deleteButtonPropsFromProps,
149
+ } as const)
150
+ : undefined;
151
+
152
+ const saveButtonProps: SaveButtonProps = {
153
+ ...(isLoading ? { disabled: true } : {}),
154
+ ...saveButtonPropsFromProps,
155
+ };
156
+
157
+ const defaultHeaderButtons = (
158
+ <>
159
+ {autoSaveProps && <AutoSaveIndicator {...autoSaveProps} />}
160
+ {hasList && <ListButton {...listButtonProps} />}
161
+ <RefreshButton {...refreshButtonProps} />
162
+ </>
163
+ );
164
+
165
+ const defaultFooterButtons = (
166
+ <>
167
+ {isDeleteButtonVisible && <DeleteButton {...deleteButtonProps} />}
168
+ <SaveButton {...saveButtonProps} />
169
+ </>
170
+ );
171
+
172
+ const buttonBack =
173
+ goBackFromProps === false || goBackFromProps === null ? null : (
174
+ <ActionIcon variant="subtle" onClick={back}>
175
+ {typeof goBackFromProps !== "undefined" ? (
176
+ goBackFromProps
177
+ ) : (
178
+ <IconArrowLeft />
179
+ )}
180
+ </ActionIcon>
181
+ );
182
+
183
+ const headerButtons = headerButtonsFromProps
184
+ ? typeof headerButtonsFromProps === "function"
185
+ ? headerButtonsFromProps({
186
+ defaultButtons: defaultHeaderButtons,
187
+ listButtonProps,
188
+ refreshButtonProps,
189
+ })
190
+ : headerButtonsFromProps
191
+ : defaultHeaderButtons;
192
+
193
+ const footerButtons = footerButtonsFromProps
194
+ ? typeof footerButtonsFromProps === "function"
195
+ ? footerButtonsFromProps({
196
+ defaultButtons: defaultFooterButtons,
197
+ deleteButtonProps,
198
+ saveButtonProps,
199
+ })
200
+ : footerButtonsFromProps
201
+ : defaultFooterButtons;
202
+
203
+ return (
204
+ <Card p="md" {...wrapperProps}>
205
+ <LoadingOverlay visible={loadingOverlayVisible} zIndex={1000} />
206
+ <Group justify="space-between" {...headerProps}>
207
+ <Stack gap="xs">
208
+ {breadcrumbComponent}
209
+ <Group gap="xs">
210
+ {buttonBack}
211
+ {title ?? (
212
+ <Title order={3} className={RefinePageHeaderClassNames.Title}>
213
+ {translate(
214
+ `${identifier}.titles.edit`,
215
+ `Edit ${getUserFriendlyName(
216
+ resource?.meta?.label ?? identifier,
217
+ "singular",
218
+ )}`,
219
+ )}
220
+ </Title>
221
+ )}
222
+ </Group>
223
+ </Stack>
224
+ <Group gap="xs" {...headerButtonProps}>
225
+ {headerButtons}
226
+ </Group>
227
+ </Group>
228
+ <Box pt="sm" {...contentProps}>
229
+ {children}
230
+ </Box>
231
+ <Group justify="right" gap="xs" mt="md" {...footerButtonProps}>
232
+ {footerButtons}
233
+ </Group>
234
+ </Card>
235
+ );
236
+ };