@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.
- package/package.json +2 -2
- package/src/__tests__/components/RestrictionsModal/CurrentAccessList/CurrentAccessList.test.tsx +173 -0
- package/src/__tests__/components/RestrictionsModal/OptionsStep/OptionsStep.test.tsx +160 -0
- package/src/__tests__/components/RestrictionsModal/RestrictionsModal.test.tsx +134 -0
- package/src/__tests__/components/RestrictionsModal/RolesList/RolesList.test.tsx +234 -0
- package/src/__tests__/components/RestrictionsModal/UsersList/UsersList.test.tsx +288 -0
- package/src/__tests__/hooks/users.test.tsx +382 -0
- package/src/api/pages.tsx +24 -1
- package/src/api/sites.tsx +14 -14
- package/src/api/structuredData.tsx +17 -7
- package/src/components/Browser/index.tsx +12 -0
- package/src/components/Browser/style.tsx +5 -1
- package/src/components/ConfigPanel/Form/index.tsx +11 -10
- package/src/components/ContentItem/index.tsx +31 -0
- package/src/components/ContentItem/style.tsx +43 -0
- package/src/components/Fields/ImageField/index.tsx +2 -1
- package/src/components/Fields/RadioField/index.tsx +3 -3
- package/src/components/Fields/RadioField/style.tsx +1 -1
- package/src/components/FloatingNote/index.tsx +12 -1
- package/src/components/FloatingNote/style.tsx +2 -1
- package/src/components/Modal/style.tsx +11 -0
- package/src/components/PageFinder/SelectionListItem/index.tsx +27 -17
- package/src/components/PageFinder/index.tsx +65 -28
- package/src/components/PageFinder/style.tsx +0 -1
- package/src/components/RestrictionsModal/CurrentAccessList/index.tsx +74 -0
- package/src/components/RestrictionsModal/CurrentAccessList/style.tsx +155 -0
- package/src/components/RestrictionsModal/OptionsStep/index.tsx +55 -0
- package/src/components/RestrictionsModal/OptionsStep/style.tsx +29 -0
- package/src/components/RestrictionsModal/RolesList/RoleItem/index.tsx +54 -0
- package/src/components/RestrictionsModal/RolesList/RoleItem/style.tsx +34 -0
- package/src/components/RestrictionsModal/RolesList/index.tsx +107 -0
- package/src/components/RestrictionsModal/RolesList/style.tsx +76 -0
- package/src/components/RestrictionsModal/UsersList/UserItem/index.tsx +78 -0
- package/src/components/RestrictionsModal/UsersList/UserItem/style.tsx +73 -0
- package/src/components/RestrictionsModal/UsersList/index.tsx +151 -0
- package/src/components/RestrictionsModal/UsersList/style.tsx +55 -0
- package/src/components/RestrictionsModal/index.tsx +181 -0
- package/src/components/RestrictionsModal/style.tsx +31 -0
- package/src/components/RestrictionsModal/utils.tsx +24 -0
- package/src/components/ScheduleModal/index.tsx +32 -8
- package/src/components/SelectionComponent/index.tsx +65 -0
- package/src/components/SelectionComponent/style.tsx +69 -0
- package/src/components/TableFilters/RoleFilter/index.tsx +3 -3
- package/src/components/TableList/TableItem/style.tsx +7 -13
- package/src/components/Tabs/index.tsx +2 -1
- package/src/components/UserRolesAndSites/index.tsx +2 -2
- package/src/components/index.tsx +6 -0
- package/src/containers/PageEditor/actions.tsx +43 -0
- package/src/containers/PageEditor/constants.tsx +1 -0
- package/src/containers/PageEditor/interfaces.tsx +26 -19
- package/src/containers/PageEditor/reducer.tsx +32 -19
- package/src/containers/Users/reducer.tsx +1 -0
- package/src/helpers/forms.tsx +3 -1
- package/src/hooks/index.tsx +2 -1
- package/src/hooks/users.tsx +154 -14
- package/src/modules/ActivityLog/LogFilters/UserFilter/index.tsx +11 -12
- package/src/modules/Content/PageItem/index.tsx +17 -10
- package/src/modules/Content/index.tsx +1 -3
- package/src/modules/GlobalEditor/Editor/index.tsx +6 -0
- package/src/modules/GlobalEditor/PageBrowser/index.tsx +6 -0
- package/src/modules/GlobalEditor/Preview/index.tsx +4 -2
- package/src/modules/GlobalEditor/index.tsx +72 -45
- package/src/modules/Navigation/Menus/List/Table/ConfigPanel/Form/index.tsx +3 -5
- package/src/modules/Navigation/Menus/List/Table/ConfigPanel/index.tsx +4 -4
- package/src/modules/Navigation/Menus/List/Table/SidePanel/Form/ConnectedField/index.tsx +2 -3
- package/src/modules/Navigation/Menus/List/Table/SidePanel/Form/index.tsx +3 -4
- package/src/modules/Navigation/Menus/List/Table/SidePanel/index.tsx +5 -6
- package/src/modules/PageEditor/Editor/index.tsx +6 -0
- package/src/modules/PageEditor/PageBrowser/index.tsx +6 -0
- package/src/modules/PageEditor/Preview/index.tsx +9 -2
- package/src/modules/PageEditor/atoms.tsx +110 -3
- package/src/modules/PageEditor/index.tsx +76 -95
- package/src/modules/Settings/Integrations/IntegrationForm/index.tsx +1 -3
- package/src/modules/Sites/SitesList/GridView/GridSiteItem/style.tsx +19 -2
- package/src/modules/Sites/SitesList/WelcomeModal/FinalStep/index.tsx +2 -2
- package/src/modules/Sites/SitesList/WelcomeModal/utils.ts +2 -2
- package/src/modules/StructuredData/Form/ConnectedField/index.tsx +2 -2
- package/src/modules/StructuredData/Form/index.tsx +120 -75
- package/src/modules/StructuredData/Form/style.tsx +7 -1
- package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +15 -8
- package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +15 -9
- package/src/modules/StructuredData/{StructuredDataList/StructuredDataItem/utils.tsx → utils.tsx} +16 -2
- package/src/modules/Users/Profile/atoms.tsx +62 -4
- package/src/modules/Users/Profile/index.tsx +83 -4
- package/src/modules/Users/Profile/style.tsx +60 -0
- package/src/modules/Users/UserCreate/SiteItem/RolesModal/RoleItem/index.tsx +3 -5
- package/src/modules/Users/UserCreate/SiteItem/index.tsx +2 -2
- package/src/modules/Users/UserCreate/atoms.tsx +69 -0
- package/src/modules/Users/UserCreate/index.tsx +149 -70
- package/src/modules/Users/UserCreate/style.tsx +24 -1
- package/src/modules/Users/UserForm/index.tsx +260 -146
- package/src/modules/Users/UserForm/style.tsx +13 -1
- package/src/modules/Users/UserList/UserItem/index.tsx +3 -3
- package/src/types/index.tsx +19 -6
- 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 {
|
|
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
|
|
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
|
|
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
|
|
194
|
+
SERVICES.GET_SITE_PAGES.dynamicUrl = `${SERVICES.GET_SITE_PAGES.dynamicUrl}&query=${query}`;
|
|
194
195
|
if (filterStructuredData)
|
|
195
|
-
SERVICES.GET_SITE_PAGES.dynamicUrl =
|
|
196
|
-
|
|
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
|
|
200
|
-
if (ignoreLang) SERVICES.GET_SITE_PAGES.dynamicUrl = SERVICES.GET_SITE_PAGES.dynamicUrl
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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} />)<{
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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;
|