@griddo/ax 11.15.9 → 11.16.0-rc.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 (95) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/components/RestrictionsModal/CurrentAccessList/CurrentAccessList.test.tsx +173 -0
  3. package/src/__tests__/components/RestrictionsModal/OptionsStep/OptionsStep.test.tsx +160 -0
  4. package/src/__tests__/components/RestrictionsModal/RestrictionsModal.test.tsx +134 -0
  5. package/src/__tests__/components/RestrictionsModal/RolesList/RolesList.test.tsx +234 -0
  6. package/src/__tests__/components/RestrictionsModal/UsersList/UsersList.test.tsx +288 -0
  7. package/src/__tests__/hooks/users.test.tsx +382 -0
  8. package/src/api/pages.tsx +24 -1
  9. package/src/api/sites.tsx +14 -14
  10. package/src/api/structuredData.tsx +17 -7
  11. package/src/components/Browser/index.tsx +12 -0
  12. package/src/components/Browser/style.tsx +5 -1
  13. package/src/components/ConfigPanel/Form/index.tsx +11 -10
  14. package/src/components/ContentItem/index.tsx +31 -0
  15. package/src/components/ContentItem/style.tsx +43 -0
  16. package/src/components/Fields/ImageField/index.tsx +2 -1
  17. package/src/components/Fields/RadioField/index.tsx +3 -3
  18. package/src/components/Fields/RadioField/style.tsx +1 -1
  19. package/src/components/FloatingNote/index.tsx +12 -1
  20. package/src/components/FloatingNote/style.tsx +2 -1
  21. package/src/components/Modal/style.tsx +11 -0
  22. package/src/components/PageFinder/SelectionListItem/index.tsx +27 -17
  23. package/src/components/PageFinder/index.tsx +65 -28
  24. package/src/components/PageFinder/style.tsx +0 -1
  25. package/src/components/RestrictionsModal/CurrentAccessList/index.tsx +74 -0
  26. package/src/components/RestrictionsModal/CurrentAccessList/style.tsx +155 -0
  27. package/src/components/RestrictionsModal/OptionsStep/index.tsx +55 -0
  28. package/src/components/RestrictionsModal/OptionsStep/style.tsx +29 -0
  29. package/src/components/RestrictionsModal/RolesList/RoleItem/index.tsx +54 -0
  30. package/src/components/RestrictionsModal/RolesList/RoleItem/style.tsx +34 -0
  31. package/src/components/RestrictionsModal/RolesList/index.tsx +107 -0
  32. package/src/components/RestrictionsModal/RolesList/style.tsx +76 -0
  33. package/src/components/RestrictionsModal/UsersList/UserItem/index.tsx +78 -0
  34. package/src/components/RestrictionsModal/UsersList/UserItem/style.tsx +73 -0
  35. package/src/components/RestrictionsModal/UsersList/index.tsx +151 -0
  36. package/src/components/RestrictionsModal/UsersList/style.tsx +55 -0
  37. package/src/components/RestrictionsModal/index.tsx +181 -0
  38. package/src/components/RestrictionsModal/style.tsx +31 -0
  39. package/src/components/RestrictionsModal/utils.tsx +24 -0
  40. package/src/components/ScheduleModal/index.tsx +32 -8
  41. package/src/components/SelectionComponent/index.tsx +65 -0
  42. package/src/components/SelectionComponent/style.tsx +69 -0
  43. package/src/components/TableFilters/RoleFilter/index.tsx +3 -3
  44. package/src/components/TableList/TableItem/style.tsx +7 -13
  45. package/src/components/Tabs/index.tsx +2 -1
  46. package/src/components/UserRolesAndSites/index.tsx +2 -2
  47. package/src/components/index.tsx +6 -0
  48. package/src/containers/PageEditor/actions.tsx +43 -0
  49. package/src/containers/PageEditor/constants.tsx +1 -0
  50. package/src/containers/PageEditor/interfaces.tsx +26 -19
  51. package/src/containers/PageEditor/reducer.tsx +32 -19
  52. package/src/containers/Users/reducer.tsx +1 -0
  53. package/src/helpers/forms.tsx +3 -1
  54. package/src/hooks/index.tsx +2 -1
  55. package/src/hooks/users.tsx +154 -14
  56. package/src/modules/ActivityLog/LogFilters/UserFilter/index.tsx +11 -12
  57. package/src/modules/Content/PageItem/index.tsx +17 -10
  58. package/src/modules/Content/index.tsx +1 -3
  59. package/src/modules/GlobalEditor/Editor/index.tsx +6 -0
  60. package/src/modules/GlobalEditor/PageBrowser/index.tsx +6 -0
  61. package/src/modules/GlobalEditor/Preview/index.tsx +4 -2
  62. package/src/modules/GlobalEditor/index.tsx +72 -45
  63. package/src/modules/Navigation/Menus/List/Table/ConfigPanel/Form/index.tsx +3 -5
  64. package/src/modules/Navigation/Menus/List/Table/ConfigPanel/index.tsx +4 -4
  65. package/src/modules/Navigation/Menus/List/Table/SidePanel/Form/ConnectedField/index.tsx +2 -3
  66. package/src/modules/Navigation/Menus/List/Table/SidePanel/Form/index.tsx +3 -4
  67. package/src/modules/Navigation/Menus/List/Table/SidePanel/index.tsx +5 -6
  68. package/src/modules/PageEditor/Editor/index.tsx +6 -0
  69. package/src/modules/PageEditor/PageBrowser/index.tsx +6 -0
  70. package/src/modules/PageEditor/Preview/index.tsx +9 -2
  71. package/src/modules/PageEditor/atoms.tsx +110 -3
  72. package/src/modules/PageEditor/index.tsx +76 -95
  73. package/src/modules/Settings/Integrations/IntegrationForm/index.tsx +1 -3
  74. package/src/modules/Sites/SitesList/GridView/GridSiteItem/style.tsx +19 -2
  75. package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/index.tsx +2 -2
  76. package/src/modules/Sites/SitesList/WelcomeModal/utils.ts +2 -2
  77. package/src/modules/StructuredData/Form/ConnectedField/index.tsx +2 -2
  78. package/src/modules/StructuredData/Form/index.tsx +120 -75
  79. package/src/modules/StructuredData/Form/style.tsx +7 -1
  80. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +15 -8
  81. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +15 -9
  82. package/src/modules/StructuredData/{StructuredDataList/StructuredDataItem/utils.tsx → utils.tsx} +16 -2
  83. package/src/modules/Users/Profile/atoms.tsx +62 -4
  84. package/src/modules/Users/Profile/index.tsx +83 -4
  85. package/src/modules/Users/Profile/style.tsx +60 -0
  86. package/src/modules/Users/UserCreate/SiteItem/RolesModal/RoleItem/index.tsx +3 -5
  87. package/src/modules/Users/UserCreate/SiteItem/index.tsx +2 -2
  88. package/src/modules/Users/UserCreate/atoms.tsx +69 -0
  89. package/src/modules/Users/UserCreate/index.tsx +149 -70
  90. package/src/modules/Users/UserCreate/style.tsx +24 -1
  91. package/src/modules/Users/UserForm/index.tsx +260 -146
  92. package/src/modules/Users/UserForm/style.tsx +13 -1
  93. package/src/modules/Users/UserList/UserItem/index.tsx +3 -3
  94. package/src/types/index.tsx +19 -6
  95. package/src/types/logs.tsx +1 -1
@@ -0,0 +1,382 @@
1
+ import "@testing-library/jest-dom";
2
+
3
+ import * as reactRedux from "react-redux";
4
+
5
+ import { usePermissionsForPage } from "@ax/hooks";
6
+ import type { PageAccessGrantsSummary } from "@ax/types";
7
+
8
+ import { renderHook } from "@testing-library/react";
9
+
10
+ jest.mock("react-redux", () => {
11
+ const actual = jest.requireActual("react-redux");
12
+ return {
13
+ ...actual,
14
+ useSelector: jest.fn(),
15
+ };
16
+ });
17
+
18
+ const { useSelector } = reactRedux;
19
+
20
+ describe("usePermissionsForPage hook", () => {
21
+ const mockRole = {
22
+ id: 1,
23
+ name: "Editor",
24
+ permissions: {
25
+ sitePermissions: [{ key: "edit_page" }, { key: "delete_page" }],
26
+ globalPermissions: [{ key: "manage_users" }],
27
+ },
28
+ };
29
+
30
+ const mockUser = {
31
+ id: 1,
32
+ email: "user@test.com",
33
+ isSuperAdmin: false,
34
+ roles: [{ siteId: 1, roles: [1] }],
35
+ pagePermissions: undefined,
36
+ };
37
+
38
+ const mockSuperAdmin = {
39
+ ...mockUser,
40
+ isSuperAdmin: true,
41
+ };
42
+
43
+ const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
44
+
45
+ const setupMockState = (state: any) => {
46
+ mockUseSelector.mockImplementation((selector: any) => selector(state));
47
+ };
48
+
49
+ beforeEach(() => {
50
+ jest.clearAllMocks();
51
+ });
52
+
53
+ describe("1. User authentication", () => {
54
+ it("should deny all permissions when user is not authenticated", () => {
55
+ setupMockState({ users: { currentUser: null, roles: [mockRole] } });
56
+
57
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page", delete: "delete_page" }));
58
+
59
+ expect(result.current.edit).toBe(false);
60
+ expect(result.current.delete).toBe(false);
61
+ });
62
+ });
63
+
64
+ describe("2. Super admin", () => {
65
+ it("should grant all permissions to super admin", () => {
66
+ setupMockState({ users: { currentUser: mockSuperAdmin, roles: [mockRole] } });
67
+
68
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page", delete: "delete_page" }));
69
+
70
+ expect(result.current.edit).toBe(true);
71
+ expect(result.current.delete).toBe(true);
72
+ });
73
+ });
74
+
75
+ describe("3. Site roles", () => {
76
+ it("should deny all permissions when user has no roles for the site", () => {
77
+ const userWithoutRoles = { ...mockUser, roles: [] };
78
+ setupMockState({ users: { currentUser: userWithoutRoles, roles: [mockRole] } });
79
+
80
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }));
81
+
82
+ expect(result.current.edit).toBe(false);
83
+ });
84
+
85
+ it("should allow access when user has roles for the site", () => {
86
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
87
+
88
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }));
89
+
90
+ expect(result.current.edit).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe("4. Page permission access (pagePermissions)", () => {
95
+ it("should allow access when user has no pagePermissions restrictions", () => {
96
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
97
+
98
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }));
99
+
100
+ expect(result.current.edit).toBe(true);
101
+ });
102
+
103
+ it("should deny access when site is not in pagePermissions (user is restricted to other sites)", () => {
104
+ const userWithPagePerms = {
105
+ ...mockUser,
106
+ pagePermissions: [{ siteId: 2, pageIds: [5] }],
107
+ };
108
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
109
+
110
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }));
111
+
112
+ expect(result.current.edit).toBe(false);
113
+ });
114
+
115
+ it("should allow access when site is not in pagePermissions but user is in accessGrants", () => {
116
+ const userWithPagePerms = {
117
+ ...mockUser,
118
+ pagePermissions: [{ siteId: 2, pageIds: [5] }],
119
+ };
120
+ const accessGrants: PageAccessGrantsSummary = { users: [1], roles: [] };
121
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
122
+
123
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, accessGrants));
124
+
125
+ expect(result.current.edit).toBe(true);
126
+ });
127
+
128
+ it("should deny access when site is in pagePermissions but page is not in pageIds", () => {
129
+ const userWithPagePerms = {
130
+ ...mockUser,
131
+ pagePermissions: [{ siteId: 1, pageIds: [5, 6] }],
132
+ };
133
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
134
+
135
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }));
136
+
137
+ expect(result.current.edit).toBe(false);
138
+ });
139
+
140
+ it("should allow access when page is in pagePermissions list", () => {
141
+ const userWithPagePerms = {
142
+ ...mockUser,
143
+ pagePermissions: [{ siteId: 1, pageIds: [1, 2, 3] }],
144
+ };
145
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
146
+
147
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }));
148
+
149
+ expect(result.current.edit).toBe(true);
150
+ });
151
+
152
+ it("should allow access with global pagePermissions entry", () => {
153
+ const userWithGlobalPagePerms = {
154
+ ...mockUser,
155
+ pagePermissions: [{ siteId: "global", pageIds: [1, 2] }],
156
+ };
157
+ setupMockState({ users: { currentUser: userWithGlobalPagePerms, roles: [mockRole] } });
158
+
159
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }));
160
+
161
+ expect(result.current.edit).toBe(true);
162
+ });
163
+ });
164
+
165
+ describe("5. Access grants", () => {
166
+ it("should allow access when no accessGrants are defined", () => {
167
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
168
+
169
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, undefined));
170
+
171
+ expect(result.current.edit).toBe(true);
172
+ });
173
+
174
+ it("should allow access when accessGrants is empty", () => {
175
+ const emptyAccessGrants: PageAccessGrantsSummary = { users: [], roles: [] };
176
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
177
+
178
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, emptyAccessGrants));
179
+
180
+ expect(result.current.edit).toBe(true);
181
+ });
182
+
183
+ it("should allow access when user is in accessGrants.users", () => {
184
+ const accessGrants: PageAccessGrantsSummary = { users: [1, 2, 3], roles: [] };
185
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
186
+
187
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, accessGrants));
188
+
189
+ expect(result.current.edit).toBe(true);
190
+ });
191
+
192
+ it("should allow access when user role is in accessGrants.roles", () => {
193
+ const accessGrants: PageAccessGrantsSummary = { users: [], roles: [1, 2, 3] };
194
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
195
+
196
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, accessGrants));
197
+
198
+ expect(result.current.edit).toBe(true);
199
+ });
200
+
201
+ it("should deny access when user is not in accessGrants and no pagePermissionAccess", () => {
202
+ const userWithPageRestriction = {
203
+ ...mockUser,
204
+ pagePermissions: [{ siteId: 1, pageIds: [2, 3] }], // Different pages
205
+ };
206
+ const accessGrants: PageAccessGrantsSummary = { users: [2, 3], roles: [2, 3] };
207
+ setupMockState({ users: { currentUser: userWithPageRestriction, roles: [mockRole] } });
208
+
209
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, accessGrants));
210
+
211
+ expect(result.current.edit).toBe(false);
212
+ });
213
+
214
+ it("should allow access when accessGrants denies but hasPagePermissionAccess is true", () => {
215
+ const userWithPagePerms = {
216
+ ...mockUser,
217
+ pagePermissions: [{ siteId: 1, pageIds: [1] }],
218
+ };
219
+ const accessGrants: PageAccessGrantsSummary = { users: [2, 3], roles: [2, 3] };
220
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
221
+
222
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, accessGrants));
223
+
224
+ expect(result.current.edit).toBe(true);
225
+ });
226
+
227
+ it("should allow access when user has no pagePermissions and accessGrants is defined but user is not in list", () => {
228
+ const accessGrants: PageAccessGrantsSummary = { users: [2, 3], roles: [2, 3] };
229
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
230
+
231
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, accessGrants));
232
+
233
+ expect(result.current.edit).toBe(true);
234
+ });
235
+
236
+ it("should allow access when user is restricted by pagePermissions but in accessGrants for the page", () => {
237
+ const userWithPagePerms = {
238
+ ...mockUser,
239
+ pagePermissions: [{ siteId: 1, pageIds: [2, 3] }], // Page 1 not in list
240
+ };
241
+ const accessGrants: PageAccessGrantsSummary = { users: [1], roles: [] }; // User 1 in accessGrants
242
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
243
+
244
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }, accessGrants));
245
+
246
+ expect(result.current.edit).toBe(true);
247
+ });
248
+ });
249
+
250
+ describe("6. Role permissions", () => {
251
+ it("should check specific permissions against role permissions", () => {
252
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
253
+
254
+ const { result } = renderHook(() =>
255
+ usePermissionsForPage(1, 1, {
256
+ edit: "edit_page",
257
+ delete: "delete_page",
258
+ publish: "publish_page",
259
+ }),
260
+ );
261
+
262
+ expect(result.current.edit).toBe(true);
263
+ expect(result.current.delete).toBe(true);
264
+ expect(result.current.publish).toBe(false);
265
+ });
266
+
267
+ it("should use global permissions when siteId is global", () => {
268
+ const userWithGlobalRoles = {
269
+ ...mockUser,
270
+ roles: [{ siteId: "global", roles: [1] }],
271
+ };
272
+ setupMockState({ users: { currentUser: userWithGlobalRoles, roles: [mockRole] } });
273
+
274
+ const { result } = renderHook(() =>
275
+ usePermissionsForPage("global", 1, {
276
+ manageUsers: "manage_users",
277
+ editPage: "edit_page",
278
+ }),
279
+ );
280
+
281
+ expect(result.current.manageUsers).toBe(true);
282
+ expect(result.current.editPage).toBe(false);
283
+ });
284
+
285
+ it("should support array of permission keys (OR logic)", () => {
286
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
287
+
288
+ const { result } = renderHook(() =>
289
+ usePermissionsForPage(1, 1, {
290
+ canModify: ["edit_page", "publish_page"],
291
+ }),
292
+ );
293
+
294
+ expect(result.current.canModify).toBe(true);
295
+ });
296
+ });
297
+
298
+ describe("7. Page languages (translations)", () => {
299
+ it("should allow access to a translation if user has access to any translation in the group", () => {
300
+ const userWithPagePerms = {
301
+ ...mockUser,
302
+ pagePermissions: [{ siteId: 1, pageIds: [100] }],
303
+ };
304
+ const pageLanguages = [
305
+ { pageId: 100, languageId: 1 }, // English
306
+ { pageId: 200, languageId: 2 }, // Spanish
307
+ { pageId: 300, languageId: 3 }, // French
308
+ ];
309
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
310
+
311
+ // User has access to pageId 100 (English), should also have access to 200 (Spanish)
312
+ const { result } = renderHook(() => usePermissionsForPage(1, 200, { edit: "edit_page" }, undefined, pageLanguages));
313
+
314
+ expect(result.current.edit).toBe(true);
315
+ });
316
+
317
+ it("should deny access to a translation if user has no access to any translation in the group", () => {
318
+ const userWithPagePerms = {
319
+ ...mockUser,
320
+ pagePermissions: [{ siteId: 1, pageIds: [999] }],
321
+ };
322
+ const pageLanguages = [
323
+ { pageId: 100, languageId: 1 },
324
+ { pageId: 200, languageId: 2 },
325
+ { pageId: 300, languageId: 3 },
326
+ ];
327
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
328
+
329
+ // User has no access to any translation in the group
330
+ const { result } = renderHook(() => usePermissionsForPage(1, 200, { edit: "edit_page" }, undefined, pageLanguages));
331
+
332
+ expect(result.current.edit).toBe(false);
333
+ });
334
+
335
+ it("should work without pageLanguages (backward compatibility)", () => {
336
+ const userWithPagePerms = {
337
+ ...mockUser,
338
+ pagePermissions: [{ siteId: 1, pageIds: [1] }],
339
+ };
340
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
341
+
342
+ const { result } = renderHook(() => usePermissionsForPage(1, 1, { edit: "edit_page" }));
343
+
344
+ expect(result.current.edit).toBe(true);
345
+ });
346
+
347
+ it("should allow access to translation via accessGrants", () => {
348
+ const userWithPagePerms = {
349
+ ...mockUser,
350
+ pagePermissions: [{ siteId: 1, pageIds: [999] }], // No access to translation group
351
+ };
352
+ const accessGrants: PageAccessGrantsSummary = { users: [1], roles: [] };
353
+ const pageLanguages = [
354
+ { pageId: 100, languageId: 1 },
355
+ { pageId: 200, languageId: 2 },
356
+ ];
357
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
358
+
359
+ // User not in translation group perms but is in accessGrants
360
+ const { result } = renderHook(() => usePermissionsForPage(1, 200, { edit: "edit_page" }, accessGrants, pageLanguages));
361
+
362
+ expect(result.current.edit).toBe(true);
363
+ });
364
+
365
+ it("should deny access when pageId is not part of any translation group", () => {
366
+ const userWithPagePerms = {
367
+ ...mockUser,
368
+ pagePermissions: [{ siteId: 1, pageIds: [100] }],
369
+ };
370
+ const pageLanguages = [
371
+ { pageId: 100, languageId: 1 },
372
+ { pageId: 200, languageId: 2 },
373
+ ];
374
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
375
+
376
+ // pageId 999 is not in pageLanguages, so treat it as a regular page
377
+ const { result } = renderHook(() => usePermissionsForPage(1, 999, { edit: "edit_page" }, undefined, pageLanguages));
378
+
379
+ expect(result.current.edit).toBe(false);
380
+ });
381
+ });
382
+ });
package/src/api/pages.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import type { IGetPagesParams } from "@ax/types";
1
+ import type { IGetPagesParams, PageAccessGrantsSummary } from "@ax/types";
2
2
 
3
3
  import type { AxiosResponse } from "axios";
4
4
 
@@ -99,6 +99,11 @@ const SERVICES: { [key: string]: IServiceConfig } = {
99
99
  endpoint: "/translations/page/",
100
100
  method: "POST",
101
101
  },
102
+ SET_PAGE_ACCESS_GRANTS: {
103
+ ...template,
104
+ endpoint: ["/site/", "/pages/", "/access-grant"],
105
+ method: "PUT",
106
+ },
102
107
  };
103
108
 
104
109
  const getPages = async (params: IGetPagesParams, filterQuery = ""): Promise<AxiosResponse> => {
@@ -280,6 +285,23 @@ const getPageTranslation = async (data: any, langID: number) => {
280
285
  return sendRequest(SERVICES.PAGE_AI_TRANSLATION, { ...data });
281
286
  };
282
287
 
288
+ const setPageAccessGrants = async (data: {
289
+ siteID: number | "global";
290
+ pageID: number;
291
+ accessGrants: PageAccessGrantsSummary;
292
+ }) => {
293
+ const { siteID, pageID, accessGrants } = data;
294
+
295
+ const {
296
+ host,
297
+ endpoint: [prefix, middle, suffix],
298
+ } = SERVICES.SET_PAGE_ACCESS_GRANTS;
299
+
300
+ SERVICES.SET_PAGE_ACCESS_GRANTS.dynamicUrl = `${host}${prefix}${siteID}${middle}${pageID}${suffix}`;
301
+
302
+ return sendRequest(SERVICES.SET_PAGE_ACCESS_GRANTS, { ...accessGrants });
303
+ };
304
+
283
305
  export default {
284
306
  getPages,
285
307
  getPageInfo,
@@ -299,4 +321,5 @@ export default {
299
321
  pageCheck,
300
322
  getPageSummary,
301
323
  getPageTranslation,
324
+ setPageAccessGrants,
302
325
  };
package/src/api/sites.tsx CHANGED
@@ -1,7 +1,9 @@
1
- import type { AxiosResponse } from "axios";
2
1
  import type { IGetGlobalPagesParams, IGetSitePagesParams, IGetSitesParams, ISite } from "@ax/types";
2
+
3
+ import type { AxiosResponse } from "axios";
4
+
3
5
  import { template } from "./config";
4
- import { sendRequest, type IServiceConfig, sendInitialRequest } from "./utils";
6
+ import { type IServiceConfig, sendInitialRequest, sendRequest } from "./utils";
5
7
 
6
8
  const SERVICES: { [key: string]: IServiceConfig } = {
7
9
  GET_ALL_SITES: {
@@ -168,9 +170,9 @@ const getStructuredSitePages = async (params: IGetSitePagesParams, structuredDat
168
170
  SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl = `${host}${prefix}${siteID}${suffix}${structuredData}?deleted=${deleted}&page=${page}&itemsPerPage=${itemsPerPage}${filterQuery}`;
169
171
 
170
172
  if (query && query.trim() !== "")
171
- SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl = SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl + `&query=${query}`;
173
+ SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl = `${SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl}&query=${query}`;
172
174
  if (format)
173
- SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl = SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl + `&format=${format}`;
175
+ SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl = `${SERVICES.GET_SITE_STRUCTURED_PAGES.dynamicUrl}&format=${format}`;
174
176
 
175
177
  return sendRequest(SERVICES.GET_SITE_STRUCTURED_PAGES);
176
178
  };
@@ -187,17 +189,15 @@ const getSitePages = async (params: IGetSitePagesParams, filterQuery = ""): Prom
187
189
  SERVICES.GET_SITE_PAGES.dynamicUrl = `${host}${prefix}${siteID}${suffix}?deleted=${deleted}${filterQuery}`;
188
190
 
189
191
  if (page && itemsPerPage)
190
- SERVICES.GET_SITE_PAGES.dynamicUrl =
191
- SERVICES.GET_SITE_PAGES.dynamicUrl + `&page=${page}&itemsPerPage=${itemsPerPage}`;
192
+ SERVICES.GET_SITE_PAGES.dynamicUrl = `${SERVICES.GET_SITE_PAGES.dynamicUrl}&page=${page}&itemsPerPage=${itemsPerPage}`;
192
193
  if (query && query.trim() !== "")
193
- SERVICES.GET_SITE_PAGES.dynamicUrl = SERVICES.GET_SITE_PAGES.dynamicUrl + `&query=${query}`;
194
+ SERVICES.GET_SITE_PAGES.dynamicUrl = `${SERVICES.GET_SITE_PAGES.dynamicUrl}&query=${query}`;
194
195
  if (filterStructuredData)
195
- SERVICES.GET_SITE_PAGES.dynamicUrl =
196
- SERVICES.GET_SITE_PAGES.dynamicUrl + `&filterStructuredData=${filterStructuredData}`;
197
- if (format) SERVICES.GET_SITE_PAGES.dynamicUrl = SERVICES.GET_SITE_PAGES.dynamicUrl + `&format=${format}`;
196
+ SERVICES.GET_SITE_PAGES.dynamicUrl = `${SERVICES.GET_SITE_PAGES.dynamicUrl}&filterStructuredData=${filterStructuredData}`;
197
+ if (format) SERVICES.GET_SITE_PAGES.dynamicUrl = `${SERVICES.GET_SITE_PAGES.dynamicUrl}&format=${format}`;
198
198
  if (filterPages)
199
- SERVICES.GET_SITE_PAGES.dynamicUrl = SERVICES.GET_SITE_PAGES.dynamicUrl + `&filterPages=${filterPages.join(",")}`;
200
- if (ignoreLang) SERVICES.GET_SITE_PAGES.dynamicUrl = SERVICES.GET_SITE_PAGES.dynamicUrl + `&ignoreLang=${ignoreLang}`;
199
+ SERVICES.GET_SITE_PAGES.dynamicUrl = `${SERVICES.GET_SITE_PAGES.dynamicUrl}&filterPages=${filterPages.join(",")}`;
200
+ if (ignoreLang) SERVICES.GET_SITE_PAGES.dynamicUrl = `${SERVICES.GET_SITE_PAGES.dynamicUrl}&ignoreLang=${ignoreLang}`;
201
201
 
202
202
  const dataHeader = {
203
203
  ...(lang && { lang }),
@@ -215,7 +215,7 @@ const getGlobalPages = async (params: IGetGlobalPagesParams, filterQuery?: strin
215
215
  if (query && query.trim() !== "") SERVICES.GET_GLOBAL_PAGES.dynamicUrl += `&query=${query}`;
216
216
  if (filterStructuredData) SERVICES.GET_GLOBAL_PAGES.dynamicUrl += `&filterStructuredData=${filterStructuredData}`;
217
217
  if (filterQuery) SERVICES.GET_GLOBAL_PAGES.dynamicUrl += filterQuery;
218
- if (format) SERVICES.GET_GLOBAL_PAGES.dynamicUrl = SERVICES.GET_GLOBAL_PAGES.dynamicUrl + `&format=${format}`;
218
+ if (format) SERVICES.GET_GLOBAL_PAGES.dynamicUrl = `${SERVICES.GET_GLOBAL_PAGES.dynamicUrl}&format=${format}`;
219
219
 
220
220
  return sendRequest(SERVICES.GET_GLOBAL_PAGES);
221
221
  };
@@ -325,7 +325,7 @@ const getSiteImages = async (
325
325
  const items = itemsPerPage ? `&itemsPerPage=${itemsPerPage}` : "";
326
326
  const thumb = thumbWidth && thumbHeight ? `&thumbWidth=${thumbWidth}&thumbHeight=${thumbHeight}` : "";
327
327
  SERVICES.GET_SITE_IMAGES.dynamicUrl = `${host}${prefix}${site}${suffix}?page=${page}${items}${thumb}${query}`;
328
- if (search) SERVICES.GET_SITE_IMAGES.dynamicUrl = SERVICES.GET_SITE_IMAGES.dynamicUrl + `&query=${search}`;
328
+ if (search) SERVICES.GET_SITE_IMAGES.dynamicUrl = `${SERVICES.GET_SITE_IMAGES.dynamicUrl}&query=${search}`;
329
329
 
330
330
  return sendRequest(SERVICES.GET_SITE_IMAGES);
331
331
  };
@@ -1,7 +1,14 @@
1
+ import type {
2
+ ICategoryGroupParams,
3
+ IExportDataParams,
4
+ IGetStructuredDataParams,
5
+ IOrderCategoryParams,
6
+ } from "@ax/types";
7
+
8
+ import type { AxiosResponse } from "axios";
9
+
1
10
  import { template } from "./config";
2
- import { IServiceConfig, sendRequest, sendInitialRequest } from "./utils";
3
- import { ICategoryGroupParams, IExportDataParams, IGetStructuredDataParams, IOrderCategoryParams } from "@ax/types";
4
- import { AxiosResponse } from "axios";
11
+ import { type IServiceConfig, sendInitialRequest, sendRequest } from "./utils";
5
12
 
6
13
  const SERVICES: { [key: string]: IServiceConfig } = {
7
14
  GET_DATA: {
@@ -127,7 +134,7 @@ const SERVICES: { [key: string]: IServiceConfig } = {
127
134
  },
128
135
  };
129
136
 
130
- const getData = (token: string | null, siteID?: number | null): Promise<AxiosResponse> => {
137
+ const getData = (token: string | null, siteID?: number | null, lang?: number): Promise<AxiosResponse> => {
131
138
  const { host, endpoint } = SERVICES.GET_DATA;
132
139
 
133
140
  if (siteID) {
@@ -136,10 +143,13 @@ const getData = (token: string | null, siteID?: number | null): Promise<AxiosRes
136
143
  SERVICES.GET_DATA.dynamicUrl = "";
137
144
  }
138
145
 
146
+ const dataHeader = {} as { lang: number };
147
+ if (lang) dataHeader.lang = lang;
148
+
139
149
  if (token) {
140
- return sendInitialRequest(SERVICES.GET_DATA, token);
150
+ return sendInitialRequest(SERVICES.GET_DATA, token, dataHeader);
141
151
  } else {
142
- return sendRequest(SERVICES.GET_DATA, {});
152
+ return sendRequest(SERVICES.GET_DATA, null, dataHeader);
143
153
  }
144
154
  };
145
155
 
@@ -267,7 +277,7 @@ const setDataStatusBulk = (dataContentID: number[], status: string): Promise<Axi
267
277
  return sendRequest(SERVICES.SET_DATA_STATUS_BULK, { ids: dataContentID });
268
278
  };
269
279
 
270
- const getContentTypes = (siteID: number | null, lang?: number): Promise<AxiosResponse> => {
280
+ const getContentTypes = (siteID: number, lang?: number): Promise<AxiosResponse> => {
271
281
  const { host, endpoint } = SERVICES.GET_CONTENT_TYPES;
272
282
  const [prefix, suffix] = endpoint;
273
283
 
@@ -51,8 +51,10 @@ const Browser = (props: IBrowserProps): JSX.Element => {
51
51
  editorType = "page",
52
52
  headingFilter,
53
53
  keywordsFilter,
54
+ isRestrictedPage = false,
54
55
  toggleHeadingsPreview,
55
56
  toggleKeywordsPreview,
57
+ onRestrictionsClick,
56
58
  } = props;
57
59
 
58
60
  const { id, entity, haveDraftPage } = content;
@@ -294,6 +296,14 @@ const Browser = (props: IBrowserProps): JSX.Element => {
294
296
  )}
295
297
  {showIframe ? (
296
298
  <S.ContentWrapper>
299
+ {isRestrictedPage && (
300
+ <S.StyledFloatingNote
301
+ message="Only some users can edit this page."
302
+ icon="users"
303
+ $short={true}
304
+ onClick={onRestrictionsClick}
305
+ />
306
+ )}
297
307
  {floatingNoteConfig && (
298
308
  <S.StyledFloatingNote
299
309
  message={`You are viewing the ${floatingNoteConfig.label} editor${isCompact ? "" : " of this page"}`}
@@ -420,8 +430,10 @@ export interface IBrowserProps {
420
430
  editorType?: "form" | "page" | "headings" | "keywords";
421
431
  headingFilter?: HeadingFilter;
422
432
  keywordsFilter?: string[];
433
+ isRestrictedPage?: boolean;
423
434
  toggleHeadingsPreview?(): void;
424
435
  toggleKeywordsPreview?(): void;
436
+ onRestrictionsClick?(): void;
425
437
  actions?: {
426
438
  setSelectedContentAction(editorID: number): void;
427
439
  deleteModuleAction?(editorID: number[]): void;
@@ -108,12 +108,16 @@ const ContentWrapper = styled.div`
108
108
  min-height: 0;
109
109
  `;
110
110
 
111
- const StyledFloatingNote = styled(({ $compact: _, ...props }) => <FloatingNote {...props} />)<{ $compact?: boolean }>`
111
+ const StyledFloatingNote = styled(({ $compact: _, ...props }) => <FloatingNote {...props} />)<{
112
+ $compact?: boolean;
113
+ $short?: boolean;
114
+ }>`
112
115
  position: absolute;
113
116
  left: ${(p) => p.theme.spacing.s};
114
117
  top: ${(p) => p.theme.spacing.s};
115
118
  right: ${(p) => p.theme.spacing.s};
116
119
  ${(p) => p.$compact && `&& { padding: ${p.theme.spacing.xs} ${p.theme.spacing.s}; }`}
120
+ width: ${(p) => (p.$short ? "268px" : "auto")};
117
121
  `;
118
122
 
119
123
  const ResolutionWrapper = styled.div`
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useMemo, useState } from "react";
2
2
 
3
3
  import { Tabs } from "@ax/components";
4
4
  import { usePermission } from "@ax/hooks";
@@ -44,11 +44,14 @@ export const Form = (props: IFormProps): JSX.Element => {
44
44
  (!isGlobal && isAllowedToEditConfigPages) || (isGlobal && isAllowedToEditGlobalData);
45
45
  const isAllowedToEditPageSEO = (!isGlobal && isAllowedToEditSiteSeo) || (isGlobal && isAllowedToEditGlobalSeo);
46
46
 
47
- const permissionsByTab: Record<string, boolean | undefined> = {
48
- content: isAllowedToEditPageContent,
49
- config: isAllowedToEditPageConfig,
50
- seo: isAllowedToEditPageSEO,
51
- };
47
+ const permissionsByTab: Record<string, boolean | undefined> = useMemo(
48
+ () => ({
49
+ content: isAllowedToEditPageContent,
50
+ config: isAllowedToEditPageConfig,
51
+ seo: isAllowedToEditPageSEO,
52
+ }),
53
+ [isAllowedToEditPageContent, isAllowedToEditPageConfig, isAllowedToEditPageSEO],
54
+ );
52
55
 
53
56
  const tabContent = schema.configTabs.find((tab: ISchemaTab) => tab.title.toLowerCase() === selectedTab);
54
57
 
@@ -78,7 +81,7 @@ export const Form = (props: IFormProps): JSX.Element => {
78
81
  );
79
82
  };
80
83
 
81
- const getTabs = (): string[] => {
84
+ const tabs = useMemo(() => {
82
85
  const isHeader = schema.type === "header";
83
86
  const isPageSchema = schema.schemaType === "page";
84
87
 
@@ -95,9 +98,7 @@ export const Form = (props: IFormProps): JSX.Element => {
95
98
  return !isPageSchema || (permKey && permissionsByTab[permKey]);
96
99
  })
97
100
  .map((tab: ISchemaTab) => tab.title.toLowerCase());
98
- };
99
-
100
- const tabs = getTabs();
101
+ }, [schema, isPage, permissionsByTab]);
101
102
 
102
103
  const handleRestore = (type: "header" | "footer") => actions.restorePageNavigationAction?.(type);
103
104
 
@@ -0,0 +1,31 @@
1
+ import { getFormattedDateWithTimezone } from "@ax/helpers";
2
+
3
+ import * as S from "./style";
4
+
5
+ const ContentItem = (props: IContentItemProps): JSX.Element => {
6
+ const { title, type, date, slug } = props;
7
+
8
+ const pageType = type || "Page";
9
+
10
+ return (
11
+ <S.ListItem>
12
+ <S.Content data-testid="content-item">
13
+ <S.Header>
14
+ <S.Type>{pageType}</S.Type>
15
+ <S.ContentDate>{getFormattedDateWithTimezone(date, "d MMM Y")}</S.ContentDate>
16
+ </S.Header>
17
+ <S.Title>{title}</S.Title>
18
+ {slug && <S.Slug>{slug}</S.Slug>}
19
+ </S.Content>
20
+ </S.ListItem>
21
+ );
22
+ };
23
+
24
+ interface IContentItemProps {
25
+ title: string;
26
+ type: string;
27
+ date: Date;
28
+ slug?: string;
29
+ }
30
+
31
+ export default ContentItem;