@griddo/ax 12.9.0-rc.1 → 12.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "12.9.0-rc.1",
4
+ "version": "12.9.0",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Diego M. Béjar <diego.bejar@secuoyas.com>",
@@ -201,5 +201,5 @@
201
201
  "publishConfig": {
202
202
  "access": "public"
203
203
  },
204
- "gitHead": "6ca432142f7b2645958c4e65471bb9b8a6e7cc82"
204
+ "gitHead": "d3a3e8da5f6a9c89f60c6055a0f133a0d7c05691"
205
205
  }
@@ -157,10 +157,16 @@ describe("Translate Button component rendering", () => {
157
157
  const translateButtonWrapper = screen.queryByTestId("translate-button-wrapper");
158
158
  const translatedNotificationWrapper = screen.queryByTestId("update-translation-button-wrapper");
159
159
  expect(translatedNotificationWrapper).toBeTruthy();
160
+ expect(translatedNotificationWrapper?.textContent).toContain("You can update it");
161
+ expect(screen.queryByText("Update translation with AI")).toBeTruthy();
160
162
  expect(translateButtonWrapper).toBeFalsy();
161
163
  });
162
164
 
163
- it("should render the notification if structured data is already translated", () => {
165
+ // sc-119164 — la retraducción solo existe para páginas. El aviso se mostraba también en
166
+ // datos y formularios, y al pulsarlo no se llamaba a ninguna API: `getRetranslationAction`
167
+ // es undefined ahí, así que el componente pintaba «The translation could not be completed»
168
+ // sin haber intentado nada. Mientras no haya endpoint, no se ofrece la acción.
169
+ it("should show the already-translated notice for structured data but NOT offer the update action", () => {
164
170
  const initialStore = {
165
171
  app: { globalSettings: { autoTranslation: true } },
166
172
  pageEditor: { editorContent: null, isIATranslated: false },
@@ -178,10 +184,35 @@ describe("Translate Button component rendering", () => {
178
184
  { store },
179
185
  );
180
186
 
181
- const translateButtonWrapper = screen.queryByTestId("translate-button-wrapper");
182
- const translatedNotificationWrapper = screen.queryByTestId("update-translation-button-wrapper");
183
- expect(translatedNotificationWrapper).toBeTruthy();
184
- expect(translateButtonWrapper).toBeFalsy();
187
+ const translatedNotificationWrapper = screen.getByTestId("update-translation-button-wrapper");
188
+ expect(translatedNotificationWrapper.textContent).toContain("already been translated");
189
+ expect(translatedNotificationWrapper.textContent).not.toContain("You can update it");
190
+ expect(screen.queryByText("Update translation with AI")).toBeFalsy();
191
+ expect(screen.queryByTestId("translate-button-wrapper")).toBeFalsy();
192
+ });
193
+
194
+ it("should show the already-translated notice for forms but NOT offer the update action", () => {
195
+ const initialStore = {
196
+ app: { globalSettings: { autoTranslation: true } },
197
+ pageEditor: { editorContent: null, isIATranslated: false },
198
+ structuredData: { form: null, isIATranslated: false },
199
+ forms: { formContent: { canBeTranslated: false, originalLanguage: 2 } },
200
+ };
201
+ const store = mockStore(initialStore);
202
+
203
+ defaultProps.contentType = "form";
204
+
205
+ render(
206
+ <ThemeProvider theme={parseTheme(globalTheme)}>
207
+ <TranslateButton {...defaultProps} />
208
+ </ThemeProvider>,
209
+ { store },
210
+ );
211
+
212
+ const wrapper = screen.getByTestId("update-translation-button-wrapper");
213
+ expect(wrapper.textContent).toContain("already been translated");
214
+ expect(screen.queryByText("Update translation with AI")).toBeFalsy();
215
+ expect(screen.queryByTestId("translate-button-wrapper")).toBeFalsy();
185
216
  });
186
217
 
187
218
  it("should render the component if page is already translated but can be translated", () => {
@@ -4,7 +4,7 @@ import "@testing-library/jest-dom";
4
4
  import * as reactRedux from "react-redux";
5
5
 
6
6
  import { usePermissionsForPage } from "@ax/hooks";
7
- import type { PageAccessGrantsSummary } from "@ax/types";
7
+ import type { IPageLanguage, PageAccessGrantsSummary } from "@ax/types";
8
8
 
9
9
  import { renderHook } from "@testing-library/react";
10
10
 
@@ -388,4 +388,39 @@ describe("usePermissionsForPage hook", () => {
388
388
  expect(result.current.edit).toBe(false);
389
389
  });
390
390
  });
391
+
392
+ describe("8. Drafts (sc-119759)", () => {
393
+ it("should not restrict a page missing from the translation group when user has no pagePermissions", () => {
394
+ // El draft nunca aparece en pageLanguages: el backend filtra draft_from_page IS NULL
395
+ const pageLanguages = [
396
+ { pageId: 100, languageId: 1 },
397
+ { pageId: 200, languageId: 2 },
398
+ ] as unknown as IPageLanguage[];
399
+ setupMockState({ users: { currentUser: mockUser, roles: [mockRole] } });
400
+
401
+ const { result } = renderHook(() =>
402
+ usePermissionsForPage(1, 555, { edit: "edit_page" }, undefined, pageLanguages),
403
+ );
404
+
405
+ expect(result.current.edit).toBe(true);
406
+ });
407
+
408
+ it("should keep denying a page missing from the group when user IS restricted by pagePermissions", () => {
409
+ const userWithPagePerms = {
410
+ ...mockUser,
411
+ pagePermissions: [{ siteId: 1, pageIds: [100] }],
412
+ };
413
+ const pageLanguages = [
414
+ { pageId: 100, languageId: 1 },
415
+ { pageId: 200, languageId: 2 },
416
+ ] as unknown as IPageLanguage[];
417
+ setupMockState({ users: { currentUser: userWithPagePerms, roles: [mockRole] } });
418
+
419
+ const { result } = renderHook(() =>
420
+ usePermissionsForPage(1, 555, { edit: "edit_page" }, undefined, pageLanguages),
421
+ );
422
+
423
+ expect(result.current.edit).toBe(false);
424
+ });
425
+ });
391
426
  });
@@ -2,7 +2,7 @@ import { getInnerFields } from "@ax/forms";
2
2
  import { FieldContainer, FieldGroup } from "@ax/components";
3
3
  import type { IErrorItem, IPage, ISite, ILanguage } from "@ax/types";
4
4
 
5
- const Field = (props: IFieldProps) => {
5
+ const Field = (props: IFieldProps): JSX.Element => {
6
6
  const {
7
7
  whiteList,
8
8
  objKey,
@@ -7,7 +7,7 @@ import Field from "../Field";
7
7
 
8
8
  import * as S from "./style";
9
9
 
10
- export const TemplateManager = (props: IProps) => {
10
+ export const TemplateManager = (props: IProps): JSX.Element => {
11
11
  const {
12
12
  template,
13
13
  selectedTab,
@@ -52,8 +52,13 @@ export const TemplateManager = (props: IProps) => {
52
52
  const isComputedField = Object.prototype.hasOwnProperty.call(field, "computed");
53
53
 
54
54
  const addedModules: string[] = modulesDataPacks.reduce((acc: string[], current: any) => {
55
- if (current.sectionList?.[template.component]?.includes(key) && !acc.includes(current.id)) {
56
- acc.push(current.id);
55
+ if (
56
+ current.sectionList &&
57
+ current.sectionList[template.component] &&
58
+ current.sectionList[template.component].includes(key) &&
59
+ !acc.includes(current.id)
60
+ ) {
61
+ return [...acc, current.id];
57
62
  }
58
63
  return acc;
59
64
  }, []);
@@ -79,56 +84,57 @@ export const TemplateManager = (props: IProps) => {
79
84
  return (
80
85
  <>
81
86
  {isConfig && templateFields && !isForm && <S.Title>Template Options</S.Title>}
82
- {templateFields
83
- ?.filter((templateField: ISchemaField) => {
84
- const isHidden = templateField.hidden || (isForm && !templateField.overwrite);
85
- return !isHidden;
86
- })
87
- .map((templateField: ISchemaField, index: number) => {
88
- const {
89
- whiteList,
90
- categories,
91
- key,
92
- fieldObjKey,
93
- mappedField,
94
- currentContent,
95
- handleUpdate,
96
- error,
97
- readonly,
98
- disabledField,
99
- } = getFieldProps(templateField);
87
+ {templateFields &&
88
+ templateFields
89
+ .filter((templateField: ISchemaField) => {
90
+ const isHidden = templateField.hidden || (isForm && !templateField.overwrite);
91
+ return !isHidden;
92
+ })
93
+ .map((templateField: ISchemaField, index: number) => {
94
+ const {
95
+ whiteList,
96
+ categories,
97
+ key,
98
+ fieldObjKey,
99
+ mappedField,
100
+ currentContent,
101
+ handleUpdate,
102
+ error,
103
+ readonly,
104
+ disabledField,
105
+ } = getFieldProps(templateField);
100
106
 
101
- return (
102
- <Field
103
- whiteList={whiteList}
104
- key={`${objKey}.${key}${index}`}
105
- objKey={fieldObjKey}
106
- field={mappedField}
107
- selectedContent={currentContent}
108
- goTo={goTo}
109
- updateValue={handleUpdate}
110
- pages={pages}
111
- actions={actions}
112
- site={site}
113
- disabled={disabled || disabledField}
114
- activatedModules={activatedModules}
115
- isTemplateActivated={isTemplateActivated}
116
- categories={categories}
117
- error={error}
118
- deleteError={deleteError}
119
- errors={errors}
120
- theme={theme}
121
- moduleCopy={moduleCopy}
122
- availableDataPacks={availableDataPacks}
123
- template={template}
124
- setHistoryPush={setHistoryPush}
125
- lang={lang}
126
- readonly={readonly}
127
- scrollEditorID={scrollEditorID}
128
- languages={languages}
129
- />
130
- );
131
- })}
107
+ return (
108
+ <Field
109
+ whiteList={whiteList}
110
+ key={`${objKey}.${key}${index}`}
111
+ objKey={fieldObjKey}
112
+ field={mappedField}
113
+ selectedContent={currentContent}
114
+ goTo={goTo}
115
+ updateValue={handleUpdate}
116
+ pages={pages}
117
+ actions={actions}
118
+ site={site}
119
+ disabled={disabled || disabledField}
120
+ activatedModules={activatedModules}
121
+ isTemplateActivated={isTemplateActivated}
122
+ categories={categories}
123
+ error={error}
124
+ deleteError={deleteError}
125
+ errors={errors}
126
+ theme={theme}
127
+ moduleCopy={moduleCopy}
128
+ availableDataPacks={availableDataPacks}
129
+ template={template}
130
+ setHistoryPush={setHistoryPush}
131
+ lang={lang}
132
+ readonly={readonly}
133
+ scrollEditorID={scrollEditorID}
134
+ languages={languages}
135
+ />
136
+ );
137
+ })}
132
138
  </>
133
139
  );
134
140
  };
@@ -47,6 +47,14 @@ const TranslateButton = (props: ITranslateButtonProps): JSX.Element => {
47
47
 
48
48
  const isOriginal = content?.originalLanguage === lang;
49
49
 
50
+ // La retraducción solo está implementada para páginas: `getContent` no define
51
+ // `getRetranslationAction` para datos ni formularios. Sin ella, el botón despachaba
52
+ // `undefined?.(...)` y pintaba «The translation could not be completed» sin haber llamado a
53
+ // ninguna API (sc-119164). Se mantiene el aviso de que el contenido ya está traducido, pero
54
+ // no se ofrece una acción inexistente. El día que Back añada el endpoint para datos o
55
+ // formularios, basta con registrar la acción en `getContent`.
56
+ const canRetranslate = !!getRetranslationAction;
57
+
50
58
  const initialState = {
51
59
  isLoading: false,
52
60
  error: false,
@@ -88,42 +96,48 @@ const TranslateButton = (props: ITranslateButtonProps): JSX.Element => {
88
96
  <>
89
97
  <S.Wrapper data-testid="update-translation-button-wrapper" isSmall={!!structuredDataForm}>
90
98
  <S.Text>
91
- This page has <strong>already been translated</strong>. You can update it with the latest changes from the
92
- source language.
99
+ This {canRetranslate ? "page" : "content"} has <strong>already been translated</strong>.
100
+ {canRetranslate && " You can update it with the latest changes from the source language."}
93
101
  </S.Text>
94
- <S.ButtonWrapper>
95
- <S.StyledButton
96
- type="button"
97
- onClick={() => toggleModal("update")}
98
- icon={!state.isLoading ? "Ia" : undefined}
99
- loader={state.isLoading ? "circle" : undefined}
100
- disabled={disabled}
101
- buttonStyle="line"
102
- >
103
- {updateButtonText}
104
- </S.StyledButton>
105
- </S.ButtonWrapper>
106
- {state.error && (
107
- <S.ErrorText>
108
- The translation could not be completed. Your previous content has been preserved.
109
- </S.ErrorText>
102
+ {canRetranslate && (
103
+ <>
104
+ <S.ButtonWrapper>
105
+ <S.StyledButton
106
+ type="button"
107
+ onClick={() => toggleModal("update")}
108
+ icon={!state.isLoading ? "Ia" : undefined}
109
+ loader={state.isLoading ? "circle" : undefined}
110
+ disabled={disabled}
111
+ buttonStyle="line"
112
+ >
113
+ {updateButtonText}
114
+ </S.StyledButton>
115
+ </S.ButtonWrapper>
116
+ {state.error && (
117
+ <S.ErrorText>
118
+ The translation could not be completed. Your previous content has been preserved.
119
+ </S.ErrorText>
120
+ )}
121
+ </>
110
122
  )}
111
123
  </S.Wrapper>
112
- <Modal
113
- isOpen={isOpen("update")}
114
- hide={() => toggleModal("update")}
115
- size="S"
116
- title="Update translation page with AI"
117
- mainAction={{ title: "Update translation with AI", icon: "Ia", onClick: handleUpdateClick }}
118
- secondaryAction={{ title: "Cancel", onClick: () => toggleModal("update") }}
119
- height="auto"
120
- >
121
- <S.ModalContent>
122
- This page <strong>already has a translation</strong>. Updating it{" "}
123
- <strong>will replace all existing text in this language</strong>, including any manual edits made after
124
- the previous translation. This action cannot be undone.
125
- </S.ModalContent>
126
- </Modal>
124
+ {canRetranslate && (
125
+ <Modal
126
+ isOpen={isOpen("update")}
127
+ hide={() => toggleModal("update")}
128
+ size="S"
129
+ title="Update translation page with AI"
130
+ mainAction={{ title: "Update translation with AI", icon: "Ia", onClick: handleUpdateClick }}
131
+ secondaryAction={{ title: "Cancel", onClick: () => toggleModal("update") }}
132
+ height="auto"
133
+ >
134
+ <S.ModalContent>
135
+ This page <strong>already has a translation</strong>. Updating it{" "}
136
+ <strong>will replace all existing text in this language</strong>, including any manual edits made after
137
+ the previous translation. This action cannot be undone.
138
+ </S.ModalContent>
139
+ </Modal>
140
+ )}
127
141
  </>
128
142
  )}
129
143
  {autoTranslation && content?.canBeTranslated ? (
@@ -190,20 +190,24 @@ const usePermissionsForPage = <T extends Record<string, string | string[]>>(
190
190
  return undefined;
191
191
  };
192
192
 
193
- const hasAccessToTranslationGroup = pageLanguages?.length
194
- ? (() => {
195
- const isPartOfGroup = pageLanguages.some((lang) => getLanguagePageId(lang) === pageId);
196
- if (!isPartOfGroup) {
197
- return false; // pageId is not in this translation group
198
- }
193
+ // La restricción por grupo de traducción solo aplica a usuarios limitados por
194
+ // pagePermissions. Sin esa restricción el grupo no debe denegar nada: para un draft
195
+ // pageLanguages es el grupo de su página original —el backend resuelve el borrador a su
196
+ // original y filtra draft_from_page IS NULL—, así que su propio id nunca aparece ahí.
197
+ const hasAccessToTranslationGroup =
198
+ pageLanguages?.length && currentUser.pagePermissions?.length
199
+ ? (() => {
200
+ const isPartOfGroup = pageLanguages.some((lang) => getLanguagePageId(lang) === pageId);
201
+ if (!isPartOfGroup) {
202
+ return false; // pageId is not in this translation group
203
+ }
199
204
 
200
- // Get all pageIds in this translation group
201
- const translationPageIds = pageLanguages
202
- .map((lang) => getLanguagePageId(lang))
203
- .filter((id): id is number => id !== undefined);
205
+ // Get all pageIds in this translation group
206
+ const translationPageIds = pageLanguages
207
+ .map((lang) => getLanguagePageId(lang))
208
+ .filter((id): id is number => id !== undefined);
204
209
 
205
- // Check if user has access to ANY of the translation pageIds
206
- if (currentUser.pagePermissions?.length) {
210
+ // Check if user has access to ANY of the translation pageIds
207
211
  const siteEntry = currentUser.pagePermissions.find(
208
212
  (entry) => entry.siteId === siteId || entry.siteId === "global",
209
213
  );
@@ -211,11 +215,8 @@ const usePermissionsForPage = <T extends Record<string, string | string[]>>(
211
215
  return false;
212
216
  }
213
217
  return translationPageIds.some((translationPageId) => siteEntry.pageIds.includes(translationPageId));
214
- }
215
-
216
- return true; // No pagePermissions restrictions, allow access
217
- })()
218
- : undefined;
218
+ })()
219
+ : undefined;
219
220
 
220
221
  // If pageId is part of a translation group, use the translation group access result
221
222
  const isPagePermissionRestricted =