@raketa-cloud/admin 0.0.14 → 0.0.15

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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/cloud/ArticleChanges.js +15 -0
  3. package/src/cloud/ArticleRevisions.js +152 -0
  4. package/src/cloud/Articles.js +133 -0
  5. package/src/cloud/documents/articlesDocuments.js +109 -469
  6. package/src/cloud/documents/common.js +10 -0
  7. package/src/handlers/article_revisions/ArticleRevisionsHandler.js +49 -0
  8. package/src/handlers/article_revisions/CodeHandler.js +90 -0
  9. package/src/handlers/article_revisions/GeneralHandler.js +95 -0
  10. package/src/handlers/article_revisions/LocksHandler.js +54 -0
  11. package/src/handlers/article_revisions/MetaHandler.js +90 -0
  12. package/src/handlers/article_revisions/PublishesHandler.js +90 -0
  13. package/src/handlers/article_revisions/RevertsHandler.js +28 -0
  14. package/src/handlers/article_revisions/TagHandler.js +89 -0
  15. package/src/handlers/article_revisions/UnlocksHandler.js +22 -0
  16. package/src/handlers/article_revisions/VisualHandler.js +84 -0
  17. package/src/handlers/article_revisions/WidgetHandler.js +47 -0
  18. package/src/handlers/articles/ArticlesHandler.js +119 -0
  19. package/src/handlers/articles/ClonesHandler.js +17 -0
  20. package/src/handlers/articles/DeletesHandler.js +68 -0
  21. package/src/handlers/articles/TranslationsHandler.js +103 -0
  22. package/src/handlers/articles/URLsHandler.js +71 -0
  23. package/src/handlers/pages/PagesHandler.js +4 -2
  24. package/src/handlers/pages/TranslationsHandler.js +7 -5
  25. package/src/index.js +22 -0
  26. package/src/routeArticles.js +65 -0
  27. package/src/routeStandard.js +2 -0
  28. package/src/setupCloud.js +12 -0
  29. package/src/tests/handlers/articles/ArticlesHandler.test/create.test.js +144 -0
  30. package/src/tests/handlers/articles/ArticlesHandler.test/create_form.test.js +41 -0
  31. package/src/tests/handlers/articles/ArticlesHandler.test/index.test.js +49 -0
  32. package/src/tests/handlers/articles/ArticlesHandler.test/show.test.js +105 -0
  33. package/src/tests/handlers/articles/ConesHandler.test/create.test.js +54 -0
  34. package/src/tests/handlers/articles/DeletesHandler.test/create.test.js +110 -0
  35. package/src/tests/handlers/articles/DeletesHandler.test/create_form.test.js +54 -0
  36. package/src/tests/handlers/articles/URLsHandler.test/create.test.js +155 -0
  37. package/src/tests/handlers/articles/URLsHandler.test/create_form.test.js +51 -0
  38. package/src/tests/handlers/pages/PagesHandler.test/create.test.js +3 -3
  39. package/src/utils.js +46 -0
  40. package/src/views/article_revisions/_nav.ejs +40 -0
  41. package/src/views/article_revisions/codes/edit_form.ejs +39 -0
  42. package/src/views/article_revisions/general/edit_form.ejs +70 -0
  43. package/src/views/article_revisions/locks/create_form.ejs +35 -0
  44. package/src/views/article_revisions/meta/edit_form.ejs +41 -0
  45. package/src/views/article_revisions/publishes/_form.ejs +57 -0
  46. package/src/views/article_revisions/publishes/create_form.ejs +10 -0
  47. package/src/views/article_revisions/tags/edit_form.ejs +64 -0
  48. package/src/views/article_revisions/visuals/edit_form.ejs +42 -0
  49. package/src/views/articles/_form.ejs +86 -0
  50. package/src/views/articles/create_form.ejs +9 -0
  51. package/src/views/articles/deletes/create_form.ejs +55 -0
  52. package/src/views/articles/edit_form.ejs +3 -0
  53. package/src/views/articles/index.ejs +161 -0
  54. package/src/views/articles/show.ejs +281 -0
  55. package/src/views/articles/translations/_form.ejs +77 -0
  56. package/src/views/articles/translations/create_form.ejs +11 -0
  57. package/src/views/articles/urls/create_form.ejs +31 -0
  58. package/src/views/page_revisions/locks/create_form.ejs +1 -1
  59. package/test/factory.js +37 -0
@@ -0,0 +1,155 @@
1
+ import URLsHandler from "../../../../handlers/articles/URLsHandler";
2
+ import Articles from "../../../../cloud/Articles";
3
+ import Factory from "../../../../../test/factory";
4
+ import { testConfig } from "../../../../../test/handlersHelper";
5
+
6
+ function buildCtx(article, updateSettings) {
7
+ return {
8
+ client: {
9
+ articles: new Articles(testConfig, "token"),
10
+ },
11
+ params: { parent_id: article.id },
12
+ resourceParams: {
13
+ change: updateSettings,
14
+ },
15
+ locale: "en",
16
+ };
17
+ }
18
+
19
+ describe("Article change url", () => {
20
+ test("create successful", async () => {
21
+ const article = Factory.build("article");
22
+ const updateSettings = {
23
+ slug: "new-slug",
24
+ redirect: "1",
25
+ };
26
+ const ctx = buildCtx(article, updateSettings);
27
+
28
+ const mockFind = jest
29
+ .spyOn(ctx.client.articles, "find")
30
+ .mockImplementation(() => article);
31
+
32
+ const mockChange = jest
33
+ .spyOn(ctx.client.articles, "updateSlug")
34
+ .mockImplementation(() => ({ article, errors: null }));
35
+
36
+ const result = await URLsHandler.create(ctx);
37
+
38
+ expect(mockFind).toHaveBeenCalledWith(article.id, "en");
39
+ expect(mockChange).toHaveBeenCalledWith(
40
+ article.id,
41
+ updateSettings.slug,
42
+ "en",
43
+ {
44
+ url: `/${article.slug}`,
45
+ destination: updateSettings.slug,
46
+ httpResponseCode: 301,
47
+ note: "Created with changed URL for a article",
48
+ }
49
+ );
50
+
51
+ expect(result).toEqual({
52
+ redirect: `/en/articles/${article.id}`,
53
+ });
54
+ });
55
+
56
+ test("create failed due to slug error", async () => {
57
+ const article = Factory.build("article");
58
+ const updateSettings = {
59
+ slug: "new-slug",
60
+ };
61
+ const ctx = buildCtx(article, updateSettings);
62
+
63
+ const mockFind = jest
64
+ .spyOn(ctx.client.articles, "find")
65
+ .mockImplementation(() => article);
66
+
67
+ const mockChange = jest
68
+ .spyOn(ctx.client.articles, "updateSlug")
69
+ .mockImplementation(() => ({
70
+ article: null,
71
+ errors: [{ codes: [], key: "slug", message: "has already been taken" }],
72
+ }));
73
+
74
+ const result = await URLsHandler.create(ctx);
75
+
76
+ expect(mockFind).toHaveBeenCalledWith(article.id, "en");
77
+ expect(mockChange).toHaveBeenCalledWith(
78
+ article.id,
79
+ updateSettings.slug,
80
+ "en",
81
+ null
82
+ );
83
+
84
+ expect(result).toEqual({
85
+ assigns: {
86
+ change: { ...updateSettings, id: article.id },
87
+ errors: { change: { slug: "has already been taken" } },
88
+ _translations: [],
89
+ },
90
+ });
91
+ });
92
+
93
+ test("create failed due to redirect create", async () => {
94
+ const article = Factory.build("article");
95
+ const updateSettings = {
96
+ slug: "new-slug",
97
+ redirect: "1",
98
+ };
99
+ const ctx = buildCtx(article, updateSettings);
100
+
101
+ const mockFind = jest
102
+ .spyOn(ctx.client.articles, "find")
103
+ .mockImplementation(() => article);
104
+
105
+ const mockChange = jest
106
+ .spyOn(ctx.client.articles, "updateSlug")
107
+ .mockImplementation(() => ({
108
+ article: null,
109
+ errors: [
110
+ {
111
+ codes: [],
112
+ key: "generalError",
113
+ message: "url: has already been taken",
114
+ },
115
+ ],
116
+ }));
117
+
118
+ const result = await URLsHandler.create(ctx);
119
+
120
+ expect(mockFind).toHaveBeenCalledWith(article.id, "en");
121
+ expect(mockChange).toHaveBeenCalledWith(
122
+ article.id,
123
+ updateSettings.slug,
124
+ "en",
125
+ {
126
+ url: `/${article.slug}`,
127
+ destination: updateSettings.slug,
128
+ httpResponseCode: 301,
129
+ note: "Created with changed URL for a article",
130
+ }
131
+ );
132
+
133
+ expect(result).toEqual({
134
+ assigns: {
135
+ change: { ...updateSettings, id: article.id },
136
+ errors: { change: { redirect: "url: has already been taken" } },
137
+ _translations: [],
138
+ },
139
+ });
140
+ });
141
+
142
+ test("create for non existing article", async () => {
143
+ const ctx = buildCtx({ id: "42" }, {});
144
+
145
+ const mockFind = jest
146
+ .spyOn(ctx.client.articles, "find")
147
+ .mockImplementation(() => null);
148
+
149
+ expect(async () => {
150
+ await URLsHandler.create(ctx);
151
+ }).rejects.toThrow("Cannot find article with id 42");
152
+
153
+ expect(mockFind).toHaveBeenCalledWith("42", "en");
154
+ });
155
+ });
@@ -0,0 +1,51 @@
1
+ import URLsHandler from "../../../../handlers/articles/URLsHandler";
2
+ import Articles from "../../../../cloud/Articles";
3
+ import Factory from "../../../../../test/factory";
4
+ import { testConfig } from "../../../../../test/handlersHelper";
5
+
6
+ function buildCtx(article) {
7
+ return {
8
+ client: {
9
+ articles: new Articles(testConfig, "token"),
10
+ },
11
+ params: { parent_id: article.id },
12
+ locale: "en",
13
+ };
14
+ }
15
+
16
+ describe("Article change url", () => {
17
+ test("create_form for existing article", async () => {
18
+ const article = Factory.build("article");
19
+ const ctx = buildCtx(article);
20
+
21
+ const mockFind = jest
22
+ .spyOn(ctx.client.articles, "find")
23
+ .mockImplementation(() => article);
24
+
25
+ const result = await URLsHandler.create_form(ctx);
26
+
27
+ expect(mockFind).toHaveBeenCalledWith(article.id, "en");
28
+
29
+ expect(result).toEqual({
30
+ assigns: {
31
+ change: { slug: article.slug, id: article.id, redirect: null },
32
+ errors: {},
33
+ _translations: [],
34
+ },
35
+ });
36
+ });
37
+
38
+ test("create_form for non existing article", async () => {
39
+ const ctx = buildCtx({ id: "42" });
40
+
41
+ const mockFind = jest
42
+ .spyOn(ctx.client.articles, "find")
43
+ .mockImplementation(() => null);
44
+
45
+ expect(async () => {
46
+ await URLsHandler.create_form(ctx);
47
+ }).rejects.toThrow("Cannot find article with id 42");
48
+
49
+ expect(mockFind).toHaveBeenCalledWith("42", "en");
50
+ });
51
+ });
@@ -51,7 +51,7 @@ describe("Pages Handler", () => {
51
51
  meta: {
52
52
  title: params.meta_title,
53
53
  description: params.meta_description,
54
- image: JSON.parse(params.meta_image || "{}"),
54
+ image: JSON.parse(params.meta_image),
55
55
  },
56
56
  credentials: {
57
57
  user: params.username,
@@ -112,7 +112,7 @@ describe("Pages Handler", () => {
112
112
  meta: {
113
113
  title: params.meta_title,
114
114
  description: params.meta_description,
115
- image: JSON.parse(params.meta_image || "{}"),
115
+ image: JSON.parse(params.meta_image),
116
116
  },
117
117
  credentials: {
118
118
  user: params.username,
@@ -129,7 +129,7 @@ describe("Pages Handler", () => {
129
129
 
130
130
  expect(result).toEqual({
131
131
  assigns: {
132
- page: { ...params },
132
+ page: { ...params, meta_image: JSON.parse(params.meta_image) },
133
133
  tags: [tag],
134
134
  errors: { page: { slug: "cannot be blank" } },
135
135
  visibilityOptions: ctx.client.pages.visibilityOptions(),
package/src/utils.js ADDED
@@ -0,0 +1,46 @@
1
+ export function toUTCISO8601String(initialDate) {
2
+ let date = null;
3
+
4
+ if (typeof initialDate === "string") {
5
+ date = new Date(initialDate);
6
+ } else {
7
+ date = initialDate;
8
+ }
9
+
10
+ return [
11
+ date.getUTCFullYear(),
12
+ "-",
13
+ (date.getUTCMonth() + 1).toString().padStart(2, "0"),
14
+ "-",
15
+ date.getUTCDate().toString().padStart(2, "0"),
16
+ "T",
17
+ date.getUTCHours().toString().padStart(2, "0"),
18
+ ":",
19
+ date.getUTCMinutes().toString().padStart(2, "0"),
20
+ ":",
21
+ date.getUTCSeconds().toString().padStart(2, "0"),
22
+ "Z",
23
+ ].join("");
24
+ }
25
+
26
+ export function toDateTimeInput(initialDate) {
27
+ let date = null;
28
+
29
+ if (typeof initialDate === "string") {
30
+ date = new Date(initialDate);
31
+ } else {
32
+ date = initialDate;
33
+ }
34
+
35
+ return [
36
+ date.getFullYear(),
37
+ "-",
38
+ (date.getMonth() + 1).toString().padStart(2, "0"),
39
+ "-",
40
+ date.getDate().toString().padStart(2, "0"),
41
+ "T",
42
+ date.getHours().toString().padStart(2, "0"),
43
+ ":",
44
+ date.getMinutes().toString().padStart(2, "0"),
45
+ ].join("");
46
+ }
@@ -0,0 +1,40 @@
1
+ <%
2
+ const activeClass = "relative top-0.5 px-4 border-b-2 border-raketa-primary pb-4";
3
+ const inactiveClass = "relative top-0.5 px-4 border-b-2 border-transparent pb-4";
4
+ %>
5
+
6
+ <div class="flex w-full pb-4">
7
+ <div class="flex items-center justify-between mb-4">
8
+ <div class="flex items-center border-b-2">
9
+ <div class=" <%= active === 'general' ? activeClass : inactiveClass %>">
10
+ <a href="<%= `/${_context.locale}/article_revisions/${settings.id}/general/edit` %>" class="text-raketa-primary font-semibold">
11
+ General
12
+ </a>
13
+ </div>
14
+
15
+ <div class="<%= active === 'visuals' ? activeClass : inactiveClass %>">
16
+ <a href="<%= `/${_context.locale}/article_revisions/${settings.id}/visuals/edit` %>" class="text-raketa-primary font-semibold">
17
+ Visuals
18
+ </a>
19
+ </div>
20
+
21
+ <div class="<%= active === 'codes' ? activeClass : inactiveClass %>">
22
+ <a href="<%= `/${_context.locale}/article_revisions/${settings.id}/codes/edit` %>" class="text-raketa-primary font-semibold">
23
+ Code customizations
24
+ </a>
25
+ </div>
26
+
27
+ <div class="<%= active === 'meta' ? activeClass : inactiveClass %>">
28
+ <a href="<%= `/${_context.locale}/article_revisions/${settings.id}/meta/edit` %>" class="text-raketa-primary font-semibold">
29
+ Open graph & meta tags
30
+ </a>
31
+ </div>
32
+
33
+ <div class="<%= active === 'tags' ? activeClass : inactiveClass %>">
34
+ <a href="<%= `/${_context.locale}/article_revisions/${settings.id}/tags/edit` %>" class="text-raketa-primary font-semibold">
35
+ Content tags
36
+ </a>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
@@ -0,0 +1,39 @@
1
+ <div class="page-wrapper">
2
+ <div class="page-title">
3
+ <h1 class="heading-1">Edit revision / Code customizations</h1>
4
+ </div>
5
+
6
+ <%- include('../_nav.ejs', { settings: code_settings, active: 'codes' }) %>
7
+
8
+ <div class="grid gap-4 md:grid-cols-3">
9
+ <div>
10
+ <% form_for({ code_settings }, errors, (f) => { %>
11
+ <form action="<%= `/${_context.locale}/article_revisions/${code_settings.id}/codes` %>" method="POST" class="form">
12
+ <%- f.field('code_settings.scripts', { as: 'textarea', }) %>
13
+ <%- f.field('code_settings.structured_data', { label: 'Structured data (JSON)', as: 'textarea', hint: 'Click here for a structured data verification tool' }) %>
14
+
15
+ <div class="form-actions">
16
+ <div class="btn-toolbar">
17
+ <%- f.submit('Save changes', { class: 'btn-primary' }) %>
18
+ <a href="<%= `/${_context.locale}/articles/${code_settings.originalId}` %>" class="btn-secondary">Back</a>
19
+ </div>
20
+ </div>
21
+ </form>
22
+ <% }) %>
23
+ </div>
24
+
25
+ <div>
26
+ <div class="help">
27
+ <div class="mb-2 flex items-center gap-1">
28
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
29
+ <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
30
+ </svg>
31
+
32
+ <h3 class="heading-3">Contextual help</h3>
33
+ </div>
34
+
35
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore nostrum fugit sint sunt distinctio ut cum, corrupti ad error, quo labore minima. Reprehenderit quasi maxime repudiandae voluptatum commodi saepe molestiae?</p>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
@@ -0,0 +1,70 @@
1
+ <div class="page-wrapper">
2
+ <div class="page-title">
3
+ <h1 class="heading-1">Edit revision / General</h1>
4
+ </div>
5
+
6
+ <%- include('../_nav.ejs', { settings: general_settings, active: 'general' }) %>
7
+
8
+ <div class="grid gap-4 md:grid-cols-3">
9
+ <div>
10
+ <% form_for({ general_settings }, errors, (f) => { %>
11
+ <form action="<%= `/${_context.locale}/article_revisions/${general_settings.id}/general` %>" method="POST" class="form">
12
+ <%- f.field('general_settings.title', { required: true }) %>
13
+ <%- f.field('general_settings.slug', { label: "URL", required: true }) %>
14
+ <%- f.field('general_settings.publish_at', { required: true, type: 'datetime-local' }) %>
15
+ <%- f.field('general_settings.visibility', { as: 'select', required: true, options: visibilityOptions }) %>
16
+
17
+ <%- f.field('general_settings.username', { required: true }) %>
18
+ <%- f.field('general_settings.password', { required: true }) %>
19
+
20
+ <div class="form-actions">
21
+ <div class="btn-toolbar">
22
+ <%- f.submit('Save changes', { class: 'btn-primary' }) %>
23
+ <a href="<%= `/${_context.locale}/articles/${general_settings.originalId}` %>" class="btn-secondary">Back</a>
24
+ </div>
25
+ </div>
26
+ </form>
27
+ <% }) %>
28
+ </div>
29
+
30
+ <div>
31
+ <div class="help">
32
+ <div class="mb-2 flex items-center gap-1">
33
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
34
+ <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
35
+ </svg>
36
+
37
+ <h3 class="heading-3">Contextual help</h3>
38
+ </div>
39
+
40
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore nostrum fugit sint sunt distinctio ut cum, corrupti ad error, quo labore minima. Reprehenderit quasi maxime repudiandae voluptatum commodi saepe molestiae?</p>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <script>
47
+ function onVisibilityChange(event) {
48
+ const newValue = event.target.value;
49
+
50
+ if (newValue === "protected") {
51
+ document.getElementById("f_username").parentElement.style.display = "block";
52
+ document.getElementById("f_username").required = true;
53
+ document.getElementById("f_password").parentElement.style.display = "block";
54
+ document.getElementById("f_password").required = true;
55
+ } else {
56
+ document.getElementById("f_username").parentElement.style.display = "none";
57
+ document.getElementById("f_username").required = false;
58
+ document.getElementById("f_password").parentElement.style.display = "none";
59
+ document.getElementById("f_password").required = false;
60
+ }
61
+ }
62
+
63
+ onVisibilityChange({
64
+ target: {
65
+ value: '<%= general_settings.visibility %>'
66
+ }
67
+ })
68
+
69
+ document.getElementById("f_visibility").addEventListener("change", onVisibilityChange)
70
+ </script>
@@ -0,0 +1,35 @@
1
+ <div class="page-wrapper">
2
+ <div class="page-title">
3
+ <h1 class="heading-1">Lock revision</h1>
4
+ </div>
5
+
6
+ <div class="grid gap-4 md:grid-cols-3">
7
+ <div>
8
+ <% form_for({ lock_settings }, errors, (f) => { %>
9
+ <form action="<%= `/${_context.locale}/article_revisions/${lock_settings.revision.id}/locks` %>" method="POST" class="form">
10
+ <%- f.field('lock_settings.message', { as: 'textarea', label: 'Lock Message', required: true }) %>
11
+ <div class="form-actions">
12
+ <div class="btn-toolbar">
13
+ <%- f.submit('Lock revision', { class: 'btn-primary' }) %>
14
+ <a href="<%= `/${_context.locale}/articles/${lock_settings.revision.originalId}` %>" class="btn-secondary">Cancel</a>
15
+ </div>
16
+ </div>
17
+ </form>
18
+ <% }) %>
19
+ </div>
20
+
21
+ <div>
22
+ <div class="help">
23
+ <div class="mb-2 flex items-center gap-1">
24
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
25
+ <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
26
+ </svg>
27
+
28
+ <h3 class="heading-3">Contextual help</h3>
29
+ </div>
30
+
31
+ <p>Locking a revision will prevent further changes to this version of the article. This action cannot be undone. Please provide a reason for locking this revision.</p>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
@@ -0,0 +1,41 @@
1
+ <div class="page-wrapper">
2
+ <div class="page-title">
3
+ <h1 class="heading-1">Edit revision / Open graph & meta tags</h1>
4
+ </div>
5
+
6
+ <%- include('../_nav.ejs', { settings: meta_settings, active: 'meta' }) %>
7
+
8
+ <div class="grid gap-4 md:grid-cols-3">
9
+ <div>
10
+ <% form_for({ meta_settings }, errors, (f) => { %>
11
+ <form action="<%= `/${_context.locale}/article_revisions/${meta_settings.id}/meta` %>" method="POST" class="form">
12
+ <%- f.field('meta_settings.title', { required: true }) %>
13
+ <%- f.field('meta_settings.description', { as: 'textarea', required: true }) %>
14
+
15
+ <raketa-image-picker name="meta_settings[image]" value="<%= JSON.stringify(meta_settings.image) %>"></raketa-image-picker>
16
+
17
+ <div class="form-actions">
18
+ <div class="btn-toolbar">
19
+ <%- f.submit('Save changes', { class: 'btn-primary' }) %>
20
+ <a href="<%= `/${_context.locale}/articles/${meta_settings.originalId}` %>" class="btn-secondary">Back</a>
21
+ </div>
22
+ </div>
23
+ </form>
24
+ <% }) %>
25
+ </div>
26
+
27
+ <div>
28
+ <div class="help">
29
+ <div class="mb-2 flex items-center gap-1">
30
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
31
+ <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
32
+ </svg>
33
+
34
+ <h3 class="heading-3">Contextual help</h3>
35
+ </div>
36
+
37
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore nostrum fugit sint sunt distinctio ut cum, corrupti ad error, quo labore minima. Reprehenderit quasi maxime repudiandae voluptatum commodi saepe molestiae?</p>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
@@ -0,0 +1,57 @@
1
+ <% form_for({ publishing }, errors, (f) => { %>
2
+ <form action="<%= `/${_context.locale}/article_revisions/${publishing.id}/publishes` %>" method="post" class="form">
3
+ <%- f.field('publishing.title'), { required: true } %>
4
+ <%- f.field('publishing.description', { as: "textarea" }) %>
5
+ <%- f.field('publishing.visibility', { as: 'select', required: true, options: visibilityOptions }) %>
6
+ <%- f.field('publishing.username', { required: true }) %>
7
+ <%- f.field('publishing.password', { required: true }) %>
8
+
9
+ <div class="form-actions">
10
+ <div class="btn-toolbar">
11
+ <%- f.submit('Save record', { class: 'btn-primary' }) %>
12
+
13
+ <a href="<%= `/${_context.locale}/articles/${publishing.originalId}` %>" class="btn-secondary">Back</a>
14
+ </div>
15
+ </div>
16
+ </form>
17
+ <% }) %>
18
+
19
+ <div>
20
+ <div class="help">
21
+ <div class="flex items-center gap-1 mb-2">
22
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
23
+ <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
24
+ </svg>
25
+
26
+ <h3 class="heading-3">Contextual help</h3>
27
+ </div>
28
+
29
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore nostrum fugit sint sunt distinctio ut cum, corrupti ad error, quo labore minima. Reprehenderit quasi maxime repudiandae voluptatum commodi saepe molestiae?</p>
30
+ </div>
31
+ </div>
32
+
33
+ <script>
34
+ function onVisibilityChange(event) {
35
+ const newValue = event.target.value;
36
+
37
+ if (newValue === "protected") {
38
+ document.getElementById("f_username").parentElement.style.display = "block";
39
+ document.getElementById("f_username").required = true;
40
+ document.getElementById("f_password").parentElement.style.display = "block";
41
+ document.getElementById("f_password").required = true;
42
+ } else {
43
+ document.getElementById("f_username").parentElement.style.display = "none";
44
+ document.getElementById("f_username").required = false;
45
+ document.getElementById("f_password").parentElement.style.display = "none";
46
+ document.getElementById("f_password").required = false;
47
+ }
48
+ }
49
+
50
+ onVisibilityChange({
51
+ target: {
52
+ value: '<%= publishing.visibility %>'
53
+ }
54
+ })
55
+
56
+ document.getElementById("f_visibility").addEventListener("change", onVisibilityChange)
57
+ </script>
@@ -0,0 +1,10 @@
1
+ <div class="page-wrapper">
2
+ <div class="page-title">
3
+ <h1 class="heading-1">Publish revision</h1>
4
+ </div>
5
+
6
+ <div class="grid gap-4 md:grid-cols-3">
7
+ <%- include('_form.ejs') %>
8
+ </div>
9
+ </div>
10
+
@@ -0,0 +1,64 @@
1
+ <div class="page-wrapper">
2
+ <div class="page-title">
3
+ <h1 class="heading-1">Edit revision / Content tags</h1>
4
+ </div>
5
+
6
+ <%- include('../_nav.ejs', { settings: tag_settings, active: 'tags' }) %>
7
+
8
+ <div class="grid gap-4 md:grid-cols-3">
9
+ <div>
10
+ <% form_for({ tag_settings }, errors, (f) => { %>
11
+ <form action="<%= `/${_context.locale}/article_revisions/${tag_settings.id}/tags` %>" method="POST" class="form">
12
+ <%- f.field('tag_settings.tags', { as: 'checkbox_group', required: true, options: tags.map(tag => [tag.id, tag.name]) }) %>
13
+
14
+ <div class="form-actions">
15
+ <div class="btn-toolbar">
16
+ <%- f.submit('Save changes', { class: 'btn-primary' }) %>
17
+ <a href="<%= `/${_context.locale}/articles/${tag_settings.originalId}` %>" class="btn-secondary">Back</a>
18
+ </div>
19
+ </div>
20
+ </form>
21
+ <% }) %>
22
+ </div>
23
+
24
+ <div>
25
+ <div class="help">
26
+ <div class="mb-2 flex items-center gap-1">
27
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
28
+ <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
29
+ </svg>
30
+
31
+ <h3 class="heading-3">Contextual help</h3>
32
+ </div>
33
+
34
+ <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore nostrum fugit sint sunt distinctio ut cum, corrupti ad error, quo labore minima. Reprehenderit quasi maxime repudiandae voluptatum commodi saepe molestiae?</p>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <script>
41
+ function onVisibilityChange(event) {
42
+ const newValue = event.target.value;
43
+
44
+ if (newValue === "protected") {
45
+ document.getElementById("f_username").parentElement.style.display = "block";
46
+ document.getElementById("f_username").required = true;
47
+ document.getElementById("f_password").parentElement.style.display = "block";
48
+ document.getElementById("f_password").required = true;
49
+ } else {
50
+ document.getElementById("f_username").parentElement.style.display = "none";
51
+ document.getElementById("f_username").required = false;
52
+ document.getElementById("f_password").parentElement.style.display = "none";
53
+ document.getElementById("f_password").required = false;
54
+ }
55
+ }
56
+
57
+ onVisibilityChange({
58
+ target: {
59
+ value: '<%= tag_settings.visibility %>'
60
+ }
61
+ })
62
+
63
+ document.getElementById("f_visibility").addEventListener("change", onVisibilityChange)
64
+ </script>