@plaudit/gutenberg-api-extensions 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/blocks/common-native-property-implementations.js +97 -19
- package/build/blocks/common-native-property-implementations.js.map +1 -1
- package/build/blocks/layered-styles.js +9 -7
- package/build/blocks/layered-styles.js.map +1 -1
- package/build/blocks/simple-block.js +10 -6
- package/build/blocks/simple-block.js.map +1 -1
- package/build/blocks/simple-native-property.js +43 -37
- package/build/blocks/simple-native-property.js.map +1 -1
- package/build/controls/AsynchronousFormTokenField.js +23 -12
- package/build/controls/AsynchronousFormTokenField.js.map +1 -1
- package/build/controls/ExtendedPostPicker.js +5 -10
- package/build/controls/ExtendedPostPicker.js.map +1 -1
- package/build/controls/InspectorPanel.js +3 -2
- package/build/controls/InspectorPanel.js.map +1 -1
- package/build/controls/LazySuggestionsComboboxControl.js +70 -0
- package/build/controls/LazySuggestionsComboboxControl.js.map +1 -0
- package/build/controls/PickOne.js +8 -7
- package/build/controls/PickOne.js.map +1 -1
- package/build/controls/SimpleToggle.js +2 -2
- package/build/controls/SimpleToggle.js.map +1 -1
- package/build/controls/SortableItemsControl.js +43 -0
- package/build/controls/SortableItemsControl.js.map +1 -0
- package/build/controls/index.js +1 -0
- package/build/controls/index.js.map +1 -1
- package/build/controls/shared.js +7 -0
- package/build/controls/shared.js.map +1 -0
- package/build/lib/plaudit-icons/column-1.js +3 -2
- package/build/lib/plaudit-icons/column-1.js.map +1 -1
- package/build/lib/plaudit-icons/column-2.js +3 -2
- package/build/lib/plaudit-icons/column-2.js.map +1 -1
- package/build/lib/plaudit-icons/column-3.js +3 -2
- package/build/lib/plaudit-icons/column-3.js.map +1 -1
- package/build/lib/plaudit-icons/placement-center.js +3 -2
- package/build/lib/plaudit-icons/placement-center.js.map +1 -1
- package/build/lib/plaudit-icons/placement-end.js +3 -2
- package/build/lib/plaudit-icons/placement-end.js.map +1 -1
- package/build/lib/plaudit-icons/placement-start.js +3 -2
- package/build/lib/plaudit-icons/placement-start.js.map +1 -1
- package/build/lib/plaudit-icons/placement-stretch.js +3 -2
- package/build/lib/plaudit-icons/placement-stretch.js.map +1 -1
- package/build/lib/plaudit-icons/plaudit-icon.js +3 -2
- package/build/lib/plaudit-icons/plaudit-icon.js.map +1 -1
- package/build/lib/plaudit-icons/reusable-block-marker.js +3 -2
- package/build/lib/plaudit-icons/reusable-block-marker.js.map +1 -1
- package/package.json +10 -10
- package/src/blocks/common-native-property-implementations.tsx +105 -15
- package/src/blocks/simple-native-property.tsx +29 -25
- package/src/controls/AsynchronousFormTokenField.tsx +13 -6
- package/src/controls/ExtendedPostPicker.tsx +9 -9
- package/src/controls/LazySuggestionsComboboxControl.tsx +84 -0
- package/src/controls/SortableItemsControl.tsx +70 -0
- package/src/controls/index.ts +1 -0
- package/src/controls/shared.ts +7 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {MediaUpload, MediaUploadCheck} from "@wordpress/block-editor";
|
|
1
|
+
import {MediaUpload, MediaUploadCheck, __experimentalLinkControl as LinkControl, type LinkControlProps} from "@wordpress/block-editor";
|
|
2
2
|
import {
|
|
3
3
|
Button,
|
|
4
4
|
Card,
|
|
5
5
|
CardBody,
|
|
6
|
+
CardFooter,
|
|
6
7
|
CardHeader,
|
|
7
8
|
FocalPointPicker,
|
|
8
9
|
RadioControl,
|
|
@@ -12,11 +13,12 @@ import {
|
|
|
12
13
|
TextareaControl,
|
|
13
14
|
TextControl,
|
|
14
15
|
ToggleControl,
|
|
15
|
-
__experimentalToggleGroupControl as ToggleGroupControl,
|
|
16
16
|
__experimentalHeading as Heading,
|
|
17
|
+
__experimentalToggleGroupControl as ToggleGroupControl,
|
|
17
18
|
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
|
|
18
19
|
__experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon
|
|
19
20
|
} from "@wordpress/components";
|
|
21
|
+
import type {ComboboxControlOption} from "@wordpress/components/build-types/combobox-control/types";
|
|
20
22
|
import type {RadioControlProps} from "@wordpress/components/build-types/radio-control/types";
|
|
21
23
|
import type {RangeControlProps} from "@wordpress/components/build-types/range-control/types";
|
|
22
24
|
import type {TextControlProps} from "@wordpress/components/build-types/text-control/types";
|
|
@@ -26,6 +28,8 @@ import type {ToggleGroupControlProps} from "@wordpress/components/build-types/to
|
|
|
26
28
|
import {useSelect} from "@wordpress/data";
|
|
27
29
|
import {__} from "@wordpress/i18n";
|
|
28
30
|
|
|
31
|
+
import {ExtendedPostPicker, type ExtendedPostPickerConstructorProps, LazySuggestionsComboboxControl, type LazySuggestionsComboboxControlProps} from "../controls";
|
|
32
|
+
import {requestPostsFromAPI} from "../controls/shared";
|
|
29
33
|
import type {PickableOptions} from "../controls/types";
|
|
30
34
|
import type {SimpleNativeProperty} from "./simple-native-property";
|
|
31
35
|
|
|
@@ -35,7 +39,7 @@ export type ActualBlockEditProps = { attributes: Record<string, any>, setAttribu
|
|
|
35
39
|
type CommonPropertyConfig<T, V> = {
|
|
36
40
|
name: string,
|
|
37
41
|
label: string,
|
|
38
|
-
default
|
|
42
|
+
default?: T,
|
|
39
43
|
enum?: T[],
|
|
40
44
|
component?: Partial<V>,
|
|
41
45
|
help?: string,
|
|
@@ -96,6 +100,69 @@ export function selectProperty(config: CommonPropertyConfig<string, never> & {
|
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
export function linkProperty(config: CommonPropertyConfig<Record<string, unknown>, LinkControlProps>): SimpleNativeProperty {
|
|
104
|
+
return {
|
|
105
|
+
name: config.name,
|
|
106
|
+
type: 'object',
|
|
107
|
+
default: config.default,
|
|
108
|
+
condition: config.condition,
|
|
109
|
+
alwaysStore: config.alwaysStore,
|
|
110
|
+
renderer(value, onChange) {
|
|
111
|
+
return wrapInCard(() => <LinkControl value={value} onChange={onChange} {...config.component} />, config.label, config.help);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function comboboxControlOptionFromAPIResponse(response: Array<{id: string, title: string}>): ComboboxControlOption[] {
|
|
117
|
+
return response.map(({id, title}) => ({value: id, label: `${title} (#${id})`}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type SharedPostPropertyType = {
|
|
121
|
+
postTypes?: string[];
|
|
122
|
+
};
|
|
123
|
+
type PostPropertyType =
|
|
124
|
+
(CommonPropertyConfig<number, LazySuggestionsComboboxControlProps>&SharedPostPropertyType&{multiple?: false})|
|
|
125
|
+
(CommonPropertyConfig<number[], ExtendedPostPickerConstructorProps>&SharedPostPropertyType&{multiple: true});
|
|
126
|
+
export function postProperty(config: PostPropertyType): SimpleNativeProperty {
|
|
127
|
+
if (config.multiple) {
|
|
128
|
+
return {
|
|
129
|
+
name: config.name,
|
|
130
|
+
type: 'array',
|
|
131
|
+
default: config.default,
|
|
132
|
+
condition: config.condition,
|
|
133
|
+
alwaysStore: config.alwaysStore,
|
|
134
|
+
renderer(value: number[]|undefined, onChange) {
|
|
135
|
+
return <ExtendedPostPicker
|
|
136
|
+
label={config.label}
|
|
137
|
+
help={config.help}
|
|
138
|
+
value={value?.map(v => v.toString())}
|
|
139
|
+
onChange={(v) => onChange(v.map(v => parseInt(v)))}
|
|
140
|
+
postTypes={config.postTypes}
|
|
141
|
+
{...config.component}
|
|
142
|
+
/>;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
} else {
|
|
146
|
+
const makeRequest = (v: string|undefined) => requestPostsFromAPI({search: v, postTypes: config.postTypes?.join(',')});
|
|
147
|
+
return {
|
|
148
|
+
name: config.name,
|
|
149
|
+
type: 'number',
|
|
150
|
+
default: config.default,
|
|
151
|
+
condition: config.condition,
|
|
152
|
+
alwaysStore: config.alwaysStore,
|
|
153
|
+
renderer(value: number|undefined, onChange) {
|
|
154
|
+
return <LazySuggestionsComboboxControl
|
|
155
|
+
value={value?.toString()}
|
|
156
|
+
onChange={v => onChange(v ? parseInt(v) : undefined)}
|
|
157
|
+
getOption={v => makeRequest(v).then(comboboxControlOptionFromAPIResponse).then(res => res[0])}
|
|
158
|
+
getSuggestions={v => makeRequest(v).then(comboboxControlOptionFromAPIResponse)}
|
|
159
|
+
{...config.component}
|
|
160
|
+
/>
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
99
166
|
export function toggleProperty(config: CommonPropertyConfig<boolean, ToggleControlProps>): SimpleNativeProperty {
|
|
100
167
|
return {
|
|
101
168
|
name: config.name,
|
|
@@ -148,7 +215,7 @@ export function toggleGroupProperty(config: ToggleGroupPropertyConfig): SimpleNa
|
|
|
148
215
|
}
|
|
149
216
|
}
|
|
150
217
|
|
|
151
|
-
export function textProperty(config: CommonPropertyConfig<string, TextControlProps
|
|
218
|
+
export function textProperty(config: CommonPropertyConfig<string, TextControlProps>&{placeholder?: string|undefined}): SimpleNativeProperty {
|
|
152
219
|
return {
|
|
153
220
|
name: config.name,
|
|
154
221
|
type: 'string',
|
|
@@ -156,12 +223,12 @@ export function textProperty(config: CommonPropertyConfig<string, TextControlPro
|
|
|
156
223
|
condition: config.condition,
|
|
157
224
|
alwaysStore: config.alwaysStore,
|
|
158
225
|
renderer(value, onChange) {
|
|
159
|
-
return <TextControl value={value} onChange={onChange} label={config.label} help={config.help} {...config.component} />;
|
|
226
|
+
return <TextControl value={value ?? ''} onChange={onChange} label={config.label} help={config.help} placeholder={config.placeholder} {...config.component} />;
|
|
160
227
|
}
|
|
161
228
|
};
|
|
162
229
|
}
|
|
163
230
|
|
|
164
|
-
export function textareaProperty(config: CommonPropertyConfig<string, TextareaControlProps
|
|
231
|
+
export function textareaProperty(config: CommonPropertyConfig<string, TextareaControlProps>&{placeholder?: string|undefined, newline?: undefined|"\n"|"br"|"p"}): SimpleNativeProperty {
|
|
165
232
|
return {
|
|
166
233
|
name: config.name,
|
|
167
234
|
type: 'string',
|
|
@@ -169,7 +236,21 @@ export function textareaProperty(config: CommonPropertyConfig<string, TextareaCo
|
|
|
169
236
|
condition: config.condition,
|
|
170
237
|
alwaysStore: config.alwaysStore,
|
|
171
238
|
renderer(value, onChange) {
|
|
172
|
-
|
|
239
|
+
let v = value ?? '';
|
|
240
|
+
if (config.newline === "br") {
|
|
241
|
+
v = v.replaceAll("<br/>", "\n");
|
|
242
|
+
} else if (config.newline === "p") {
|
|
243
|
+
v = v.replaceAll(/<p>(.*?)<\/p>/gi, "$1\n");
|
|
244
|
+
}
|
|
245
|
+
return <TextareaControl value={v} onChange={v => {
|
|
246
|
+
if (config.newline === "br") {
|
|
247
|
+
onChange(v.replaceAll(/\r?\n/g, "<br/>"));
|
|
248
|
+
} else if (config.newline === "p") {
|
|
249
|
+
onChange(v.split(/\r?\n/g).map(v => "<p>" + v + "</p>").join(""));
|
|
250
|
+
} else {
|
|
251
|
+
onChange(v);
|
|
252
|
+
}
|
|
253
|
+
}} label={config.label} help={config.help} placeholder={config.placeholder} {...config.component} />;
|
|
173
254
|
}
|
|
174
255
|
};
|
|
175
256
|
}
|
|
@@ -248,20 +329,29 @@ export function imageProperty(config: Omit<CommonPropertyConfig<ImagePropertyDat
|
|
|
248
329
|
<Button onClick={() => onChange({...value, media: {id: 0, url: ''}})} isDestructive>{__('Remove image', 'plaudit')}</Button>
|
|
249
330
|
</MediaUploadCheck>
|
|
250
331
|
</>;
|
|
251
|
-
return
|
|
252
|
-
<CardHeader>
|
|
253
|
-
<Heading>{config.label}</Heading>
|
|
254
|
-
</CardHeader>
|
|
255
|
-
<CardBody>
|
|
332
|
+
return wrapInCard(() =>
|
|
256
333
|
<div className="editor-post-featured-image">
|
|
257
334
|
{value.media?.id ? imageUploadedVersion : noImageUploadedVersion}
|
|
258
|
-
</div
|
|
259
|
-
|
|
260
|
-
</Card>;
|
|
335
|
+
</div>,
|
|
336
|
+
config.label, config.help);
|
|
261
337
|
}
|
|
262
338
|
};
|
|
263
339
|
}
|
|
264
340
|
|
|
341
|
+
function wrapInCard(wrapped: () => ReactElement, label: string, help?: string): ReactElement {
|
|
342
|
+
return <Card>
|
|
343
|
+
<CardHeader>
|
|
344
|
+
<Heading>{label}</Heading>
|
|
345
|
+
</CardHeader>
|
|
346
|
+
<CardBody>
|
|
347
|
+
{wrapped()}
|
|
348
|
+
</CardBody>
|
|
349
|
+
{help && <CardFooter>
|
|
350
|
+
{help}
|
|
351
|
+
</CardFooter>}
|
|
352
|
+
</Card>;
|
|
353
|
+
}
|
|
354
|
+
|
|
265
355
|
function asOptions<T extends string|number>(options: PickableOptions<T>, noSelectionValue?: T): Array<{ value: T, label: string }> {
|
|
266
356
|
const res = options.map(opt => ({value: opt[0], label: typeof opt[1] === 'string' ? opt[1] : opt[1].text}));
|
|
267
357
|
if (noSelectionValue !== undefined && !res.some(opt => !opt.value)) {
|
|
@@ -12,9 +12,9 @@ type GenericSimpleNativeProperty<T, V extends 'string'|'number'|'boolean'|'array
|
|
|
12
12
|
name: string,
|
|
13
13
|
type: V,
|
|
14
14
|
enum?: T[],
|
|
15
|
-
default
|
|
15
|
+
default?: T,
|
|
16
16
|
alwaysStore?: boolean,
|
|
17
|
-
renderer(value: T, onChange: (v: T|undefined) => void): React.JSX.Element,
|
|
17
|
+
renderer(value: T|undefined, onChange: (v: T|undefined) => void): React.JSX.Element,
|
|
18
18
|
condition?(blockEditProps: ActualBlockEditProps): boolean
|
|
19
19
|
};
|
|
20
20
|
export type SimpleNativeProperty = GenericSimpleNativeProperty<string, 'string'>&{enum?: string[]}
|
|
@@ -134,39 +134,43 @@ export function installSimpleNativePropertiesSupport() {
|
|
|
134
134
|
const propPath = prop.name.split('.');
|
|
135
135
|
|
|
136
136
|
let existingValue;
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
existingValue
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
137
|
+
if (prop.default !== undefined) {
|
|
138
|
+
if (propPath.length === 1) {
|
|
139
|
+
existingValue = blockEditProps.attributes[prop.name];
|
|
140
|
+
if (existingValue === undefined) {
|
|
141
|
+
existingValue = prop.default;
|
|
142
|
+
blockEditProps.setAttributes({[prop.name]: prop.default});
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
let graphExistingValue = blockEditProps.attributes[propPath[0]];
|
|
146
|
+
if (graphExistingValue === undefined) {
|
|
147
|
+
blockEditProps.attributes[propPath[0]] = graphExistingValue = {};
|
|
148
|
+
}
|
|
149
|
+
for (let i = 1; i < propPath.length; i++) {
|
|
150
|
+
if (graphExistingValue[propPath[i]] === undefined) {
|
|
151
|
+
for (; i < propPath.length - 1; i++) {
|
|
152
|
+
graphExistingValue = graphExistingValue[propPath[i]] = {};
|
|
153
|
+
}
|
|
154
|
+
graphExistingValue[propPath[propPath.length - 1]] = existingValue = prop.default;
|
|
155
|
+
blockEditProps.setAttributes({[propPath[0]]: blockEditProps.attributes[propPath[0]]});
|
|
156
|
+
break;
|
|
157
|
+
} else {
|
|
158
|
+
graphExistingValue = graphExistingValue[propPath[i]];
|
|
152
159
|
}
|
|
153
|
-
graphExistingValue[propPath[propPath.length - 1]] = existingValue = prop.default;
|
|
154
|
-
blockEditProps.setAttributes({[propPath[0]]: blockEditProps.attributes[propPath[0]]});
|
|
155
|
-
break;
|
|
156
|
-
} else {
|
|
157
|
-
graphExistingValue = graphExistingValue[propPath[i]];
|
|
158
160
|
}
|
|
161
|
+
existingValue = graphExistingValue ?? prop.default;
|
|
159
162
|
}
|
|
160
|
-
|
|
163
|
+
} else {
|
|
164
|
+
existingValue = undefined;
|
|
161
165
|
}
|
|
162
166
|
let ele: React.JSX.Element;
|
|
163
167
|
if (prop.type === "array") {// If the value is not an array or is a sparse array, then it will cause unrecoverable errors upon conversion to PHP
|
|
164
|
-
if (!Array.isArray(existingValue) || existingValue.length > existingValue.filter(() => true).length) {
|
|
168
|
+
if (existingValue !== undefined && (!Array.isArray(existingValue) || existingValue.length > existingValue.filter(() => true).length)) {
|
|
165
169
|
throw new Error(`Invalid value passed to an array-type property: ${existingValue}`);
|
|
166
170
|
}
|
|
167
171
|
ele = prop.renderer(existingValue, value => setDottedAttribute(blockEditProps, prop.name, value));
|
|
168
172
|
} else if (prop.type === "object") {
|
|
169
|
-
if (Array.isArray(existingValue) || typeof existingValue !== 'object') {
|
|
173
|
+
if (existingValue !== undefined && (Array.isArray(existingValue) || typeof existingValue !== 'object')) {
|
|
170
174
|
throw new Error(`Invalid value passed to an object-type property: ${existingValue}`);
|
|
171
175
|
}
|
|
172
176
|
ele = prop.renderer(existingValue, value => setDottedAttribute(blockEditProps, prop.name, value));
|
|
@@ -3,6 +3,7 @@ import type {TokenItem} from "@wordpress/components/build-types/form-token-field
|
|
|
3
3
|
import {__} from "@wordpress/i18n";
|
|
4
4
|
import {useEffect, useState} from '@wordpress/element';
|
|
5
5
|
import debounce from "debounce";
|
|
6
|
+
import React from "react";
|
|
6
7
|
|
|
7
8
|
// The strange values correspond to the literals that are expected by TokenItem.status, which allows the assignment code to be cleaner
|
|
8
9
|
export const enum ValidationState {
|
|
@@ -13,7 +14,8 @@ export const enum ValidationState {
|
|
|
13
14
|
|
|
14
15
|
export interface AsynchronousFormTokenFieldProps {
|
|
15
16
|
label: string;
|
|
16
|
-
|
|
17
|
+
help?: string;
|
|
18
|
+
value?: string[];
|
|
17
19
|
onChange: (value: string[]) => void;
|
|
18
20
|
|
|
19
21
|
validationQuery(tokens: string[]): Promise<Array<TokenItem>>;
|
|
@@ -21,6 +23,8 @@ export interface AsynchronousFormTokenFieldProps {
|
|
|
21
23
|
makeTokenFromSuggestion(suggestion: string): TokenItem;
|
|
22
24
|
validValues: Map<string, ValidationState>;
|
|
23
25
|
validator?: (value: string) => boolean;
|
|
26
|
+
|
|
27
|
+
multiple?: boolean;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export function AsynchronousFormTokenField(props: AsynchronousFormTokenFieldProps) {
|
|
@@ -103,19 +107,21 @@ export function AsynchronousFormTokenField(props: AsynchronousFormTokenFieldProp
|
|
|
103
107
|
}));
|
|
104
108
|
|
|
105
109
|
useEffect(() => {
|
|
106
|
-
props.validationQuery(props.value).then(data => {
|
|
110
|
+
props.validationQuery(props.value ?? []).then(data => {
|
|
107
111
|
const tokenLabels = new Map<string, string|undefined>();
|
|
108
112
|
for (const rep of data) {
|
|
109
113
|
tokenLabels.set(rep.value, rep.title);
|
|
110
114
|
props.validValues.set(rep.value, ValidationState.Valid);
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
props.validValues.
|
|
117
|
+
if (props.value) {
|
|
118
|
+
for (const value of props.value) {
|
|
119
|
+
if (!props.validValues.has(value)) {
|
|
120
|
+
props.validValues.set(value, ValidationState.Invalid);
|
|
121
|
+
}
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
|
-
setCurrentTokens(props.value
|
|
124
|
+
setCurrentTokens(props.value?.map(value => ({value, title: tokenLabels.get(value) ?? value, status: props.validValues.get(value)})) ?? []);
|
|
119
125
|
setIsInitializing(false);
|
|
120
126
|
});
|
|
121
127
|
}, []);
|
|
@@ -145,6 +151,7 @@ export function AsynchronousFormTokenField(props: AsynchronousFormTokenFieldProp
|
|
|
145
151
|
displayTransform={token => tokenTitleMappings.get(token) ?? token}
|
|
146
152
|
onInputChange={mySuggestionRequestQueue.debouncer}
|
|
147
153
|
/>
|
|
154
|
+
{props.help && <div><span className="components-form-token-field__help">{props.help}</span></div>}
|
|
148
155
|
{isLoading && <div><Spinner /><span className="components-form-token-field__help">{__("Updating Suggestions")}</span></div>}
|
|
149
156
|
{isValidating && <div><Spinner /><span className="components-form-token-field__help">{__("Validating")}</span></div>}
|
|
150
157
|
</>;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import apiFetch from '@wordpress/api-fetch';
|
|
2
1
|
import {AsynchronousFormTokenField, ValidationState} from "./AsynchronousFormTokenField";
|
|
3
2
|
import {useState} from "@wordpress/element";
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
import {requestPostsFromAPI} from "./shared";
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
5
7
|
|
|
6
8
|
type ExtendedPostPickerProps = {
|
|
7
9
|
onChange: (value: string[]) => void;
|
|
8
10
|
label: string;
|
|
11
|
+
help?: string;
|
|
9
12
|
postTypes: string[];
|
|
10
13
|
placeholder: string;
|
|
11
|
-
value
|
|
14
|
+
value?: string[];
|
|
15
|
+
multiple?: boolean;
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
export type ExtendedPostPickerConstructorProps = Partial<Omit<ExtendedPostPickerProps, 'onChange'|'value'|'label'>> & Pick<ExtendedPostPickerProps, 'onChange'|'value'|'label'>;
|
|
15
19
|
|
|
16
|
-
async function makeAPIRequest(data: {search?: string, ids?: string, postTypes?: string}) {
|
|
17
|
-
return (await apiFetch<Array<{ id: number, title: string }>>({path: addQueryArgs("/plaudit/common/v1/post-table-search", data)}))
|
|
18
|
-
.map(item => ({id: item.id.toString(), title: item.title}));
|
|
19
|
-
}
|
|
20
20
|
export function ExtendedPostPicker(props: ExtendedPostPickerConstructorProps) {
|
|
21
21
|
const [validPostIds, _] = useState(new Map<string, ValidationState>());
|
|
22
22
|
const value = props.value ?? [];
|
|
@@ -34,9 +34,9 @@ export function ExtendedPostPicker(props: ExtendedPostPickerConstructorProps) {
|
|
|
34
34
|
return {value: token, status: ValidationState.Invalid, title: token};
|
|
35
35
|
}
|
|
36
36
|
}}
|
|
37
|
-
suggestionQuery={input =>
|
|
37
|
+
suggestionQuery={input => requestPostsFromAPI({search: input, postTypes: props.postTypes?.join(',')})
|
|
38
38
|
.then(posts => posts.map(post => `${post.title} (#${post.id})`))}
|
|
39
|
-
validationQuery={idsBeingValidated =>
|
|
39
|
+
validationQuery={idsBeingValidated => requestPostsFromAPI({ids: idsBeingValidated.join(','), postTypes: props.postTypes?.join(',')})
|
|
40
40
|
.then(posts => posts.map(post => ({value: post.id, title: post.title, status: ValidationState.Valid})))}
|
|
41
41
|
validValues={validPostIds}
|
|
42
42
|
validator={token => /\(#([0-9]+)\)$/.exec(token)?.[1] !== undefined}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {BaseControl, ComboboxControl, Spinner} from "@wordpress/components";
|
|
2
|
+
import type {ComboboxControlOption, ComboboxControlProps} from "@wordpress/components/build-types/combobox-control/types";
|
|
3
|
+
import {useEffect, useState} from "@wordpress/element";
|
|
4
|
+
import {__} from "@wordpress/i18n";
|
|
5
|
+
|
|
6
|
+
import debounce from "debounce";
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
|
|
10
|
+
export type LazySuggestionsComboboxControlProps = Omit<ComboboxControlProps, 'options'> & {
|
|
11
|
+
getOption(value?: string): Promise<ComboboxControlOption|undefined>;
|
|
12
|
+
getSuggestions(filterValue: string): Promise<ComboboxControlOption[]>;
|
|
13
|
+
};
|
|
14
|
+
export function LazySuggestionsComboboxControl(props: LazySuggestionsComboboxControlProps) {
|
|
15
|
+
const [isInitializing, setIsInitializing] = useState(true);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
|
|
18
|
+
const [suggestions, setSuggestions] = useState<ComboboxControlOption[]>([]);
|
|
19
|
+
|
|
20
|
+
const [mySuggestionRequestQueue] = useState<{
|
|
21
|
+
queue: Promise<boolean>, currentRequest: number, debouncer: (input: string) => void
|
|
22
|
+
}>(() => ({
|
|
23
|
+
queue: Promise.resolve(true),
|
|
24
|
+
currentRequest: 0,
|
|
25
|
+
debouncer: debounce((input: string) => {
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
const myNumber = ++mySuggestionRequestQueue.currentRequest;
|
|
28
|
+
mySuggestionRequestQueue.queue = mySuggestionRequestQueue.queue.then(async () => {
|
|
29
|
+
let suggestions: ComboboxControlOption[];
|
|
30
|
+
if (input.length < 2) {
|
|
31
|
+
if (props.value) {
|
|
32
|
+
const propValue = await props.getOption(props.value);
|
|
33
|
+
if (propValue) {
|
|
34
|
+
suggestions = [propValue];
|
|
35
|
+
} else {
|
|
36
|
+
suggestions = [];
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
suggestions = [];
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
suggestions = await props.getSuggestions(input);
|
|
43
|
+
}
|
|
44
|
+
if (myNumber === mySuggestionRequestQueue.currentRequest) {
|
|
45
|
+
if (props.onFilterValueChange) {
|
|
46
|
+
props.onFilterValueChange(input);
|
|
47
|
+
}
|
|
48
|
+
setSuggestions(suggestions);
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
53
|
+
}, 500)
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (props.value) {
|
|
58
|
+
props.getOption(props.value).then(option => {
|
|
59
|
+
setSuggestions(option ? [option] : []);
|
|
60
|
+
setIsInitializing(false);
|
|
61
|
+
}, () => {
|
|
62
|
+
//TODO: Add a notice for the error
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
setIsInitializing(false);
|
|
66
|
+
}
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
if (isInitializing) {
|
|
70
|
+
return <BaseControl {...props}>
|
|
71
|
+
<Spinner /><span>{__(`Initializing ${props.label}`)}</span>
|
|
72
|
+
</BaseControl>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return <>
|
|
76
|
+
<ComboboxControl
|
|
77
|
+
{...props}
|
|
78
|
+
options={suggestions}
|
|
79
|
+
onFilterValueChange={mySuggestionRequestQueue.debouncer}
|
|
80
|
+
allowReset={props.allowReset !== false}
|
|
81
|
+
/>
|
|
82
|
+
{isLoading && <div><Spinner /><span className="components-base-control__help">{__("Updating Suggestions")}</span></div>}
|
|
83
|
+
</>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from "@wordpress/element";
|
|
2
|
+
|
|
3
|
+
import React, {type PropsWithChildren, type ReactNode} from "react";
|
|
4
|
+
import {Button} from "@wordpress/components";
|
|
5
|
+
|
|
6
|
+
export type SortableItemsControlProps<D> = {
|
|
7
|
+
value: D[],
|
|
8
|
+
onChange: (value: D[]) => void,
|
|
9
|
+
childProducer: (datum: D, props: { onChange: (datum: D) => void, index: number }) => ReactNode,
|
|
10
|
+
emptyValue: D
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function SortableItemsControl<D>(props: SortableItemsControlProps<D>) {
|
|
14
|
+
const value = [...props.value];
|
|
15
|
+
const containerRef = useRef<HTMLDivElement|null>(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
|
|
18
|
+
}, [containerRef]);
|
|
19
|
+
const [dirtiness, setDirtiness] = useState(0);
|
|
20
|
+
const makeDirty = () => setDirtiness(dirtiness + 1);
|
|
21
|
+
|
|
22
|
+
const onChangeByIndex = (datum: D, index: number) => {
|
|
23
|
+
value[index] = datum;
|
|
24
|
+
props.onChange(value);
|
|
25
|
+
makeDirty();
|
|
26
|
+
};
|
|
27
|
+
const moveByIndex = (index: number, direction: -1|1) => {
|
|
28
|
+
const moved = value.splice(index, 1);
|
|
29
|
+
value.splice(index + direction, 0, ...moved);
|
|
30
|
+
makeDirty();
|
|
31
|
+
};
|
|
32
|
+
const remove = (index: number) => {
|
|
33
|
+
value.splice(index, 1);
|
|
34
|
+
props.onChange(value);
|
|
35
|
+
makeDirty();
|
|
36
|
+
}
|
|
37
|
+
return <div>
|
|
38
|
+
<div ref={containerRef}>
|
|
39
|
+
{...props.value.map((datum, index) => <SortableItem index={index} children={makeSortableItemChildren(props, datum, index, onChangeByIndex, remove)}/>)}
|
|
40
|
+
</div>
|
|
41
|
+
<div>
|
|
42
|
+
<Button onClick={() => {
|
|
43
|
+
value.push(props.emptyValue);
|
|
44
|
+
makeDirty();
|
|
45
|
+
}}>
|
|
46
|
+
Add Row
|
|
47
|
+
</Button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeSortableItemChildren<D>(
|
|
53
|
+
props: SortableItemsControlProps<D>, datum: D, index: number, onChangeByIndex: (datum: D, index: number) => void, remove: (index: number) => void
|
|
54
|
+
) {
|
|
55
|
+
return props.childProducer(datum, {index, onChange: datum => onChangeByIndex(datum, index)});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function SortableItem(props: PropsWithChildren<{ index: number }>) {
|
|
59
|
+
return <div data-index={props.index}>
|
|
60
|
+
<div className="plaudit-sortable-items-drag-handler">
|
|
61
|
+
//TODO: Make this look pretty
|
|
62
|
+
</div>
|
|
63
|
+
<div>
|
|
64
|
+
{props.children}
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
//TODO: Add a removal button
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
}
|
package/src/controls/index.ts
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import apiFetch from "@wordpress/api-fetch";
|
|
2
|
+
import {addQueryArgs} from "@wordpress/url";
|
|
3
|
+
|
|
4
|
+
export async function requestPostsFromAPI(data: {search?: string, ids?: string, postTypes?: string|undefined}) {
|
|
5
|
+
return (await apiFetch<Array<{ id: number, title: string }>>({path: addQueryArgs("/plaudit/common/v1/post-table-search", data)}))
|
|
6
|
+
.map(item => ({id: item.id.toString(), title: item.title}));
|
|
7
|
+
}
|