@wordpress/abilities 0.1.1-next.dc3f6d3c1.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/CHANGELOG.md +9 -0
- package/LICENSE.md +788 -0
- package/README.md +302 -0
- package/build/api.js +189 -0
- package/build/api.js.map +7 -0
- package/build/index.js +53 -0
- package/build/index.js.map +7 -0
- package/build/store/actions.js +183 -0
- package/build/store/actions.js.map +7 -0
- package/build/store/constants.js +58 -0
- package/build/store/constants.js.map +7 -0
- package/build/store/index.js +77 -0
- package/build/store/index.js.map +7 -0
- package/build/store/reducer.js +126 -0
- package/build/store/reducer.js.map +7 -0
- package/build/store/resolvers.js +107 -0
- package/build/store/resolvers.js.map +7 -0
- package/build/store/selectors.js +64 -0
- package/build/store/selectors.js.map +7 -0
- package/build/store/tests/actions.test.js +792 -0
- package/build/store/tests/actions.test.js.map +7 -0
- package/build/store/tests/reducer.test.js +743 -0
- package/build/store/tests/reducer.test.js.map +7 -0
- package/build/store/tests/resolvers.test.js +520 -0
- package/build/store/tests/resolvers.test.js.map +7 -0
- package/build/store/tests/selectors.test.js +349 -0
- package/build/store/tests/selectors.test.js.map +7 -0
- package/build/tests/api.test.js +546 -0
- package/build/tests/api.test.js.map +7 -0
- package/build/tests/validation.test.js +453 -0
- package/build/tests/validation.test.js.map +7 -0
- package/build/types.js +19 -0
- package/build/types.js.map +7 -0
- package/build/validation.js +144 -0
- package/build/validation.js.map +7 -0
- package/build-module/api.js +146 -0
- package/build-module/api.js.map +7 -0
- package/build-module/index.js +28 -0
- package/build-module/index.js.map +7 -0
- package/build-module/store/actions.js +161 -0
- package/build-module/store/actions.js.map +7 -0
- package/build-module/store/constants.js +24 -0
- package/build-module/store/constants.js.map +7 -0
- package/build-module/store/index.js +47 -0
- package/build-module/store/index.js.map +7 -0
- package/build-module/store/reducer.js +112 -0
- package/build-module/store/reducer.js.map +7 -0
- package/build-module/store/resolvers.js +84 -0
- package/build-module/store/resolvers.js.map +7 -0
- package/build-module/store/selectors.js +36 -0
- package/build-module/store/selectors.js.map +7 -0
- package/build-module/store/tests/actions.test.js +804 -0
- package/build-module/store/tests/actions.test.js.map +7 -0
- package/build-module/store/tests/reducer.test.js +726 -0
- package/build-module/store/tests/reducer.test.js.map +7 -0
- package/build-module/store/tests/resolvers.test.js +523 -0
- package/build-module/store/tests/resolvers.test.js.map +7 -0
- package/build-module/store/tests/selectors.test.js +352 -0
- package/build-module/store/tests/selectors.test.js.map +7 -0
- package/build-module/tests/api.test.js +530 -0
- package/build-module/tests/api.test.js.map +7 -0
- package/build-module/tests/validation.test.js +451 -0
- package/build-module/tests/validation.test.js.map +7 -0
- package/build-module/types.js +1 -0
- package/build-module/types.js.map +7 -0
- package/build-module/validation.js +109 -0
- package/build-module/validation.js.map +7 -0
- package/build-types/api.d.ts +138 -0
- package/build-types/api.d.ts.map +1 -0
- package/build-types/index.d.ts +41 -0
- package/build-types/index.d.ts.map +1 -0
- package/build-types/store/actions.d.ts +76 -0
- package/build-types/store/actions.d.ts.map +1 -0
- package/build-types/store/constants.d.ts +14 -0
- package/build-types/store/constants.d.ts.map +1 -0
- package/build-types/store/index.d.ts +7 -0
- package/build-types/store/index.d.ts.map +1 -0
- package/build-types/store/reducer.d.ts +22 -0
- package/build-types/store/reducer.d.ts.map +1 -0
- package/build-types/store/resolvers.d.ts +47 -0
- package/build-types/store/resolvers.d.ts.map +1 -0
- package/build-types/store/selectors.d.ts +37 -0
- package/build-types/store/selectors.d.ts.map +1 -0
- package/build-types/types.d.ts +163 -0
- package/build-types/types.d.ts.map +1 -0
- package/build-types/validation.d.ts +23 -0
- package/build-types/validation.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/api.ts +339 -0
- package/src/index.ts +65 -0
- package/src/store/actions.ts +247 -0
- package/src/store/constants.ts +15 -0
- package/src/store/index.ts +55 -0
- package/src/store/reducer.ts +184 -0
- package/src/store/resolvers.ts +151 -0
- package/src/store/selectors.ts +82 -0
- package/src/store/tests/actions.test.ts +950 -0
- package/src/store/tests/reducer.test.ts +861 -0
- package/src/store/tests/resolvers.test.ts +642 -0
- package/src/store/tests/selectors.test.ts +412 -0
- package/src/tests/api.test.ts +655 -0
- package/src/tests/validation.test.ts +532 -0
- package/src/types.ts +190 -0
- package/src/validation.ts +207 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wordpress/abilities",
|
|
3
|
+
"version": "0.1.1-next.dc3f6d3c1.0",
|
|
4
|
+
"description": "JavaScript client for WordPress Abilities API.",
|
|
5
|
+
"author": "The WordPress Contributors",
|
|
6
|
+
"license": "GPL-2.0-or-later",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"wordpress",
|
|
9
|
+
"abilities",
|
|
10
|
+
"api",
|
|
11
|
+
"ai"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/abilities/README.md",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/WordPress/gutenberg.git",
|
|
17
|
+
"directory": "packages/abilities"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/WordPress/gutenberg/issues"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.12.0",
|
|
24
|
+
"npm": ">=8.19.2"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"build",
|
|
28
|
+
"build-module",
|
|
29
|
+
"build-types",
|
|
30
|
+
"src",
|
|
31
|
+
"*.md"
|
|
32
|
+
],
|
|
33
|
+
"main": "build/index.js",
|
|
34
|
+
"module": "build-module/index.js",
|
|
35
|
+
"react-native": "src/index",
|
|
36
|
+
"wpScript": true,
|
|
37
|
+
"types": "build-types",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@wordpress/api-fetch": "^7.35.1-next.dc3f6d3c1.0",
|
|
40
|
+
"@wordpress/core-data": "^7.35.1-next.dc3f6d3c1.0",
|
|
41
|
+
"@wordpress/data": "^10.35.1-next.dc3f6d3c1.0",
|
|
42
|
+
"@wordpress/i18n": "^6.8.1-next.dc3f6d3c1.0",
|
|
43
|
+
"@wordpress/url": "^4.35.1-next.dc3f6d3c1.0",
|
|
44
|
+
"ajv": "^8.17.1",
|
|
45
|
+
"ajv-draft-04": "^1.0.0",
|
|
46
|
+
"ajv-formats": "^3.0.1"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"gitHead": "f73b5e69b34fbaccfb8c47783f4f993059ff1a41"
|
|
52
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { dispatch, resolveSelect } from '@wordpress/data';
|
|
5
|
+
import apiFetch from '@wordpress/api-fetch';
|
|
6
|
+
import { addQueryArgs } from '@wordpress/url';
|
|
7
|
+
import { sprintf } from '@wordpress/i18n';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Internal dependencies
|
|
11
|
+
*/
|
|
12
|
+
import { store } from './store';
|
|
13
|
+
import type {
|
|
14
|
+
Ability,
|
|
15
|
+
AbilityCategory,
|
|
16
|
+
AbilityCategoryArgs,
|
|
17
|
+
AbilitiesQueryArgs,
|
|
18
|
+
AbilityInput,
|
|
19
|
+
AbilityOutput,
|
|
20
|
+
} from './types';
|
|
21
|
+
import { validateValueFromSchema } from './validation';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get all available abilities with optional filtering.
|
|
25
|
+
*
|
|
26
|
+
* @param args Optional query arguments to filter. Defaults to empty object.
|
|
27
|
+
* @return Promise resolving to array of abilities.
|
|
28
|
+
*/
|
|
29
|
+
export async function getAbilities(
|
|
30
|
+
args: AbilitiesQueryArgs = {}
|
|
31
|
+
): Promise< Ability[] > {
|
|
32
|
+
return await resolveSelect( store ).getAbilities( args );
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a specific ability by name.
|
|
37
|
+
*
|
|
38
|
+
* @param name The ability name.
|
|
39
|
+
* @return Promise resolving to the ability or null if not found.
|
|
40
|
+
*/
|
|
41
|
+
export async function getAbility( name: string ): Promise< Ability | null > {
|
|
42
|
+
return await resolveSelect( store ).getAbility( name );
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all available ability categories.
|
|
47
|
+
*
|
|
48
|
+
* @return Promise resolving to array of categories.
|
|
49
|
+
*/
|
|
50
|
+
export async function getAbilityCategories(): Promise< AbilityCategory[] > {
|
|
51
|
+
return await resolveSelect( store ).getAbilityCategories();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get a specific ability category by slug.
|
|
56
|
+
*
|
|
57
|
+
* @param slug The category slug.
|
|
58
|
+
* @return Promise resolving to the category or null if not found.
|
|
59
|
+
*/
|
|
60
|
+
export async function getAbilityCategory(
|
|
61
|
+
slug: string
|
|
62
|
+
): Promise< AbilityCategory | null > {
|
|
63
|
+
return await resolveSelect( store ).getAbilityCategory( slug );
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Register a client-side ability.
|
|
68
|
+
*
|
|
69
|
+
* Client abilities are executed locally in the browser and must include
|
|
70
|
+
* a callback function. The ability will be validated by the store action,
|
|
71
|
+
* and an error will be thrown if validation fails.
|
|
72
|
+
*
|
|
73
|
+
* Categories will be automatically fetched from the REST API if they
|
|
74
|
+
* haven't been loaded yet, so you don't need to call getAbilityCategories()
|
|
75
|
+
* before registering abilities.
|
|
76
|
+
*
|
|
77
|
+
* @param ability The ability definition including callback.
|
|
78
|
+
* @return Promise that resolves when registration is complete.
|
|
79
|
+
* @throws {Error} If the ability fails validation.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```js
|
|
83
|
+
* await registerAbility({
|
|
84
|
+
* name: 'my-plugin/navigate',
|
|
85
|
+
* label: 'Navigate to URL',
|
|
86
|
+
* description: 'Navigates to a URL within WordPress admin',
|
|
87
|
+
* category: 'navigation',
|
|
88
|
+
* input_schema: {
|
|
89
|
+
* type: 'object',
|
|
90
|
+
* properties: {
|
|
91
|
+
* url: { type: 'string' }
|
|
92
|
+
* },
|
|
93
|
+
* required: ['url']
|
|
94
|
+
* },
|
|
95
|
+
* callback: async ({ url }) => {
|
|
96
|
+
* window.location.href = url;
|
|
97
|
+
* return { success: true };
|
|
98
|
+
* }
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export async function registerAbility( ability: Ability ): Promise< void > {
|
|
103
|
+
await dispatch( store ).registerAbility( ability );
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Unregister an ability from the store.
|
|
108
|
+
*
|
|
109
|
+
* Remove a client-side ability from the store.
|
|
110
|
+
* Note: This will return an error for server-side abilities.
|
|
111
|
+
*
|
|
112
|
+
* @param name The ability name to unregister.
|
|
113
|
+
*/
|
|
114
|
+
export function unregisterAbility( name: string ): void {
|
|
115
|
+
dispatch( store ).unregisterAbility( name );
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Register a client-side ability category.
|
|
120
|
+
*
|
|
121
|
+
* Categories registered on the client are stored alongside server-side categories
|
|
122
|
+
* in the same store and can be used when registering client side abilities.
|
|
123
|
+
* This is useful when registering client-side abilities that introduce new
|
|
124
|
+
* categories not defined by the server.
|
|
125
|
+
*
|
|
126
|
+
* Categories will be automatically fetched from the REST API if they haven't been
|
|
127
|
+
* loaded yet to check for duplicates against server-side categories.
|
|
128
|
+
*
|
|
129
|
+
* @param slug Category slug (lowercase alphanumeric with dashes only).
|
|
130
|
+
* @param args Category arguments (label, description, optional meta).
|
|
131
|
+
* @return Promise that resolves when registration is complete.
|
|
132
|
+
* @throws {Error} If the category fails validation.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```js
|
|
136
|
+
* // Register a new category for block editor abilities
|
|
137
|
+
* await registerAbilityCategory('block-editor', {
|
|
138
|
+
* label: 'Block Editor',
|
|
139
|
+
* description: 'Abilities for interacting with the WordPress block editor'
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* // Then register abilities using this category
|
|
143
|
+
* await registerAbility({
|
|
144
|
+
* name: 'my-plugin/insert-block',
|
|
145
|
+
* label: 'Insert Block',
|
|
146
|
+
* description: 'Inserts a block into the editor',
|
|
147
|
+
* category: 'block-editor',
|
|
148
|
+
* callback: async ({ blockType }) => {
|
|
149
|
+
* // Implementation
|
|
150
|
+
* return { success: true };
|
|
151
|
+
* }
|
|
152
|
+
* });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export async function registerAbilityCategory(
|
|
156
|
+
slug: string,
|
|
157
|
+
args: AbilityCategoryArgs
|
|
158
|
+
): Promise< void > {
|
|
159
|
+
await dispatch( store ).registerAbilityCategory( slug, args );
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Unregister an ability category.
|
|
164
|
+
*
|
|
165
|
+
* Removes a category from the store.
|
|
166
|
+
*
|
|
167
|
+
* @param slug The category slug to unregister.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```js
|
|
171
|
+
* unregisterAbilityCategory('block-editor');
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export function unregisterAbilityCategory( slug: string ): void {
|
|
175
|
+
dispatch( store ).unregisterAbilityCategory( slug );
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Execute a client-side ability.
|
|
180
|
+
*
|
|
181
|
+
* @param ability The ability to execute.
|
|
182
|
+
* @param input Input parameters for the ability.
|
|
183
|
+
* @return Promise resolving to the ability execution result.
|
|
184
|
+
* @throws Error if validation fails or execution errors.
|
|
185
|
+
*/
|
|
186
|
+
async function executeClientAbility(
|
|
187
|
+
ability: Ability,
|
|
188
|
+
input: AbilityInput
|
|
189
|
+
): Promise< AbilityOutput > {
|
|
190
|
+
if ( ! ability.callback ) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
sprintf(
|
|
193
|
+
'Client ability %s is missing callback function',
|
|
194
|
+
ability.name
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check permission callback if defined
|
|
200
|
+
if ( ability.permissionCallback ) {
|
|
201
|
+
const hasPermission = await ability.permissionCallback( input );
|
|
202
|
+
if ( ! hasPermission ) {
|
|
203
|
+
const error = new Error(
|
|
204
|
+
sprintf( 'Permission denied for ability: %s', ability.name )
|
|
205
|
+
);
|
|
206
|
+
( error as any ).code = 'ability_permission_denied';
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if ( ability.input_schema ) {
|
|
212
|
+
const inputValidation = validateValueFromSchema(
|
|
213
|
+
input,
|
|
214
|
+
ability.input_schema,
|
|
215
|
+
'input'
|
|
216
|
+
);
|
|
217
|
+
if ( inputValidation !== true ) {
|
|
218
|
+
const error = new Error(
|
|
219
|
+
sprintf(
|
|
220
|
+
'Ability "%1$s" has invalid input. Reason: %2$s',
|
|
221
|
+
ability.name,
|
|
222
|
+
inputValidation
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
( error as any ).code = 'ability_invalid_input';
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let result: AbilityOutput;
|
|
231
|
+
try {
|
|
232
|
+
result = await ability.callback( input );
|
|
233
|
+
} catch ( error ) {
|
|
234
|
+
// eslint-disable-next-line no-console
|
|
235
|
+
console.error(
|
|
236
|
+
`Error executing client ability ${ ability.name }:`,
|
|
237
|
+
error
|
|
238
|
+
);
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if ( ability.output_schema ) {
|
|
243
|
+
const outputValidation = validateValueFromSchema(
|
|
244
|
+
result,
|
|
245
|
+
ability.output_schema,
|
|
246
|
+
'output'
|
|
247
|
+
);
|
|
248
|
+
if ( outputValidation !== true ) {
|
|
249
|
+
const error = new Error(
|
|
250
|
+
sprintf(
|
|
251
|
+
'Ability "%1$s" has invalid output. Reason: %2$s',
|
|
252
|
+
ability.name,
|
|
253
|
+
outputValidation
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
( error as any ).code = 'ability_invalid_output';
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Execute a server-side ability.
|
|
266
|
+
*
|
|
267
|
+
* @param ability The ability to execute.
|
|
268
|
+
* @param input Input parameters for the ability.
|
|
269
|
+
* @return Promise resolving to the ability execution result.
|
|
270
|
+
* @throws Error if the API call fails.
|
|
271
|
+
*/
|
|
272
|
+
async function executeServerAbility(
|
|
273
|
+
ability: Ability,
|
|
274
|
+
input: AbilityInput
|
|
275
|
+
): Promise< AbilityOutput > {
|
|
276
|
+
let method = 'POST';
|
|
277
|
+
if ( !! ability.meta?.annotations?.readonly ) {
|
|
278
|
+
method = 'GET';
|
|
279
|
+
} else if (
|
|
280
|
+
!! ability.meta?.annotations?.destructive &&
|
|
281
|
+
!! ability.meta?.annotations?.idempotent
|
|
282
|
+
) {
|
|
283
|
+
method = 'DELETE';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let path = `/wp-abilities/v1/abilities/${ ability.name }/run`;
|
|
287
|
+
const options: {
|
|
288
|
+
method: string;
|
|
289
|
+
data?: { input: AbilityInput };
|
|
290
|
+
} = {
|
|
291
|
+
method,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if ( [ 'GET', 'DELETE' ].includes( method ) && input !== null ) {
|
|
295
|
+
// For GET and DELETE requests, pass the input directly.
|
|
296
|
+
path = addQueryArgs( path, { input } );
|
|
297
|
+
} else if ( method === 'POST' && input !== null ) {
|
|
298
|
+
options.data = { input };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Note: Input and output validation happens on the server side for these abilities.
|
|
302
|
+
try {
|
|
303
|
+
return await apiFetch< AbilityOutput >( {
|
|
304
|
+
path,
|
|
305
|
+
...options,
|
|
306
|
+
} );
|
|
307
|
+
} catch ( error ) {
|
|
308
|
+
// eslint-disable-next-line no-console
|
|
309
|
+
console.error( `Error executing ability ${ ability.name }:`, error );
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Execute an ability.
|
|
316
|
+
*
|
|
317
|
+
* Determines whether to execute locally (client abilities) or remotely (server abilities)
|
|
318
|
+
* based on whether the ability has a callback function.
|
|
319
|
+
*
|
|
320
|
+
* @param name The ability name.
|
|
321
|
+
* @param input Optional input parameters for the ability.
|
|
322
|
+
* @return Promise resolving to the ability execution result.
|
|
323
|
+
* @throws Error if the ability is not found or execution fails.
|
|
324
|
+
*/
|
|
325
|
+
export async function executeAbility(
|
|
326
|
+
name: string,
|
|
327
|
+
input?: AbilityInput
|
|
328
|
+
): Promise< AbilityOutput > {
|
|
329
|
+
const ability = await getAbility( name );
|
|
330
|
+
if ( ! ability ) {
|
|
331
|
+
throw new Error( sprintf( 'Ability not found: %s', name ) );
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if ( ability.callback ) {
|
|
335
|
+
return executeClientAbility( ability, input );
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return executeServerAbility( ability, input );
|
|
339
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress Abilities API Client
|
|
3
|
+
*
|
|
4
|
+
* This package provides a client for interacting with the
|
|
5
|
+
* WordPress Abilities API, allowing you to list, retrieve, and execute
|
|
6
|
+
* abilities from client-side code.
|
|
7
|
+
*
|
|
8
|
+
* @package
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Public API functions
|
|
13
|
+
*/
|
|
14
|
+
export {
|
|
15
|
+
getAbilities,
|
|
16
|
+
getAbility,
|
|
17
|
+
getAbilityCategories,
|
|
18
|
+
getAbilityCategory,
|
|
19
|
+
executeAbility,
|
|
20
|
+
registerAbility,
|
|
21
|
+
unregisterAbility,
|
|
22
|
+
registerAbilityCategory,
|
|
23
|
+
unregisterAbilityCategory,
|
|
24
|
+
} from './api';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The store can be used directly with @wordpress/data via selectors
|
|
28
|
+
* in React components with useSelect.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```js
|
|
32
|
+
* import { useSelect } from '@wordpress/data';
|
|
33
|
+
* import { store as abilitiesStore } from '@wordpress/abilities';
|
|
34
|
+
*
|
|
35
|
+
* function MyComponent() {
|
|
36
|
+
* const abilities = useSelect(
|
|
37
|
+
* (select) => select(abilitiesStore).getAbilities(),
|
|
38
|
+
* []
|
|
39
|
+
* );
|
|
40
|
+
* // Use abilities...
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export { store } from './store';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Type definitions
|
|
48
|
+
*/
|
|
49
|
+
export type {
|
|
50
|
+
Ability,
|
|
51
|
+
AbilityCategory,
|
|
52
|
+
AbilityCategoryArgs,
|
|
53
|
+
AbilitiesState,
|
|
54
|
+
AbilitiesQueryArgs,
|
|
55
|
+
AbilityCallback,
|
|
56
|
+
PermissionCallback,
|
|
57
|
+
AbilityInput,
|
|
58
|
+
AbilityOutput,
|
|
59
|
+
ValidationError,
|
|
60
|
+
} from './types';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validation utilities
|
|
64
|
+
*/
|
|
65
|
+
export { validateValueFromSchema } from './validation';
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { sprintf } from '@wordpress/i18n';
|
|
5
|
+
import { resolveSelect } from '@wordpress/data';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies
|
|
9
|
+
*/
|
|
10
|
+
import type { Ability, AbilityCategory, AbilityCategoryArgs } from '../types';
|
|
11
|
+
import {
|
|
12
|
+
RECEIVE_ABILITIES,
|
|
13
|
+
REGISTER_ABILITY,
|
|
14
|
+
UNREGISTER_ABILITY,
|
|
15
|
+
RECEIVE_CATEGORIES,
|
|
16
|
+
REGISTER_ABILITY_CATEGORY,
|
|
17
|
+
UNREGISTER_ABILITY_CATEGORY,
|
|
18
|
+
STORE_NAME,
|
|
19
|
+
} from './constants';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns an action object used to receive abilities into the store.
|
|
23
|
+
*
|
|
24
|
+
* @param abilities Array of abilities to store.
|
|
25
|
+
* @return Action object.
|
|
26
|
+
*/
|
|
27
|
+
export function receiveAbilities( abilities: Ability[] ) {
|
|
28
|
+
return {
|
|
29
|
+
type: RECEIVE_ABILITIES,
|
|
30
|
+
abilities,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns an action object used to receive categories into the store.
|
|
36
|
+
*
|
|
37
|
+
* @param categories Array of categories to store.
|
|
38
|
+
* @return Action object.
|
|
39
|
+
*/
|
|
40
|
+
export function receiveCategories( categories: AbilityCategory[] ) {
|
|
41
|
+
return {
|
|
42
|
+
type: RECEIVE_CATEGORIES,
|
|
43
|
+
categories,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Registers an ability in the store.
|
|
49
|
+
*
|
|
50
|
+
* This action validates the ability before registration. If validation fails,
|
|
51
|
+
* an error will be thrown. Categories will be automatically fetched from the
|
|
52
|
+
* REST API if they haven't been loaded yet.
|
|
53
|
+
*
|
|
54
|
+
* @param ability The ability to register.
|
|
55
|
+
* @return Action object or function.
|
|
56
|
+
* @throws {Error} If validation fails.
|
|
57
|
+
*/
|
|
58
|
+
export function registerAbility( ability: Ability ) {
|
|
59
|
+
// @ts-expect-error - registry types are not yet available
|
|
60
|
+
return async ( { select, dispatch } ) => {
|
|
61
|
+
if ( ! ability.name ) {
|
|
62
|
+
throw new Error( 'Ability name is required' );
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate name format matches server implementation
|
|
66
|
+
if ( ! /^[a-z0-9-]+\/[a-z0-9-]+$/.test( ability.name ) ) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if ( ! ability.label ) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
sprintf( 'Ability "%s" must have a label', ability.name )
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ( ! ability.description ) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
sprintf( 'Ability "%s" must have a description', ability.name )
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ( ! ability.category ) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
sprintf( 'Ability "%s" must have a category', ability.name )
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate category format
|
|
91
|
+
if ( ! /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test( ability.category ) ) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
sprintf(
|
|
94
|
+
'Ability "%1$s" has an invalid category. Category must be lowercase alphanumeric with dashes only Got: "%2$s"',
|
|
95
|
+
ability.name,
|
|
96
|
+
ability.category
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Ensure categories are loaded before validating
|
|
102
|
+
const categories =
|
|
103
|
+
await resolveSelect( STORE_NAME ).getAbilityCategories();
|
|
104
|
+
const existingCategory = categories.find(
|
|
105
|
+
( cat: AbilityCategory ) => cat.slug === ability.category
|
|
106
|
+
);
|
|
107
|
+
if ( ! existingCategory ) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
sprintf(
|
|
110
|
+
'Ability "%1$s" references non-existent category "%2$s". Please register the category first.',
|
|
111
|
+
ability.name,
|
|
112
|
+
ability.category
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Client-side abilities must have a callback
|
|
118
|
+
if ( ability.callback && typeof ability.callback !== 'function' ) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
sprintf(
|
|
121
|
+
'Ability "%s" has an invalid callback. Callback must be a function',
|
|
122
|
+
ability.name
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check if ability is already registered
|
|
128
|
+
const existingAbility = select.getAbility( ability.name );
|
|
129
|
+
if ( existingAbility ) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
sprintf( 'Ability "%s" is already registered', ability.name )
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// All validation passed, dispatch the registration action
|
|
136
|
+
dispatch( {
|
|
137
|
+
type: REGISTER_ABILITY,
|
|
138
|
+
ability,
|
|
139
|
+
} );
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Returns an action object used to unregister a client-side ability.
|
|
145
|
+
*
|
|
146
|
+
* @param name The name of the ability to unregister.
|
|
147
|
+
* @return Action object.
|
|
148
|
+
*/
|
|
149
|
+
export function unregisterAbility( name: string ) {
|
|
150
|
+
return {
|
|
151
|
+
type: UNREGISTER_ABILITY,
|
|
152
|
+
name,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Registers a client-side ability category in the store.
|
|
158
|
+
*
|
|
159
|
+
* This action validates the category before registration. If validation fails,
|
|
160
|
+
* an error will be thrown. Categories will be automatically fetched from the
|
|
161
|
+
* REST API if they haven't been loaded yet to check for duplicates.
|
|
162
|
+
*
|
|
163
|
+
* @param slug The unique category slug identifier.
|
|
164
|
+
* @param args Category arguments (label, description, optional meta).
|
|
165
|
+
* @return Action object or function.
|
|
166
|
+
* @throws {Error} If validation fails.
|
|
167
|
+
*/
|
|
168
|
+
export function registerAbilityCategory(
|
|
169
|
+
slug: string,
|
|
170
|
+
args: AbilityCategoryArgs
|
|
171
|
+
) {
|
|
172
|
+
// @ts-expect-error - registry types are not yet available
|
|
173
|
+
return async ( { select, dispatch } ) => {
|
|
174
|
+
if ( ! slug ) {
|
|
175
|
+
throw new Error( 'Category slug is required' );
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Validate slug format matches server implementation
|
|
179
|
+
if ( ! /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test( slug ) ) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
'Category slug must contain only lowercase alphanumeric characters and dashes.'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Ensure categories are loaded before checking for duplicates
|
|
186
|
+
await resolveSelect( STORE_NAME ).getAbilityCategories();
|
|
187
|
+
const existingCategory = select.getAbilityCategory( slug );
|
|
188
|
+
if ( existingCategory ) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
sprintf( 'Category "%s" is already registered.', slug )
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Validate label presence and type (matches PHP empty() + is_string())
|
|
195
|
+
if ( ! args.label || typeof args.label !== 'string' ) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
'The category properties must contain a `label` string.'
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Validate description presence and type (matches PHP empty() + is_string())
|
|
202
|
+
if ( ! args.description || typeof args.description !== 'string' ) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'The category properties must contain a `description` string.'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (
|
|
209
|
+
args.meta !== undefined &&
|
|
210
|
+
( typeof args.meta !== 'object' || Array.isArray( args.meta ) )
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
'The category properties should provide a valid `meta` object.'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const category: AbilityCategory = {
|
|
218
|
+
slug,
|
|
219
|
+
label: args.label,
|
|
220
|
+
description: args.description,
|
|
221
|
+
meta: {
|
|
222
|
+
...( args.meta || {} ),
|
|
223
|
+
// Internal implementation note: Client-registered categories will have `meta._clientRegistered` set to `true` to differentiate them from server-fetched categories.
|
|
224
|
+
// This is used internally by the resolver to determine whether to fetch categories from the server.
|
|
225
|
+
_clientRegistered: true,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
dispatch( {
|
|
230
|
+
type: REGISTER_ABILITY_CATEGORY,
|
|
231
|
+
category,
|
|
232
|
+
} );
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Returns an action object used to unregister a client-side ability category.
|
|
238
|
+
*
|
|
239
|
+
* @param slug The slug of the category to unregister.
|
|
240
|
+
* @return Action object.
|
|
241
|
+
*/
|
|
242
|
+
export function unregisterAbilityCategory( slug: string ) {
|
|
243
|
+
return {
|
|
244
|
+
type: UNREGISTER_ABILITY_CATEGORY,
|
|
245
|
+
slug,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store constants
|
|
3
|
+
*/
|
|
4
|
+
export const STORE_NAME = 'core/abilities';
|
|
5
|
+
export const ENTITY_KIND = 'root';
|
|
6
|
+
export const ENTITY_NAME = 'abilities';
|
|
7
|
+
export const ENTITY_NAME_CATEGORIES = 'ability-categories';
|
|
8
|
+
|
|
9
|
+
// Action types
|
|
10
|
+
export const RECEIVE_ABILITIES = 'RECEIVE_ABILITIES';
|
|
11
|
+
export const REGISTER_ABILITY = 'REGISTER_ABILITY';
|
|
12
|
+
export const UNREGISTER_ABILITY = 'UNREGISTER_ABILITY';
|
|
13
|
+
export const RECEIVE_CATEGORIES = 'RECEIVE_CATEGORIES';
|
|
14
|
+
export const REGISTER_ABILITY_CATEGORY = 'REGISTER_ABILITY_CATEGORY';
|
|
15
|
+
export const UNREGISTER_ABILITY_CATEGORY = 'UNREGISTER_ABILITY_CATEGORY';
|