@raketa-cloud/admin 0.0.57 → 0.0.58

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raketa-cloud/admin",
3
- "version": "0.0.57",
3
+ "version": "0.0.58",
4
4
  "main": "src/index.js",
5
5
  "scripts": {
6
6
  "dev": "npx tailwindcss -i ./src/styles/application.css -o ../../apps/admin/static/application.css --watch",
@@ -1,21 +1,73 @@
1
+ const index = async (ctx) => {
2
+ const { parent_id } = ctx.params;
3
+ const { translations } = ctx.query;
4
+ let translatedPages = [];
5
+
6
+ if (translations) {
7
+ const translationPairs = translations.split(";").map((pair) => {
8
+ const [locale, id] = pair.split(":");
9
+ return { locale, id };
10
+ });
11
+
12
+ translatedPages = await Promise.all(
13
+ translationPairs.map(async ({ locale, id }) => {
14
+ const page = await ctx.client.pages.find(id, locale);
15
+
16
+ if (!page) {
17
+ throw new Error(
18
+ `Cannot find translated page with id ${id} and locale ${locale}`
19
+ );
20
+ }
21
+
22
+ const revisions = await ctx.client.pageRevisions.all(id, locale);
23
+ const openRevision = revisions.list[0];
24
+
25
+ return {
26
+ ...page,
27
+ translationLocale: locale,
28
+ revisionId: openRevision?.id,
29
+ };
30
+ })
31
+ );
32
+ }
33
+
34
+ const items = await ctx.client.pages.list(
35
+ ctx.query,
36
+ ctx.locale,
37
+ ctx.resourceConfig.pageType
38
+ );
39
+
40
+ const originalPage = await ctx.client.pages.find(parent_id, ctx.locale);
41
+
42
+ return {
43
+ assigns: {
44
+ items,
45
+ visibilityOptions: ctx.client.pages.visibilityOptions(),
46
+ translations: translatedPages,
47
+ parent_id,
48
+ pageTitle: originalPage.title,
49
+ },
50
+ };
51
+ };
52
+
1
53
  const create_form = async (ctx) => {
2
- const { target_locale } = ctx.query;
3
- const target_locate_tags = await ctx.client.tags.all(target_locale);
54
+ const { target_locale = "" } = ctx.query;
4
55
 
5
56
  const { parent_id } = ctx.params;
6
57
  const blueprint = await ctx.client.pages.find(parent_id, ctx.locale);
7
58
 
59
+ if (!blueprint) {
60
+ throw new Error(`Cannot find page with id ${parent_id}`);
61
+ }
62
+
8
63
  const resource = structuredClone(blueprint);
9
- resource.tags = [];
10
64
 
11
65
  return {
12
66
  assigns: {
13
67
  resource,
14
68
  blueprint,
15
69
  targetLocale: target_locale,
16
- tags: target_locate_tags.list,
17
70
  errors: {},
18
- visibilityOptions: ctx.client.pages.visibilityOptions(),
19
71
  _translations: [],
20
72
  },
21
73
  };
@@ -23,51 +75,93 @@ const create_form = async (ctx) => {
23
75
 
24
76
  const create = async (ctx) => {
25
77
  const { parent_id } = ctx.params;
26
- const blueprint = await ctx.client.pages.find(parent_id, ctx.locale);
78
+ const originalLocale = ctx.locale;
79
+ const blueprint = await ctx.client.pages.find(parent_id, originalLocale);
27
80
 
28
- const { target_locale } = ctx.resourceParams;
29
- const params = { ...ctx.resourceParams.resource };
81
+ const formLanguages = Array.isArray(ctx.resourceParams.resource.languages)
82
+ ? ctx.resourceParams.resource.languages
83
+ : [ctx.resourceParams.resource.languages].filter(Boolean);
30
84
 
31
- const resource = ctx.client.pages.deepMerge(
32
- {
33
- settings: {
34
- ...blueprint.settings,
35
- },
36
- widgets: blueprint.widgets,
37
- categories: blueprint.categories.map((category) => category.id),
38
- tags: [],
39
- },
40
- params
85
+ const existingTranslations = Object.keys(blueprint.translations || {});
86
+ const selectedLocales = formLanguages.filter(
87
+ (locale) =>
88
+ locale !== originalLocale && !existingTranslations.includes(locale)
41
89
  );
42
90
 
43
- const { errors } = await ctx.client.pages.translate(
44
- parent_id,
45
- target_locale,
46
- resource
47
- );
91
+ const params = {
92
+ title: blueprint.title,
93
+ tags: [],
94
+ visibility: "private",
95
+ slug: blueprint.slug,
96
+ };
48
97
 
49
- if (errors) {
50
- const tags = await ctx.client.tags.all(target_locale);
51
-
52
- return {
53
- assigns: {
54
- resource,
55
- blueprint,
56
- targetLocale: target_locale,
57
- tags: tags.list,
58
- errors: { resource: ctx.client.pages.formatErrors(errors) },
59
- visibilityOptions: ctx.client.pages.visibilityOptions(),
60
- _translations: [],
98
+ const translationIds = [];
99
+
100
+ for (const locale of selectedLocales) {
101
+ const resource = ctx.client.pages.deepMerge(
102
+ {
103
+ settings: blueprint.settings,
104
+ widgets: blueprint.widgets,
105
+ categories: blueprint.categories.map((category) => category.id),
61
106
  },
62
- };
107
+ params
108
+ );
109
+
110
+ const { errors, page } = await ctx.client.pages.translate(
111
+ parent_id,
112
+ locale,
113
+ resource
114
+ );
115
+
116
+ if (errors) {
117
+ return {
118
+ assigns: {
119
+ resource: {
120
+ ...resource,
121
+ translations: blueprint.translations || {},
122
+ languages: formLanguages,
123
+ },
124
+ blueprint,
125
+ targetLocale: locale,
126
+ errors: { resource: ctx.client.pages.formatErrors(errors) },
127
+ },
128
+ };
129
+ }
130
+
131
+ if (page) {
132
+ translationIds.push(`${locale}:${page.id}`);
133
+ }
134
+
135
+ const { errors: revisionErrors } = await ctx.client.pageRevisions.create(
136
+ page.id,
137
+ locale
138
+ );
139
+
140
+ if (revisionErrors) {
141
+ return {
142
+ assigns: {
143
+ resource: {
144
+ ...resource,
145
+ translations: blueprint.translations || {},
146
+ languages: formLanguages,
147
+ },
148
+ blueprint,
149
+ targetLocale: locale,
150
+ errors: {
151
+ resource: ctx.client.pages.formatErrors(revisionErrors),
152
+ },
153
+ },
154
+ };
155
+ }
63
156
  }
64
157
 
65
158
  return {
66
- redirect: `/${ctx.locale}/${ctx.resourceConfig.pageTypePlural}/${parent_id}`,
159
+ redirect: `/${originalLocale}/${ctx.resourceConfig.pageTypePlural}/${parent_id}/translations?translations=${translationIds.join(";")}`,
67
160
  };
68
161
  };
69
162
 
70
163
  export default {
164
+ index,
71
165
  create_form,
72
166
  create,
73
167
  };
@@ -1,22 +1,22 @@
1
- import routeCollection from "./routeCollection";
2
- import routeNestedCollection from "./routeNestedCollection";
3
- import routeNestedMember from "./routeNestedMember";
4
- import TranslationsHandler from "./handlers/pages/TranslationsHandler";
5
- import PageRevisionsHandler from "./handlers/page_revisions/PageRevisionsHandler";
6
- import GeneralHandler from "./handlers/page_revisions/GeneralHandler";
7
- import VisualHandler from "./handlers/page_revisions/VisualHandler";
8
1
  import CodeHandler from "./handlers/page_revisions/CodeHandler";
2
+ import GeneralHandler from "./handlers/page_revisions/GeneralHandler";
3
+ import LocksHandler from "./handlers/page_revisions/LocksHandler";
9
4
  import MetaHandler from "./handlers/page_revisions/MetaHandler";
5
+ import PageRevisionsHandler from "./handlers/page_revisions/PageRevisionsHandler";
6
+ import PublishesHandler from "./handlers/page_revisions/PublishesHandler";
7
+ import RevertsHandler from "./handlers/page_revisions/RevertsHandler";
10
8
  import TagHandler from "./handlers/page_revisions/TagHandler";
11
- import LocksHandler from "./handlers/page_revisions/LocksHandler";
12
9
  import UnlocksHandler from "./handlers/page_revisions/UnlocksHandler";
10
+ import VisualHandler from "./handlers/page_revisions/VisualHandler";
11
+ import WidgetHandler from "./handlers/page_revisions/WidgetHandler";
13
12
  import ClonesHandler from "./handlers/pages/ClonesHandler";
14
- import URLsHandler from "./handlers/pages/URLsHandler";
15
13
  import DeletesHandler from "./handlers/pages/DeletesHandler";
16
- import RevertsHandler from "./handlers/page_revisions/RevertsHandler";
17
- import PublishesHandler from "./handlers/page_revisions/PublishesHandler";
18
- import WidgetHandler from "./handlers/page_revisions/WidgetHandler";
19
14
  import PagesHandler from "./handlers/pages/PagesHandler";
15
+ import TranslationsHandler from "./handlers/pages/TranslationsHandler";
16
+ import URLsHandler from "./handlers/pages/URLsHandler";
17
+ import routeCollection from "./routeCollection";
18
+ import routeNestedCollection from "./routeNestedCollection";
19
+ import routeNestedMember from "./routeNestedMember";
20
20
 
21
21
  function routePagesModule(
22
22
  adapter,
@@ -0,0 +1,279 @@
1
+ import PageRevisions from "../../../../cloud/PageRevisions";
2
+ import Pages from "../../../../cloud/Pages";
3
+ import TranslationsHandler from "../../../../handlers/pages/TranslationsHandler";
4
+ import Factory from "../../../support/factory";
5
+
6
+ function buildCtx(params, page) {
7
+ return {
8
+ client: {
9
+ pages: new Pages(() => ({}), "token"),
10
+ pageRevisions: new PageRevisions(() => ({}), "token"),
11
+ },
12
+ params: { parent_id: page.id },
13
+ resourceParams: {
14
+ resource: params,
15
+ },
16
+ locale: "en",
17
+ resourceConfig: {
18
+ pageType: "page",
19
+ pageTypePlural: "pages",
20
+ },
21
+ };
22
+ }
23
+
24
+ describe("Translation Handler", () => {
25
+ test("create fails with translation errors", async () => {
26
+ const page = Factory.build("page", {
27
+ translations: {},
28
+ categories: [{ id: "cat1" }],
29
+ });
30
+ const params = {
31
+ languages: ["fr"],
32
+ };
33
+
34
+ const ctx = buildCtx(params, page);
35
+
36
+ const mockFind = jest
37
+ .spyOn(ctx.client.pages, "find")
38
+ .mockImplementation(() => page);
39
+
40
+ const translationErrors = [{ key: "title", message: "cannot be blank" }];
41
+ const mockTranslate = jest
42
+ .spyOn(ctx.client.pages, "translate")
43
+ .mockImplementation(() => ({
44
+ page: null,
45
+ errors: translationErrors,
46
+ }));
47
+
48
+ const mockFormatErrors = jest
49
+ .spyOn(ctx.client.pages, "formatErrors")
50
+ .mockImplementation(() => ({ title: "cannot be blank" }));
51
+
52
+ const result = await TranslationsHandler.create(ctx);
53
+
54
+ expect(mockFind).toHaveBeenCalledWith(page.id, "en");
55
+ expect(mockTranslate).toHaveBeenCalled();
56
+ expect(mockFormatErrors).toHaveBeenCalledWith(translationErrors);
57
+
58
+ expect(result).toEqual({
59
+ assigns: {
60
+ resource: {
61
+ title: page.title,
62
+ tags: [],
63
+ visibility: "private",
64
+ slug: page.slug,
65
+ settings: page.settings,
66
+ widgets: page.widgets,
67
+ categories: ["cat1"],
68
+ translations: {},
69
+ languages: ["fr"],
70
+ },
71
+ blueprint: page,
72
+ targetLocale: "fr",
73
+ errors: { resource: { title: "cannot be blank" } },
74
+ },
75
+ });
76
+ });
77
+
78
+ test("create successfully multiple translations with already one is existing", async () => {
79
+ const page = Factory.build("page", {
80
+ translations: { de: "de-id" },
81
+ categories: [{ id: "cat1" }],
82
+ });
83
+
84
+ const params = {
85
+ languages: ["fr", "es", "de"],
86
+ };
87
+
88
+ const ctx = buildCtx(params, page);
89
+
90
+ const mockFind = jest
91
+ .spyOn(ctx.client.pages, "find")
92
+ .mockImplementation(() => page);
93
+
94
+ const mockTranslate = jest
95
+ .spyOn(ctx.client.pages, "translate")
96
+ .mockImplementation((parentId, locale) => ({
97
+ page: { id: `translated-page-${locale}` },
98
+ errors: null,
99
+ }));
100
+
101
+ const mockCreateRevision = jest
102
+ .spyOn(ctx.client.pageRevisions, "create")
103
+ .mockImplementation(() => ({ errors: null }));
104
+
105
+ const result = await TranslationsHandler.create(ctx);
106
+
107
+ expect(mockFind).toHaveBeenCalledWith(page.id, "en");
108
+
109
+ expect(mockTranslate).toHaveBeenCalledTimes(2);
110
+ expect(mockTranslate).toHaveBeenCalledWith(
111
+ page.id,
112
+ "fr",
113
+ expect.objectContaining({
114
+ title: page.title,
115
+ tags: [],
116
+ visibility: "private",
117
+ slug: page.slug,
118
+ })
119
+ );
120
+ expect(mockTranslate).toHaveBeenCalledWith(
121
+ page.id,
122
+ "es",
123
+ expect.objectContaining({
124
+ title: page.title,
125
+ tags: [],
126
+ visibility: "private",
127
+ slug: page.slug,
128
+ })
129
+ );
130
+
131
+ expect(mockCreateRevision).toHaveBeenCalledTimes(2);
132
+ expect(mockCreateRevision).toHaveBeenCalledWith("translated-page-fr", "fr");
133
+ expect(mockCreateRevision).toHaveBeenCalledWith("translated-page-es", "es");
134
+
135
+ expect(result).toEqual({
136
+ redirect: `/en/pages/${page.id}/translations?translations=fr:translated-page-fr;es:translated-page-es`,
137
+ });
138
+ });
139
+
140
+ test("create translations fails if any translation fails", async () => {
141
+ const page = Factory.build("page", {
142
+ translations: {},
143
+ categories: [{ id: "some-category-uno" }],
144
+ });
145
+
146
+ const params = {
147
+ languages: ["fr", "es"],
148
+ };
149
+
150
+ const ctx = buildCtx(params, page);
151
+
152
+ const mockFind = jest
153
+ .spyOn(ctx.client.pages, "find")
154
+ .mockImplementation(() => page);
155
+
156
+ const translationResponses = {
157
+ fr: { page: { id: "translated-page-fr" }, errors: null },
158
+ es: {
159
+ page: null,
160
+ errors: [{ key: "title", message: "invalid for spanish" }],
161
+ },
162
+ };
163
+
164
+ const mockTranslate = jest
165
+ .spyOn(ctx.client.pages, "translate")
166
+ .mockImplementation((parentId, locale) => translationResponses[locale]);
167
+
168
+ const mockFormatErrors = jest
169
+ .spyOn(ctx.client.pages, "formatErrors")
170
+ .mockImplementation(() => ({
171
+ title: "invalid for spanish",
172
+ }));
173
+
174
+ const mockCreateRevision = jest
175
+ .spyOn(ctx.client.pageRevisions, "create")
176
+ .mockImplementation(() => ({ errors: null }));
177
+
178
+ const result = await TranslationsHandler.create(ctx);
179
+
180
+ expect(mockFind).toHaveBeenCalledWith(page.id, "en");
181
+
182
+ expect(mockTranslate).toHaveBeenCalledWith(
183
+ page.id,
184
+ "fr",
185
+ expect.objectContaining({
186
+ title: page.title,
187
+ tags: [],
188
+ visibility: "private",
189
+ slug: page.slug,
190
+ })
191
+ );
192
+
193
+ expect(mockTranslate).toHaveBeenCalledWith(
194
+ page.id,
195
+ "es",
196
+ expect.objectContaining({
197
+ title: page.title,
198
+ tags: [],
199
+ visibility: "private",
200
+ slug: page.slug,
201
+ })
202
+ );
203
+
204
+ expect(result).toEqual({
205
+ assigns: {
206
+ resource: {
207
+ title: page.title,
208
+ tags: [],
209
+ visibility: "private",
210
+ slug: page.slug,
211
+ settings: page.settings,
212
+ widgets: page.widgets,
213
+ categories: ["some-category-uno"],
214
+ translations: {},
215
+ languages: ["fr", "es"],
216
+ },
217
+ blueprint: page,
218
+ targetLocale: "es",
219
+ errors: { resource: { title: "invalid for spanish" } },
220
+ },
221
+ });
222
+ });
223
+
224
+ test("create translation fails with revision errors", async () => {
225
+ const page = Factory.build("page", {
226
+ translations: {},
227
+ categories: [{ id: "cat1" }],
228
+ });
229
+ const params = {
230
+ languages: ["fr"],
231
+ };
232
+
233
+ const ctx = buildCtx(params, page);
234
+
235
+ const mockFind = jest
236
+ .spyOn(ctx.client.pages, "find")
237
+ .mockImplementation(() => page);
238
+
239
+ const mockTranslate = jest
240
+ .spyOn(ctx.client.pages, "translate")
241
+ .mockImplementation(() => ({
242
+ page: { id: "translated-page-id" },
243
+ errors: null,
244
+ }));
245
+
246
+ const revisionErrors = [{ key: "base", message: "something went wrong" }];
247
+ const mockCreateRevision = jest
248
+ .spyOn(ctx.client.pageRevisions, "create")
249
+ .mockImplementation(() => ({ errors: revisionErrors }));
250
+
251
+ const mockFormatErrors = jest
252
+ .spyOn(ctx.client.pages, "formatErrors")
253
+ .mockImplementation(() => ({ base: "something went wrong" }));
254
+
255
+ const result = await TranslationsHandler.create(ctx);
256
+
257
+ expect(mockCreateRevision).toHaveBeenCalledWith("translated-page-id", "fr");
258
+ expect(mockFormatErrors).toHaveBeenCalledWith(revisionErrors);
259
+
260
+ expect(result).toEqual({
261
+ assigns: {
262
+ resource: {
263
+ title: page.title,
264
+ tags: [],
265
+ visibility: "private",
266
+ slug: page.slug,
267
+ settings: page.settings,
268
+ widgets: page.widgets,
269
+ categories: ["cat1"],
270
+ translations: {},
271
+ languages: ["fr"],
272
+ },
273
+ blueprint: page,
274
+ targetLocale: "fr",
275
+ errors: { resource: { base: "something went wrong" } },
276
+ },
277
+ });
278
+ });
279
+ });
@@ -0,0 +1,71 @@
1
+ import Pages from "../../../../cloud/Pages";
2
+ import TranslationsHandler from "../../../../handlers/pages/TranslationsHandler";
3
+ import Factory from "../../../support/factory";
4
+
5
+ function buildCtx(page) {
6
+ return {
7
+ client: {
8
+ pages: new Pages(() => ({}), "token"),
9
+ },
10
+ params: { parent_id: page.id },
11
+ query: { target_locale: "" },
12
+ locale: "en",
13
+ resourceConfig: {
14
+ pageType: "page",
15
+ pageTypePlural: "pages",
16
+ },
17
+ };
18
+ }
19
+
20
+ describe("Page translation form", () => {
21
+ test("create_form when a page exists with target locale", async () => {
22
+ const page = Factory.build("page");
23
+ const ctx = buildCtx(page);
24
+ ctx.query.target_locale = "fr";
25
+
26
+ const mockFindPage = jest
27
+ .spyOn(ctx.client.pages, "find")
28
+ .mockImplementation(() => page);
29
+
30
+ const result = await TranslationsHandler.create_form(ctx);
31
+
32
+ expect(mockFindPage).toHaveBeenCalledWith(page.id, "en");
33
+
34
+ const expectedResource = structuredClone(page);
35
+
36
+ expect(result).toEqual({
37
+ assigns: {
38
+ resource: expectedResource,
39
+ blueprint: page,
40
+ targetLocale: "fr",
41
+ errors: {},
42
+ _translations: [],
43
+ },
44
+ });
45
+ });
46
+
47
+ test("create_form when a page exists without target locale", async () => {
48
+ const page = Factory.build("page");
49
+ const ctx = buildCtx(page);
50
+
51
+ const mockFindPage = jest
52
+ .spyOn(ctx.client.pages, "find")
53
+ .mockImplementation(() => page);
54
+
55
+ const result = await TranslationsHandler.create_form(ctx);
56
+
57
+ expect(mockFindPage).toHaveBeenCalledWith(page.id, "en");
58
+
59
+ const expectedResource = structuredClone(page);
60
+
61
+ expect(result).toEqual({
62
+ assigns: {
63
+ resource: expectedResource,
64
+ blueprint: page,
65
+ targetLocale: "",
66
+ errors: {},
67
+ _translations: [],
68
+ },
69
+ });
70
+ });
71
+ });
@@ -0,0 +1,87 @@
1
+ import PageRevisions from "../../../../cloud/PageRevisions";
2
+ import Pages from "../../../../cloud/Pages";
3
+ import TranslationsHandler from "../../../../handlers/pages/TranslationsHandler";
4
+ import Factory from "../../../support/factory";
5
+
6
+ describe("Translations Handler", () => {
7
+ function buildCtx() {
8
+ const originalPage = Factory.build("page", {
9
+ id: "original-page-id",
10
+ title: "Test Page Title",
11
+ });
12
+
13
+ return {
14
+ client: {
15
+ pages: new Pages(() => ({}), "token"),
16
+ pageRevisions: new PageRevisions(() => ({}), "token"),
17
+ },
18
+ params: { parent_id: originalPage.id },
19
+ query: { translations: "fr:fr-page-id;es:es-page-id" },
20
+ locale: "en",
21
+ resourceConfig: {
22
+ pageType: "page",
23
+ pageTypePlural: "pages",
24
+ },
25
+ originalPage,
26
+ };
27
+ }
28
+
29
+ test("index", async () => {
30
+ const frPage = Factory.build("page", { id: "fr-page-id" });
31
+ const esPage = Factory.build("page", { id: "es-page-id" });
32
+ const esRevision = Factory.build("page_revision", { id: "es-revision-id" });
33
+ const frRevision = Factory.build("page_revision", { id: "fr-revision-id" });
34
+ const ctx = buildCtx();
35
+
36
+ const mockedFind = jest
37
+ .spyOn(ctx.client.pages, "find")
38
+ .mockImplementation((id, locale) => {
39
+ if (locale === "fr") return frPage;
40
+ if (locale === "es") return esPage;
41
+ if (id === "original-page-id") return ctx.originalPage;
42
+ });
43
+
44
+ const mockedRevisions = jest
45
+ .spyOn(ctx.client.pageRevisions, "all")
46
+ .mockImplementation((id, locale) => ({
47
+ list: locale === "fr" ? [frRevision] : [esRevision],
48
+ }));
49
+
50
+ const mockedList = jest
51
+ .spyOn(ctx.client.pages, "list")
52
+ .mockImplementation(() => ({}));
53
+
54
+ const result = await TranslationsHandler.index(ctx);
55
+
56
+ expect(mockedFind).toHaveBeenCalledWith("fr-page-id", "fr");
57
+ expect(mockedFind).toHaveBeenCalledWith("es-page-id", "es");
58
+ expect(mockedRevisions).toHaveBeenCalledWith("fr-page-id", "fr");
59
+ expect(mockedRevisions).toHaveBeenCalledWith("es-page-id", "es");
60
+ expect(mockedList).toHaveBeenCalledWith(
61
+ { translations: "fr:fr-page-id;es:es-page-id" },
62
+ "en",
63
+ "page"
64
+ );
65
+
66
+ expect(result).toEqual({
67
+ assigns: {
68
+ items: {},
69
+ visibilityOptions: ctx.client.pages.visibilityOptions(),
70
+ translations: [
71
+ {
72
+ ...frPage,
73
+ translationLocale: "fr",
74
+ revisionId: "fr-revision-id",
75
+ },
76
+ {
77
+ ...esPage,
78
+ translationLocale: "es",
79
+ revisionId: "es-revision-id",
80
+ },
81
+ ],
82
+ parent_id: "original-page-id",
83
+ pageTitle: "Test Page Title",
84
+ },
85
+ });
86
+ });
87
+ });
@@ -5,6 +5,14 @@
5
5
  </div>
6
6
 
7
7
  <div class="flex flex-wrap items-center gap-1 mb-6 -mt-4">
8
+ <%
9
+ const availableLocales = Object.keys(_context.locales);
10
+ const totalPossibleTranslations = availableLocales.length - 1;
11
+ const existingTranslationsCount = Object.keys(resource.translations).length;
12
+ const canBeTranslated = existingTranslationsCount < totalPossibleTranslations;
13
+ %>
14
+
15
+
8
16
  <% Object.keys(_context.locales).forEach(key => { %>
9
17
  <% const { name, _code, flag} = _context.locales[key] %>
10
18
  <% if (key === _context.locale) { %>
@@ -19,6 +27,12 @@
19
27
  </a>
20
28
  <% } %>
21
29
  <% }); %>
30
+
31
+ <% if (canBeTranslated) { %>
32
+ <a href="<%= `/${_context.locale}/${_context.resourceConfig.pageTypePlural}/${resource.id}/translations/new` %>" class="btn-text">
33
+ Translate
34
+ </a>
35
+ <% } %>
22
36
  </div>
23
37
 
24
38
  <% if (revisions.length) { %>
@@ -155,4 +169,4 @@
155
169
  </tbody>
156
170
  </table>
157
171
  </div>
158
- </div>
172
+ </div>
@@ -1,9 +1,65 @@
1
1
  <div class="page-wrapper">
2
- <div class="page-title">
3
- <h1 class="heading-1">Create <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.pageType)) %> translation</h1>
2
+ <div class="page-title flex flex-col items-start">
3
+ <h1 class="heading-1">Translate from <%= _context.locales[_context.locale].name %></h1>
4
+ <h1 class="body-2 opacity-50"><%= blueprint.title %></h1>
4
5
  </div>
5
6
 
6
7
  <div class="grid gap-4 md:grid-cols-3">
7
- <%- render_partial('_form.ejs') %>
8
+ <% form_for({ resource }, errors, (f) => { %>
9
+ <form action="<%= `/${_context.locale}/${_context.resourceConfig.pageTypePlural}/${blueprint.id}/translations` %>" method="post" class="form">
10
+ <h6>Available languages:</h6>
11
+ <% if (targetLocale) { %>
12
+ <input type="hidden" name="resource[languages][]" value="<%= targetLocale %>">
13
+ <% } %>
14
+ <div class="checkbox-group !max-h-fit mb-3">
15
+ <% Object.entries(_context.locales).forEach(([key, locale]) => {
16
+ const isExistingTranslation = resource.translations[key];
17
+ const isCurrentLocale = key === _context.locale;
18
+ const wasSelected = Array.isArray(resource.languages) && resource.languages.includes(key);
19
+ const isChecked = isExistingTranslation || isCurrentLocale || key === targetLocale || wasSelected;
20
+ const isDisabled = isExistingTranslation || isCurrentLocale || key === targetLocale;
21
+ %>
22
+ <% if(!isCurrentLocale) { %>
23
+ <label class="<%= isDisabled && key !== targetLocale ? 'opacity-50' : '' %>">
24
+ <input type="<%= isCurrentLocale ? 'hidden' : 'checkbox' %>" name="resource[languages][]" value="<%= key %>" <%= isChecked ? 'checked' : '' %> <%= isDisabled ? 'disabled' : '' %>>
25
+ <%= locale.name %>
26
+ </label>
27
+ <% } %>
28
+ <% }) %>
29
+ </div>
30
+
31
+ <% if (errors?.resource && targetLocale) { %>
32
+ <div>
33
+ <p class="text-sm text-red-600 font-medium mb-1">
34
+ Failed to create <%= _context.locales[targetLocale].name %> translation.
35
+ </p>
36
+ <% Object.entries(errors.resource).forEach(([errorKey, error]) => { %>
37
+ <p class="text-sm text-red-600 font-medium mb-3"> Reason is <%= errorKey %>: <%= error %></p>
38
+ <% }) %>
39
+ </div>
40
+ <% } %>
41
+
42
+ <div class="form-actions">
43
+ <div class="btn-toolbar">
44
+ <%- f.submit('Initiate translations', { class: 'btn-primary' }) %>
45
+ <a href="<%= `/${_context.locale}/${_context.resourceConfig.pageTypePlural}/${blueprint.id}` %>" class="btn-secondary">Cancel</a>
46
+ </div>
47
+ </div>
48
+ </form>
49
+ <% }) %>
50
+
51
+ <div>
52
+ <div class="help">
53
+ <div class="flex items-center gap-1 mb-2">
54
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
55
+ <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" />
56
+ </svg>
57
+
58
+ <h3 class="heading-3">Contextual help</h3>
59
+ </div>
60
+
61
+ <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>
62
+ </div>
63
+ </div>
8
64
  </div>
9
- </div>
65
+ </div>
@@ -0,0 +1,54 @@
1
+ <div class="page-wrapper">
2
+ <div class="page-title flex flex-col items-start">
3
+ <h1 class="heading-1">Translations initiated</h1>
4
+ <h1 class="body-2 opacity-50"><%= pageTitle %></h1>
5
+ </div>
6
+
7
+ <% if (translations && translations.length > 0) { %>
8
+ <table class="data-grid alt">
9
+ <thead>
10
+ <tr>
11
+ <th>Locale</th>
12
+ <th>&nbsp;</th>
13
+ <th>&nbsp;</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% translations.forEach(translation => { %>
18
+ <tr class="border-b border-mono-25">
19
+ <td class="py-4 pl-2 w-full">
20
+ <div class="flex gap-1 items-center">
21
+ <a href="<%= `/${translation.translationLocale}/${_context.resourceConfig.pageTypePlural}/${translation.id}` %>" class="btn-text">
22
+ <%= _context.locales[translation.translationLocale].name %>
23
+ </a>
24
+ </div>
25
+ </td>
26
+
27
+ <td class="whitespace-nowrap">
28
+ <div class="body-3 text-raketa-foreground/50">
29
+ <div class="flex items-center gap-1">
30
+ <a href="<%= `/${translation.translationLocale}/page_revisions/${translation.revisionId}/general/edit` %>" class="btn-text">
31
+ Update Settings
32
+ </a>
33
+ </div>
34
+ </div>
35
+ </td>
36
+
37
+ <td class="whitespace-nowrap">
38
+ <div class="body-3 text-raketa-foreground/50">
39
+ <div class="flex items-center gap-1">
40
+ <a href="/remote-login?redirect_to=/<%= translation.translationLocale %>/admin/<%= _context.resourceConfig.pageType %>-revisions/<%= translation.revisionId %>/builder" target="_blank" class="btn-text">
41
+ Update Content
42
+ </a>
43
+ </div>
44
+ </div>
45
+ </td>
46
+ </tr>
47
+ <% }) %>
48
+ </tbody>
49
+ </table>
50
+ <% } %>
51
+ <a href="<%= `/${_context.locale}/${_context.resourceConfig.pageTypePlural}/${parent_id}` %>" class="btn-primary mt-8">
52
+ Go to original
53
+ </a>
54
+ </div>
@@ -1,44 +0,0 @@
1
- <% form_for({ resource }, errors, (f) => { %>
2
- <form action="<%= `/${_context.locale}/${_context.resourceConfig.pageTypePlural}/${blueprint.id}/translations` %>" method="post" class="form">
3
- <input type="hidden" name="target_locale" value="<%= targetLocale %>" />
4
-
5
- <%- f.field('resource.title', { required: true }) %>
6
- <%- f.field('resource.slug', { label: "URL", required: true }) %>
7
- <%- f.field('resource.visibility', { as: 'select', required: true, options: visibilityOptions }) %>
8
-
9
- <%- include('../../_shared/_credentials_input.ejs', { f, visibilityFieldId: "f_resource_visibility"}) %>
10
- <%- include('../../_shared/_layout_input.ejs', { f }) %>
11
-
12
- <%- f.field('resource.settings.meta.title', { required: true, label: "Meta title" }) %>
13
- <%- f.field('resource.settings.meta.description', { as: "textarea", required: true, label: "Meta description" }) %>
14
-
15
- <raketa-image-picker name="resource[settings][meta][image]" value="<%= JSON.stringify(resource.settings?.meta?.image || {}) %>" label="Meta image"></raketa-image-picker>
16
-
17
- <h6>Original tags:</h6>
18
- <div><%= blueprint.tags.map((tag) => tag.name).join(", ")%></div>
19
-
20
- <%- f.field('resource.tags', { as: 'checkbox_group', options: tags.map(tag => [tag.id, tag.name]) }) %>
21
-
22
- <div class="form-actions">
23
- <div class="btn-toolbar">
24
- <%- f.submit('Save record', { class: 'btn-primary' }) %>
25
-
26
- <a href="<%= `/${_context.locale}/${_context.resourceConfig.pageTypePlural}/${blueprint.id}` %>" class="btn-secondary">Cancel</a>
27
- </div>
28
- </div>
29
- </form>
30
- <% }) %>
31
-
32
- <div>
33
- <div class="help">
34
- <div class="flex items-center gap-1 mb-2">
35
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
36
- <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" />
37
- </svg>
38
-
39
- <h3 class="heading-3">Contextual help</h3>
40
- </div>
41
-
42
- <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>
43
- </div>
44
- </div>