@raketa-cloud/admin 1.6.0 → 1.6.2

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 (31) hide show
  1. package/dist/application.css +5 -0
  2. package/package.json +6 -6
  3. package/src/cloud/Articles.js +20 -2
  4. package/src/cloud/Events.js +21 -3
  5. package/src/cloud/Pages.js +15 -0
  6. package/src/cloud/documents/articlesDocuments.js +29 -0
  7. package/src/cloud/documents/eventsDocuments.js +36 -0
  8. package/src/cloud/documents/pagesDocuments.js +28 -0
  9. package/src/handlers/TagsAPIHandler.js +11 -0
  10. package/src/handlers/articles/ArticlesAPIHandler.js +15 -0
  11. package/src/handlers/articles/ChangeHandler.js +22 -0
  12. package/src/handlers/events/ChangeHandler.js +22 -0
  13. package/src/handlers/events/EventsAPIHandler.js +15 -0
  14. package/src/handlers/pages/ChangeHandler.js +22 -0
  15. package/src/handlers/pages/PagesAPIHandler.js +15 -0
  16. package/src/helpers/AuthHelper.js +7 -1
  17. package/src/index.js +1 -0
  18. package/src/routeArticlesModule.js +11 -0
  19. package/src/routeEventsModule.js +11 -0
  20. package/src/routePagesModule.js +11 -0
  21. package/src/routeStandard.js +8 -6
  22. package/src/tests/handlers/TagsHandlerAPI.test/index.test.js +38 -0
  23. package/src/tests/handlers/articles/ArticlesAPIHandler.test/index.test.js +43 -0
  24. package/src/tests/handlers/articles/ChangeHandler.test/show.test.js +59 -0
  25. package/src/tests/handlers/events/ChangeHandler.test/show.test.js +59 -0
  26. package/src/tests/handlers/events/EventsAPIHandler.test/index.test.js +39 -0
  27. package/src/tests/handlers/pages/ChangeHandler.test/show.test.js +59 -0
  28. package/src/tests/handlers/pages/PagesAPIHandler.test/index.test.js +39 -0
  29. package/src/tests/helpers/AuthHelper.test.js +12 -0
  30. package/src/views/layouts/application.ejs +20 -10
  31. package/src/views/pages/_shared/history_table.ejs +2 -2
@@ -2111,6 +2111,11 @@ select {
2111
2111
  line-height: 1.25rem;
2112
2112
  }
2113
2113
 
2114
+ .text-xs {
2115
+ font-size: 0.75rem;
2116
+ line-height: 1rem;
2117
+ }
2118
+
2114
2119
  .font-bold {
2115
2120
  font-weight: 700;
2116
2121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src/",
@@ -12,16 +12,16 @@
12
12
  "prepare": "npx tailwindcss -i ./src/styles/application.css -o ./dist/application.css"
13
13
  },
14
14
  "devDependencies": {
15
- "@babel/core": "7.26.10",
16
- "@babel/preset-env": "7.26.9",
17
- "@babel/preset-typescript": "7.26.0",
15
+ "@babel/core": "7.27.1",
16
+ "@babel/preset-env": "7.27.2",
17
+ "@babel/preset-typescript": "7.27.1",
18
18
  "@repo/typescript-config": "*",
19
19
  "@tailwindcss/forms": "0.5.7",
20
20
  "@tailwindcss/typography": "0.5.13",
21
- "@types/node": "22.13.10",
21
+ "@types/node": "22.15.19",
22
22
  "jest": "29.7.0",
23
23
  "tailwindcss": "3.4.4",
24
- "typescript": "5.8.2"
24
+ "typescript": "5.8.3"
25
25
  },
26
26
  "dependencies": {
27
27
  "ejs": "3.1.10",
@@ -1,15 +1,16 @@
1
- import { toDateInput, dateToUTCISO8601String } from "../utils";
1
+ import { dateToUTCISO8601String, toDateInput } from "../utils";
2
2
  import RaketaClient from "./RaketaClient";
3
3
  import {
4
4
  ArticleByIdDocument,
5
+ ArticleChangePreviewDocument,
5
6
  ArticlesDocument,
6
7
  ChangeArticleSlugDocument,
7
8
  CloneArticleDocument,
8
9
  CreateArticleDocument,
9
- buildBatchTranslateArticleDocument,
10
10
  DeleteArticleDocument,
11
11
  MiniArticlesDocument,
12
12
  TranslateArticleDocument,
13
+ buildBatchTranslateArticleDocument,
13
14
  } from "./documents/articlesDocuments";
14
15
 
15
16
  export default class Articles extends RaketaClient {
@@ -191,4 +192,21 @@ export default class Articles extends RaketaClient {
191
192
 
192
193
  return { article, errors };
193
194
  }
195
+
196
+ async getChangePreview(articleId, version, locale) {
197
+ const input = {
198
+ articleId,
199
+ version: Number.parseInt(version, 10),
200
+ locale,
201
+ };
202
+
203
+ const { articleChangePreview } = await this.query(
204
+ ArticleChangePreviewDocument,
205
+ {
206
+ input,
207
+ }
208
+ );
209
+
210
+ return articleChangePreview;
211
+ }
194
212
  }
@@ -1,20 +1,21 @@
1
1
  import {
2
+ dateToUTCISO8601String,
3
+ toDateInput,
2
4
  toDateTimeInput,
3
5
  toUTCISO8601String,
4
- toDateInput,
5
- dateToUTCISO8601String,
6
6
  } from "../utils";
7
7
  import RaketaClient from "./RaketaClient";
8
8
  import {
9
9
  ChangeEventSlugDocument,
10
10
  CloneEventDocument,
11
- buildBatchTranslateEventDocument,
12
11
  CreateEventDocument,
13
12
  DeleteEventDocument,
14
13
  EventByIdDocument,
14
+ EventChangePreviewDocument,
15
15
  EventsDocument,
16
16
  MiniEventsDocument,
17
17
  TranslateEventDocument,
18
+ buildBatchTranslateEventDocument,
18
19
  } from "./documents/eventsDocuments";
19
20
 
20
21
  export default class Events extends RaketaClient {
@@ -206,4 +207,21 @@ export default class Events extends RaketaClient {
206
207
 
207
208
  return { event, errors };
208
209
  }
210
+
211
+ async getChangePreview(eventId, version, locale) {
212
+ const input = {
213
+ eventId,
214
+ version: Number.parseInt(version, 10),
215
+ locale,
216
+ };
217
+
218
+ const { eventChangePreview } = await this.query(
219
+ EventChangePreviewDocument,
220
+ {
221
+ input,
222
+ }
223
+ );
224
+
225
+ return eventChangePreview;
226
+ }
209
227
  }
@@ -6,6 +6,7 @@ import {
6
6
  DeletePageDocument,
7
7
  MiniPagesDocument,
8
8
  PageByIdDocument,
9
+ PageChangePreviewDocument,
9
10
  PagesDocument,
10
11
  TranslatePageDocument,
11
12
  buildBatchTranslatePageDocument,
@@ -182,4 +183,18 @@ export default class Pages extends RaketaClient {
182
183
 
183
184
  return { page, errors };
184
185
  }
186
+
187
+ async getChangePreview(pageId, version, locale) {
188
+ const input = {
189
+ pageId,
190
+ version: Number.parseInt(version, 10),
191
+ locale,
192
+ };
193
+
194
+ const { pageChangePreview } = await this.query(PageChangePreviewDocument, {
195
+ input,
196
+ });
197
+
198
+ return pageChangePreview;
199
+ }
185
200
  }
@@ -315,6 +315,34 @@ const UnlockArticleRevisionDocument = gql`
315
315
  }
316
316
  `;
317
317
 
318
+ const ArticleChangePreviewDocument = gql`
319
+ query ($input: ArticleChangePreviewInput!) {
320
+ articleChangePreview(input: $input) {
321
+ article {
322
+ id
323
+ slug
324
+ title
325
+ widgets
326
+ settings
327
+ visibility
328
+ publishAt
329
+ translations
330
+ tags ${tagFragment()}
331
+ websiteSettings
332
+ }
333
+ articleChange {
334
+ version
335
+ title
336
+ description
337
+ operator
338
+ operatorMeta
339
+ source
340
+ insertedAt
341
+ }
342
+ }
343
+ }
344
+ `;
345
+
318
346
  export {
319
347
  ArticleByIdDocument,
320
348
  ArticlesDocument,
@@ -340,4 +368,5 @@ export {
340
368
  buildBatchTranslateArticleDocument,
341
369
  buildBatchStartArticleRevisionDocument,
342
370
  ArticleEditorsDocument,
371
+ ArticleChangePreviewDocument,
343
372
  };
@@ -323,6 +323,41 @@ const UnlockEventRevisionDocument = gql`
323
323
  }
324
324
  `;
325
325
 
326
+ const EventChangePreviewDocument = gql`
327
+ query ($input: EventChangePreviewInput!) {
328
+ eventChangePreview(input: $input) {
329
+ event {
330
+ id
331
+ slug
332
+ title
333
+ widgets
334
+ settings
335
+ visibility
336
+ publishAt
337
+ startsAt
338
+ endsAt
339
+ eventType {
340
+ id
341
+ slug
342
+ title
343
+ }
344
+ translations
345
+ tags ${tagFragment()}
346
+ websiteSettings
347
+ }
348
+ eventChange {
349
+ version
350
+ title
351
+ description
352
+ operator
353
+ operatorMeta
354
+ source
355
+ insertedAt
356
+ }
357
+ }
358
+ }
359
+ `;
360
+
326
361
  export {
327
362
  EventByIdDocument,
328
363
  EventsDocument,
@@ -348,4 +383,5 @@ export {
348
383
  buildBatchTranslateEventDocument,
349
384
  EventHistoryDocument,
350
385
  EventEditorsDocument,
386
+ EventChangePreviewDocument,
351
387
  };
@@ -311,6 +311,33 @@ const UnlockPageRevisionDocument = gql`
311
311
  }
312
312
  `;
313
313
 
314
+ const PageChangePreviewDocument = gql`
315
+ query ($input: PageChangePreviewInput!) {
316
+ pageChangePreview(input: $input) {
317
+ page {
318
+ id
319
+ slug
320
+ title
321
+ widgets
322
+ settings
323
+ visibility
324
+ translations
325
+ tags ${tagFragment()}
326
+ websiteSettings
327
+ }
328
+ pageChange {
329
+ version
330
+ title
331
+ description
332
+ operator
333
+ operatorMeta
334
+ source
335
+ insertedAt
336
+ }
337
+ }
338
+ }
339
+ `;
340
+
314
341
  export {
315
342
  PageByIdDocument,
316
343
  PagesDocument,
@@ -336,4 +363,5 @@ export {
336
363
  PageHistoryDocument,
337
364
  buildBatchStartPageRevisionDocument,
338
365
  PageEditorsDocument,
366
+ PageChangePreviewDocument,
339
367
  };
@@ -0,0 +1,11 @@
1
+ const index = async (ctx) => {
2
+ const items = await ctx.client.tags.list(ctx.query, ctx.locale);
3
+
4
+ return {
5
+ json: {
6
+ ...items,
7
+ },
8
+ };
9
+ };
10
+
11
+ export default { index };
@@ -0,0 +1,15 @@
1
+ const index = async (ctx) => {
2
+ const items = await ctx.client.articles.listAll(
3
+ ctx.query,
4
+ ctx.locale,
5
+ ctx.resourceConfig.articleType
6
+ );
7
+
8
+ return {
9
+ json: {
10
+ ...items,
11
+ },
12
+ };
13
+ };
14
+
15
+ export default { index };
@@ -0,0 +1,22 @@
1
+ const show = async (ctx) => {
2
+ const articleId = ctx.params.parent_id;
3
+ const version = ctx.query.version;
4
+
5
+ const item = await ctx.client.articles.getChangePreview(
6
+ articleId,
7
+ version,
8
+ ctx.locale
9
+ );
10
+
11
+ if (!item) {
12
+ throw new Error(`Missing ${ctx.resourceConfig.articleType} #${articleId}`);
13
+ }
14
+
15
+ return {
16
+ json: {
17
+ ...item,
18
+ },
19
+ };
20
+ };
21
+
22
+ export default { show };
@@ -0,0 +1,22 @@
1
+ const show = async (ctx) => {
2
+ const eventId = ctx.params.parent_id;
3
+ const version = ctx.query.version;
4
+
5
+ const item = await ctx.client.events.getChangePreview(
6
+ eventId,
7
+ version,
8
+ ctx.locale
9
+ );
10
+
11
+ if (!item) {
12
+ throw new Error(`Missing ${ctx.resourceConfig.eventType} #${eventId}`);
13
+ }
14
+
15
+ return {
16
+ json: {
17
+ ...item,
18
+ },
19
+ };
20
+ };
21
+
22
+ export default { show };
@@ -0,0 +1,15 @@
1
+ const index = async (ctx) => {
2
+ const items = await ctx.client.events.listAll(
3
+ ctx.query,
4
+ ctx.locale,
5
+ ctx.resourceConfig.eventType
6
+ );
7
+
8
+ return {
9
+ json: {
10
+ ...items,
11
+ },
12
+ };
13
+ };
14
+
15
+ export default { index };
@@ -0,0 +1,22 @@
1
+ const show = async (ctx) => {
2
+ const pageId = ctx.params.parent_id;
3
+ const version = ctx.query.version;
4
+
5
+ const item = await ctx.client.pages.getChangePreview(
6
+ pageId,
7
+ version,
8
+ ctx.locale
9
+ );
10
+
11
+ if (!item) {
12
+ throw new Error(`Missing ${ctx.resourceConfig.pageType} #${pageId}`);
13
+ }
14
+
15
+ return {
16
+ json: {
17
+ ...item,
18
+ },
19
+ };
20
+ };
21
+
22
+ export default { show };
@@ -0,0 +1,15 @@
1
+ const index = async (ctx) => {
2
+ const items = await ctx.client.pages.listAll(
3
+ ctx.query,
4
+ ctx.locale,
5
+ ctx.resourceConfig.pageType
6
+ );
7
+
8
+ return {
9
+ json: {
10
+ ...items,
11
+ },
12
+ };
13
+ };
14
+
15
+ export default { index };
@@ -2,7 +2,13 @@ const AuthHelper = {
2
2
  cookie_key: "auth_cookie",
3
3
 
4
4
  parseJWT: (token) => {
5
- return JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
5
+ const tokenPayload = token?.split(".")?.[1];
6
+
7
+ if(!tokenPayload){
8
+ return "";
9
+ }
10
+
11
+ return JSON.parse(Buffer.from(tokenPayload, "base64").toString());
6
12
  },
7
13
 
8
14
  verifyJWT: (token) => {
package/src/index.js CHANGED
@@ -46,6 +46,7 @@ export { default as RedirectsHandler } from "./handlers/RedirectsHandler";
46
46
  export { default as TagsHandler } from "./handlers/TagsHandler";
47
47
 
48
48
  export { default as PageClonesHandler } from "./handlers/pages/ClonesHandler";
49
+ export { default as ChangeHandler } from "./handlers/pages/ChangeHandler";
49
50
  export { default as PageDeletesHandler } from "./handlers/pages/DeletesHandler";
50
51
  export { default as PageTranslationsHandler } from "./handlers/pages/TranslationsHandler";
51
52
  export { default as PageURLsHandler } from "./handlers/pages/URLsHandler";
@@ -10,7 +10,9 @@ import TagHandler from "./handlers/article_revisions/TagHandler";
10
10
  import UnlocksHandler from "./handlers/article_revisions/UnlocksHandler";
11
11
  import VisualHandler from "./handlers/article_revisions/VisualHandler";
12
12
  import WidgetHandler from "./handlers/article_revisions/WidgetHandler";
13
+ import ArticlesAPIHandler from "./handlers/articles/ArticlesAPIHandler";
13
14
  import ArticlesHandler from "./handlers/articles/ArticlesHandler";
15
+ import ChangeHandler from "./handlers/articles/ChangeHandler";
14
16
  import ClonesHandler from "./handlers/articles/ClonesHandler";
15
17
  import DeletesHandler from "./handlers/articles/DeletesHandler";
16
18
  import HistoryHandler from "./handlers/articles/HistoryHandler";
@@ -146,6 +148,15 @@ function routeArticlesModule(
146
148
  WidgetHandler,
147
149
  resourceConfig
148
150
  );
151
+
152
+ routeCollection(adapter, "api/articles", ArticlesAPIHandler, resourceConfig);
153
+ routeNestedMember(
154
+ adapter,
155
+ "api/article",
156
+ "change",
157
+ ChangeHandler,
158
+ resourceConfig
159
+ );
149
160
  }
150
161
 
151
162
  export default routeArticlesModule;
@@ -10,8 +10,10 @@ import TagHandler from "./handlers/event_revisions/TagHandler";
10
10
  import UnlocksHandler from "./handlers/event_revisions/UnlocksHandler";
11
11
  import VisualHandler from "./handlers/event_revisions/VisualHandler";
12
12
  import WidgetHandler from "./handlers/event_revisions/WidgetHandler";
13
+ import ChangeHandler from "./handlers/events/ChangeHandler";
13
14
  import ClonesHandler from "./handlers/events/ClonesHandler";
14
15
  import DeletesHandler from "./handlers/events/DeletesHandler";
16
+ import EventsAPIHandler from "./handlers/events/EventsAPIHandler";
15
17
  import EventsHandler from "./handlers/events/EventsHandler";
16
18
  import HistoryHandler from "./handlers/events/HistoryHandler";
17
19
  import TranslationsHandler from "./handlers/events/TranslationsHandler";
@@ -161,6 +163,15 @@ function routeEventsModule(
161
163
  WidgetHandler,
162
164
  resourceConfig
163
165
  );
166
+
167
+ routeCollection(adapter, "api/events", EventsAPIHandler, resourceConfig);
168
+ routeNestedMember(
169
+ adapter,
170
+ "api/event",
171
+ "change",
172
+ ChangeHandler,
173
+ resourceConfig
174
+ );
164
175
  }
165
176
 
166
177
  export default routeEventsModule;
@@ -10,9 +10,11 @@ import TagHandler from "./handlers/page_revisions/TagHandler";
10
10
  import UnlocksHandler from "./handlers/page_revisions/UnlocksHandler";
11
11
  import VisualHandler from "./handlers/page_revisions/VisualHandler";
12
12
  import WidgetHandler from "./handlers/page_revisions/WidgetHandler";
13
+ import ChangeHandler from "./handlers/pages/ChangeHandler";
13
14
  import ClonesHandler from "./handlers/pages/ClonesHandler";
14
15
  import DeletesHandler from "./handlers/pages/DeletesHandler";
15
16
  import HistoryHandler from "./handlers/pages/HistoryHandler";
17
+ import PagesAPIHandler from "./handlers/pages/PagesAPIHandler";
16
18
  import PagesHandler from "./handlers/pages/PagesHandler";
17
19
  import TranslationsHandler from "./handlers/pages/TranslationsHandler";
18
20
  import URLsHandler from "./handlers/pages/URLsHandler";
@@ -158,6 +160,15 @@ function routePagesModule(
158
160
  WidgetHandler,
159
161
  resourceConfig
160
162
  );
163
+
164
+ routeCollection(adapter, "api/pages", PagesAPIHandler, resourceConfig);
165
+ routeNestedMember(
166
+ adapter,
167
+ "api/page",
168
+ "change",
169
+ ChangeHandler,
170
+ resourceConfig
171
+ );
161
172
  }
162
173
 
163
174
  export default routePagesModule;
@@ -1,11 +1,12 @@
1
- import routeCollection from "./routeCollection";
2
- import routeSettings from "./routeSettings";
3
- import routePagesModule from "./routePagesModule";
4
- import routeArticlesModule from "./routeArticlesModule";
5
- import routeEventsModule from "./routeEventsModule";
1
+ import DashboardsHandler from "./handlers/DashboardsHandler";
6
2
  import RedirectsHandler from "./handlers/RedirectsHandler";
3
+ import TagsAPIHandler from "./handlers/TagsAPIHandler";
7
4
  import TagsHandler from "./handlers/TagsHandler";
8
- import DashboardsHandler from "./handlers/DashboardsHandler";
5
+ import routeArticlesModule from "./routeArticlesModule";
6
+ import routeCollection from "./routeCollection";
7
+ import routeEventsModule from "./routeEventsModule";
8
+ import routePagesModule from "./routePagesModule";
9
+ import routeSettings from "./routeSettings";
9
10
 
10
11
  function routeStandard(adapter) {
11
12
  adapter.routeCollection(
@@ -26,6 +27,7 @@ function routeStandard(adapter) {
26
27
 
27
28
  routeCollection(adapter, "redirects", RedirectsHandler);
28
29
  routeCollection(adapter, "tags", TagsHandler);
30
+ routeCollection(adapter, "api/tags", TagsAPIHandler);
29
31
  routeSettings(adapter);
30
32
  routePagesModule(adapter);
31
33
  routeArticlesModule(adapter);
@@ -0,0 +1,38 @@
1
+ import Tags from "../../../cloud/Tags";
2
+ import TagsAPIHandler from "../../../handlers/TagsAPIHandler";
3
+
4
+ import Factory from "../../support/factory";
5
+
6
+ describe("Pages API Handler", () => {
7
+ function buildCtx() {
8
+ return {
9
+ client: {
10
+ tags: new Tags(() => ({}), "token"),
11
+ },
12
+ query: { foo: "bar" },
13
+ locale: "en",
14
+ };
15
+ }
16
+
17
+ test("index", async () => {
18
+ const tags = [
19
+ Factory.build("tag", { id: "1", name: "First Tag" }),
20
+ Factory.build("tag", { id: "2", name: "Second Tag" }),
21
+ ];
22
+
23
+ const ctx = buildCtx();
24
+
25
+ const mockedList = jest
26
+ .spyOn(ctx.client.tags, "list")
27
+ .mockImplementation(() => ({ tags: tags }));
28
+
29
+ const result = await TagsAPIHandler.index(ctx);
30
+
31
+ expect(mockedList).toHaveBeenCalledWith({ foo: "bar" }, "en");
32
+ expect(result).toEqual({
33
+ json: {
34
+ tags: tags,
35
+ },
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,43 @@
1
+ import Articles from "../../../../cloud/Articles";
2
+ import ArticlesAPIHandler from "../../../../handlers/articles/ArticlesAPIHandler";
3
+
4
+ import Factory from "../../../support/factory";
5
+
6
+ describe("Articles API Handler", () => {
7
+ function buildCtx() {
8
+ return {
9
+ client: {
10
+ articles: new Articles(() => ({}), "token"),
11
+ },
12
+ query: { foo: "bar" },
13
+ locale: "en",
14
+ resourceConfig: {
15
+ articleType: "article",
16
+ articleTypePlural: "articles",
17
+ },
18
+ };
19
+ }
20
+
21
+ test("index", async () => {
22
+ const article = Factory.build("article");
23
+ const ctx = buildCtx();
24
+
25
+ const mockedArticleList = jest
26
+ .spyOn(ctx.client.articles, "listAll")
27
+ .mockImplementation(() => ({ list: [article] }));
28
+
29
+ const result = await ArticlesAPIHandler.index(ctx);
30
+
31
+ expect(mockedArticleList).toHaveBeenCalledWith(
32
+ { foo: "bar" },
33
+ "en",
34
+ "article"
35
+ );
36
+
37
+ expect(result).toEqual({
38
+ json: {
39
+ list: [article],
40
+ },
41
+ });
42
+ });
43
+ });
@@ -0,0 +1,59 @@
1
+ import ArticleChanges from "../../../../cloud/ArticleChanges";
2
+ import ArticleRevisions from "../../../../cloud/ArticleRevisions";
3
+ import Articles from "../../../../cloud/Articles";
4
+ import ChangeHandler from "../../../../handlers/articles/ChangeHandler";
5
+
6
+ import Factory from "../../../support/factory";
7
+
8
+ describe("ChangeHandler", () => {
9
+ function buildCtx(article) {
10
+ return {
11
+ client: {
12
+ articles: new Articles(() => ({}), "token"),
13
+ articleChanges: new ArticleChanges(() => ({}), "token"),
14
+ articleRevisions: new ArticleRevisions(() => ({}), "token"),
15
+ },
16
+ params: { parent_id: article.id },
17
+ query: { version: 49 },
18
+ locale: "en",
19
+ resourceConfig: {
20
+ articleType: "article",
21
+ articleTypePlural: "articles",
22
+ },
23
+ };
24
+ }
25
+
26
+ test("show for existing article", async () => {
27
+ const article = Factory.build("article");
28
+
29
+ const ctx = buildCtx(article);
30
+
31
+ const mockedArticleFind = jest
32
+ .spyOn(ctx.client.articles, "getChangePreview")
33
+ .mockImplementation(() => [article]);
34
+
35
+ const result = await ChangeHandler.show(ctx);
36
+
37
+ expect(mockedArticleFind).toHaveBeenCalledWith(article.id, 49, "en");
38
+
39
+ expect(result).toEqual({
40
+ json: {
41
+ ...[article],
42
+ },
43
+ });
44
+ });
45
+
46
+ test("show for non existing article", async () => {
47
+ const ctx = buildCtx({ id: "42" });
48
+
49
+ const mockedArticleFind = jest
50
+ .spyOn(ctx.client.articles, "getChangePreview")
51
+ .mockImplementation(() => null);
52
+
53
+ expect(async () => {
54
+ await ChangeHandler.show(ctx);
55
+ }).rejects.toThrow("Missing article #42");
56
+
57
+ expect(mockedArticleFind).toHaveBeenCalledWith("42", 49, "en");
58
+ });
59
+ });
@@ -0,0 +1,59 @@
1
+ import EventChanges from "../../../../cloud/EventChanges";
2
+ import EventRevisions from "../../../../cloud/EventRevisions";
3
+ import Events from "../../../../cloud/Events";
4
+ import ChangeHandler from "../../../../handlers/events/ChangeHandler";
5
+
6
+ import Factory from "../../../support/factory";
7
+
8
+ describe("ChangeHandler", () => {
9
+ function buildCtx(event) {
10
+ return {
11
+ client: {
12
+ events: new Events(() => ({}), "token"),
13
+ eventChanges: new EventChanges(() => ({}), "token"),
14
+ eventRevisions: new EventRevisions(() => ({}), "token"),
15
+ },
16
+ params: { parent_id: event.id },
17
+ query: { version: 49 },
18
+ locale: "en",
19
+ resourceConfig: {
20
+ eventType: "event",
21
+ eventTypePlural: "events",
22
+ },
23
+ };
24
+ }
25
+
26
+ test("show for existing event", async () => {
27
+ const event = Factory.build("event");
28
+
29
+ const ctx = buildCtx(event);
30
+
31
+ const mockedEventFind = jest
32
+ .spyOn(ctx.client.events, "getChangePreview")
33
+ .mockImplementation(() => [event]);
34
+
35
+ const result = await ChangeHandler.show(ctx);
36
+
37
+ expect(mockedEventFind).toHaveBeenCalledWith(event.id, 49, "en");
38
+
39
+ expect(result).toEqual({
40
+ json: {
41
+ ...[event],
42
+ },
43
+ });
44
+ });
45
+
46
+ test("show for non existing event", async () => {
47
+ const ctx = buildCtx({ id: "42" });
48
+
49
+ const mockedEventFind = jest
50
+ .spyOn(ctx.client.events, "getChangePreview")
51
+ .mockImplementation(() => null);
52
+
53
+ expect(async () => {
54
+ await ChangeHandler.show(ctx);
55
+ }).rejects.toThrow("Missing event #42");
56
+
57
+ expect(mockedEventFind).toHaveBeenCalledWith("42", 49, "en");
58
+ });
59
+ });
@@ -0,0 +1,39 @@
1
+ import Events from "../../../../cloud/Events";
2
+ import EventsAPIHandler from "../../../../handlers/events/EventsAPIHandler";
3
+
4
+ import Factory from "../../../support/factory";
5
+
6
+ describe("Events API Handler", () => {
7
+ function buildCtx() {
8
+ return {
9
+ client: {
10
+ events: new Events(() => ({}), "token"),
11
+ },
12
+ query: { foo: "bar" },
13
+ locale: "en",
14
+ resourceConfig: {
15
+ eventType: "event",
16
+ eventTypePlural: "events",
17
+ },
18
+ };
19
+ }
20
+
21
+ test("index", async () => {
22
+ const event = Factory.build("event");
23
+ const ctx = buildCtx();
24
+
25
+ const mockedEventList = jest
26
+ .spyOn(ctx.client.events, "listAll")
27
+ .mockImplementation(() => ({ list: [event] }));
28
+
29
+ const result = await EventsAPIHandler.index(ctx);
30
+
31
+ expect(mockedEventList).toHaveBeenCalledWith({ foo: "bar" }, "en", "event");
32
+
33
+ expect(result).toEqual({
34
+ json: {
35
+ list: [event],
36
+ },
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,59 @@
1
+ import PageChanges from "../../../../cloud/PageChanges";
2
+ import PageRevisions from "../../../../cloud/PageRevisions";
3
+ import Pages from "../../../../cloud/Pages";
4
+ import ChangeHandler from "../../../../handlers/pages/ChangeHandler";
5
+
6
+ import Factory from "../../../support/factory";
7
+
8
+ describe("ChangeHandler", () => {
9
+ function buildCtx(page) {
10
+ return {
11
+ client: {
12
+ pages: new Pages(() => ({}), "token"),
13
+ pageChanges: new PageChanges(() => ({}), "token"),
14
+ pageRevisions: new PageRevisions(() => ({}), "token"),
15
+ },
16
+ params: { parent_id: page.id },
17
+ query: { version: 49 },
18
+ locale: "en",
19
+ resourceConfig: {
20
+ pageType: "page",
21
+ pageTypePlural: "pages",
22
+ },
23
+ };
24
+ }
25
+
26
+ test("show for existing page", async () => {
27
+ const page = Factory.build("page");
28
+
29
+ const ctx = buildCtx(page);
30
+
31
+ const mockedPageFind = jest
32
+ .spyOn(ctx.client.pages, "getChangePreview")
33
+ .mockImplementation(() => [page]);
34
+
35
+ const result = await ChangeHandler.show(ctx);
36
+
37
+ expect(mockedPageFind).toHaveBeenCalledWith(page.id, 49, "en");
38
+
39
+ expect(result).toEqual({
40
+ json: {
41
+ ...[page],
42
+ },
43
+ });
44
+ });
45
+
46
+ test("show for non existing page", async () => {
47
+ const ctx = buildCtx({ id: "42" });
48
+
49
+ const mockedPageFind = jest
50
+ .spyOn(ctx.client.pages, "getChangePreview")
51
+ .mockImplementation(() => null);
52
+
53
+ expect(async () => {
54
+ await ChangeHandler.show(ctx);
55
+ }).rejects.toThrow("Missing page #42");
56
+
57
+ expect(mockedPageFind).toHaveBeenCalledWith("42", 49, "en");
58
+ });
59
+ });
@@ -0,0 +1,39 @@
1
+ import Pages from "../../../../cloud/Pages";
2
+ import PagesAPIHandler from "../../../../handlers/pages/PagesAPIHandler";
3
+
4
+ import Factory from "../../../support/factory";
5
+
6
+ describe("Pages API Handler", () => {
7
+ function buildCtx() {
8
+ return {
9
+ client: {
10
+ pages: new Pages(() => ({}), "token"),
11
+ },
12
+ query: { foo: "bar" },
13
+ locale: "en",
14
+ resourceConfig: {
15
+ pageType: "page",
16
+ pageTypePlural: "pages",
17
+ },
18
+ };
19
+ }
20
+
21
+ test("index", async () => {
22
+ const page = Factory.build("page");
23
+ const ctx = buildCtx();
24
+
25
+ const mockedPageList = jest
26
+ .spyOn(ctx.client.pages, "listAll")
27
+ .mockImplementation(() => ({ list: [page] }));
28
+
29
+ const result = await PagesAPIHandler.index(ctx);
30
+
31
+ expect(mockedPageList).toHaveBeenCalledWith({ foo: "bar" }, "en", "page");
32
+
33
+ expect(result).toEqual({
34
+ json: {
35
+ list: [page],
36
+ },
37
+ });
38
+ });
39
+ });
@@ -49,6 +49,18 @@ describe("AuthHelper", () => {
49
49
  const result = AuthHelper.verifyJWT(mockToken);
50
50
  expect(result).toBe(false);
51
51
  });
52
+
53
+ test("returns false for invalid token", () => {
54
+ const invalidToken = "header";
55
+ const invalidToken2 = "header.";
56
+
57
+ const result = AuthHelper.verifyJWT(invalidToken);
58
+ expect(result).toBe(false);
59
+
60
+ const result2 = AuthHelper.verifyJWT(invalidToken2);
61
+ expect(result2).toBe(false);
62
+ })
63
+
52
64
  });
53
65
 
54
66
  describe("isAuthenticated", () => {
@@ -64,24 +64,27 @@
64
64
  <div class="my-4">
65
65
  <% _context.layout.sidebar.forEach((menu, idx) => { %>
66
66
  <% if (menu.submenus) { %>
67
+ <% const pathMatchesMenuItem = menu.submenus.some(item => _context.path.includes(item.link)) %>
67
68
 
68
69
  <div class="relative group-[.expanded]:hidden group/item">
69
- <div class="aspect-square w-full flex items-center justify-center group-hover/item:bg-raketa-primary">
70
- <div class="size-6 opacity-100 group-[.expanded]:opacity-50 text-raketa-primary group-hover/item:text-white">
70
+ <div title="<%= menu.label %>" class="aspect-square w-full flex items-center justify-center group-hover/item:bg-raketa-primary <%= pathMatchesMenuItem ? "bg-raketa-primary" : ""%>">
71
+ <div class="size-6 opacity-100 group-[.expanded]:opacity-50 text-raketa-primary group-hover/item:text-white <%= pathMatchesMenuItem ? "text-white" : ""%>">
71
72
  <%- atob(menu.icon) %>
72
73
  </div>
73
74
  </div>
74
75
 
75
76
  <div class="absolute z-50 top-0 left-[64px] w-[250px] border-l bg-[#F7F7FC] shadow-lg hidden group-hover/item:block">
76
77
  <% menu.submenus.forEach(submenu => { %>
77
- <a href="<%= `/${_context.locale}${submenu.link}` %>" class="block py-3 px-4 body-2 text-raketa-foreground/50 hover:text-raketa-foreground transition-all ease-in-out duration-300"><%= submenu.label %></a>
78
+ <a href="<%= `/${_context.locale}${submenu.link}` %>" title="<%= submenu.label %>" class="block py-3 px-4 body-2 <%= _context.path.includes(submenu.link) ? "text-raketa-foreground" : "text-raketa-foreground/50 hover:text-raketa-foreground transition-all ease-in-out duration-300" %>"><%= submenu.label %></a>
78
79
  <% }) %>
79
80
  </div>
80
81
  </div>
81
82
 
82
83
  <div class="hidden group-[.expanded]:block group/item">
83
- <details class="unstyled-details">
84
- <summary class="relative flex items-center gap-3 px-5 py-4 text-raketa-primary cursor-pointer transition-all duration-300 ease-in-out">
84
+ <details class="unstyled-details" <%= menu.submenus.some(submenu =>
85
+ _context.path.includes(submenu.link))
86
+ ? "open" : ''%>>
87
+ <summary title="<%= menu.label %>" class="relative flex items-center gap-3 px-5 py-4 text-raketa-primary cursor-pointer transition-all duration-300 ease-in-out">
85
88
  <div class="size-6 opacity-100 group-[.expanded]:opacity-50">
86
89
  <%- atob(menu.icon) %>
87
90
  </div>
@@ -95,16 +98,23 @@
95
98
 
96
99
  <div class="pl-12 py-4 bg-raketa-background/50 group-[.expanded]:block hidden">
97
100
  <% menu.submenus.forEach(submenu => { %>
98
- <a href="<%= `/${_context.locale}${submenu.link}` %>" class="block py-2 body-2 text-raketa-foreground/50 hover:text-raketa-foreground transition-all ease-in-out duration-300"><%= submenu.label %></a>
101
+ <a href="<%= `/${_context.locale}${submenu.link}` %>" title="<%= submenu.label %>" class="block py-2 body-2 <%= _context.path.includes(submenu.link) ? "text-raketa-foreground" : "text-raketa-foreground/50 hover:text-raketa-foreground transition-all ease-in-out duration-300" %>"><%= submenu.label %></a>
99
102
  <% }) %>
100
103
  </div>
101
104
  </details>
102
105
  </div>
103
106
 
104
107
  <% } else { %>
105
-
106
- <div class="group/item">
107
- <a href="<%= `/${_context.locale}${menu.link}` %>" title="<%= menu.label %>" class="relative flex items-center gap-3 px-5 py-4 text-raketa-primary hover:bg-raketa-primary hover:text-white group-[.is-active]/item:bg-raketa-primary group-[.is-active]/item:text-white">
108
+ <%
109
+ const menuLink = menu.link.replace('/', '');
110
+ const directMatch = _context.path.includes(`/${_context.locale}/${menuLink}`);
111
+ const singularMenuLink = menuLink.endsWith('s') ? menuLink.slice(0, -1) : menuLink;
112
+ const revisionMatch = _context.path.includes(`/${_context.locale}/${singularMenuLink}_revision`);
113
+ const pathMatchesMenuItem = directMatch || revisionMatch;
114
+ %>
115
+
116
+ <div class="group/item <%= pathMatchesMenuItem ? 'is-active' : '' %>">
117
+ <a href="<%= `/${_context.locale}${menu.link}` %>" title="<%= menu.label %>" class="relative flex items-center gap-3 px-5 py-4 <%= pathMatchesMenuItem ? 'bg-raketa-primary text-white' : 'text-raketa-primary hover:bg-raketa-primary hover:text-white' %> group-[.is-active]/item:bg-raketa-primary group-[.is-active]/item:text-white">
108
118
  <div class="size-6 opacity-100 group-[.expanded]:opacity-50">
109
119
  <%- atob(menu.icon) %>
110
120
  </div>
@@ -300,4 +310,4 @@
300
310
  </script>
301
311
  </body>
302
312
 
303
- </html>
313
+ </html>
@@ -50,9 +50,9 @@
50
50
  </td>
51
51
 
52
52
  <td class="py-4 pl-2">
53
- <div class="flex items-center gap-2">
53
+ <div class="flex flex-col gap-2">
54
54
  <%= history.changeReason(change) %>
55
- </div>
55
+ <%- !skipPagination && change.description ? `<p class="text-xs opacity-60">${change.description}</p>` : '' %> </div>
56
56
  </td>
57
57
 
58
58
  <td>