@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 +2 -2
- package/src/__tests__/components/Fields/TranslateButton/TranslateButton.test.tsx +36 -5
- package/src/__tests__/hooks/users.test.tsx +36 -1
- package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +1 -1
- package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/TemplateManager/index.tsx +58 -52
- package/src/components/Fields/TranslateButton/index.tsx +47 -33
- package/src/hooks/users.tsx +18 -17
- package/src/__tests__/hooks/shortcuts/engine.test.ts +0 -344
- package/src/__tests__/hooks/shortcuts/hooks.test.tsx +0 -259
- package/src/hooks/shortcuts/README.md +0 -515
- package/src/hooks/shortcuts/engine.ts +0 -205
- package/src/hooks/shortcuts/index.ts +0 -48
- package/src/hooks/shortcuts/presets.ts +0 -144
- package/src/hooks/shortcuts/types.ts +0 -23
- package/src/hooks/shortcuts/useShortcuts.ts +0 -106
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
|
|
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": "
|
|
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
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
expect(translatedNotificationWrapper).
|
|
184
|
-
expect(
|
|
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,
|
package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/TemplateManager/index.tsx
CHANGED
|
@@ -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 (
|
|
56
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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>.
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 ? (
|
package/src/hooks/users.tsx
CHANGED
|
@@ -190,20 +190,24 @@ const usePermissionsForPage = <T extends Record<string, string | string[]>>(
|
|
|
190
190
|
return undefined;
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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 =
|