@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.
Files changed (53) hide show
  1. package/build/blocks/common-native-property-implementations.js +97 -19
  2. package/build/blocks/common-native-property-implementations.js.map +1 -1
  3. package/build/blocks/layered-styles.js +9 -7
  4. package/build/blocks/layered-styles.js.map +1 -1
  5. package/build/blocks/simple-block.js +10 -6
  6. package/build/blocks/simple-block.js.map +1 -1
  7. package/build/blocks/simple-native-property.js +43 -37
  8. package/build/blocks/simple-native-property.js.map +1 -1
  9. package/build/controls/AsynchronousFormTokenField.js +23 -12
  10. package/build/controls/AsynchronousFormTokenField.js.map +1 -1
  11. package/build/controls/ExtendedPostPicker.js +5 -10
  12. package/build/controls/ExtendedPostPicker.js.map +1 -1
  13. package/build/controls/InspectorPanel.js +3 -2
  14. package/build/controls/InspectorPanel.js.map +1 -1
  15. package/build/controls/LazySuggestionsComboboxControl.js +70 -0
  16. package/build/controls/LazySuggestionsComboboxControl.js.map +1 -0
  17. package/build/controls/PickOne.js +8 -7
  18. package/build/controls/PickOne.js.map +1 -1
  19. package/build/controls/SimpleToggle.js +2 -2
  20. package/build/controls/SimpleToggle.js.map +1 -1
  21. package/build/controls/SortableItemsControl.js +43 -0
  22. package/build/controls/SortableItemsControl.js.map +1 -0
  23. package/build/controls/index.js +1 -0
  24. package/build/controls/index.js.map +1 -1
  25. package/build/controls/shared.js +7 -0
  26. package/build/controls/shared.js.map +1 -0
  27. package/build/lib/plaudit-icons/column-1.js +3 -2
  28. package/build/lib/plaudit-icons/column-1.js.map +1 -1
  29. package/build/lib/plaudit-icons/column-2.js +3 -2
  30. package/build/lib/plaudit-icons/column-2.js.map +1 -1
  31. package/build/lib/plaudit-icons/column-3.js +3 -2
  32. package/build/lib/plaudit-icons/column-3.js.map +1 -1
  33. package/build/lib/plaudit-icons/placement-center.js +3 -2
  34. package/build/lib/plaudit-icons/placement-center.js.map +1 -1
  35. package/build/lib/plaudit-icons/placement-end.js +3 -2
  36. package/build/lib/plaudit-icons/placement-end.js.map +1 -1
  37. package/build/lib/plaudit-icons/placement-start.js +3 -2
  38. package/build/lib/plaudit-icons/placement-start.js.map +1 -1
  39. package/build/lib/plaudit-icons/placement-stretch.js +3 -2
  40. package/build/lib/plaudit-icons/placement-stretch.js.map +1 -1
  41. package/build/lib/plaudit-icons/plaudit-icon.js +3 -2
  42. package/build/lib/plaudit-icons/plaudit-icon.js.map +1 -1
  43. package/build/lib/plaudit-icons/reusable-block-marker.js +3 -2
  44. package/build/lib/plaudit-icons/reusable-block-marker.js.map +1 -1
  45. package/package.json +10 -10
  46. package/src/blocks/common-native-property-implementations.tsx +105 -15
  47. package/src/blocks/simple-native-property.tsx +29 -25
  48. package/src/controls/AsynchronousFormTokenField.tsx +13 -6
  49. package/src/controls/ExtendedPostPicker.tsx +9 -9
  50. package/src/controls/LazySuggestionsComboboxControl.tsx +84 -0
  51. package/src/controls/SortableItemsControl.tsx +70 -0
  52. package/src/controls/index.ts +1 -0
  53. 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: T,
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>): SimpleNativeProperty {
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>): SimpleNativeProperty {
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
- return <TextareaControl value={value} onChange={onChange} label={config.label} help={config.help} {...config.component} />;
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 <Card>
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
- </CardBody>
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: T,
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 (propPath.length === 1) {
138
- existingValue = blockEditProps.attributes[prop.name];
139
- if (existingValue === undefined) {
140
- existingValue = prop.default;
141
- blockEditProps.setAttributes({[prop.name]: prop.default});
142
- }
143
- } else {
144
- let graphExistingValue = blockEditProps.attributes[propPath[0]];
145
- if (graphExistingValue === undefined) {
146
- blockEditProps.attributes[propPath[0]] = graphExistingValue = {};
147
- }
148
- for (let i = 1; i < propPath.length; i++) {
149
- if (graphExistingValue[propPath[i]] === undefined) {
150
- for (; i < propPath.length - 1; i++) {
151
- graphExistingValue = graphExistingValue[propPath[i]] = {};
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
- existingValue = graphExistingValue ?? prop.default;
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
- value: string[];
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
- for (const value of props.value) {
114
- if (!props.validValues.has(value)) {
115
- props.validValues.set(value, ValidationState.Invalid);
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.map(value => ({value, title: tokenLabels.get(value) ?? value, status: props.validValues.get(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
- import {addQueryArgs} from "@wordpress/url";
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: string[]
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 => makeAPIRequest({search: input, postTypes: props.postTypes?.join(',')})
37
+ suggestionQuery={input => requestPostsFromAPI({search: input, postTypes: props.postTypes?.join(',')})
38
38
  .then(posts => posts.map(post => `${post.title} (#${post.id})`))}
39
- validationQuery={idsBeingValidated => makeAPIRequest({ids: idsBeingValidated.join(','), postTypes: props.postTypes?.join(',')})
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
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./AsynchronousFormTokenField";
2
2
  export * from "./ExtendedPostPicker";
3
3
  export * from "./InspectorPanel";
4
+ export * from "./LazySuggestionsComboboxControl";
4
5
  export * from "./SimpleToggle";
5
6
  export * from "./PickOne";
@@ -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
+ }