@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE.md +788 -0
  3. package/README.md +302 -0
  4. package/build/api.js +189 -0
  5. package/build/api.js.map +7 -0
  6. package/build/index.js +53 -0
  7. package/build/index.js.map +7 -0
  8. package/build/store/actions.js +183 -0
  9. package/build/store/actions.js.map +7 -0
  10. package/build/store/constants.js +58 -0
  11. package/build/store/constants.js.map +7 -0
  12. package/build/store/index.js +77 -0
  13. package/build/store/index.js.map +7 -0
  14. package/build/store/reducer.js +126 -0
  15. package/build/store/reducer.js.map +7 -0
  16. package/build/store/resolvers.js +107 -0
  17. package/build/store/resolvers.js.map +7 -0
  18. package/build/store/selectors.js +64 -0
  19. package/build/store/selectors.js.map +7 -0
  20. package/build/store/tests/actions.test.js +792 -0
  21. package/build/store/tests/actions.test.js.map +7 -0
  22. package/build/store/tests/reducer.test.js +743 -0
  23. package/build/store/tests/reducer.test.js.map +7 -0
  24. package/build/store/tests/resolvers.test.js +520 -0
  25. package/build/store/tests/resolvers.test.js.map +7 -0
  26. package/build/store/tests/selectors.test.js +349 -0
  27. package/build/store/tests/selectors.test.js.map +7 -0
  28. package/build/tests/api.test.js +546 -0
  29. package/build/tests/api.test.js.map +7 -0
  30. package/build/tests/validation.test.js +453 -0
  31. package/build/tests/validation.test.js.map +7 -0
  32. package/build/types.js +19 -0
  33. package/build/types.js.map +7 -0
  34. package/build/validation.js +144 -0
  35. package/build/validation.js.map +7 -0
  36. package/build-module/api.js +146 -0
  37. package/build-module/api.js.map +7 -0
  38. package/build-module/index.js +28 -0
  39. package/build-module/index.js.map +7 -0
  40. package/build-module/store/actions.js +161 -0
  41. package/build-module/store/actions.js.map +7 -0
  42. package/build-module/store/constants.js +24 -0
  43. package/build-module/store/constants.js.map +7 -0
  44. package/build-module/store/index.js +47 -0
  45. package/build-module/store/index.js.map +7 -0
  46. package/build-module/store/reducer.js +112 -0
  47. package/build-module/store/reducer.js.map +7 -0
  48. package/build-module/store/resolvers.js +84 -0
  49. package/build-module/store/resolvers.js.map +7 -0
  50. package/build-module/store/selectors.js +36 -0
  51. package/build-module/store/selectors.js.map +7 -0
  52. package/build-module/store/tests/actions.test.js +804 -0
  53. package/build-module/store/tests/actions.test.js.map +7 -0
  54. package/build-module/store/tests/reducer.test.js +726 -0
  55. package/build-module/store/tests/reducer.test.js.map +7 -0
  56. package/build-module/store/tests/resolvers.test.js +523 -0
  57. package/build-module/store/tests/resolvers.test.js.map +7 -0
  58. package/build-module/store/tests/selectors.test.js +352 -0
  59. package/build-module/store/tests/selectors.test.js.map +7 -0
  60. package/build-module/tests/api.test.js +530 -0
  61. package/build-module/tests/api.test.js.map +7 -0
  62. package/build-module/tests/validation.test.js +451 -0
  63. package/build-module/tests/validation.test.js.map +7 -0
  64. package/build-module/types.js +1 -0
  65. package/build-module/types.js.map +7 -0
  66. package/build-module/validation.js +109 -0
  67. package/build-module/validation.js.map +7 -0
  68. package/build-types/api.d.ts +138 -0
  69. package/build-types/api.d.ts.map +1 -0
  70. package/build-types/index.d.ts +41 -0
  71. package/build-types/index.d.ts.map +1 -0
  72. package/build-types/store/actions.d.ts +76 -0
  73. package/build-types/store/actions.d.ts.map +1 -0
  74. package/build-types/store/constants.d.ts +14 -0
  75. package/build-types/store/constants.d.ts.map +1 -0
  76. package/build-types/store/index.d.ts +7 -0
  77. package/build-types/store/index.d.ts.map +1 -0
  78. package/build-types/store/reducer.d.ts +22 -0
  79. package/build-types/store/reducer.d.ts.map +1 -0
  80. package/build-types/store/resolvers.d.ts +47 -0
  81. package/build-types/store/resolvers.d.ts.map +1 -0
  82. package/build-types/store/selectors.d.ts +37 -0
  83. package/build-types/store/selectors.d.ts.map +1 -0
  84. package/build-types/types.d.ts +163 -0
  85. package/build-types/types.d.ts.map +1 -0
  86. package/build-types/validation.d.ts +23 -0
  87. package/build-types/validation.d.ts.map +1 -0
  88. package/package.json +52 -0
  89. package/src/api.ts +339 -0
  90. package/src/index.ts +65 -0
  91. package/src/store/actions.ts +247 -0
  92. package/src/store/constants.ts +15 -0
  93. package/src/store/index.ts +55 -0
  94. package/src/store/reducer.ts +184 -0
  95. package/src/store/resolvers.ts +151 -0
  96. package/src/store/selectors.ts +82 -0
  97. package/src/store/tests/actions.test.ts +950 -0
  98. package/src/store/tests/reducer.test.ts +861 -0
  99. package/src/store/tests/resolvers.test.ts +642 -0
  100. package/src/store/tests/selectors.test.ts +412 -0
  101. package/src/tests/api.test.ts +655 -0
  102. package/src/tests/validation.test.ts +532 -0
  103. package/src/types.ts +190 -0
  104. package/src/validation.ts +207 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { createReduxStore, register, dispatch } from '@wordpress/data';
5
+ import { store as coreStore } from '@wordpress/core-data';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import reducer from './reducer';
12
+ import * as actions from './actions';
13
+ import * as selectors from './selectors';
14
+ import * as resolvers from './resolvers';
15
+ import {
16
+ STORE_NAME,
17
+ ENTITY_KIND,
18
+ ENTITY_NAME,
19
+ ENTITY_NAME_CATEGORIES,
20
+ } from './constants';
21
+
22
+ /**
23
+ * The abilities store definition.
24
+ */
25
+ export const store = createReduxStore( STORE_NAME, {
26
+ reducer,
27
+ actions,
28
+ selectors,
29
+ resolvers,
30
+ } );
31
+
32
+ register( store );
33
+
34
+ dispatch( coreStore ).addEntities( [
35
+ {
36
+ name: ENTITY_NAME,
37
+ kind: ENTITY_KIND,
38
+ key: 'name',
39
+ baseURL: '/wp-abilities/v1/abilities',
40
+ baseURLParams: { context: 'edit' },
41
+ plural: 'abilities',
42
+ label: __( 'Abilities' ),
43
+ supportsPagination: true,
44
+ },
45
+ {
46
+ name: ENTITY_NAME_CATEGORIES,
47
+ kind: ENTITY_KIND,
48
+ key: 'slug',
49
+ baseURL: '/wp-abilities/v1/categories',
50
+ baseURLParams: { context: 'edit' },
51
+ plural: 'ability-categories',
52
+ label: __( 'Ability Categories' ),
53
+ supportsPagination: true,
54
+ },
55
+ ] );
@@ -0,0 +1,184 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { combineReducers } from '@wordpress/data';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type { Ability, AbilityCategory } from '../types';
10
+ import {
11
+ RECEIVE_ABILITIES,
12
+ REGISTER_ABILITY,
13
+ UNREGISTER_ABILITY,
14
+ RECEIVE_CATEGORIES,
15
+ REGISTER_ABILITY_CATEGORY,
16
+ UNREGISTER_ABILITY_CATEGORY,
17
+ } from './constants';
18
+
19
+ /**
20
+ * Valid keys for an Ability object.
21
+ * Used to filter out non-standard properties from server responses.
22
+ */
23
+ const ABILITY_KEYS = [
24
+ 'name',
25
+ 'label',
26
+ 'description',
27
+ 'category',
28
+ 'input_schema',
29
+ 'output_schema',
30
+ 'meta',
31
+ 'callback',
32
+ 'permissionCallback',
33
+ ] as const;
34
+
35
+ /**
36
+ * Valid keys for an AbilityCategory object.
37
+ * Used to filter out non-standard properties from server responses.
38
+ */
39
+ const CATEGORY_KEYS = [ 'slug', 'label', 'description', 'meta' ] as const;
40
+
41
+ /**
42
+ * Sanitizes an ability object to only include valid properties.
43
+ * This ensures consistent shape regardless of source (server/client).
44
+ *
45
+ * @param ability Raw ability object that may contain extra properties.
46
+ * @return Sanitized ability with only valid properties.
47
+ */
48
+ function sanitizeAbility( ability: any ): Ability {
49
+ return Object.keys( ability )
50
+ .filter(
51
+ ( key ) =>
52
+ ABILITY_KEYS.includes( key as any ) &&
53
+ ability[ key ] !== undefined
54
+ )
55
+ .reduce(
56
+ ( obj, key ) => ( { ...obj, [ key ]: ability[ key ] } ),
57
+ {} as Ability
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Sanitizes a category object to only include valid properties.
63
+ * This ensures consistent shape regardless of source.
64
+ *
65
+ * @param category Raw category object that may contain extra properties.
66
+ * @return Sanitized category with only valid properties.
67
+ */
68
+ function sanitizeCategory( category: any ): AbilityCategory {
69
+ return Object.keys( category )
70
+ .filter(
71
+ ( key ) =>
72
+ CATEGORY_KEYS.includes( key as any ) &&
73
+ category[ key ] !== undefined
74
+ )
75
+ .reduce(
76
+ ( obj, key ) => ( { ...obj, [ key ]: category[ key ] } ),
77
+ {} as AbilityCategory
78
+ );
79
+ }
80
+
81
+ interface AbilitiesAction {
82
+ type: string;
83
+ abilities?: Ability[];
84
+ ability?: Ability;
85
+ categories?: AbilityCategory[];
86
+ category?: AbilityCategory;
87
+ name?: string;
88
+ slug?: string;
89
+ }
90
+
91
+ const DEFAULT_STATE: Record< string, Ability > = {};
92
+
93
+ /**
94
+ * Reducer managing the abilities by name.
95
+ *
96
+ * @param state Current state.
97
+ * @param action Dispatched action.
98
+ * @return New state.
99
+ */
100
+ function abilitiesByName(
101
+ state: Record< string, Ability > = DEFAULT_STATE,
102
+ action: AbilitiesAction
103
+ ): Record< string, Ability > {
104
+ switch ( action.type ) {
105
+ case RECEIVE_ABILITIES: {
106
+ if ( ! action.abilities ) {
107
+ return state;
108
+ }
109
+ const newState: Record< string, Ability > = {};
110
+ action.abilities.forEach( ( ability ) => {
111
+ newState[ ability.name ] = sanitizeAbility( ability );
112
+ } );
113
+ return newState;
114
+ }
115
+ case REGISTER_ABILITY: {
116
+ if ( ! action.ability ) {
117
+ return state;
118
+ }
119
+ return {
120
+ ...state,
121
+ [ action.ability.name ]: sanitizeAbility( action.ability ),
122
+ };
123
+ }
124
+ case UNREGISTER_ABILITY: {
125
+ if ( ! action.name || ! state[ action.name ] ) {
126
+ return state;
127
+ }
128
+ const { [ action.name ]: _, ...newState } = state;
129
+ return newState;
130
+ }
131
+ default:
132
+ return state;
133
+ }
134
+ }
135
+
136
+ const DEFAULT_CATEGORIES_STATE: Record< string, AbilityCategory > = {};
137
+
138
+ /**
139
+ * Reducer managing the categories by slug.
140
+ *
141
+ * @param state Current state.
142
+ * @param action Dispatched action.
143
+ * @return New state.
144
+ */
145
+ function categoriesBySlug(
146
+ state: Record< string, AbilityCategory > = DEFAULT_CATEGORIES_STATE,
147
+ action: AbilitiesAction
148
+ ): Record< string, AbilityCategory > {
149
+ switch ( action.type ) {
150
+ case RECEIVE_CATEGORIES: {
151
+ if ( ! action.categories ) {
152
+ return state;
153
+ }
154
+ const newState: Record< string, AbilityCategory > = {};
155
+ action.categories.forEach( ( category ) => {
156
+ newState[ category.slug ] = sanitizeCategory( category );
157
+ } );
158
+ return newState;
159
+ }
160
+ case REGISTER_ABILITY_CATEGORY: {
161
+ if ( ! action.category ) {
162
+ return state;
163
+ }
164
+ return {
165
+ ...state,
166
+ [ action.category.slug ]: sanitizeCategory( action.category ),
167
+ };
168
+ }
169
+ case UNREGISTER_ABILITY_CATEGORY: {
170
+ if ( ! action.slug || ! state[ action.slug ] ) {
171
+ return state;
172
+ }
173
+ const { [ action.slug ]: _, ...newState } = state;
174
+ return newState;
175
+ }
176
+ default:
177
+ return state;
178
+ }
179
+ }
180
+
181
+ export default combineReducers( {
182
+ abilitiesByName,
183
+ categoriesBySlug,
184
+ } );
@@ -0,0 +1,151 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { store as coreStore } from '@wordpress/core-data';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type { Ability, AbilityCategory } from '../types';
10
+ import { ENTITY_KIND, ENTITY_NAME, ENTITY_NAME_CATEGORIES } from './constants';
11
+ import {
12
+ receiveAbilities,
13
+ receiveCategories,
14
+ registerAbility,
15
+ registerAbilityCategory,
16
+ } from './actions';
17
+
18
+ /**
19
+ * Resolver for getAbilities selector.
20
+ * Fetches all abilities from the server.
21
+ *
22
+ * The resolver only fetches once (without query args filter) and stores all abilities.
23
+ * Query args filtering handled client-side by the selector for better performance
24
+ * and to avoid multiple API requests when filtering by different categories.
25
+ */
26
+ export function getAbilities() {
27
+ // @ts-expect-error - registry types are not yet available
28
+ return async ( { dispatch, registry, select } ) => {
29
+ const existingAbilities = select.getAbilities();
30
+
31
+ // Check if we have any server-side abilities (abilities without callbacks)
32
+ // Client abilities have callbacks and are registered immediately on page load
33
+ // We only want to skip fetching if we've already fetched server abilities
34
+ const hasServerAbilities = existingAbilities.some(
35
+ ( ability: Ability ) => ! ability.callback
36
+ );
37
+
38
+ if ( hasServerAbilities ) {
39
+ return;
40
+ }
41
+
42
+ const abilities = await registry
43
+ .resolveSelect( coreStore )
44
+ .getEntityRecords( ENTITY_KIND, ENTITY_NAME, {
45
+ per_page: -1,
46
+ } );
47
+
48
+ dispatch( receiveAbilities( abilities || [] ) );
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Resolver for getAbility selector.
54
+ * Fetches a specific ability from the server if not already in store.
55
+ *
56
+ * @param name Ability name.
57
+ */
58
+ export function getAbility( name: string ) {
59
+ // @ts-expect-error - registry types are not yet available
60
+ return async ( { dispatch, registry, select } ) => {
61
+ // Check if ability already exists in store (i.e. client ability or already fetched)
62
+ const existingAbility = select.getAbility( name );
63
+ if ( existingAbility ) {
64
+ return;
65
+ }
66
+
67
+ try {
68
+ const ability = await registry
69
+ .resolveSelect( coreStore )
70
+ .getEntityRecord( ENTITY_KIND, ENTITY_NAME, name );
71
+
72
+ if ( ability ) {
73
+ await dispatch( registerAbility( ability ) );
74
+ }
75
+ } catch ( error ) {
76
+ // If ability doesn't exist or error, we'll return undefined from the selector
77
+ // eslint-disable-next-line no-console
78
+ console.debug( `Ability not found: ${ name }` );
79
+ }
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Resolver for getAbilityCategories selector.
85
+ * Fetches all categories from the server.
86
+ *
87
+ * The resolver only fetches once and stores all categories.
88
+ */
89
+ export function getAbilityCategories() {
90
+ // @ts-expect-error - registry types are not yet available
91
+ return async ( { dispatch, registry, select } ) => {
92
+ const existingCategories = select.getAbilityCategories();
93
+
94
+ // Check if we have any server-side categories (categories without meta._clientRegistered flag)
95
+ // Client categories have meta._clientRegistered=true and might be registered immediately
96
+ // We only want to skip fetching if we've already fetched server categories
97
+ const hasServerCategories = existingCategories.some(
98
+ ( category: AbilityCategory ) => ! category.meta?._clientRegistered
99
+ );
100
+
101
+ if ( hasServerCategories ) {
102
+ return;
103
+ }
104
+
105
+ const categories = await registry
106
+ .resolveSelect( coreStore )
107
+ .getEntityRecords( ENTITY_KIND, ENTITY_NAME_CATEGORIES, {
108
+ per_page: -1,
109
+ } );
110
+
111
+ dispatch( receiveCategories( categories || [] ) );
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Resolver for getAbilityCategory selector.
117
+ * Fetches a specific category from the server if not already in store.
118
+ *
119
+ * @param slug Category slug.
120
+ */
121
+ export function getAbilityCategory( slug: string ) {
122
+ // @ts-expect-error - registry types are not yet available
123
+ return async ( { dispatch, registry, select } ) => {
124
+ // Check if category already exists in store (either client-registered or server-fetched).
125
+ // This prevents unnecessary network requests while allowing client-side categories
126
+ // to be retrieved immediately without hitting the API.
127
+ const existingCategory = select.getAbilityCategory( slug );
128
+ if ( existingCategory ) {
129
+ return;
130
+ }
131
+
132
+ try {
133
+ const category = await registry
134
+ .resolveSelect( coreStore )
135
+ .getEntityRecord( ENTITY_KIND, ENTITY_NAME_CATEGORIES, slug );
136
+
137
+ if ( category ) {
138
+ await dispatch(
139
+ registerAbilityCategory( category.slug, {
140
+ label: category.label,
141
+ description: category.description,
142
+ meta: category.meta,
143
+ } )
144
+ );
145
+ }
146
+ } catch ( error ) {
147
+ // eslint-disable-next-line no-console
148
+ console.debug( `Category not found: ${ slug }` );
149
+ }
150
+ };
151
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { createSelector } from '@wordpress/data';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type {
10
+ Ability,
11
+ AbilityCategory,
12
+ AbilitiesQueryArgs,
13
+ AbilitiesState,
14
+ } from '../types';
15
+
16
+ /**
17
+ * Returns all registered abilities.
18
+ * Optionally filters by query arguments.
19
+ *
20
+ * @param state Store state.
21
+ * @param args Optional query arguments to filter. Defaults to empty object.
22
+ * @return Array of abilities.
23
+ */
24
+ export const getAbilities = createSelector(
25
+ (
26
+ state: AbilitiesState,
27
+ { category }: AbilitiesQueryArgs = {}
28
+ ): Ability[] => {
29
+ const abilities = Object.values( state.abilitiesByName );
30
+ if ( category ) {
31
+ return abilities.filter(
32
+ ( ability ) => ability.category === category
33
+ );
34
+ }
35
+ return abilities;
36
+ },
37
+ ( state: AbilitiesState, category?: string ) => [
38
+ state.abilitiesByName,
39
+ category,
40
+ ]
41
+ );
42
+
43
+ /**
44
+ * Returns a specific ability by name.
45
+ *
46
+ * @param state Store state.
47
+ * @param name Ability name.
48
+ * @return Ability object or undefined if not found.
49
+ */
50
+ export function getAbility(
51
+ state: AbilitiesState,
52
+ name: string
53
+ ): Ability | undefined {
54
+ return state.abilitiesByName[ name ];
55
+ }
56
+
57
+ /**
58
+ * Returns all registered ability categories.
59
+ *
60
+ * @param state Store state.
61
+ * @return Array of categories.
62
+ */
63
+ export const getAbilityCategories = createSelector(
64
+ ( state: AbilitiesState ): AbilityCategory[] => {
65
+ return Object.values( state.categoriesBySlug );
66
+ },
67
+ ( state: AbilitiesState ) => [ state.categoriesBySlug ]
68
+ );
69
+
70
+ /**
71
+ * Returns a specific ability category by slug.
72
+ *
73
+ * @param state Store state.
74
+ * @param slug Category slug.
75
+ * @return Category object or undefined if not found.
76
+ */
77
+ export function getAbilityCategory(
78
+ state: AbilitiesState,
79
+ slug: string
80
+ ): AbilityCategory | undefined {
81
+ return state.categoriesBySlug[ slug ];
82
+ }