@griddo/ax 12.8.2 → 12.8.3

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.8.2",
4
+ "version": "12.8.3",
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": "00a702fad2cd346b673d8454d818d9e0e9d470f3"
204
+ "gitHead": "2583a2f1211251425b5766dd3377e5ec38db7a89"
205
205
  }
@@ -0,0 +1,263 @@
1
+ import { Router } from "react-router-dom";
2
+
3
+ import { parseTheme } from "@ax/helpers";
4
+ import Content from "@ax/modules/Content";
5
+ import { history } from "@ax/routes";
6
+ import globalTheme from "@ax/themes/theme.json";
7
+
8
+ import configureStore from "redux-mock-store";
9
+ import thunk from "redux-thunk";
10
+ import { ThemeProvider } from "styled-components";
11
+ import { mock } from "vitest-mock-extended";
12
+
13
+ import { act, cleanup, fireEvent, render, screen } from "./../../../../config/tests/test-utils";
14
+
15
+ const bulkDelete = vi.fn();
16
+ const getSitePages = vi.fn();
17
+
18
+ vi.mock("@ax/api", async (importOriginal) => {
19
+ const actual = (await importOriginal()) as any;
20
+
21
+ return {
22
+ ...actual,
23
+ pages: {
24
+ ...actual.pages,
25
+ bulkDelete: (...args: any[]) => bulkDelete(...args),
26
+ getSitePages: (...args: any[]) => getSitePages(...args),
27
+ },
28
+ };
29
+ });
30
+
31
+ // El mock global de `components` (config/tests/componentsMock.cjs) trae una plantilla
32
+ // `default` sin `type` que no existe en el catálogo real. getTemplatesFilters lee
33
+ // `type.value` de cada plantilla, así que esa entrada rompe el montaje del listado.
34
+ vi.mock("components", async (importOriginal) => {
35
+ const actual = (await importOriginal()) as any;
36
+ const source = actual.config ? actual : actual.default;
37
+ const { default: _defaultTemplate, ...templates } = source.config.schemas.ui.templates;
38
+
39
+ return {
40
+ ...source,
41
+ config: {
42
+ ...source.config,
43
+ schemas: {
44
+ ...source.config.schemas,
45
+ ui: { ...source.config.schemas.ui, templates },
46
+ },
47
+ },
48
+ };
49
+ });
50
+
51
+ afterEach(cleanup);
52
+
53
+ const buildPage = (id: number) => ({
54
+ id,
55
+ editorID: id,
56
+ title: `Page ${id}`,
57
+ slug: `/page-${id}`,
58
+ fullPath: { site: "/site-1", page: `/page-${id}` },
59
+ templateId: "BasicTemplate",
60
+ origin: "SITE",
61
+ isHome: false,
62
+ canBeUnpublished: true,
63
+ manuallyImported: false,
64
+ structuredDataContent: null,
65
+ publicationScheduled: null,
66
+ accessGrants: null,
67
+ haveDraftPage: null,
68
+ liveStatus: { status: "offline" },
69
+ pageLanguages: [
70
+ { pageId: id, languageId: 4, locale: "en_GB" },
71
+ { pageId: id + 100, languageId: 2, locale: "es_ES" },
72
+ ],
73
+ modified: "2026-08-01T10:00:00.000Z",
74
+ categories: [],
75
+ isIndexed: true,
76
+ metaTitle: "",
77
+ metaDescription: "",
78
+ });
79
+
80
+ const pages = [1, 2, 3, 4, 5].map(buildPage);
81
+ const language = { id: 4, locale: "en_GB", language: "English", label: "EN", isDefault: true };
82
+
83
+ const initialStore = {
84
+ app: {
85
+ isLoading: false,
86
+ isSaving: false,
87
+ lang: { id: 4, locale: "en_GB" },
88
+ globalLangs: [language],
89
+ globalSettings: { skipReviewOnPublish: false },
90
+ error: null,
91
+ token: "eyJhbGciOiJIUzI",
92
+ },
93
+ sites: {
94
+ currentSiteInfo: { id: 1, name: "Site 1", hidden: false },
95
+ currentSitePages: pages,
96
+ allSitePages: pages,
97
+ currentFilter: "unique-pages",
98
+ totalItems: pages.length,
99
+ currentSiteLanguages: [language],
100
+ currentSiteErrorPages: [],
101
+ sitesByLang: [],
102
+ contentFilters: null,
103
+ currentSearch: "",
104
+ },
105
+ pageEditor: { template: null, currentPageLanguages: [], errors: [] },
106
+ structuredData: {
107
+ structuredData: { global: [], site: [] },
108
+ currentDataContent: [],
109
+ currentStructuredData: null,
110
+ },
111
+ dataPacks: { activated: [], templates: [] },
112
+ users: {
113
+ currentUser: { id: 1, isSuperAdmin: true },
114
+ currentPermissions: [],
115
+ globalPermissions: [],
116
+ roles: [],
117
+ },
118
+ };
119
+
120
+ // El store se lee de una variable mutable para poder simular el paginador: cambiar
121
+ // de página sustituye `currentSitePages` sin desmontar el componente, que es lo que
122
+ // hace la app de verdad. `redux-mock-store` admite una función como estado.
123
+ const renderContent = async (visiblePages = pages) => {
124
+ let state = { ...initialStore, sites: { ...initialStore.sites, currentSitePages: visiblePages } };
125
+ const store = configureStore([thunk])(() => state);
126
+
127
+ await act(async () => {
128
+ render(
129
+ <Router history={history}>
130
+ <ThemeProvider theme={parseTheme(globalTheme)}>
131
+ <Content {...mock<any>()} />
132
+ </ThemeProvider>
133
+ </Router>,
134
+ { store },
135
+ );
136
+ });
137
+
138
+ const goToPageWith = async (nextPages: ReturnType<typeof buildPage>[]) => {
139
+ state = { ...state, sites: { ...state.sites, currentSitePages: nextPages } };
140
+ await act(async () => {
141
+ store.dispatch({ type: "@@test/PAGINATE" });
142
+ });
143
+ };
144
+
145
+ return { store, goToPageWith };
146
+ };
147
+
148
+ const checkboxOf = (pageId: number) =>
149
+ screen
150
+ .getAllByTestId("check-field-input")
151
+ .find((input) => input.getAttribute("value") === String(pageId)) as HTMLElement;
152
+
153
+ const selectPage = async (pageId: number) => {
154
+ await act(async () => {
155
+ fireEvent.click(checkboxOf(pageId));
156
+ });
157
+ };
158
+
159
+ const selectRangeTo = async (pageId: number) => {
160
+ await act(async () => {
161
+ fireEvent.keyDown(window, { key: "Shift" });
162
+ });
163
+ await selectPage(pageId);
164
+ await act(async () => {
165
+ fireEvent.keyUp(window, { key: "Shift" });
166
+ });
167
+ };
168
+
169
+ const confirmBulkDelete = async (allLanguages = false) => {
170
+ await act(async () => {
171
+ fireEvent.click(screen.getByText("delete"));
172
+ });
173
+ if (allLanguages) {
174
+ await act(async () => {
175
+ fireEvent.click(screen.getByText("Delete all languages versions"));
176
+ });
177
+ }
178
+ await act(async () => {
179
+ fireEvent.click(screen.getByText("Delete page"));
180
+ });
181
+ };
182
+
183
+ describe("[UC-AX-explore-manage-pages] Content — borrado en bulk", () => {
184
+ beforeEach(() => {
185
+ bulkDelete.mockResolvedValue({ status: 200, data: {} });
186
+ getSitePages.mockResolvedValue({ status: 200, data: { items: [], totalItems: 0 } });
187
+ window.HTMLElement.prototype.scrollTo = vi.fn();
188
+ });
189
+
190
+ it("borra todas las páginas de un rango seleccionado con Shift+click", async () => {
191
+ await renderContent();
192
+
193
+ await selectPage(1);
194
+ await selectRangeTo(5);
195
+
196
+ // El contador de la barra y el borrado tienen que ver lo mismo.
197
+ expect(screen.getByText("5 selected")).toBeInTheDocument();
198
+
199
+ await confirmBulkDelete();
200
+
201
+ expect(bulkDelete).toHaveBeenCalledTimes(1);
202
+ expect(bulkDelete.mock.calls[0][0]).toEqual([1, 2, 3, 4, 5]);
203
+ });
204
+
205
+ it("con 'all languages versions' borra las versiones de todo el rango", async () => {
206
+ await renderContent();
207
+
208
+ await selectPage(1);
209
+ await selectRangeTo(5);
210
+
211
+ await confirmBulkDelete(true);
212
+
213
+ expect(bulkDelete).toHaveBeenCalledTimes(1);
214
+ // Cada página lleva su versión en inglés (id) y en español (id + 100).
215
+ expect([...bulkDelete.mock.calls[0][0]].sort((a, b) => a - b)).toEqual([1, 2, 3, 4, 5, 101, 102, 103, 104, 105]);
216
+ });
217
+
218
+ it("borra las páginas seleccionadas en otra página del paginador", async () => {
219
+ const firstPage = pages.slice(0, 3);
220
+ const secondPage = [6, 7, 8].map(buildPage);
221
+ const { goToPageWith } = await renderContent(firstPage);
222
+
223
+ await selectPage(1);
224
+ await goToPageWith(secondPage);
225
+ await selectPage(7);
226
+
227
+ await confirmBulkDelete();
228
+
229
+ expect(bulkDelete).toHaveBeenCalledTimes(1);
230
+ expect(bulkDelete.mock.calls[0][0]).toEqual([1, 7]);
231
+ });
232
+
233
+ it("con 'all languages versions' incluye las versiones de las páginas fuera de vista", async () => {
234
+ const firstPage = pages.slice(0, 3);
235
+ const secondPage = [6, 7, 8].map(buildPage);
236
+ const { goToPageWith } = await renderContent(firstPage);
237
+
238
+ await selectPage(1);
239
+ await goToPageWith(secondPage);
240
+ await selectPage(7);
241
+
242
+ await confirmBulkDelete(true);
243
+
244
+ expect(bulkDelete).toHaveBeenCalledTimes(1);
245
+ // La página 1 ya no está a la vista, pero sus dos versiones tienen que caer igual.
246
+ expect([...bulkDelete.mock.calls[0][0]].sort((a, b) => a - b)).toEqual([1, 7, 101, 107]);
247
+ });
248
+
249
+ it("borra todas las páginas seleccionadas con clics sueltos", async () => {
250
+ await renderContent();
251
+
252
+ await selectPage(1);
253
+ await selectPage(3);
254
+ await selectPage(5);
255
+
256
+ expect(screen.getByText("3 selected")).toBeInTheDocument();
257
+
258
+ await confirmBulkDelete();
259
+
260
+ expect(bulkDelete).toHaveBeenCalledTimes(1);
261
+ expect(bulkDelete.mock.calls[0][0]).toEqual([1, 3, 5]);
262
+ });
263
+ });
@@ -0,0 +1,42 @@
1
+ import { useSelectedPages } from "@ax/modules/Content/hooks";
2
+ import type { IPage } from "@ax/types";
3
+
4
+ import { renderHook } from "@testing-library/react";
5
+
6
+ const buildPage = (id: number, languages = 1) =>
7
+ ({
8
+ id,
9
+ title: `Page ${id}`,
10
+ pageLanguages: Array.from({ length: languages }, (_, index) => ({ pageId: id + index * 100 })),
11
+ }) as unknown as IPage;
12
+
13
+ describe("[UC-AX-explore-manage-pages] useSelectedPages", () => {
14
+ it("resuelve una página por cada id seleccionado", () => {
15
+ const visiblePages = [buildPage(1), buildPage(2), buildPage(3)];
16
+
17
+ const { result } = renderHook(() => useSelectedPages([1, 3], visiblePages));
18
+
19
+ expect(result.current.map((page) => page.id)).toEqual([1, 3]);
20
+ });
21
+
22
+ it("recuerda las páginas seleccionadas en otra página del paginador", () => {
23
+ const firstPage = [buildPage(1), buildPage(2)];
24
+ const secondPage = [buildPage(3), buildPage(4)];
25
+
26
+ const { result, rerender } = renderHook(
27
+ ({ selectedIds, visiblePages }) => useSelectedPages(selectedIds, visiblePages),
28
+ { initialProps: { selectedIds: [1], visiblePages: firstPage } },
29
+ );
30
+
31
+ // El usuario pasa a la siguiente página del paginador y sigue seleccionando.
32
+ rerender({ selectedIds: [1, 3], visiblePages: secondPage });
33
+
34
+ expect(result.current.map((page) => page.id)).toEqual([1, 3]);
35
+ });
36
+
37
+ it("descarta los ids que no corresponden a ninguna página conocida", () => {
38
+ const { result } = renderHook(() => useSelectedPages([1, 999], [buildPage(1)]));
39
+
40
+ expect(result.current.map((page) => page.id)).toEqual([1]);
41
+ });
42
+ });
@@ -1,5 +1,6 @@
1
- import { useState } from "react";
2
- import type { IQueryValue } from "@ax/types";
1
+ import { useRef, useState } from "react";
2
+
3
+ import type { IPage, IQueryValue } from "@ax/types";
3
4
 
4
5
  const useSortedListStatus = () => {
5
6
  const sortedInitialState: {
@@ -106,4 +107,16 @@ const useFilterQuery = (values?: Record<string, IQueryValue[]> | null) => {
106
107
  };
107
108
  };
108
109
 
109
- export { useSortedListStatus, useFilterQuery };
110
+ const useSelectedPages = (selectedIds: number[], visiblePages: IPage[]): IPage[] => {
111
+ // La selección sobrevive al cambio de página del paginador, así que hay que
112
+ // recordar las páginas que ya no están a la vista para poder resolverlas.
113
+ const knownPages = useRef(new Map<number, IPage>());
114
+
115
+ visiblePages.forEach((page) => {
116
+ knownPages.current.set(page.id, page);
117
+ });
118
+
119
+ return selectedIds.map((id) => knownPages.current.get(id)).filter((page): page is IPage => !!page);
120
+ };
121
+
122
+ export { useSortedListStatus, useFilterQuery, useSelectedPages };
@@ -58,7 +58,7 @@ import StructuredDataItem from "./../StructuredData/StructuredDataList/Structure
58
58
  import { DeleteModal } from "./atoms";
59
59
  import BulkHeader from "./BulkHeader";
60
60
  import ContentFilters from "./ContentFilters";
61
- import { useFilterQuery, useSortedListStatus } from "./hooks";
61
+ import { useFilterQuery, useSelectedPages, useSortedListStatus } from "./hooks";
62
62
  import NewContentModal from "./NewContentModal";
63
63
  import PageItem from "./PageItem";
64
64
  import {
@@ -194,7 +194,6 @@ const Content = (props: IProps): JSX.Element => {
194
194
  const [deleteAllVersions, setDeleteAllVersions] = useState(false);
195
195
  const [arePagesTranslated, setArePagesTranslated] = useState(false);
196
196
  const [templateInstanceError, setTemplateInstanceError] = useState({ error: false, templateName: "" });
197
- const [pagesSelected, setPagesSelected] = useState<any[]>([]);
198
197
  const [selectedOption, setSelectedOption] = useState<string | null>(null);
199
198
  const history = useHistory();
200
199
 
@@ -220,6 +219,8 @@ const Content = (props: IProps): JSX.Element => {
220
219
  setHoverCheck,
221
220
  } = useBulkSelection(contentIds);
222
221
 
222
+ const pagesSelected = useSelectedPages(selectedItems.all, currentSitePages);
223
+
223
224
  const { isVisible, toggleToast, setIsVisible } = useToast();
224
225
  const {
225
226
  isVisible: isVisibleRemovedToast,
@@ -390,25 +391,11 @@ const Content = (props: IProps): JSX.Element => {
390
391
 
391
392
  const bulkFilter = (bulkSelection: number[]) => filterByStatus(bulkSelection, currentSitePages);
392
393
 
393
- const handleAddToBulk = (item: ICheck) => {
394
- const page = currentSitePages.find((page: IPage) => page.id === item.value);
394
+ const handleAddToBulk = (item: ICheck) => addToBulkSelection(item, bulkFilter);
395
395
 
396
- item.isChecked && !pagesSelected.includes(item)
397
- ? setPagesSelected((pagesSelected) => [...pagesSelected, page])
398
- : setPagesSelected(pagesSelected.filter((page: IPage) => page.id !== item.value));
396
+ const handleSelectAll = () => selectAllItems(bulkFilter);
399
397
 
400
- addToBulkSelection(item, bulkFilter);
401
- };
402
-
403
- const handleSelectAll = () => {
404
- selectAllItems(bulkFilter);
405
- setPagesSelected(isStructuredData ? currentDataContent : currentSitePages);
406
- };
407
-
408
- const unselectAllItems = () => {
409
- resetBulkSelection();
410
- setPagesSelected([]);
411
- };
398
+ const unselectAllItems = () => resetBulkSelection();
412
399
 
413
400
  const selectItems = () => (checkState.isAllSelected ? unselectAllItems() : handleSelectAll());
414
401
 
@@ -485,12 +472,9 @@ const Content = (props: IProps): JSX.Element => {
485
472
  const bulkDelete = async () => {
486
473
  let allPageVersions: number[] = [];
487
474
  if (deleteAllVersions) {
488
- const selectedPages = currentSitePages.filter((page) => selectedItems.all.includes(page.id));
489
- const pageLanguages = selectedPages.map((element) => element.pageLanguages);
490
- pageLanguages.forEach((element) => {
491
- const ids = element.map((lang: IPageLanguage) => lang.pageId);
492
- allPageVersions = [...allPageVersions, ...ids];
493
- });
475
+ allPageVersions = pagesSelected.flatMap((page: IPage) =>
476
+ page.pageLanguages.map((lang: IPageLanguage) => lang.pageId),
477
+ );
494
478
  }
495
479
 
496
480
  isStructuredData
@@ -511,8 +495,7 @@ const Content = (props: IProps): JSX.Element => {
511
495
  };
512
496
 
513
497
  const handleToggleDeleteModal = () => {
514
- const selectedPages = currentSitePages.filter((page) => selectedItems.all.includes(page.id));
515
- const hasTranslations = selectedPages.some((page) => page.pageLanguages.length > 1);
498
+ const hasTranslations = pagesSelected.some((page: IPage) => page.pageLanguages.length > 1);
516
499
  setArePagesTranslated(hasTranslations);
517
500
  toggleDeleteModal();
518
501
  };