@elementor/editor-components 3.32.0-87 → 3.32.0-89
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/dist/index.js +283 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +283 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -2
- package/src/api.ts +29 -0
- package/src/components/create-component-form/create-component-form.tsx +220 -0
- package/src/components/create-component-form/hooks/use-form.ts +72 -0
- package/src/components/create-component-form/utils/component-form-schema.ts +34 -0
- package/src/components/create-component-form/utils/replace-element-with-component.ts +16 -0
- package/src/hooks/use-components.ts +13 -0
- package/src/hooks/use-create-component.ts +13 -0
- package/src/init.ts +7 -0
- package/src/types.ts +3 -0
package/dist/index.js
CHANGED
|
@@ -35,8 +35,9 @@ __export(index_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
|
|
37
37
|
// src/init.ts
|
|
38
|
+
var import_editor = require("@elementor/editor");
|
|
38
39
|
var import_editor_elements_panel = require("@elementor/editor-elements-panel");
|
|
39
|
-
var
|
|
40
|
+
var import_i18n3 = require("@wordpress/i18n");
|
|
40
41
|
|
|
41
42
|
// src/components/components-tab.tsx
|
|
42
43
|
var React = __toESM(require("react"));
|
|
@@ -45,13 +46,293 @@ function ComponentsTab() {
|
|
|
45
46
|
return /* @__PURE__ */ React.createElement(import_ui.Box, { px: 2 }, "This is the Components tab.");
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// src/components/create-component-form/create-component-form.tsx
|
|
50
|
+
var React2 = __toESM(require("react"));
|
|
51
|
+
var import_react2 = require("react");
|
|
52
|
+
var import_editor_elements2 = require("@elementor/editor-elements");
|
|
53
|
+
var import_editor_ui = require("@elementor/editor-ui");
|
|
54
|
+
var import_icons = require("@elementor/icons");
|
|
55
|
+
var import_ui2 = require("@elementor/ui");
|
|
56
|
+
var import_i18n2 = require("@wordpress/i18n");
|
|
57
|
+
|
|
58
|
+
// src/hooks/use-components.ts
|
|
59
|
+
var import_query = require("@elementor/query");
|
|
60
|
+
|
|
61
|
+
// src/api.ts
|
|
62
|
+
var import_http_client = require("@elementor/http-client");
|
|
63
|
+
var BASE_URL = "elementor/v1/components";
|
|
64
|
+
var apiClient = {
|
|
65
|
+
get: () => (0, import_http_client.httpService)().get(`${BASE_URL}`).then((res) => res.data.data),
|
|
66
|
+
create: (payload) => (0, import_http_client.httpService)().post(`${BASE_URL}`, payload).then((res) => res.data.data)
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/hooks/use-components.ts
|
|
70
|
+
var COMPONENTS_QUERY_KEY = "components";
|
|
71
|
+
var useComponents = () => {
|
|
72
|
+
return (0, import_query.useQuery)({
|
|
73
|
+
queryKey: [COMPONENTS_QUERY_KEY],
|
|
74
|
+
queryFn: apiClient.get,
|
|
75
|
+
staleTime: Infinity
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/hooks/use-create-component.ts
|
|
80
|
+
var import_query2 = require("@elementor/query");
|
|
81
|
+
var useCreateComponentMutation = () => {
|
|
82
|
+
const queryClient = (0, import_query2.useQueryClient)();
|
|
83
|
+
return (0, import_query2.useMutation)({
|
|
84
|
+
mutationFn: apiClient.create,
|
|
85
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: [COMPONENTS_QUERY_KEY] })
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/components/create-component-form/hooks/use-form.ts
|
|
90
|
+
var import_react = require("react");
|
|
91
|
+
var useForm = (initialValues) => {
|
|
92
|
+
const [values, setValues] = (0, import_react.useState)(initialValues);
|
|
93
|
+
const [errors, setErrors] = (0, import_react.useState)({});
|
|
94
|
+
const isValid = (0, import_react.useMemo)(() => {
|
|
95
|
+
return !Object.values(errors).some((error) => error);
|
|
96
|
+
}, [errors]);
|
|
97
|
+
const handleChange = (e, field, validationSchema) => {
|
|
98
|
+
const updated = { ...values, [field]: e.target.value };
|
|
99
|
+
setValues(updated);
|
|
100
|
+
const { success, errors: validationErrors } = validateForm(updated, validationSchema);
|
|
101
|
+
if (!success) {
|
|
102
|
+
setErrors(validationErrors);
|
|
103
|
+
} else {
|
|
104
|
+
setErrors({});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const validate = (validationSchema) => {
|
|
108
|
+
const { success, errors: validationErrors, parsedValues } = validateForm(values, validationSchema);
|
|
109
|
+
if (!success) {
|
|
110
|
+
setErrors(validationErrors);
|
|
111
|
+
return { success };
|
|
112
|
+
}
|
|
113
|
+
setErrors({});
|
|
114
|
+
return { success, parsedValues };
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
values,
|
|
118
|
+
errors,
|
|
119
|
+
isValid,
|
|
120
|
+
handleChange,
|
|
121
|
+
validateForm: validate
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
var validateForm = (values, schema) => {
|
|
125
|
+
const result = schema.safeParse(values);
|
|
126
|
+
if (result.success) {
|
|
127
|
+
return { success: true, parsedValues: result.data };
|
|
128
|
+
}
|
|
129
|
+
const errors = {};
|
|
130
|
+
Object.entries(result.error.formErrors.fieldErrors).forEach(
|
|
131
|
+
([field, error]) => {
|
|
132
|
+
errors[field] = error[0];
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
return { success: false, errors };
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/components/create-component-form/utils/component-form-schema.ts
|
|
139
|
+
var import_schema = require("@elementor/schema");
|
|
140
|
+
var import_i18n = require("@wordpress/i18n");
|
|
141
|
+
var MIN_NAME_LENGTH = 2;
|
|
142
|
+
var MAX_NAME_LENGTH = 50;
|
|
143
|
+
var createBaseComponentSchema = (existingNames) => {
|
|
144
|
+
return import_schema.z.object({
|
|
145
|
+
componentName: import_schema.z.string().trim().max(
|
|
146
|
+
MAX_NAME_LENGTH,
|
|
147
|
+
(0, import_i18n.__)("Component name is too long. Please keep it under 50 characters.", "elementor")
|
|
148
|
+
).refine((value) => !existingNames.includes(value), {
|
|
149
|
+
message: (0, import_i18n.__)("Component name already exists", "elementor")
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
var createSubmitComponentSchema = (existingNames) => {
|
|
154
|
+
const baseSchema = createBaseComponentSchema(existingNames);
|
|
155
|
+
return baseSchema.extend({
|
|
156
|
+
componentName: baseSchema.shape.componentName.refine((value) => value.length > 0, {
|
|
157
|
+
message: (0, import_i18n.__)("Component name is required.", "elementor")
|
|
158
|
+
}).refine((value) => value.length >= MIN_NAME_LENGTH, {
|
|
159
|
+
message: (0, import_i18n.__)("Component name is too short. Please enter at least 2 characters.", "elementor")
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/components/create-component-form/utils/replace-element-with-component.ts
|
|
165
|
+
var import_editor_elements = require("@elementor/editor-elements");
|
|
166
|
+
var import_editor_props = require("@elementor/editor-props");
|
|
167
|
+
var replaceElementWithComponent = async (element, componentId) => {
|
|
168
|
+
(0, import_editor_elements.replaceElement)({
|
|
169
|
+
currentElement: element,
|
|
170
|
+
newElement: {
|
|
171
|
+
elType: "widget",
|
|
172
|
+
widgetType: "e-component",
|
|
173
|
+
settings: {
|
|
174
|
+
component_id: import_editor_props.numberPropTypeUtil.create(componentId)
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
withHistory: false
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/components/create-component-form/create-component-form.tsx
|
|
182
|
+
function CreateComponentForm() {
|
|
183
|
+
const [element, setElement] = (0, import_react2.useState)(null);
|
|
184
|
+
const [anchorPosition, setAnchorPosition] = (0, import_react2.useState)();
|
|
185
|
+
const [resultNotification, setResultNotification] = (0, import_react2.useState)(null);
|
|
186
|
+
const { mutate: createComponent, isPending } = useCreateComponentMutation();
|
|
187
|
+
(0, import_react2.useEffect)(() => {
|
|
188
|
+
const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = "elementor/editor/open-save-as-component-form";
|
|
189
|
+
const openPopup = (event) => {
|
|
190
|
+
setElement({ element: event.detail.element, elementLabel: (0, import_editor_elements2.getElementLabel)(event.detail.element.id) });
|
|
191
|
+
setAnchorPosition(event.detail.anchorPosition);
|
|
192
|
+
};
|
|
193
|
+
window.addEventListener(OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup);
|
|
194
|
+
return () => {
|
|
195
|
+
window.removeEventListener(OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup);
|
|
196
|
+
};
|
|
197
|
+
}, []);
|
|
198
|
+
const handleSave = async (values) => {
|
|
199
|
+
if (!element) {
|
|
200
|
+
throw new Error(`Can't save element as component: element not found`);
|
|
201
|
+
}
|
|
202
|
+
createComponent(
|
|
203
|
+
{
|
|
204
|
+
name: values.componentName,
|
|
205
|
+
content: [element.element.model.toJSON({ remove: ["default"] })]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
onSuccess: (result) => {
|
|
209
|
+
if (!element) {
|
|
210
|
+
throw new Error(`Can't replace element with component: element not found`);
|
|
211
|
+
}
|
|
212
|
+
replaceElementWithComponent(element.element, result.component_id);
|
|
213
|
+
setResultNotification({
|
|
214
|
+
show: true,
|
|
215
|
+
// Translators: %1$s: Component name, %2$s: Component ID
|
|
216
|
+
message: (0, import_i18n2.__)("Component saved successfully as: %1$s (ID: %2$s)", "elementor").replace("%1$s", values.componentName).replace("%2$s", result.component_id.toString()),
|
|
217
|
+
type: "success"
|
|
218
|
+
});
|
|
219
|
+
resetAndClosePopup();
|
|
220
|
+
},
|
|
221
|
+
onError: () => {
|
|
222
|
+
const errorMessage = (0, import_i18n2.__)("Failed to save component. Please try again.", "elementor");
|
|
223
|
+
setResultNotification({
|
|
224
|
+
show: true,
|
|
225
|
+
message: errorMessage,
|
|
226
|
+
type: "error"
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
const resetAndClosePopup = () => {
|
|
233
|
+
setElement(null);
|
|
234
|
+
setAnchorPosition(void 0);
|
|
235
|
+
};
|
|
236
|
+
return /* @__PURE__ */ React2.createElement(import_editor_ui.ThemeProvider, null, /* @__PURE__ */ React2.createElement(
|
|
237
|
+
import_ui2.Popover,
|
|
238
|
+
{
|
|
239
|
+
open: element !== null,
|
|
240
|
+
onClose: resetAndClosePopup,
|
|
241
|
+
anchorReference: "anchorPosition",
|
|
242
|
+
anchorPosition
|
|
243
|
+
},
|
|
244
|
+
element !== null && /* @__PURE__ */ React2.createElement(
|
|
245
|
+
Form,
|
|
246
|
+
{
|
|
247
|
+
initialValues: { componentName: element.elementLabel },
|
|
248
|
+
handleSave,
|
|
249
|
+
isSubmitting: isPending,
|
|
250
|
+
closePopup: resetAndClosePopup
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
), /* @__PURE__ */ React2.createElement(import_ui2.Snackbar, { open: resultNotification?.show, onClose: () => setResultNotification(null) }, /* @__PURE__ */ React2.createElement(
|
|
254
|
+
import_ui2.Alert,
|
|
255
|
+
{
|
|
256
|
+
onClose: () => setResultNotification(null),
|
|
257
|
+
severity: resultNotification?.type,
|
|
258
|
+
sx: { width: "100%" }
|
|
259
|
+
},
|
|
260
|
+
resultNotification?.message
|
|
261
|
+
)));
|
|
262
|
+
}
|
|
263
|
+
var FONT_SIZE = "tiny";
|
|
264
|
+
var Form = ({
|
|
265
|
+
initialValues,
|
|
266
|
+
handleSave,
|
|
267
|
+
isSubmitting,
|
|
268
|
+
closePopup
|
|
269
|
+
}) => {
|
|
270
|
+
const { values, errors, isValid, handleChange, validateForm: validateForm2 } = useForm(initialValues);
|
|
271
|
+
const { data: components } = useComponents();
|
|
272
|
+
const existingComponentNames = (0, import_react2.useMemo)(() => {
|
|
273
|
+
return components?.map((component) => component.name) ?? [];
|
|
274
|
+
}, [components]);
|
|
275
|
+
const changeValidationSchema = (0, import_react2.useMemo)(
|
|
276
|
+
() => createBaseComponentSchema(existingComponentNames),
|
|
277
|
+
[existingComponentNames]
|
|
278
|
+
);
|
|
279
|
+
const submitValidationSchema = (0, import_react2.useMemo)(
|
|
280
|
+
() => createSubmitComponentSchema(existingComponentNames),
|
|
281
|
+
[existingComponentNames]
|
|
282
|
+
);
|
|
283
|
+
const handleSubmit = () => {
|
|
284
|
+
const { success, parsedValues } = validateForm2(submitValidationSchema);
|
|
285
|
+
if (success) {
|
|
286
|
+
handleSave(parsedValues);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
return /* @__PURE__ */ React2.createElement(import_ui2.Stack, { alignItems: "start", width: "268px" }, /* @__PURE__ */ React2.createElement(
|
|
290
|
+
import_ui2.Stack,
|
|
291
|
+
{
|
|
292
|
+
direction: "row",
|
|
293
|
+
alignItems: "center",
|
|
294
|
+
py: 1,
|
|
295
|
+
px: 1.5,
|
|
296
|
+
sx: { columnGap: 0.5, borderBottom: "1px solid", borderColor: "divider", width: "100%" }
|
|
297
|
+
},
|
|
298
|
+
/* @__PURE__ */ React2.createElement(import_icons.StarIcon, { fontSize: FONT_SIZE }),
|
|
299
|
+
/* @__PURE__ */ React2.createElement(import_ui2.Typography, { variant: "caption", sx: { color: "text.primary", fontWeight: "500", lineHeight: 1 } }, (0, import_i18n2.__)("Save as a component", "elementor"))
|
|
300
|
+
), /* @__PURE__ */ React2.createElement(import_ui2.Grid, { container: true, gap: 0.75, alignItems: "start", p: 1.5 }, /* @__PURE__ */ React2.createElement(import_ui2.Grid, { item: true, xs: 12 }, /* @__PURE__ */ React2.createElement(import_ui2.FormLabel, { htmlFor: "component-name", size: "tiny" }, (0, import_i18n2.__)("Name", "elementor"))), /* @__PURE__ */ React2.createElement(import_ui2.Grid, { item: true, xs: 12 }, /* @__PURE__ */ React2.createElement(
|
|
301
|
+
import_ui2.TextField,
|
|
302
|
+
{
|
|
303
|
+
id: "component-name",
|
|
304
|
+
size: FONT_SIZE,
|
|
305
|
+
fullWidth: true,
|
|
306
|
+
value: values.componentName,
|
|
307
|
+
onChange: (e) => handleChange(e, "componentName", changeValidationSchema),
|
|
308
|
+
inputProps: { style: { color: "text.primary", fontWeight: "600" } },
|
|
309
|
+
error: Boolean(errors.componentName),
|
|
310
|
+
helperText: errors.componentName
|
|
311
|
+
}
|
|
312
|
+
))), /* @__PURE__ */ React2.createElement(import_ui2.Stack, { direction: "row", justifyContent: "flex-end", alignSelf: "end", py: 1, px: 1.5 }, /* @__PURE__ */ React2.createElement(import_ui2.Button, { onClick: closePopup, disabled: isSubmitting, color: "secondary", variant: "text", size: "small" }, (0, import_i18n2.__)("Cancel", "elementor")), /* @__PURE__ */ React2.createElement(
|
|
313
|
+
import_ui2.Button,
|
|
314
|
+
{
|
|
315
|
+
onClick: handleSubmit,
|
|
316
|
+
disabled: isSubmitting || !isValid,
|
|
317
|
+
variant: "contained",
|
|
318
|
+
color: "primary",
|
|
319
|
+
size: "small"
|
|
320
|
+
},
|
|
321
|
+
isSubmitting ? (0, import_i18n2.__)("Creating\u2026", "elementor") : (0, import_i18n2.__)("Create", "elementor")
|
|
322
|
+
)));
|
|
323
|
+
};
|
|
324
|
+
|
|
48
325
|
// src/init.ts
|
|
49
326
|
function init() {
|
|
50
327
|
(0, import_editor_elements_panel.injectTab)({
|
|
51
328
|
id: "components",
|
|
52
|
-
label: (0,
|
|
329
|
+
label: (0, import_i18n3.__)("Components", "elementor"),
|
|
53
330
|
component: ComponentsTab
|
|
54
331
|
});
|
|
332
|
+
(0, import_editor.injectIntoTop)({
|
|
333
|
+
id: "create-component-popup",
|
|
334
|
+
component: CreateComponentForm
|
|
335
|
+
});
|
|
55
336
|
}
|
|
56
337
|
// Annotate the CommonJS export names for ESM import in node:
|
|
57
338
|
0 && (module.exports = {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/init.ts","../src/components/components-tab.tsx"],"sourcesContent":["export { init } from './init';\n","import { injectTab } from '@elementor/editor-elements-panel';\nimport { __ } from '@wordpress/i18n';\n\nimport { ComponentsTab } from './components/components-tab';\n\nexport function init() {\n\tinjectTab( {\n\t\tid: 'components',\n\t\tlabel: __( 'Components', 'elementor' ),\n\t\tcomponent: ComponentsTab,\n\t} );\n}\n","import * as React from 'react';\nimport { Box } from '@elementor/ui';\n\nexport function ComponentsTab() {\n\treturn <Box px={ 2 }>This is the Components tab.</Box>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mCAA0B;AAC1B,kBAAmB;;;ACDnB,YAAuB;AACvB,gBAAoB;AAEb,SAAS,gBAAgB;AAC/B,SAAO,oCAAC,iBAAI,IAAK,KAAI,6BAA2B;AACjD;;;ADAO,SAAS,OAAO;AACtB,8CAAW;AAAA,IACV,IAAI;AAAA,IACJ,WAAO,gBAAI,cAAc,WAAY;AAAA,IACrC,WAAW;AAAA,EACZ,CAAE;AACH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/init.ts","../src/components/components-tab.tsx","../src/components/create-component-form/create-component-form.tsx","../src/hooks/use-components.ts","../src/api.ts","../src/hooks/use-create-component.ts","../src/components/create-component-form/hooks/use-form.ts","../src/components/create-component-form/utils/component-form-schema.ts","../src/components/create-component-form/utils/replace-element-with-component.ts"],"sourcesContent":["export { init } from './init';\n","import { injectIntoTop } from '@elementor/editor';\nimport { injectTab } from '@elementor/editor-elements-panel';\nimport { __ } from '@wordpress/i18n';\n\nimport { ComponentsTab } from './components/components-tab';\nimport { CreateComponentForm } from './components/create-component-form/create-component-form';\n\nexport function init() {\n\tinjectTab( {\n\t\tid: 'components',\n\t\tlabel: __( 'Components', 'elementor' ),\n\t\tcomponent: ComponentsTab,\n\t} );\n\n\tinjectIntoTop( {\n\t\tid: 'create-component-popup',\n\t\tcomponent: CreateComponentForm,\n\t} );\n}\n","import * as React from 'react';\nimport { Box } from '@elementor/ui';\n\nexport function ComponentsTab() {\n\treturn <Box px={ 2 }>This is the Components tab.</Box>;\n}\n","import * as React from 'react';\nimport { useEffect, useMemo, useState } from 'react';\nimport { getElementLabel, type V1Element } from '@elementor/editor-elements';\nimport { ThemeProvider } from '@elementor/editor-ui';\nimport { StarIcon } from '@elementor/icons';\nimport { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from '@elementor/ui';\nimport { __ } from '@wordpress/i18n';\n\nimport { type CreateComponentResponse } from '../../api';\nimport { useComponents } from '../../hooks/use-components';\nimport { useCreateComponentMutation } from '../../hooks/use-create-component';\nimport { type ComponentFormValues } from '../../types';\nimport { useForm } from './hooks/use-form';\nimport { createBaseComponentSchema, createSubmitComponentSchema } from './utils/component-form-schema';\nimport { replaceElementWithComponent } from './utils/replace-element-with-component';\n\ntype SaveAsComponentEventData = {\n\telement: V1Element;\n\tanchorPosition: { top: number; left: number };\n};\n\ntype ResultNotification = {\n\tshow: boolean;\n\tmessage: string;\n\ttype: 'success' | 'error';\n};\n\nexport function CreateComponentForm() {\n\tconst [ element, setElement ] = useState< {\n\t\telement: V1Element;\n\t\telementLabel: string;\n\t} | null >( null );\n\n\tconst [ anchorPosition, setAnchorPosition ] = useState< { top: number; left: number } >();\n\n\tconst [ resultNotification, setResultNotification ] = useState< ResultNotification | null >( null );\n\n\tconst { mutate: createComponent, isPending } = useCreateComponentMutation();\n\n\tuseEffect( () => {\n\t\tconst OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';\n\n\t\tconst openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {\n\t\t\tsetElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );\n\t\t\tsetAnchorPosition( event.detail.anchorPosition );\n\t\t};\n\n\t\twindow.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );\n\t\t};\n\t}, [] );\n\n\tconst handleSave = async ( values: ComponentFormValues ) => {\n\t\tif ( ! element ) {\n\t\t\tthrow new Error( `Can't save element as component: element not found` );\n\t\t}\n\n\t\tcreateComponent(\n\t\t\t{\n\t\t\t\tname: values.componentName,\n\t\t\t\tcontent: [ element.element.model.toJSON( { remove: [ 'default' ] } ) ],\n\t\t\t},\n\t\t\t{\n\t\t\t\tonSuccess: ( result: CreateComponentResponse ) => {\n\t\t\t\t\tif ( ! element ) {\n\t\t\t\t\t\tthrow new Error( `Can't replace element with component: element not found` );\n\t\t\t\t\t}\n\n\t\t\t\t\treplaceElementWithComponent( element.element, result.component_id );\n\n\t\t\t\t\tsetResultNotification( {\n\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\t// Translators: %1$s: Component name, %2$s: Component ID\n\t\t\t\t\t\tmessage: __( 'Component saved successfully as: %1$s (ID: %2$s)', 'elementor' )\n\t\t\t\t\t\t\t.replace( '%1$s', values.componentName )\n\t\t\t\t\t\t\t.replace( '%2$s', result.component_id.toString() ),\n\t\t\t\t\t\ttype: 'success',\n\t\t\t\t\t} );\n\n\t\t\t\t\tresetAndClosePopup();\n\t\t\t\t},\n\t\t\t\tonError: () => {\n\t\t\t\t\tconst errorMessage = __( 'Failed to save component. Please try again.', 'elementor' );\n\t\t\t\t\tsetResultNotification( {\n\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\tmessage: errorMessage,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t} );\n\t\t\t\t},\n\t\t\t}\n\t\t);\n\t};\n\n\tconst resetAndClosePopup = () => {\n\t\tsetElement( null );\n\t\tsetAnchorPosition( undefined );\n\t};\n\n\treturn (\n\t\t<ThemeProvider>\n\t\t\t<Popover\n\t\t\t\topen={ element !== null }\n\t\t\t\tonClose={ resetAndClosePopup }\n\t\t\t\tanchorReference=\"anchorPosition\"\n\t\t\t\tanchorPosition={ anchorPosition }\n\t\t\t>\n\t\t\t\t{ element !== null && (\n\t\t\t\t\t<Form\n\t\t\t\t\t\tinitialValues={ { componentName: element.elementLabel } }\n\t\t\t\t\t\thandleSave={ handleSave }\n\t\t\t\t\t\tisSubmitting={ isPending }\n\t\t\t\t\t\tclosePopup={ resetAndClosePopup }\n\t\t\t\t\t/>\n\t\t\t\t) }\n\t\t\t</Popover>\n\t\t\t<Snackbar open={ resultNotification?.show } onClose={ () => setResultNotification( null ) }>\n\t\t\t\t<Alert\n\t\t\t\t\tonClose={ () => setResultNotification( null ) }\n\t\t\t\t\tseverity={ resultNotification?.type }\n\t\t\t\t\tsx={ { width: '100%' } }\n\t\t\t\t>\n\t\t\t\t\t{ resultNotification?.message }\n\t\t\t\t</Alert>\n\t\t\t</Snackbar>\n\t\t</ThemeProvider>\n\t);\n}\n\nconst FONT_SIZE = 'tiny';\n\nconst Form = ( {\n\tinitialValues,\n\thandleSave,\n\tisSubmitting,\n\tclosePopup,\n}: {\n\tinitialValues: ComponentFormValues;\n\thandleSave: ( values: ComponentFormValues ) => void;\n\tisSubmitting: boolean;\n\tclosePopup: () => void;\n} ) => {\n\tconst { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );\n\n\tconst { data: components } = useComponents();\n\n\tconst existingComponentNames = useMemo( () => {\n\t\treturn components?.map( ( component ) => component.name ) ?? [];\n\t}, [ components ] );\n\n\tconst changeValidationSchema = useMemo(\n\t\t() => createBaseComponentSchema( existingComponentNames ),\n\t\t[ existingComponentNames ]\n\t);\n\tconst submitValidationSchema = useMemo(\n\t\t() => createSubmitComponentSchema( existingComponentNames ),\n\t\t[ existingComponentNames ]\n\t);\n\n\tconst handleSubmit = () => {\n\t\tconst { success, parsedValues } = validateForm( submitValidationSchema );\n\n\t\tif ( success ) {\n\t\t\thandleSave( parsedValues );\n\t\t}\n\t};\n\n\treturn (\n\t\t<Stack alignItems=\"start\" width=\"268px\">\n\t\t\t<Stack\n\t\t\t\tdirection=\"row\"\n\t\t\t\talignItems=\"center\"\n\t\t\t\tpy={ 1 }\n\t\t\t\tpx={ 1.5 }\n\t\t\t\tsx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }\n\t\t\t>\n\t\t\t\t<StarIcon fontSize={ FONT_SIZE } />\n\t\t\t\t<Typography variant=\"caption\" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>\n\t\t\t\t\t{ __( 'Save as a component', 'elementor' ) }\n\t\t\t\t</Typography>\n\t\t\t</Stack>\n\t\t\t<Grid container gap={ 0.75 } alignItems=\"start\" p={ 1.5 }>\n\t\t\t\t<Grid item xs={ 12 }>\n\t\t\t\t\t<FormLabel htmlFor={ 'component-name' } size=\"tiny\">\n\t\t\t\t\t\t{ __( 'Name', 'elementor' ) }\n\t\t\t\t\t</FormLabel>\n\t\t\t\t</Grid>\n\t\t\t\t<Grid item xs={ 12 }>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tid={ 'component-name' }\n\t\t\t\t\t\tsize={ FONT_SIZE }\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tvalue={ values.componentName }\n\t\t\t\t\t\tonChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>\n\t\t\t\t\t\t\thandleChange( e, 'componentName', changeValidationSchema )\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }\n\t\t\t\t\t\terror={ Boolean( errors.componentName ) }\n\t\t\t\t\t\thelperText={ errors.componentName }\n\t\t\t\t\t/>\n\t\t\t\t</Grid>\n\t\t\t</Grid>\n\t\t\t<Stack direction=\"row\" justifyContent=\"flex-end\" alignSelf=\"end\" py={ 1 } px={ 1.5 }>\n\t\t\t\t<Button onClick={ closePopup } disabled={ isSubmitting } color=\"secondary\" variant=\"text\" size=\"small\">\n\t\t\t\t\t{ __( 'Cancel', 'elementor' ) }\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tonClick={ handleSubmit }\n\t\t\t\t\tdisabled={ isSubmitting || ! isValid }\n\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t>\n\t\t\t\t\t{ isSubmitting ? __( 'Creating…', 'elementor' ) : __( 'Create', 'elementor' ) }\n\t\t\t\t</Button>\n\t\t\t</Stack>\n\t\t</Stack>\n\t);\n};\n","import { useQuery } from '@elementor/query';\n\nimport { apiClient } from '../api';\n\nexport const COMPONENTS_QUERY_KEY = 'components';\n\nexport const useComponents = () => {\n\treturn useQuery( {\n\t\tqueryKey: [ COMPONENTS_QUERY_KEY ],\n\t\tqueryFn: apiClient.get,\n\t\tstaleTime: Infinity,\n\t} );\n};\n","import { type V1ElementModelProps } from '@elementor/editor-elements';\nimport { type HttpResponse, httpService } from '@elementor/http-client';\n\nconst BASE_URL = 'elementor/v1/components';\n\ntype CreateComponentPayload = {\n\tname: string;\n\tcontent: V1ElementModelProps[];\n};\n\ntype GetComponentResponse = Array< {\n\tcomponent_id: number;\n\tname: string;\n} >;\n\nexport type CreateComponentResponse = {\n\tcomponent_id: number;\n};\n\nexport const apiClient = {\n\tget: () =>\n\t\thttpService()\n\t\t\t.get< HttpResponse< GetComponentResponse > >( `${ BASE_URL }` )\n\t\t\t.then( ( res ) => res.data.data ),\n\tcreate: ( payload: CreateComponentPayload ) =>\n\t\thttpService()\n\t\t\t.post< HttpResponse< CreateComponentResponse > >( `${ BASE_URL }`, payload )\n\t\t\t.then( ( res ) => res.data.data ),\n};\n","import { useMutation, useQueryClient } from '@elementor/query';\n\nimport { apiClient } from '../api';\nimport { COMPONENTS_QUERY_KEY } from './use-components';\n\nexport const useCreateComponentMutation = () => {\n\tconst queryClient = useQueryClient();\n\n\treturn useMutation( {\n\t\tmutationFn: apiClient.create,\n\t\tonSuccess: () => queryClient.invalidateQueries( { queryKey: [ COMPONENTS_QUERY_KEY ] } ),\n\t} );\n};\n","import { useMemo, useState } from 'react';\nimport { type z } from '@elementor/schema';\n\nexport const useForm = < TValues extends Record< string, unknown > >( initialValues: TValues ) => {\n\tconst [ values, setValues ] = useState< TValues >( initialValues );\n\tconst [ errors, setErrors ] = useState< Partial< Record< keyof TValues, string > > >( {} );\n\n\tconst isValid = useMemo( () => {\n\t\treturn ! Object.values( errors ).some( ( error ) => error );\n\t}, [ errors ] );\n\n\tconst handleChange = (\n\t\te: React.ChangeEvent< HTMLInputElement >,\n\t\tfield: keyof TValues,\n\t\tvalidationSchema: z.ZodType< TValues >\n\t) => {\n\t\tconst updated = { ...values, [ field ]: e.target.value };\n\t\tsetValues( updated );\n\n\t\tconst { success, errors: validationErrors } = validateForm( updated, validationSchema );\n\n\t\tif ( ! success ) {\n\t\t\tsetErrors( validationErrors );\n\t\t} else {\n\t\t\tsetErrors( {} );\n\t\t}\n\t};\n\n\tconst validate = (\n\t\tvalidationSchema: z.ZodType< TValues >\n\t): { success: true; parsedValues: TValues } | { success: false; parsedValues?: never } => {\n\t\tconst { success, errors: validationErrors, parsedValues } = validateForm( values, validationSchema );\n\n\t\tif ( ! success ) {\n\t\t\tsetErrors( validationErrors );\n\t\t\treturn { success };\n\t\t}\n\t\tsetErrors( {} );\n\t\treturn { success, parsedValues };\n\t};\n\n\treturn {\n\t\tvalues,\n\t\terrors,\n\t\tisValid,\n\t\thandleChange,\n\t\tvalidateForm: validate,\n\t};\n};\n\nconst validateForm = < TValues extends Record< string, unknown > >(\n\tvalues: TValues,\n\tschema: z.ZodType< TValues >\n):\n\t| { success: false; parsedValues?: never; errors: Partial< Record< keyof TValues, string > > }\n\t| { success: true; parsedValues: TValues; errors?: never } => {\n\tconst result = schema.safeParse( values );\n\n\tif ( result.success ) {\n\t\treturn { success: true, parsedValues: result.data };\n\t}\n\n\tconst errors = {} as Partial< Record< keyof TValues, string > >;\n\n\t( Object.entries( result.error.formErrors.fieldErrors ) as Array< [ keyof TValues, string[] ] > ).forEach(\n\t\t( [ field, error ] ) => {\n\t\t\terrors[ field ] = error[ 0 ];\n\t\t}\n\t);\n\n\treturn { success: false, errors };\n};\n","import { z } from '@elementor/schema';\nimport { __ } from '@wordpress/i18n';\n\nconst MIN_NAME_LENGTH = 2;\nconst MAX_NAME_LENGTH = 50;\n\nexport const createBaseComponentSchema = ( existingNames: string[] ) => {\n\treturn z.object( {\n\t\tcomponentName: z\n\t\t\t.string()\n\t\t\t.trim()\n\t\t\t.max(\n\t\t\t\tMAX_NAME_LENGTH,\n\t\t\t\t__( 'Component name is too long. Please keep it under 50 characters.', 'elementor' )\n\t\t\t)\n\t\t\t.refine( ( value ) => ! existingNames.includes( value ), {\n\t\t\t\tmessage: __( 'Component name already exists', 'elementor' ),\n\t\t\t} ),\n\t} );\n};\n\nexport const createSubmitComponentSchema = ( existingNames: string[] ) => {\n\tconst baseSchema = createBaseComponentSchema( existingNames );\n\n\treturn baseSchema.extend( {\n\t\tcomponentName: baseSchema.shape.componentName\n\t\t\t.refine( ( value ) => value.length > 0, {\n\t\t\t\tmessage: __( 'Component name is required.', 'elementor' ),\n\t\t\t} )\n\t\t\t.refine( ( value ) => value.length >= MIN_NAME_LENGTH, {\n\t\t\t\tmessage: __( 'Component name is too short. Please enter at least 2 characters.', 'elementor' ),\n\t\t\t} ),\n\t} );\n};\n","import { replaceElement, type V1Element } from '@elementor/editor-elements';\nimport { numberPropTypeUtil } from '@elementor/editor-props';\n\nexport const replaceElementWithComponent = async ( element: V1Element, componentId: number ) => {\n\treplaceElement( {\n\t\tcurrentElement: element,\n\t\tnewElement: {\n\t\t\telType: 'widget',\n\t\t\twidgetType: 'e-component',\n\t\t\tsettings: {\n\t\t\t\tcomponent_id: numberPropTypeUtil.create( componentId ),\n\t\t\t},\n\t\t},\n\t\twithHistory: false,\n\t} );\n};\n"],"mappings":"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","names":["import_i18n","React","import_react","import_editor_elements","import_ui","import_i18n","import_query","validateForm"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/init.ts
|
|
2
|
+
import { injectIntoTop } from "@elementor/editor";
|
|
2
3
|
import { injectTab } from "@elementor/editor-elements-panel";
|
|
3
|
-
import { __ } from "@wordpress/i18n";
|
|
4
|
+
import { __ as __3 } from "@wordpress/i18n";
|
|
4
5
|
|
|
5
6
|
// src/components/components-tab.tsx
|
|
6
7
|
import * as React from "react";
|
|
@@ -9,13 +10,293 @@ function ComponentsTab() {
|
|
|
9
10
|
return /* @__PURE__ */ React.createElement(Box, { px: 2 }, "This is the Components tab.");
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
// src/components/create-component-form/create-component-form.tsx
|
|
14
|
+
import * as React2 from "react";
|
|
15
|
+
import { useEffect, useMemo as useMemo2, useState as useState2 } from "react";
|
|
16
|
+
import { getElementLabel } from "@elementor/editor-elements";
|
|
17
|
+
import { ThemeProvider } from "@elementor/editor-ui";
|
|
18
|
+
import { StarIcon } from "@elementor/icons";
|
|
19
|
+
import { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from "@elementor/ui";
|
|
20
|
+
import { __ as __2 } from "@wordpress/i18n";
|
|
21
|
+
|
|
22
|
+
// src/hooks/use-components.ts
|
|
23
|
+
import { useQuery } from "@elementor/query";
|
|
24
|
+
|
|
25
|
+
// src/api.ts
|
|
26
|
+
import { httpService } from "@elementor/http-client";
|
|
27
|
+
var BASE_URL = "elementor/v1/components";
|
|
28
|
+
var apiClient = {
|
|
29
|
+
get: () => httpService().get(`${BASE_URL}`).then((res) => res.data.data),
|
|
30
|
+
create: (payload) => httpService().post(`${BASE_URL}`, payload).then((res) => res.data.data)
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/hooks/use-components.ts
|
|
34
|
+
var COMPONENTS_QUERY_KEY = "components";
|
|
35
|
+
var useComponents = () => {
|
|
36
|
+
return useQuery({
|
|
37
|
+
queryKey: [COMPONENTS_QUERY_KEY],
|
|
38
|
+
queryFn: apiClient.get,
|
|
39
|
+
staleTime: Infinity
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/hooks/use-create-component.ts
|
|
44
|
+
import { useMutation, useQueryClient } from "@elementor/query";
|
|
45
|
+
var useCreateComponentMutation = () => {
|
|
46
|
+
const queryClient = useQueryClient();
|
|
47
|
+
return useMutation({
|
|
48
|
+
mutationFn: apiClient.create,
|
|
49
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: [COMPONENTS_QUERY_KEY] })
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/components/create-component-form/hooks/use-form.ts
|
|
54
|
+
import { useMemo, useState } from "react";
|
|
55
|
+
var useForm = (initialValues) => {
|
|
56
|
+
const [values, setValues] = useState(initialValues);
|
|
57
|
+
const [errors, setErrors] = useState({});
|
|
58
|
+
const isValid = useMemo(() => {
|
|
59
|
+
return !Object.values(errors).some((error) => error);
|
|
60
|
+
}, [errors]);
|
|
61
|
+
const handleChange = (e, field, validationSchema) => {
|
|
62
|
+
const updated = { ...values, [field]: e.target.value };
|
|
63
|
+
setValues(updated);
|
|
64
|
+
const { success, errors: validationErrors } = validateForm(updated, validationSchema);
|
|
65
|
+
if (!success) {
|
|
66
|
+
setErrors(validationErrors);
|
|
67
|
+
} else {
|
|
68
|
+
setErrors({});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const validate = (validationSchema) => {
|
|
72
|
+
const { success, errors: validationErrors, parsedValues } = validateForm(values, validationSchema);
|
|
73
|
+
if (!success) {
|
|
74
|
+
setErrors(validationErrors);
|
|
75
|
+
return { success };
|
|
76
|
+
}
|
|
77
|
+
setErrors({});
|
|
78
|
+
return { success, parsedValues };
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
values,
|
|
82
|
+
errors,
|
|
83
|
+
isValid,
|
|
84
|
+
handleChange,
|
|
85
|
+
validateForm: validate
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
var validateForm = (values, schema) => {
|
|
89
|
+
const result = schema.safeParse(values);
|
|
90
|
+
if (result.success) {
|
|
91
|
+
return { success: true, parsedValues: result.data };
|
|
92
|
+
}
|
|
93
|
+
const errors = {};
|
|
94
|
+
Object.entries(result.error.formErrors.fieldErrors).forEach(
|
|
95
|
+
([field, error]) => {
|
|
96
|
+
errors[field] = error[0];
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
return { success: false, errors };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// src/components/create-component-form/utils/component-form-schema.ts
|
|
103
|
+
import { z } from "@elementor/schema";
|
|
104
|
+
import { __ } from "@wordpress/i18n";
|
|
105
|
+
var MIN_NAME_LENGTH = 2;
|
|
106
|
+
var MAX_NAME_LENGTH = 50;
|
|
107
|
+
var createBaseComponentSchema = (existingNames) => {
|
|
108
|
+
return z.object({
|
|
109
|
+
componentName: z.string().trim().max(
|
|
110
|
+
MAX_NAME_LENGTH,
|
|
111
|
+
__("Component name is too long. Please keep it under 50 characters.", "elementor")
|
|
112
|
+
).refine((value) => !existingNames.includes(value), {
|
|
113
|
+
message: __("Component name already exists", "elementor")
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
var createSubmitComponentSchema = (existingNames) => {
|
|
118
|
+
const baseSchema = createBaseComponentSchema(existingNames);
|
|
119
|
+
return baseSchema.extend({
|
|
120
|
+
componentName: baseSchema.shape.componentName.refine((value) => value.length > 0, {
|
|
121
|
+
message: __("Component name is required.", "elementor")
|
|
122
|
+
}).refine((value) => value.length >= MIN_NAME_LENGTH, {
|
|
123
|
+
message: __("Component name is too short. Please enter at least 2 characters.", "elementor")
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/components/create-component-form/utils/replace-element-with-component.ts
|
|
129
|
+
import { replaceElement } from "@elementor/editor-elements";
|
|
130
|
+
import { numberPropTypeUtil } from "@elementor/editor-props";
|
|
131
|
+
var replaceElementWithComponent = async (element, componentId) => {
|
|
132
|
+
replaceElement({
|
|
133
|
+
currentElement: element,
|
|
134
|
+
newElement: {
|
|
135
|
+
elType: "widget",
|
|
136
|
+
widgetType: "e-component",
|
|
137
|
+
settings: {
|
|
138
|
+
component_id: numberPropTypeUtil.create(componentId)
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
withHistory: false
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/components/create-component-form/create-component-form.tsx
|
|
146
|
+
function CreateComponentForm() {
|
|
147
|
+
const [element, setElement] = useState2(null);
|
|
148
|
+
const [anchorPosition, setAnchorPosition] = useState2();
|
|
149
|
+
const [resultNotification, setResultNotification] = useState2(null);
|
|
150
|
+
const { mutate: createComponent, isPending } = useCreateComponentMutation();
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = "elementor/editor/open-save-as-component-form";
|
|
153
|
+
const openPopup = (event) => {
|
|
154
|
+
setElement({ element: event.detail.element, elementLabel: getElementLabel(event.detail.element.id) });
|
|
155
|
+
setAnchorPosition(event.detail.anchorPosition);
|
|
156
|
+
};
|
|
157
|
+
window.addEventListener(OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup);
|
|
158
|
+
return () => {
|
|
159
|
+
window.removeEventListener(OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup);
|
|
160
|
+
};
|
|
161
|
+
}, []);
|
|
162
|
+
const handleSave = async (values) => {
|
|
163
|
+
if (!element) {
|
|
164
|
+
throw new Error(`Can't save element as component: element not found`);
|
|
165
|
+
}
|
|
166
|
+
createComponent(
|
|
167
|
+
{
|
|
168
|
+
name: values.componentName,
|
|
169
|
+
content: [element.element.model.toJSON({ remove: ["default"] })]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
onSuccess: (result) => {
|
|
173
|
+
if (!element) {
|
|
174
|
+
throw new Error(`Can't replace element with component: element not found`);
|
|
175
|
+
}
|
|
176
|
+
replaceElementWithComponent(element.element, result.component_id);
|
|
177
|
+
setResultNotification({
|
|
178
|
+
show: true,
|
|
179
|
+
// Translators: %1$s: Component name, %2$s: Component ID
|
|
180
|
+
message: __2("Component saved successfully as: %1$s (ID: %2$s)", "elementor").replace("%1$s", values.componentName).replace("%2$s", result.component_id.toString()),
|
|
181
|
+
type: "success"
|
|
182
|
+
});
|
|
183
|
+
resetAndClosePopup();
|
|
184
|
+
},
|
|
185
|
+
onError: () => {
|
|
186
|
+
const errorMessage = __2("Failed to save component. Please try again.", "elementor");
|
|
187
|
+
setResultNotification({
|
|
188
|
+
show: true,
|
|
189
|
+
message: errorMessage,
|
|
190
|
+
type: "error"
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
const resetAndClosePopup = () => {
|
|
197
|
+
setElement(null);
|
|
198
|
+
setAnchorPosition(void 0);
|
|
199
|
+
};
|
|
200
|
+
return /* @__PURE__ */ React2.createElement(ThemeProvider, null, /* @__PURE__ */ React2.createElement(
|
|
201
|
+
Popover,
|
|
202
|
+
{
|
|
203
|
+
open: element !== null,
|
|
204
|
+
onClose: resetAndClosePopup,
|
|
205
|
+
anchorReference: "anchorPosition",
|
|
206
|
+
anchorPosition
|
|
207
|
+
},
|
|
208
|
+
element !== null && /* @__PURE__ */ React2.createElement(
|
|
209
|
+
Form,
|
|
210
|
+
{
|
|
211
|
+
initialValues: { componentName: element.elementLabel },
|
|
212
|
+
handleSave,
|
|
213
|
+
isSubmitting: isPending,
|
|
214
|
+
closePopup: resetAndClosePopup
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
), /* @__PURE__ */ React2.createElement(Snackbar, { open: resultNotification?.show, onClose: () => setResultNotification(null) }, /* @__PURE__ */ React2.createElement(
|
|
218
|
+
Alert,
|
|
219
|
+
{
|
|
220
|
+
onClose: () => setResultNotification(null),
|
|
221
|
+
severity: resultNotification?.type,
|
|
222
|
+
sx: { width: "100%" }
|
|
223
|
+
},
|
|
224
|
+
resultNotification?.message
|
|
225
|
+
)));
|
|
226
|
+
}
|
|
227
|
+
var FONT_SIZE = "tiny";
|
|
228
|
+
var Form = ({
|
|
229
|
+
initialValues,
|
|
230
|
+
handleSave,
|
|
231
|
+
isSubmitting,
|
|
232
|
+
closePopup
|
|
233
|
+
}) => {
|
|
234
|
+
const { values, errors, isValid, handleChange, validateForm: validateForm2 } = useForm(initialValues);
|
|
235
|
+
const { data: components } = useComponents();
|
|
236
|
+
const existingComponentNames = useMemo2(() => {
|
|
237
|
+
return components?.map((component) => component.name) ?? [];
|
|
238
|
+
}, [components]);
|
|
239
|
+
const changeValidationSchema = useMemo2(
|
|
240
|
+
() => createBaseComponentSchema(existingComponentNames),
|
|
241
|
+
[existingComponentNames]
|
|
242
|
+
);
|
|
243
|
+
const submitValidationSchema = useMemo2(
|
|
244
|
+
() => createSubmitComponentSchema(existingComponentNames),
|
|
245
|
+
[existingComponentNames]
|
|
246
|
+
);
|
|
247
|
+
const handleSubmit = () => {
|
|
248
|
+
const { success, parsedValues } = validateForm2(submitValidationSchema);
|
|
249
|
+
if (success) {
|
|
250
|
+
handleSave(parsedValues);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
return /* @__PURE__ */ React2.createElement(Stack, { alignItems: "start", width: "268px" }, /* @__PURE__ */ React2.createElement(
|
|
254
|
+
Stack,
|
|
255
|
+
{
|
|
256
|
+
direction: "row",
|
|
257
|
+
alignItems: "center",
|
|
258
|
+
py: 1,
|
|
259
|
+
px: 1.5,
|
|
260
|
+
sx: { columnGap: 0.5, borderBottom: "1px solid", borderColor: "divider", width: "100%" }
|
|
261
|
+
},
|
|
262
|
+
/* @__PURE__ */ React2.createElement(StarIcon, { fontSize: FONT_SIZE }),
|
|
263
|
+
/* @__PURE__ */ React2.createElement(Typography, { variant: "caption", sx: { color: "text.primary", fontWeight: "500", lineHeight: 1 } }, __2("Save as a component", "elementor"))
|
|
264
|
+
), /* @__PURE__ */ React2.createElement(Grid, { container: true, gap: 0.75, alignItems: "start", p: 1.5 }, /* @__PURE__ */ React2.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React2.createElement(FormLabel, { htmlFor: "component-name", size: "tiny" }, __2("Name", "elementor"))), /* @__PURE__ */ React2.createElement(Grid, { item: true, xs: 12 }, /* @__PURE__ */ React2.createElement(
|
|
265
|
+
TextField,
|
|
266
|
+
{
|
|
267
|
+
id: "component-name",
|
|
268
|
+
size: FONT_SIZE,
|
|
269
|
+
fullWidth: true,
|
|
270
|
+
value: values.componentName,
|
|
271
|
+
onChange: (e) => handleChange(e, "componentName", changeValidationSchema),
|
|
272
|
+
inputProps: { style: { color: "text.primary", fontWeight: "600" } },
|
|
273
|
+
error: Boolean(errors.componentName),
|
|
274
|
+
helperText: errors.componentName
|
|
275
|
+
}
|
|
276
|
+
))), /* @__PURE__ */ React2.createElement(Stack, { direction: "row", justifyContent: "flex-end", alignSelf: "end", py: 1, px: 1.5 }, /* @__PURE__ */ React2.createElement(Button, { onClick: closePopup, disabled: isSubmitting, color: "secondary", variant: "text", size: "small" }, __2("Cancel", "elementor")), /* @__PURE__ */ React2.createElement(
|
|
277
|
+
Button,
|
|
278
|
+
{
|
|
279
|
+
onClick: handleSubmit,
|
|
280
|
+
disabled: isSubmitting || !isValid,
|
|
281
|
+
variant: "contained",
|
|
282
|
+
color: "primary",
|
|
283
|
+
size: "small"
|
|
284
|
+
},
|
|
285
|
+
isSubmitting ? __2("Creating\u2026", "elementor") : __2("Create", "elementor")
|
|
286
|
+
)));
|
|
287
|
+
};
|
|
288
|
+
|
|
12
289
|
// src/init.ts
|
|
13
290
|
function init() {
|
|
14
291
|
injectTab({
|
|
15
292
|
id: "components",
|
|
16
|
-
label:
|
|
293
|
+
label: __3("Components", "elementor"),
|
|
17
294
|
component: ComponentsTab
|
|
18
295
|
});
|
|
296
|
+
injectIntoTop({
|
|
297
|
+
id: "create-component-popup",
|
|
298
|
+
component: CreateComponentForm
|
|
299
|
+
});
|
|
19
300
|
}
|
|
20
301
|
export {
|
|
21
302
|
init
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/init.ts","../src/components/components-tab.tsx"],"sourcesContent":["import { injectTab } from '@elementor/editor-elements-panel';\nimport { __ } from '@wordpress/i18n';\n\nimport { ComponentsTab } from './components/components-tab';\n\nexport function init() {\n\tinjectTab( {\n\t\tid: 'components',\n\t\tlabel: __( 'Components', 'elementor' ),\n\t\tcomponent: ComponentsTab,\n\t} );\n}\n","import * as React from 'react';\nimport { Box } from '@elementor/ui';\n\nexport function ComponentsTab() {\n\treturn <Box px={ 2 }>This is the Components tab.</Box>;\n}\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,UAAU;;;ACDnB,YAAY,WAAW;AACvB,SAAS,WAAW;AAEb,SAAS,gBAAgB;AAC/B,SAAO,oCAAC,OAAI,IAAK,KAAI,6BAA2B;AACjD;;;ADAO,SAAS,OAAO;AACtB,YAAW;AAAA,IACV,IAAI;AAAA,IACJ,OAAO,GAAI,cAAc,WAAY;AAAA,IACrC,WAAW;AAAA,EACZ,CAAE;AACH;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/init.ts","../src/components/components-tab.tsx","../src/components/create-component-form/create-component-form.tsx","../src/hooks/use-components.ts","../src/api.ts","../src/hooks/use-create-component.ts","../src/components/create-component-form/hooks/use-form.ts","../src/components/create-component-form/utils/component-form-schema.ts","../src/components/create-component-form/utils/replace-element-with-component.ts"],"sourcesContent":["import { injectIntoTop } from '@elementor/editor';\nimport { injectTab } from '@elementor/editor-elements-panel';\nimport { __ } from '@wordpress/i18n';\n\nimport { ComponentsTab } from './components/components-tab';\nimport { CreateComponentForm } from './components/create-component-form/create-component-form';\n\nexport function init() {\n\tinjectTab( {\n\t\tid: 'components',\n\t\tlabel: __( 'Components', 'elementor' ),\n\t\tcomponent: ComponentsTab,\n\t} );\n\n\tinjectIntoTop( {\n\t\tid: 'create-component-popup',\n\t\tcomponent: CreateComponentForm,\n\t} );\n}\n","import * as React from 'react';\nimport { Box } from '@elementor/ui';\n\nexport function ComponentsTab() {\n\treturn <Box px={ 2 }>This is the Components tab.</Box>;\n}\n","import * as React from 'react';\nimport { useEffect, useMemo, useState } from 'react';\nimport { getElementLabel, type V1Element } from '@elementor/editor-elements';\nimport { ThemeProvider } from '@elementor/editor-ui';\nimport { StarIcon } from '@elementor/icons';\nimport { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from '@elementor/ui';\nimport { __ } from '@wordpress/i18n';\n\nimport { type CreateComponentResponse } from '../../api';\nimport { useComponents } from '../../hooks/use-components';\nimport { useCreateComponentMutation } from '../../hooks/use-create-component';\nimport { type ComponentFormValues } from '../../types';\nimport { useForm } from './hooks/use-form';\nimport { createBaseComponentSchema, createSubmitComponentSchema } from './utils/component-form-schema';\nimport { replaceElementWithComponent } from './utils/replace-element-with-component';\n\ntype SaveAsComponentEventData = {\n\telement: V1Element;\n\tanchorPosition: { top: number; left: number };\n};\n\ntype ResultNotification = {\n\tshow: boolean;\n\tmessage: string;\n\ttype: 'success' | 'error';\n};\n\nexport function CreateComponentForm() {\n\tconst [ element, setElement ] = useState< {\n\t\telement: V1Element;\n\t\telementLabel: string;\n\t} | null >( null );\n\n\tconst [ anchorPosition, setAnchorPosition ] = useState< { top: number; left: number } >();\n\n\tconst [ resultNotification, setResultNotification ] = useState< ResultNotification | null >( null );\n\n\tconst { mutate: createComponent, isPending } = useCreateComponentMutation();\n\n\tuseEffect( () => {\n\t\tconst OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';\n\n\t\tconst openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {\n\t\t\tsetElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );\n\t\t\tsetAnchorPosition( event.detail.anchorPosition );\n\t\t};\n\n\t\twindow.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );\n\t\t};\n\t}, [] );\n\n\tconst handleSave = async ( values: ComponentFormValues ) => {\n\t\tif ( ! element ) {\n\t\t\tthrow new Error( `Can't save element as component: element not found` );\n\t\t}\n\n\t\tcreateComponent(\n\t\t\t{\n\t\t\t\tname: values.componentName,\n\t\t\t\tcontent: [ element.element.model.toJSON( { remove: [ 'default' ] } ) ],\n\t\t\t},\n\t\t\t{\n\t\t\t\tonSuccess: ( result: CreateComponentResponse ) => {\n\t\t\t\t\tif ( ! element ) {\n\t\t\t\t\t\tthrow new Error( `Can't replace element with component: element not found` );\n\t\t\t\t\t}\n\n\t\t\t\t\treplaceElementWithComponent( element.element, result.component_id );\n\n\t\t\t\t\tsetResultNotification( {\n\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\t// Translators: %1$s: Component name, %2$s: Component ID\n\t\t\t\t\t\tmessage: __( 'Component saved successfully as: %1$s (ID: %2$s)', 'elementor' )\n\t\t\t\t\t\t\t.replace( '%1$s', values.componentName )\n\t\t\t\t\t\t\t.replace( '%2$s', result.component_id.toString() ),\n\t\t\t\t\t\ttype: 'success',\n\t\t\t\t\t} );\n\n\t\t\t\t\tresetAndClosePopup();\n\t\t\t\t},\n\t\t\t\tonError: () => {\n\t\t\t\t\tconst errorMessage = __( 'Failed to save component. Please try again.', 'elementor' );\n\t\t\t\t\tsetResultNotification( {\n\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\tmessage: errorMessage,\n\t\t\t\t\t\ttype: 'error',\n\t\t\t\t\t} );\n\t\t\t\t},\n\t\t\t}\n\t\t);\n\t};\n\n\tconst resetAndClosePopup = () => {\n\t\tsetElement( null );\n\t\tsetAnchorPosition( undefined );\n\t};\n\n\treturn (\n\t\t<ThemeProvider>\n\t\t\t<Popover\n\t\t\t\topen={ element !== null }\n\t\t\t\tonClose={ resetAndClosePopup }\n\t\t\t\tanchorReference=\"anchorPosition\"\n\t\t\t\tanchorPosition={ anchorPosition }\n\t\t\t>\n\t\t\t\t{ element !== null && (\n\t\t\t\t\t<Form\n\t\t\t\t\t\tinitialValues={ { componentName: element.elementLabel } }\n\t\t\t\t\t\thandleSave={ handleSave }\n\t\t\t\t\t\tisSubmitting={ isPending }\n\t\t\t\t\t\tclosePopup={ resetAndClosePopup }\n\t\t\t\t\t/>\n\t\t\t\t) }\n\t\t\t</Popover>\n\t\t\t<Snackbar open={ resultNotification?.show } onClose={ () => setResultNotification( null ) }>\n\t\t\t\t<Alert\n\t\t\t\t\tonClose={ () => setResultNotification( null ) }\n\t\t\t\t\tseverity={ resultNotification?.type }\n\t\t\t\t\tsx={ { width: '100%' } }\n\t\t\t\t>\n\t\t\t\t\t{ resultNotification?.message }\n\t\t\t\t</Alert>\n\t\t\t</Snackbar>\n\t\t</ThemeProvider>\n\t);\n}\n\nconst FONT_SIZE = 'tiny';\n\nconst Form = ( {\n\tinitialValues,\n\thandleSave,\n\tisSubmitting,\n\tclosePopup,\n}: {\n\tinitialValues: ComponentFormValues;\n\thandleSave: ( values: ComponentFormValues ) => void;\n\tisSubmitting: boolean;\n\tclosePopup: () => void;\n} ) => {\n\tconst { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );\n\n\tconst { data: components } = useComponents();\n\n\tconst existingComponentNames = useMemo( () => {\n\t\treturn components?.map( ( component ) => component.name ) ?? [];\n\t}, [ components ] );\n\n\tconst changeValidationSchema = useMemo(\n\t\t() => createBaseComponentSchema( existingComponentNames ),\n\t\t[ existingComponentNames ]\n\t);\n\tconst submitValidationSchema = useMemo(\n\t\t() => createSubmitComponentSchema( existingComponentNames ),\n\t\t[ existingComponentNames ]\n\t);\n\n\tconst handleSubmit = () => {\n\t\tconst { success, parsedValues } = validateForm( submitValidationSchema );\n\n\t\tif ( success ) {\n\t\t\thandleSave( parsedValues );\n\t\t}\n\t};\n\n\treturn (\n\t\t<Stack alignItems=\"start\" width=\"268px\">\n\t\t\t<Stack\n\t\t\t\tdirection=\"row\"\n\t\t\t\talignItems=\"center\"\n\t\t\t\tpy={ 1 }\n\t\t\t\tpx={ 1.5 }\n\t\t\t\tsx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }\n\t\t\t>\n\t\t\t\t<StarIcon fontSize={ FONT_SIZE } />\n\t\t\t\t<Typography variant=\"caption\" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>\n\t\t\t\t\t{ __( 'Save as a component', 'elementor' ) }\n\t\t\t\t</Typography>\n\t\t\t</Stack>\n\t\t\t<Grid container gap={ 0.75 } alignItems=\"start\" p={ 1.5 }>\n\t\t\t\t<Grid item xs={ 12 }>\n\t\t\t\t\t<FormLabel htmlFor={ 'component-name' } size=\"tiny\">\n\t\t\t\t\t\t{ __( 'Name', 'elementor' ) }\n\t\t\t\t\t</FormLabel>\n\t\t\t\t</Grid>\n\t\t\t\t<Grid item xs={ 12 }>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tid={ 'component-name' }\n\t\t\t\t\t\tsize={ FONT_SIZE }\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tvalue={ values.componentName }\n\t\t\t\t\t\tonChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>\n\t\t\t\t\t\t\thandleChange( e, 'componentName', changeValidationSchema )\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }\n\t\t\t\t\t\terror={ Boolean( errors.componentName ) }\n\t\t\t\t\t\thelperText={ errors.componentName }\n\t\t\t\t\t/>\n\t\t\t\t</Grid>\n\t\t\t</Grid>\n\t\t\t<Stack direction=\"row\" justifyContent=\"flex-end\" alignSelf=\"end\" py={ 1 } px={ 1.5 }>\n\t\t\t\t<Button onClick={ closePopup } disabled={ isSubmitting } color=\"secondary\" variant=\"text\" size=\"small\">\n\t\t\t\t\t{ __( 'Cancel', 'elementor' ) }\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tonClick={ handleSubmit }\n\t\t\t\t\tdisabled={ isSubmitting || ! isValid }\n\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t>\n\t\t\t\t\t{ isSubmitting ? __( 'Creating…', 'elementor' ) : __( 'Create', 'elementor' ) }\n\t\t\t\t</Button>\n\t\t\t</Stack>\n\t\t</Stack>\n\t);\n};\n","import { useQuery } from '@elementor/query';\n\nimport { apiClient } from '../api';\n\nexport const COMPONENTS_QUERY_KEY = 'components';\n\nexport const useComponents = () => {\n\treturn useQuery( {\n\t\tqueryKey: [ COMPONENTS_QUERY_KEY ],\n\t\tqueryFn: apiClient.get,\n\t\tstaleTime: Infinity,\n\t} );\n};\n","import { type V1ElementModelProps } from '@elementor/editor-elements';\nimport { type HttpResponse, httpService } from '@elementor/http-client';\n\nconst BASE_URL = 'elementor/v1/components';\n\ntype CreateComponentPayload = {\n\tname: string;\n\tcontent: V1ElementModelProps[];\n};\n\ntype GetComponentResponse = Array< {\n\tcomponent_id: number;\n\tname: string;\n} >;\n\nexport type CreateComponentResponse = {\n\tcomponent_id: number;\n};\n\nexport const apiClient = {\n\tget: () =>\n\t\thttpService()\n\t\t\t.get< HttpResponse< GetComponentResponse > >( `${ BASE_URL }` )\n\t\t\t.then( ( res ) => res.data.data ),\n\tcreate: ( payload: CreateComponentPayload ) =>\n\t\thttpService()\n\t\t\t.post< HttpResponse< CreateComponentResponse > >( `${ BASE_URL }`, payload )\n\t\t\t.then( ( res ) => res.data.data ),\n};\n","import { useMutation, useQueryClient } from '@elementor/query';\n\nimport { apiClient } from '../api';\nimport { COMPONENTS_QUERY_KEY } from './use-components';\n\nexport const useCreateComponentMutation = () => {\n\tconst queryClient = useQueryClient();\n\n\treturn useMutation( {\n\t\tmutationFn: apiClient.create,\n\t\tonSuccess: () => queryClient.invalidateQueries( { queryKey: [ COMPONENTS_QUERY_KEY ] } ),\n\t} );\n};\n","import { useMemo, useState } from 'react';\nimport { type z } from '@elementor/schema';\n\nexport const useForm = < TValues extends Record< string, unknown > >( initialValues: TValues ) => {\n\tconst [ values, setValues ] = useState< TValues >( initialValues );\n\tconst [ errors, setErrors ] = useState< Partial< Record< keyof TValues, string > > >( {} );\n\n\tconst isValid = useMemo( () => {\n\t\treturn ! Object.values( errors ).some( ( error ) => error );\n\t}, [ errors ] );\n\n\tconst handleChange = (\n\t\te: React.ChangeEvent< HTMLInputElement >,\n\t\tfield: keyof TValues,\n\t\tvalidationSchema: z.ZodType< TValues >\n\t) => {\n\t\tconst updated = { ...values, [ field ]: e.target.value };\n\t\tsetValues( updated );\n\n\t\tconst { success, errors: validationErrors } = validateForm( updated, validationSchema );\n\n\t\tif ( ! success ) {\n\t\t\tsetErrors( validationErrors );\n\t\t} else {\n\t\t\tsetErrors( {} );\n\t\t}\n\t};\n\n\tconst validate = (\n\t\tvalidationSchema: z.ZodType< TValues >\n\t): { success: true; parsedValues: TValues } | { success: false; parsedValues?: never } => {\n\t\tconst { success, errors: validationErrors, parsedValues } = validateForm( values, validationSchema );\n\n\t\tif ( ! success ) {\n\t\t\tsetErrors( validationErrors );\n\t\t\treturn { success };\n\t\t}\n\t\tsetErrors( {} );\n\t\treturn { success, parsedValues };\n\t};\n\n\treturn {\n\t\tvalues,\n\t\terrors,\n\t\tisValid,\n\t\thandleChange,\n\t\tvalidateForm: validate,\n\t};\n};\n\nconst validateForm = < TValues extends Record< string, unknown > >(\n\tvalues: TValues,\n\tschema: z.ZodType< TValues >\n):\n\t| { success: false; parsedValues?: never; errors: Partial< Record< keyof TValues, string > > }\n\t| { success: true; parsedValues: TValues; errors?: never } => {\n\tconst result = schema.safeParse( values );\n\n\tif ( result.success ) {\n\t\treturn { success: true, parsedValues: result.data };\n\t}\n\n\tconst errors = {} as Partial< Record< keyof TValues, string > >;\n\n\t( Object.entries( result.error.formErrors.fieldErrors ) as Array< [ keyof TValues, string[] ] > ).forEach(\n\t\t( [ field, error ] ) => {\n\t\t\terrors[ field ] = error[ 0 ];\n\t\t}\n\t);\n\n\treturn { success: false, errors };\n};\n","import { z } from '@elementor/schema';\nimport { __ } from '@wordpress/i18n';\n\nconst MIN_NAME_LENGTH = 2;\nconst MAX_NAME_LENGTH = 50;\n\nexport const createBaseComponentSchema = ( existingNames: string[] ) => {\n\treturn z.object( {\n\t\tcomponentName: z\n\t\t\t.string()\n\t\t\t.trim()\n\t\t\t.max(\n\t\t\t\tMAX_NAME_LENGTH,\n\t\t\t\t__( 'Component name is too long. Please keep it under 50 characters.', 'elementor' )\n\t\t\t)\n\t\t\t.refine( ( value ) => ! existingNames.includes( value ), {\n\t\t\t\tmessage: __( 'Component name already exists', 'elementor' ),\n\t\t\t} ),\n\t} );\n};\n\nexport const createSubmitComponentSchema = ( existingNames: string[] ) => {\n\tconst baseSchema = createBaseComponentSchema( existingNames );\n\n\treturn baseSchema.extend( {\n\t\tcomponentName: baseSchema.shape.componentName\n\t\t\t.refine( ( value ) => value.length > 0, {\n\t\t\t\tmessage: __( 'Component name is required.', 'elementor' ),\n\t\t\t} )\n\t\t\t.refine( ( value ) => value.length >= MIN_NAME_LENGTH, {\n\t\t\t\tmessage: __( 'Component name is too short. Please enter at least 2 characters.', 'elementor' ),\n\t\t\t} ),\n\t} );\n};\n","import { replaceElement, type V1Element } from '@elementor/editor-elements';\nimport { numberPropTypeUtil } from '@elementor/editor-props';\n\nexport const replaceElementWithComponent = async ( element: V1Element, componentId: number ) => {\n\treplaceElement( {\n\t\tcurrentElement: element,\n\t\tnewElement: {\n\t\t\telType: 'widget',\n\t\t\twidgetType: 'e-component',\n\t\t\tsettings: {\n\t\t\t\tcomponent_id: numberPropTypeUtil.create( componentId ),\n\t\t\t},\n\t\t},\n\t\twithHistory: false,\n\t} );\n};\n"],"mappings":"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","names":["__","React","useMemo","useState","__","useState","__","validateForm","useMemo","__"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-components",
|
|
3
3
|
"description": "Elementor editor components",
|
|
4
|
-
"version": "3.32.0-
|
|
4
|
+
"version": "3.32.0-89",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -40,8 +40,16 @@
|
|
|
40
40
|
"dev": "tsup --config=../../tsup.dev.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@elementor/editor": "3.32.0-89",
|
|
44
|
+
"@elementor/editor-elements": "3.32.0-89",
|
|
45
|
+
"@elementor/editor-elements-panel": "3.32.0-89",
|
|
46
|
+
"@elementor/editor-props": "3.32.0-89",
|
|
47
|
+
"@elementor/editor-ui": "3.32.0-89",
|
|
48
|
+
"@elementor/http-client": "3.32.0-89",
|
|
49
|
+
"@elementor/icons": "1.46.0",
|
|
50
|
+
"@elementor/query": "3.32.0-89",
|
|
51
|
+
"@elementor/schema": "3.32.0-89",
|
|
43
52
|
"@elementor/ui": "1.36.8",
|
|
44
|
-
"@elementor/editor-elements-panel": "3.32.0-87",
|
|
45
53
|
"@wordpress/i18n": "^5.13.0"
|
|
46
54
|
},
|
|
47
55
|
"peerDependencies": {
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type V1ElementModelProps } from '@elementor/editor-elements';
|
|
2
|
+
import { type HttpResponse, httpService } from '@elementor/http-client';
|
|
3
|
+
|
|
4
|
+
const BASE_URL = 'elementor/v1/components';
|
|
5
|
+
|
|
6
|
+
type CreateComponentPayload = {
|
|
7
|
+
name: string;
|
|
8
|
+
content: V1ElementModelProps[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type GetComponentResponse = Array< {
|
|
12
|
+
component_id: number;
|
|
13
|
+
name: string;
|
|
14
|
+
} >;
|
|
15
|
+
|
|
16
|
+
export type CreateComponentResponse = {
|
|
17
|
+
component_id: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const apiClient = {
|
|
21
|
+
get: () =>
|
|
22
|
+
httpService()
|
|
23
|
+
.get< HttpResponse< GetComponentResponse > >( `${ BASE_URL }` )
|
|
24
|
+
.then( ( res ) => res.data.data ),
|
|
25
|
+
create: ( payload: CreateComponentPayload ) =>
|
|
26
|
+
httpService()
|
|
27
|
+
.post< HttpResponse< CreateComponentResponse > >( `${ BASE_URL }`, payload )
|
|
28
|
+
.then( ( res ) => res.data.data ),
|
|
29
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { getElementLabel, type V1Element } from '@elementor/editor-elements';
|
|
4
|
+
import { ThemeProvider } from '@elementor/editor-ui';
|
|
5
|
+
import { StarIcon } from '@elementor/icons';
|
|
6
|
+
import { Alert, Button, FormLabel, Grid, Popover, Snackbar, Stack, TextField, Typography } from '@elementor/ui';
|
|
7
|
+
import { __ } from '@wordpress/i18n';
|
|
8
|
+
|
|
9
|
+
import { type CreateComponentResponse } from '../../api';
|
|
10
|
+
import { useComponents } from '../../hooks/use-components';
|
|
11
|
+
import { useCreateComponentMutation } from '../../hooks/use-create-component';
|
|
12
|
+
import { type ComponentFormValues } from '../../types';
|
|
13
|
+
import { useForm } from './hooks/use-form';
|
|
14
|
+
import { createBaseComponentSchema, createSubmitComponentSchema } from './utils/component-form-schema';
|
|
15
|
+
import { replaceElementWithComponent } from './utils/replace-element-with-component';
|
|
16
|
+
|
|
17
|
+
type SaveAsComponentEventData = {
|
|
18
|
+
element: V1Element;
|
|
19
|
+
anchorPosition: { top: number; left: number };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ResultNotification = {
|
|
23
|
+
show: boolean;
|
|
24
|
+
message: string;
|
|
25
|
+
type: 'success' | 'error';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function CreateComponentForm() {
|
|
29
|
+
const [ element, setElement ] = useState< {
|
|
30
|
+
element: V1Element;
|
|
31
|
+
elementLabel: string;
|
|
32
|
+
} | null >( null );
|
|
33
|
+
|
|
34
|
+
const [ anchorPosition, setAnchorPosition ] = useState< { top: number; left: number } >();
|
|
35
|
+
|
|
36
|
+
const [ resultNotification, setResultNotification ] = useState< ResultNotification | null >( null );
|
|
37
|
+
|
|
38
|
+
const { mutate: createComponent, isPending } = useCreateComponentMutation();
|
|
39
|
+
|
|
40
|
+
useEffect( () => {
|
|
41
|
+
const OPEN_SAVE_AS_COMPONENT_FORM_EVENT = 'elementor/editor/open-save-as-component-form';
|
|
42
|
+
|
|
43
|
+
const openPopup = ( event: CustomEvent< SaveAsComponentEventData > ) => {
|
|
44
|
+
setElement( { element: event.detail.element, elementLabel: getElementLabel( event.detail.element.id ) } );
|
|
45
|
+
setAnchorPosition( event.detail.anchorPosition );
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
window.addEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
window.removeEventListener( OPEN_SAVE_AS_COMPONENT_FORM_EVENT, openPopup as EventListener );
|
|
52
|
+
};
|
|
53
|
+
}, [] );
|
|
54
|
+
|
|
55
|
+
const handleSave = async ( values: ComponentFormValues ) => {
|
|
56
|
+
if ( ! element ) {
|
|
57
|
+
throw new Error( `Can't save element as component: element not found` );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
createComponent(
|
|
61
|
+
{
|
|
62
|
+
name: values.componentName,
|
|
63
|
+
content: [ element.element.model.toJSON( { remove: [ 'default' ] } ) ],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
onSuccess: ( result: CreateComponentResponse ) => {
|
|
67
|
+
if ( ! element ) {
|
|
68
|
+
throw new Error( `Can't replace element with component: element not found` );
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
replaceElementWithComponent( element.element, result.component_id );
|
|
72
|
+
|
|
73
|
+
setResultNotification( {
|
|
74
|
+
show: true,
|
|
75
|
+
// Translators: %1$s: Component name, %2$s: Component ID
|
|
76
|
+
message: __( 'Component saved successfully as: %1$s (ID: %2$s)', 'elementor' )
|
|
77
|
+
.replace( '%1$s', values.componentName )
|
|
78
|
+
.replace( '%2$s', result.component_id.toString() ),
|
|
79
|
+
type: 'success',
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
resetAndClosePopup();
|
|
83
|
+
},
|
|
84
|
+
onError: () => {
|
|
85
|
+
const errorMessage = __( 'Failed to save component. Please try again.', 'elementor' );
|
|
86
|
+
setResultNotification( {
|
|
87
|
+
show: true,
|
|
88
|
+
message: errorMessage,
|
|
89
|
+
type: 'error',
|
|
90
|
+
} );
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const resetAndClosePopup = () => {
|
|
97
|
+
setElement( null );
|
|
98
|
+
setAnchorPosition( undefined );
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<ThemeProvider>
|
|
103
|
+
<Popover
|
|
104
|
+
open={ element !== null }
|
|
105
|
+
onClose={ resetAndClosePopup }
|
|
106
|
+
anchorReference="anchorPosition"
|
|
107
|
+
anchorPosition={ anchorPosition }
|
|
108
|
+
>
|
|
109
|
+
{ element !== null && (
|
|
110
|
+
<Form
|
|
111
|
+
initialValues={ { componentName: element.elementLabel } }
|
|
112
|
+
handleSave={ handleSave }
|
|
113
|
+
isSubmitting={ isPending }
|
|
114
|
+
closePopup={ resetAndClosePopup }
|
|
115
|
+
/>
|
|
116
|
+
) }
|
|
117
|
+
</Popover>
|
|
118
|
+
<Snackbar open={ resultNotification?.show } onClose={ () => setResultNotification( null ) }>
|
|
119
|
+
<Alert
|
|
120
|
+
onClose={ () => setResultNotification( null ) }
|
|
121
|
+
severity={ resultNotification?.type }
|
|
122
|
+
sx={ { width: '100%' } }
|
|
123
|
+
>
|
|
124
|
+
{ resultNotification?.message }
|
|
125
|
+
</Alert>
|
|
126
|
+
</Snackbar>
|
|
127
|
+
</ThemeProvider>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const FONT_SIZE = 'tiny';
|
|
132
|
+
|
|
133
|
+
const Form = ( {
|
|
134
|
+
initialValues,
|
|
135
|
+
handleSave,
|
|
136
|
+
isSubmitting,
|
|
137
|
+
closePopup,
|
|
138
|
+
}: {
|
|
139
|
+
initialValues: ComponentFormValues;
|
|
140
|
+
handleSave: ( values: ComponentFormValues ) => void;
|
|
141
|
+
isSubmitting: boolean;
|
|
142
|
+
closePopup: () => void;
|
|
143
|
+
} ) => {
|
|
144
|
+
const { values, errors, isValid, handleChange, validateForm } = useForm< ComponentFormValues >( initialValues );
|
|
145
|
+
|
|
146
|
+
const { data: components } = useComponents();
|
|
147
|
+
|
|
148
|
+
const existingComponentNames = useMemo( () => {
|
|
149
|
+
return components?.map( ( component ) => component.name ) ?? [];
|
|
150
|
+
}, [ components ] );
|
|
151
|
+
|
|
152
|
+
const changeValidationSchema = useMemo(
|
|
153
|
+
() => createBaseComponentSchema( existingComponentNames ),
|
|
154
|
+
[ existingComponentNames ]
|
|
155
|
+
);
|
|
156
|
+
const submitValidationSchema = useMemo(
|
|
157
|
+
() => createSubmitComponentSchema( existingComponentNames ),
|
|
158
|
+
[ existingComponentNames ]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const handleSubmit = () => {
|
|
162
|
+
const { success, parsedValues } = validateForm( submitValidationSchema );
|
|
163
|
+
|
|
164
|
+
if ( success ) {
|
|
165
|
+
handleSave( parsedValues );
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Stack alignItems="start" width="268px">
|
|
171
|
+
<Stack
|
|
172
|
+
direction="row"
|
|
173
|
+
alignItems="center"
|
|
174
|
+
py={ 1 }
|
|
175
|
+
px={ 1.5 }
|
|
176
|
+
sx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%' } }
|
|
177
|
+
>
|
|
178
|
+
<StarIcon fontSize={ FONT_SIZE } />
|
|
179
|
+
<Typography variant="caption" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>
|
|
180
|
+
{ __( 'Save as a component', 'elementor' ) }
|
|
181
|
+
</Typography>
|
|
182
|
+
</Stack>
|
|
183
|
+
<Grid container gap={ 0.75 } alignItems="start" p={ 1.5 }>
|
|
184
|
+
<Grid item xs={ 12 }>
|
|
185
|
+
<FormLabel htmlFor={ 'component-name' } size="tiny">
|
|
186
|
+
{ __( 'Name', 'elementor' ) }
|
|
187
|
+
</FormLabel>
|
|
188
|
+
</Grid>
|
|
189
|
+
<Grid item xs={ 12 }>
|
|
190
|
+
<TextField
|
|
191
|
+
id={ 'component-name' }
|
|
192
|
+
size={ FONT_SIZE }
|
|
193
|
+
fullWidth
|
|
194
|
+
value={ values.componentName }
|
|
195
|
+
onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>
|
|
196
|
+
handleChange( e, 'componentName', changeValidationSchema )
|
|
197
|
+
}
|
|
198
|
+
inputProps={ { style: { color: 'text.primary', fontWeight: '600' } } }
|
|
199
|
+
error={ Boolean( errors.componentName ) }
|
|
200
|
+
helperText={ errors.componentName }
|
|
201
|
+
/>
|
|
202
|
+
</Grid>
|
|
203
|
+
</Grid>
|
|
204
|
+
<Stack direction="row" justifyContent="flex-end" alignSelf="end" py={ 1 } px={ 1.5 }>
|
|
205
|
+
<Button onClick={ closePopup } disabled={ isSubmitting } color="secondary" variant="text" size="small">
|
|
206
|
+
{ __( 'Cancel', 'elementor' ) }
|
|
207
|
+
</Button>
|
|
208
|
+
<Button
|
|
209
|
+
onClick={ handleSubmit }
|
|
210
|
+
disabled={ isSubmitting || ! isValid }
|
|
211
|
+
variant="contained"
|
|
212
|
+
color="primary"
|
|
213
|
+
size="small"
|
|
214
|
+
>
|
|
215
|
+
{ isSubmitting ? __( 'Creating…', 'elementor' ) : __( 'Create', 'elementor' ) }
|
|
216
|
+
</Button>
|
|
217
|
+
</Stack>
|
|
218
|
+
</Stack>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { type z } from '@elementor/schema';
|
|
3
|
+
|
|
4
|
+
export const useForm = < TValues extends Record< string, unknown > >( initialValues: TValues ) => {
|
|
5
|
+
const [ values, setValues ] = useState< TValues >( initialValues );
|
|
6
|
+
const [ errors, setErrors ] = useState< Partial< Record< keyof TValues, string > > >( {} );
|
|
7
|
+
|
|
8
|
+
const isValid = useMemo( () => {
|
|
9
|
+
return ! Object.values( errors ).some( ( error ) => error );
|
|
10
|
+
}, [ errors ] );
|
|
11
|
+
|
|
12
|
+
const handleChange = (
|
|
13
|
+
e: React.ChangeEvent< HTMLInputElement >,
|
|
14
|
+
field: keyof TValues,
|
|
15
|
+
validationSchema: z.ZodType< TValues >
|
|
16
|
+
) => {
|
|
17
|
+
const updated = { ...values, [ field ]: e.target.value };
|
|
18
|
+
setValues( updated );
|
|
19
|
+
|
|
20
|
+
const { success, errors: validationErrors } = validateForm( updated, validationSchema );
|
|
21
|
+
|
|
22
|
+
if ( ! success ) {
|
|
23
|
+
setErrors( validationErrors );
|
|
24
|
+
} else {
|
|
25
|
+
setErrors( {} );
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const validate = (
|
|
30
|
+
validationSchema: z.ZodType< TValues >
|
|
31
|
+
): { success: true; parsedValues: TValues } | { success: false; parsedValues?: never } => {
|
|
32
|
+
const { success, errors: validationErrors, parsedValues } = validateForm( values, validationSchema );
|
|
33
|
+
|
|
34
|
+
if ( ! success ) {
|
|
35
|
+
setErrors( validationErrors );
|
|
36
|
+
return { success };
|
|
37
|
+
}
|
|
38
|
+
setErrors( {} );
|
|
39
|
+
return { success, parsedValues };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
values,
|
|
44
|
+
errors,
|
|
45
|
+
isValid,
|
|
46
|
+
handleChange,
|
|
47
|
+
validateForm: validate,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const validateForm = < TValues extends Record< string, unknown > >(
|
|
52
|
+
values: TValues,
|
|
53
|
+
schema: z.ZodType< TValues >
|
|
54
|
+
):
|
|
55
|
+
| { success: false; parsedValues?: never; errors: Partial< Record< keyof TValues, string > > }
|
|
56
|
+
| { success: true; parsedValues: TValues; errors?: never } => {
|
|
57
|
+
const result = schema.safeParse( values );
|
|
58
|
+
|
|
59
|
+
if ( result.success ) {
|
|
60
|
+
return { success: true, parsedValues: result.data };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const errors = {} as Partial< Record< keyof TValues, string > >;
|
|
64
|
+
|
|
65
|
+
( Object.entries( result.error.formErrors.fieldErrors ) as Array< [ keyof TValues, string[] ] > ).forEach(
|
|
66
|
+
( [ field, error ] ) => {
|
|
67
|
+
errors[ field ] = error[ 0 ];
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return { success: false, errors };
|
|
72
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from '@elementor/schema';
|
|
2
|
+
import { __ } from '@wordpress/i18n';
|
|
3
|
+
|
|
4
|
+
const MIN_NAME_LENGTH = 2;
|
|
5
|
+
const MAX_NAME_LENGTH = 50;
|
|
6
|
+
|
|
7
|
+
export const createBaseComponentSchema = ( existingNames: string[] ) => {
|
|
8
|
+
return z.object( {
|
|
9
|
+
componentName: z
|
|
10
|
+
.string()
|
|
11
|
+
.trim()
|
|
12
|
+
.max(
|
|
13
|
+
MAX_NAME_LENGTH,
|
|
14
|
+
__( 'Component name is too long. Please keep it under 50 characters.', 'elementor' )
|
|
15
|
+
)
|
|
16
|
+
.refine( ( value ) => ! existingNames.includes( value ), {
|
|
17
|
+
message: __( 'Component name already exists', 'elementor' ),
|
|
18
|
+
} ),
|
|
19
|
+
} );
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const createSubmitComponentSchema = ( existingNames: string[] ) => {
|
|
23
|
+
const baseSchema = createBaseComponentSchema( existingNames );
|
|
24
|
+
|
|
25
|
+
return baseSchema.extend( {
|
|
26
|
+
componentName: baseSchema.shape.componentName
|
|
27
|
+
.refine( ( value ) => value.length > 0, {
|
|
28
|
+
message: __( 'Component name is required.', 'elementor' ),
|
|
29
|
+
} )
|
|
30
|
+
.refine( ( value ) => value.length >= MIN_NAME_LENGTH, {
|
|
31
|
+
message: __( 'Component name is too short. Please enter at least 2 characters.', 'elementor' ),
|
|
32
|
+
} ),
|
|
33
|
+
} );
|
|
34
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { replaceElement, type V1Element } from '@elementor/editor-elements';
|
|
2
|
+
import { numberPropTypeUtil } from '@elementor/editor-props';
|
|
3
|
+
|
|
4
|
+
export const replaceElementWithComponent = async ( element: V1Element, componentId: number ) => {
|
|
5
|
+
replaceElement( {
|
|
6
|
+
currentElement: element,
|
|
7
|
+
newElement: {
|
|
8
|
+
elType: 'widget',
|
|
9
|
+
widgetType: 'e-component',
|
|
10
|
+
settings: {
|
|
11
|
+
component_id: numberPropTypeUtil.create( componentId ),
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
withHistory: false,
|
|
15
|
+
} );
|
|
16
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useQuery } from '@elementor/query';
|
|
2
|
+
|
|
3
|
+
import { apiClient } from '../api';
|
|
4
|
+
|
|
5
|
+
export const COMPONENTS_QUERY_KEY = 'components';
|
|
6
|
+
|
|
7
|
+
export const useComponents = () => {
|
|
8
|
+
return useQuery( {
|
|
9
|
+
queryKey: [ COMPONENTS_QUERY_KEY ],
|
|
10
|
+
queryFn: apiClient.get,
|
|
11
|
+
staleTime: Infinity,
|
|
12
|
+
} );
|
|
13
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@elementor/query';
|
|
2
|
+
|
|
3
|
+
import { apiClient } from '../api';
|
|
4
|
+
import { COMPONENTS_QUERY_KEY } from './use-components';
|
|
5
|
+
|
|
6
|
+
export const useCreateComponentMutation = () => {
|
|
7
|
+
const queryClient = useQueryClient();
|
|
8
|
+
|
|
9
|
+
return useMutation( {
|
|
10
|
+
mutationFn: apiClient.create,
|
|
11
|
+
onSuccess: () => queryClient.invalidateQueries( { queryKey: [ COMPONENTS_QUERY_KEY ] } ),
|
|
12
|
+
} );
|
|
13
|
+
};
|
package/src/init.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { injectIntoTop } from '@elementor/editor';
|
|
1
2
|
import { injectTab } from '@elementor/editor-elements-panel';
|
|
2
3
|
import { __ } from '@wordpress/i18n';
|
|
3
4
|
|
|
4
5
|
import { ComponentsTab } from './components/components-tab';
|
|
6
|
+
import { CreateComponentForm } from './components/create-component-form/create-component-form';
|
|
5
7
|
|
|
6
8
|
export function init() {
|
|
7
9
|
injectTab( {
|
|
@@ -9,4 +11,9 @@ export function init() {
|
|
|
9
11
|
label: __( 'Components', 'elementor' ),
|
|
10
12
|
component: ComponentsTab,
|
|
11
13
|
} );
|
|
14
|
+
|
|
15
|
+
injectIntoTop( {
|
|
16
|
+
id: 'create-component-popup',
|
|
17
|
+
component: CreateComponentForm,
|
|
18
|
+
} );
|
|
12
19
|
}
|
package/src/types.ts
ADDED