@griddo/ax 11.16.0 → 11.17.1-rc.1

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/README.md CHANGED
@@ -53,4 +53,4 @@ node_modules/.bin/biome lint . --only=noConsole
53
53
  - :PACKAGE_WITH_PROTOCOL: scoped and bare packages with a protocol.
54
54
  - :PATH: absolute and relative paths.
55
55
  - :URL: sources starting with https:// and http://.
56
- - :BLANK_LINE: blank line.
56
+ - :BLANK_LINE: blank line.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "11.16.0",
4
+ "version": "11.17.1-rc.1",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Diego M. Béjar <diego.bejar@secuoyas.com>",
@@ -219,5 +219,5 @@
219
219
  "publishConfig": {
220
220
  "access": "public"
221
221
  },
222
- "gitHead": "8e8d3b302199787e7e6bc798d147718d0ea416de"
222
+ "gitHead": "125fe7a1da2586d0328a23e43f7db7d7439c64ee"
223
223
  }
@@ -1,16 +1,21 @@
1
- import * as React from "react";
2
-
3
- import { ThemeProvider } from "styled-components";
4
- import { render, cleanup, screen, fireEvent } from "../../../../../config/jest/test-utils";
5
1
  import { mock } from "jest-mock-extended";
6
2
  import configureStore from "redux-mock-store";
3
+ import { ThemeProvider } from "styled-components";
4
+
5
+ import { cleanup, fireEvent, render, screen, waitFor } from "../../../../../config/jest/test-utils";
7
6
  import "@testing-library/jest-dom";
8
- import thunk from "redux-thunk";
9
7
 
8
+ import GlobalPageForm, { type IGlobalPageFormProps } from "@ax/components/ConfigPanel/GlobalPageForm";
10
9
  import { parseTheme } from "@ax/helpers";
11
- import GlobalPageForm, { IGlobalPageFormProps } from "@ax/components/ConfigPanel/GlobalPageForm";
12
10
  import globalTheme from "@ax/themes/theme.json";
13
11
 
12
+ import thunk from "redux-thunk";
13
+
14
+ jest.mock("@ax/hooks", () => ({
15
+ ...jest.requireActual("@ax/hooks"),
16
+ useGlobalPermission: jest.fn(() => true),
17
+ }));
18
+
14
19
  beforeEach(() => {
15
20
  cleanup();
16
21
  });
@@ -127,7 +132,7 @@ describe("GlobalPageForm component rendering", () => {
127
132
  { store },
128
133
  );
129
134
 
130
- expect(screen.getByText(/This page doesn\'t have a header. Click/i)).toBeInTheDocument();
135
+ expect(screen.getByText(/This page doesn't have a header. Click/i)).toBeInTheDocument();
131
136
  });
132
137
 
133
138
  it("should render the component with no footer warning text", () => {
@@ -140,7 +145,7 @@ describe("GlobalPageForm component rendering", () => {
140
145
  </ThemeProvider>,
141
146
  { store },
142
147
  );
143
- expect(screen.getByText(/This page doesn\'t have a footer. Click/i)).toBeInTheDocument();
148
+ expect(screen.getByText(/This page doesn't have a footer. Click/i)).toBeInTheDocument();
144
149
  });
145
150
 
146
151
  it("should render the connectedField component", () => {
@@ -166,9 +171,9 @@ describe("GlobalPageForm component rendering", () => {
166
171
  });
167
172
 
168
173
  describe("GlobalPageForm component rendering", () => {
169
- it("should trigger handleClick", () => {
174
+ it("should trigger handleClick", async () => {
170
175
  defaultProps.selectedTab = "content";
171
- const getGlobalFromLocalPageActionMock = jest.fn();
176
+ const getGlobalFromLocalPageActionMock = jest.fn(() => Promise.resolve());
172
177
  const saveCurrentSiteInfoActionMock = jest.fn();
173
178
  defaultProps.actions.saveCurrentSiteInfoAction = saveCurrentSiteInfoActionMock;
174
179
  defaultProps.actions.getGlobalFromLocalPageAction = getGlobalFromLocalPageActionMock;
@@ -185,8 +190,10 @@ describe("GlobalPageForm component rendering", () => {
185
190
  const editButton = screen.getByText<HTMLButtonElement>("Edit");
186
191
  expect(editButton).toBeTruthy();
187
192
  fireEvent.click(editButton);
188
- expect(getGlobalFromLocalPageActionMock).toBeCalled();
189
- expect(saveCurrentSiteInfoActionMock).toBeCalled();
193
+ await waitFor(() => {
194
+ expect(getGlobalFromLocalPageActionMock).toBeCalled();
195
+ expect(saveCurrentSiteInfoActionMock).toBeCalled();
196
+ });
190
197
  });
191
198
 
192
199
  it("should trigger handleRestoreHeader", () => {
@@ -202,7 +209,7 @@ describe("GlobalPageForm component rendering", () => {
202
209
  { store },
203
210
  );
204
211
 
205
- expect(screen.getByText(/This page doesn\'t have a header. Click/i)).toBeInTheDocument();
212
+ expect(screen.getByText(/This page doesn't have a header. Click/i)).toBeInTheDocument();
206
213
  const headerLink = screen.getByTestId("header-link");
207
214
  expect(headerLink).toBeTruthy();
208
215
  fireEvent.click(headerLink);
@@ -222,7 +229,7 @@ describe("GlobalPageForm component rendering", () => {
222
229
  { store },
223
230
  );
224
231
 
225
- expect(screen.getByText(/This page doesn\'t have a footer. Click/i)).toBeInTheDocument();
232
+ expect(screen.getByText(/This page doesn't have a footer. Click/i)).toBeInTheDocument();
226
233
  const footerLink = screen.getByTestId("footer-link");
227
234
  expect(footerLink).toBeTruthy();
228
235
  fireEvent.click(footerLink);
@@ -1,11 +1,12 @@
1
- import { ThemeProvider } from "styled-components";
2
- import { render, cleanup, screen, fireEvent } from "@testing-library/react";
3
1
  import { parseTheme } from "@ax/helpers";
2
+
3
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
4
+ import { ThemeProvider } from "styled-components";
4
5
  import "@testing-library/jest-dom";
5
6
 
6
7
  import CurrentAccessList from "@ax/components/RestrictionsModal/CurrentAccessList";
7
- import type { IUser } from "@ax/types";
8
8
  import globalTheme from "@ax/themes/theme.json";
9
+ import type { IUser } from "@ax/types";
9
10
 
10
11
  jest.mock("react-redux", () => ({
11
12
  ...jest.requireActual("react-redux"),
@@ -29,7 +30,6 @@ const defaultProps: {
29
30
  currentAccessGrants: { users: number[]; roles: number[] };
30
31
  onEdit: jest.Mock;
31
32
  users: IUser[];
32
- canSetPermissions: boolean;
33
33
  } = {
34
34
  restrictionType: "users",
35
35
  currentAccessGrants: {
@@ -59,7 +59,6 @@ const defaultProps: {
59
59
  pagePermissions: [],
60
60
  },
61
61
  ],
62
- canSetPermissions: true,
63
62
  };
64
63
 
65
64
  const renderComponent = (props = defaultProps) =>
@@ -90,23 +89,44 @@ describe("CurrentAccessList component rendering", () => {
90
89
  expect(screen.getByText(/Only these roles/)).toBeTruthy();
91
90
  });
92
91
 
93
- it("should render the Manage User Access button when canSetPermissions is true", () => {
94
- renderComponent({ ...defaultProps, canSetPermissions: true });
92
+ it("should render the Manage User Access button", () => {
93
+ renderComponent();
95
94
 
96
95
  expect(screen.getByText("MANAGE USER ACCESS")).toBeTruthy();
97
96
  });
98
97
 
99
- it("should not render the Manage User Access button when canSetPermissions is false", () => {
100
- renderComponent({ ...defaultProps, canSetPermissions: false });
101
-
102
- expect(screen.queryByText("MANAGE USER ACCESS")).toBeNull();
103
- });
104
-
105
98
  it("should filter to show only selected users when restrictionType is users", () => {
106
99
  const allUsers = [
107
- { id: 1, username: "john", name: "John Doe", email: "john@example.com", failed: 0, roles: [], isSuperAdmin: false, pagePermissions: [] },
108
- { id: 2, username: "jane", name: "Jane Smith", email: "jane@example.com", failed: 0, roles: [], isSuperAdmin: false, pagePermissions: [] },
109
- { id: 3, username: "bob", name: "Bob Wilson", email: "bob@example.com", failed: 0, roles: [], isSuperAdmin: false, pagePermissions: [] },
100
+ {
101
+ id: 1,
102
+ username: "john",
103
+ name: "John Doe",
104
+ email: "john@example.com",
105
+ failed: 0,
106
+ roles: [],
107
+ isSuperAdmin: false,
108
+ pagePermissions: [],
109
+ },
110
+ {
111
+ id: 2,
112
+ username: "jane",
113
+ name: "Jane Smith",
114
+ email: "jane@example.com",
115
+ failed: 0,
116
+ roles: [],
117
+ isSuperAdmin: false,
118
+ pagePermissions: [],
119
+ },
120
+ {
121
+ id: 3,
122
+ username: "bob",
123
+ name: "Bob Wilson",
124
+ email: "bob@example.com",
125
+ failed: 0,
126
+ roles: [],
127
+ isSuperAdmin: false,
128
+ pagePermissions: [],
129
+ },
110
130
  ];
111
131
 
112
132
  renderComponent({
@@ -128,7 +148,6 @@ describe("CurrentAccessList component events", () => {
128
148
 
129
149
  renderComponent({
130
150
  ...defaultProps,
131
- canSetPermissions: true,
132
151
  onEdit: onEditMock,
133
152
  });
134
153
 
@@ -153,9 +172,36 @@ describe("CurrentAccessList component events", () => {
153
172
 
154
173
  it("should handle multiple users correctly", () => {
155
174
  const allUsers = [
156
- { id: 1, username: "john", name: "John Doe", email: "john@example.com", failed: 0, roles: [], isSuperAdmin: false, pagePermissions: [] },
157
- { id: 2, username: "jane", name: "Jane Smith", email: "jane@example.com", failed: 0, roles: [], isSuperAdmin: false, pagePermissions: [] },
158
- { id: 3, username: "bob", name: "Bob Wilson", email: "bob@example.com", failed: 0, roles: [], isSuperAdmin: false, pagePermissions: [] },
175
+ {
176
+ id: 1,
177
+ username: "john",
178
+ name: "John Doe",
179
+ email: "john@example.com",
180
+ failed: 0,
181
+ roles: [],
182
+ isSuperAdmin: false,
183
+ pagePermissions: [],
184
+ },
185
+ {
186
+ id: 2,
187
+ username: "jane",
188
+ name: "Jane Smith",
189
+ email: "jane@example.com",
190
+ failed: 0,
191
+ roles: [],
192
+ isSuperAdmin: false,
193
+ pagePermissions: [],
194
+ },
195
+ {
196
+ id: 3,
197
+ username: "bob",
198
+ name: "Bob Wilson",
199
+ email: "bob@example.com",
200
+ failed: 0,
201
+ roles: [],
202
+ isSuperAdmin: false,
203
+ pagePermissions: [],
204
+ },
159
205
  ];
160
206
 
161
207
  renderComponent({
@@ -1,11 +1,11 @@
1
- import { act } from "@testing-library/react";
1
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2
2
  import { ThemeProvider } from "styled-components";
3
- import { render, cleanup, screen, fireEvent } from "@testing-library/react";
4
3
  import "@testing-library/jest-dom";
5
4
 
6
5
  import RestrictionsModal from "@ax/components/RestrictionsModal";
7
6
  import { parseTheme } from "@ax/helpers";
8
7
  import globalTheme from "@ax/themes/theme.json";
8
+ import type { PageAccessGrantsSummary } from "@ax/types";
9
9
 
10
10
  jest.mock("@ax/components/RestrictionsModal/utils", () => ({
11
11
  fetchUsers: jest.fn().mockResolvedValue([]),
@@ -32,13 +32,18 @@ jest.mock("react-redux", () => ({
32
32
 
33
33
  afterEach(cleanup);
34
34
 
35
- const defaultProps = {
35
+ const defaultProps: {
36
+ isOpen: boolean;
37
+ toggleModal: jest.Mock;
38
+ currentAccessGrants: PageAccessGrantsSummary | undefined;
39
+ onUpdateAccessGrants: jest.Mock;
40
+ isSaving: boolean;
41
+ } = {
36
42
  isOpen: false,
37
43
  toggleModal: jest.fn(),
38
44
  currentAccessGrants: undefined,
39
45
  onUpdateAccessGrants: jest.fn().mockResolvedValue(undefined),
40
46
  isSaving: false,
41
- canSetPermissions: true,
42
47
  };
43
48
 
44
49
  const renderComponent = (props = defaultProps) =>
@@ -386,7 +386,7 @@ describe("Sites module events", () => {
386
386
 
387
387
  const actionMenuItem = screen.getAllByTestId("action-menu-item");
388
388
  expect(actionMenuItem.length).toEqual(2);
389
- fireEvent.click(actionMenuItem[0]);
389
+ fireEvent.click(actionMenuItem[1]);
390
390
  const deleteModal = screen.getByTestId("delete-modal");
391
391
  expect(deleteModal).toBeTruthy();
392
392
  });
@@ -407,7 +407,7 @@ describe("Sites module events", () => {
407
407
 
408
408
  const actionMenuItem = screen.getAllByTestId("action-menu-item");
409
409
  expect(actionMenuItem.length).toEqual(2);
410
- fireEvent.click(actionMenuItem[0]);
410
+ fireEvent.click(actionMenuItem[1]);
411
411
  const deleteModal = screen.getByTestId("delete-modal");
412
412
  expect(deleteModal).toBeTruthy();
413
413
  });
@@ -426,7 +426,7 @@ describe("Sites module events", () => {
426
426
 
427
427
  const actionMenuItem = screen.getAllByTestId("action-menu-item");
428
428
  expect(actionMenuItem.length).toEqual(2);
429
- fireEvent.click(actionMenuItem[1]);
429
+ fireEvent.click(actionMenuItem[0]);
430
430
  const publishModal = screen.getByTestId("publish-modal");
431
431
  expect(publishModal).toBeTruthy();
432
432
  });
@@ -447,8 +447,227 @@ describe("Sites module events", () => {
447
447
 
448
448
  const actionMenuItem = screen.getAllByTestId("action-menu-item");
449
449
  expect(actionMenuItem.length).toEqual(2);
450
+ fireEvent.click(actionMenuItem[0]);
451
+ const publishModal = screen.getByTestId("publish-modal");
452
+ expect(publishModal).toBeTruthy();
453
+ });
454
+
455
+ it("should disable delete button when confirmation input is incorrect", async () => {
456
+ await renderSitesList();
457
+
458
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
459
+
460
+ fireEvent.click(moreInfoButton[0]);
461
+ const actionMenuItem = screen.getAllByTestId("action-menu-item");
462
+ fireEvent.click(actionMenuItem[1]);
463
+
464
+ const deleteModal = screen.getByTestId("delete-modal");
465
+ expect(deleteModal).toBeTruthy();
466
+
467
+ const mainDeleteButton = screen.getByRole("button", { name: "Delete Site" });
468
+ expect((mainDeleteButton as HTMLButtonElement).disabled).toBe(true);
469
+ });
470
+
471
+ it("should enable delete button when confirmation input is correct", async () => {
472
+ await renderSitesList();
473
+
474
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
475
+
476
+ fireEvent.click(moreInfoButton[0]);
477
+ const actionMenuItem = screen.getAllByTestId("action-menu-item");
478
+ fireEvent.click(actionMenuItem[1]);
479
+
480
+ const deleteModal = screen.getByTestId("delete-modal");
481
+ expect(deleteModal).toBeTruthy();
482
+
483
+ const input = screen.getByPlaceholderText("Type here") as HTMLInputElement;
484
+ await act(async () => {
485
+ fireEvent.change(input, { target: { value: "SITE 1" } });
486
+ });
487
+
488
+ await waitFor(() => {
489
+ const mainDeleteButton = screen.getByRole("button", { name: "Delete Site" });
490
+ expect((mainDeleteButton as HTMLButtonElement).disabled).toBe(false);
491
+ });
492
+ });
493
+
494
+ it("should close delete modal when clicking cancel", async () => {
495
+ await renderSitesList();
496
+
497
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
498
+ fireEvent.click(moreInfoButton[0]);
499
+
500
+ const actionMenuItem = screen.getAllByTestId("action-menu-item");
501
+ fireEvent.click(actionMenuItem[1]);
502
+
503
+ const deleteModal = screen.getByTestId("delete-modal");
504
+ expect(deleteModal).toBeTruthy();
505
+
506
+ const cancelButton = screen.getByRole("button", { name: "Cancel" });
507
+ fireEvent.click(cancelButton);
508
+
509
+ expect(screen.queryByTestId("delete-modal")).not.toBeTruthy();
510
+ });
511
+
512
+ it("should show view online option for published sites", async () => {
513
+ const publishedSitesData = {
514
+ ...initialStore,
515
+ sites: {
516
+ totalItems: 1,
517
+ sites: [
518
+ {
519
+ ...sitesDataMock.sites[0],
520
+ isPublished: true,
521
+ updated: true,
522
+ },
523
+ ],
524
+ recentSites: [],
525
+ config: sitesDataMock.config,
526
+ },
527
+ };
528
+
529
+ await renderSitesList(publishedSitesData);
530
+
531
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
532
+ fireEvent.click(moreInfoButton[0]);
533
+
534
+ const actionMenuItem = screen.getAllByTestId("action-menu-item");
535
+ expect(actionMenuItem.length).toBeGreaterThan(2);
536
+
537
+ const viewOnlineItem = screen.getByText("View online");
538
+ expect(viewOnlineItem).toBeTruthy();
539
+ });
540
+
541
+ it("should show unpublish and republish options for published sites", async () => {
542
+ const publishedSitesData = {
543
+ ...initialStore,
544
+ sites: {
545
+ totalItems: 1,
546
+ sites: [
547
+ {
548
+ ...sitesDataMock.sites[0],
549
+ isPublished: true,
550
+ updated: true,
551
+ },
552
+ ],
553
+ recentSites: [],
554
+ config: sitesDataMock.config,
555
+ },
556
+ };
557
+
558
+ await renderSitesList(publishedSitesData);
559
+
560
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
561
+ fireEvent.click(moreInfoButton[0]);
562
+
563
+ const unpublishItem = screen.getByText("Unpublish site");
564
+ const republishItem = screen.getByText("Republish site");
565
+
566
+ expect(unpublishItem).toBeTruthy();
567
+ expect(republishItem).toBeTruthy();
568
+ });
569
+
570
+ it("should open republish modal when clicking republish option", async () => {
571
+ const publishedSitesData = {
572
+ ...initialStore,
573
+ sites: {
574
+ totalItems: 1,
575
+ sites: [
576
+ {
577
+ ...sitesDataMock.sites[0],
578
+ isPublished: true,
579
+ updated: true,
580
+ },
581
+ ],
582
+ recentSites: [],
583
+ config: sitesDataMock.config,
584
+ },
585
+ };
586
+
587
+ await renderSitesList(publishedSitesData);
588
+
589
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
590
+ fireEvent.click(moreInfoButton[0]);
591
+
592
+ const republishItem = screen.getByText("Republish site");
593
+ fireEvent.click(republishItem);
594
+
595
+ const republishModal = screen.getByTestId("republish-site-modal");
596
+ expect(republishModal).toBeTruthy();
597
+ });
598
+
599
+ it("should open unpublish modal when clicking unpublish option", async () => {
600
+ const publishedSitesData = {
601
+ ...initialStore,
602
+ sites: {
603
+ totalItems: 1,
604
+ sites: [
605
+ {
606
+ ...sitesDataMock.sites[0],
607
+ isPublished: true,
608
+ updated: true,
609
+ },
610
+ ],
611
+ recentSites: [],
612
+ config: sitesDataMock.config,
613
+ },
614
+ };
615
+
616
+ await renderSitesList(publishedSitesData);
617
+
618
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
619
+ fireEvent.click(moreInfoButton[0]);
620
+
621
+ const actionMenuItem = screen.getAllByTestId("action-menu-item");
450
622
  fireEvent.click(actionMenuItem[1]);
623
+
451
624
  const publishModal = screen.getByTestId("publish-modal");
452
625
  expect(publishModal).toBeTruthy();
453
626
  });
627
+
628
+ it("should render publish action for unpublished sites", async () => {
629
+ await renderSitesList();
630
+
631
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
632
+ fireEvent.click(moreInfoButton[0]);
633
+
634
+ const publishItem = screen.getByText("Publish site");
635
+ expect(publishItem).toBeTruthy();
636
+ });
637
+
638
+ it("should not show view online option for unpublished sites", async () => {
639
+ await renderSitesList();
640
+
641
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
642
+ fireEvent.click(moreInfoButton[0]);
643
+
644
+ const viewOnlineItem = screen.queryByText("View online");
645
+ expect(viewOnlineItem).not.toBeTruthy();
646
+ });
647
+
648
+ it("should show 4 action items for published sites", async () => {
649
+ const publishedSitesData = {
650
+ ...initialStore,
651
+ sites: {
652
+ totalItems: 1,
653
+ sites: [
654
+ {
655
+ ...sitesDataMock.sites[0],
656
+ isPublished: true,
657
+ updated: true,
658
+ },
659
+ ],
660
+ recentSites: [],
661
+ config: sitesDataMock.config,
662
+ },
663
+ };
664
+
665
+ await renderSitesList(publishedSitesData);
666
+
667
+ const moreInfoButton = screen.getAllByTestId("more-info-button");
668
+ fireEvent.click(moreInfoButton[0]);
669
+
670
+ const actionMenuItem = screen.getAllByTestId("action-menu-item");
671
+ expect(actionMenuItem.length).toEqual(4);
672
+ });
454
673
  });
package/src/api/sites.tsx CHANGED
@@ -86,6 +86,11 @@ const SERVICES: { [key: string]: IServiceConfig } = {
86
86
  endpoint: "/site/unpublish/bulk",
87
87
  method: "POST",
88
88
  },
89
+ REPUBLISH_SITE: {
90
+ ...template,
91
+ endpoint: ["/site/", "/render-all"],
92
+ method: "POST",
93
+ },
89
94
  IMPORT_PAGE: {
90
95
  ...template,
91
96
  endpoint: ["/site/", "/pages/global/imports/"],
@@ -374,6 +379,16 @@ const unpublishSite = (siteID: number) => {
374
379
 
375
380
  const unpublishSiteBulk = (ids: number[]) => sendRequest(SERVICES.UNPUBLISH_SITE_BULK, { sites: ids });
376
381
 
382
+ const republishSite = (siteID: number) => {
383
+ const {
384
+ host,
385
+ endpoint: [prefix, suffix],
386
+ } = SERVICES.REPUBLISH_SITE;
387
+
388
+ SERVICES.REPUBLISH_SITE.dynamicUrl = `${host}${prefix}${siteID}${suffix}`;
389
+ return sendRequest(SERVICES.REPUBLISH_SITE);
390
+ };
391
+
377
392
  const getSelectSites = () => sendRequest(SERVICES.GET_SELECT_SITES);
378
393
 
379
394
  const setSiteActivity = async (siteID: number) => {
@@ -398,6 +413,7 @@ export default {
398
413
  publishSiteBulk,
399
414
  unpublishSite,
400
415
  unpublishSiteBulk,
416
+ republishSite,
401
417
  getGlobalPages,
402
418
  getGlobalPagesLight,
403
419
  importPage,
@@ -31,6 +31,7 @@ const AsyncSelect = (props: IAsyncSelectProps): JSX.Element => {
31
31
  languages,
32
32
  contentLanguages,
33
33
  forceLanguage,
34
+ menuInPortal,
34
35
  } = props;
35
36
 
36
37
  const initialState: IState = {
@@ -175,6 +176,7 @@ const AsyncSelect = (props: IAsyncSelectProps): JSX.Element => {
175
176
 
176
177
  return (
177
178
  <div data-testid="asyncSelect">
179
+ <S.MenuGlobalStyle />
178
180
  <S.StyledSelect
179
181
  name={name}
180
182
  value={getObjectValue(value, state.items) || ""}
@@ -194,6 +196,8 @@ const AsyncSelect = (props: IAsyncSelectProps): JSX.Element => {
194
196
  inputValue={state.inputText}
195
197
  components={{ Menu }}
196
198
  onKeyDown={handleKeyDown}
199
+ menuPortalTarget={menuInPortal ? document.body : undefined}
200
+ styles={menuInPortal ? { menuPortal: (base) => ({ ...base, zIndex: 1280 }) } : undefined}
197
201
  />
198
202
  </div>
199
203
  );
@@ -237,6 +241,7 @@ export interface IAsyncSelectProps {
237
241
  languages?: ILanguage[];
238
242
  contentLanguages?: "current" | "all" | string[];
239
243
  forceLanguage?: number;
244
+ menuInPortal?: boolean;
240
245
  }
241
246
 
242
247
  export default AsyncSelect;
@@ -1,5 +1,5 @@
1
- import styled, { css } from "styled-components";
2
1
  import AsyncSelect from "react-select/async";
2
+ import styled, { createGlobalStyle } from "styled-components";
3
3
 
4
4
  export const StyledSelect = styled(AsyncSelect)<{
5
5
  isDisabled: boolean | undefined;
@@ -49,65 +49,6 @@ export const StyledSelect = styled(AsyncSelect)<{
49
49
  border-color: ${(p) => p.theme.color.interactiveDisabled};
50
50
  }
51
51
 
52
- .react-select__menu {
53
- box-shadow: none;
54
- border-radius: 4px;
55
- min-width: calc(${(p) => p.theme.spacing.xl} * 4);
56
- max-width: calc(${(p) => p.theme.spacing.xl} * 6);
57
- margin-top: 0;
58
- z-index: 99;
59
-
60
- .react-select__menu-list {
61
- border: 1px solid ${(p) => p.theme.color.uiLine};
62
- border-radius: 4px;
63
- max-height: calc(${(p) => p.theme.spacing.l} * 3);
64
- overflow: auto;
65
- padding: 0;
66
-
67
- &::-webkit-scrollbar {
68
- -webkit-appearance: none;
69
- width: 4px;
70
- height: 100%;
71
- }
72
-
73
- &::-webkit-scrollbar-thumb {
74
- border-radius: 4px;
75
- background-color: ${(p) => p.theme.colors.iconNonActive};
76
- }
77
-
78
- .react-select__option {
79
- background-color: ${(p) => p.theme.color.interactiveBackground};
80
- min-height: ${(p) => p.theme.spacing.l};
81
- padding: ${(p) => `13px ${p.theme.spacing.s} 12px ${p.theme.spacing.s}`};
82
- &:first-child {
83
- ${(p) =>
84
- !p.required &&
85
- p.isClearable &&
86
- css`
87
- font-style: italic;
88
- color: ${(p) => p.theme.color.textLowEmphasis};
89
- background-color: ${(p) => p.theme.color.uiLineInverse};
90
- `}
91
- }
92
- :hover {
93
- cursor: pointer;
94
- background-color: ${(p) => p.theme.color.overlayHoverPrimary};
95
- }
96
- :focus {
97
- background-color: ${(p) => p.theme.color.overlayFocusPrimary};
98
- border: 0.5px solid ${(p) => p.theme.color.interactive02};
99
- }
100
- :active {
101
- background-color: ${(p) => p.theme.color.overlayPressedPrimary};
102
- }
103
- }
104
- .react-select__option--is-selected {
105
- color: ${(p) => p.theme.color.textHighEmphasis};
106
- background-color: ${(p) => p.theme.color.overlayPressedPrimary};
107
- }
108
- }
109
- }
110
-
111
52
  &.inline {
112
53
  ${(p) => p.theme.textStyle.uiS};
113
54
  text-transform: capitalize;
@@ -161,3 +102,59 @@ export const StyledSelect = styled(AsyncSelect)<{
161
102
  }
162
103
  }
163
104
  `;
105
+
106
+ // Estilos del menú aplicados de forma global: cuando el select usa menuInPortal
107
+ // (menuPortalTarget={document.body}) el menú queda fuera del árbol de StyledSelect,
108
+ // por lo que las reglas anidadas no le alcanzarían. Al ser global sirve para ambos casos.
109
+ export const MenuGlobalStyle = createGlobalStyle`
110
+ .react-select__menu {
111
+ ${(p: any) => p.theme.textStyle.fieldContent};
112
+ color: ${(p) => p.theme.color.textHighEmphasis};
113
+ box-shadow: none !important;
114
+ border-radius: 4px;
115
+ min-width: calc(${(p) => p.theme.spacing.xl} * 4);
116
+ max-width: calc(${(p) => p.theme.spacing.xl} * 6);
117
+ margin-top: 0 !important;
118
+
119
+ .react-select__menu-list {
120
+ border: 1px solid ${(p) => p.theme.color.uiLine};
121
+ border-radius: 4px;
122
+ max-height: calc(${(p) => p.theme.spacing.l} * 3);
123
+ overflow: auto;
124
+ padding: 0;
125
+
126
+ &::-webkit-scrollbar {
127
+ -webkit-appearance: none;
128
+ width: 4px;
129
+ height: 100%;
130
+ }
131
+
132
+ &::-webkit-scrollbar-thumb {
133
+ border-radius: 4px;
134
+ background-color: ${(p) => p.theme.colors.iconNonActive};
135
+ }
136
+
137
+ .react-select__option {
138
+ background-color: ${(p) => p.theme.color.interactiveBackground};
139
+ min-height: ${(p) => p.theme.spacing.l};
140
+ padding: ${(p) => `13px ${p.theme.spacing.s} 12px ${p.theme.spacing.s}`};
141
+
142
+ :hover {
143
+ cursor: pointer;
144
+ background-color: ${(p) => p.theme.color.overlayHoverPrimary};
145
+ }
146
+ :focus {
147
+ background-color: ${(p) => p.theme.color.overlayFocusPrimary};
148
+ border: 0.5px solid ${(p) => p.theme.color.interactive02};
149
+ }
150
+ :active {
151
+ background-color: ${(p) => p.theme.color.overlayPressedPrimary};
152
+ }
153
+ }
154
+ .react-select__option--is-selected {
155
+ color: ${(p) => p.theme.color.textHighEmphasis};
156
+ background-color: ${(p) => p.theme.color.overlayPressedPrimary};
157
+ }
158
+ }
159
+ }
160
+ `;
@@ -0,0 +1,22 @@
1
+ import * as React from "react";
2
+ const SvgRepublish = (props) => (
3
+ <svg
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width={24}
6
+ height={24}
7
+ fill="none"
8
+ {...props}
9
+ >
10
+ <path
11
+ fill="#AFC628"
12
+ d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96ZM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95A5.469 5.469 0 0 1 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11A2.98 2.98 0 0 1 22 15c0 1.65-1.35 3-3 3Z"
13
+ />
14
+ <path
15
+ fill="#AFC628"
16
+ stroke="#AFC628"
17
+ strokeWidth={0.25}
18
+ d="M15.784 12.895a4.125 4.125 0 0 1-4.104 3.713 4.092 4.092 0 0 1-2.92-1.228l-.791.791-.214.213v-2.826h2.827l-.214.213-.864.865a3.046 3.046 0 0 0 2.176.922 3.078 3.078 0 0 0 3.048-2.69l.014-.11h1.056l-.014.137Zm-8.222-.688.014-.137a4.124 4.124 0 0 1 4.104-3.712c1.145 0 2.177.472 2.921 1.226l.79-.79.214-.213v2.826h-2.827l.214-.213.865-.866a3.05 3.05 0 0 0-2.177-.92 3.077 3.077 0 0 0-3.048 2.69l-.014.11H7.562Z"
19
+ />
20
+ </svg>
21
+ );
22
+ export default SvgRepublish;
@@ -0,0 +1,4 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M19.35 10.04C18.67 6.59 15.64 4 12 4C9.11 4 6.6 5.64 5.35 8.04C2.34 8.36 0 10.91 0 14C0 17.31 2.69 20 6 20H19C21.76 20 24 17.76 24 15C24 12.36 21.95 10.22 19.35 10.04ZM19 18H6C3.79 18 2 16.21 2 14C2 11.95 3.53 10.24 5.56 10.03L6.63 9.92L7.13 8.97C8.08 7.14 9.94 6 12 6C14.62 6 16.88 7.86 17.39 10.43L17.69 11.93L19.22 12.04C20.78 12.14 22 13.45 22 15C22 16.65 20.65 18 19 18Z" fill="#AFC628"/>
3
+ <path d="M15.784 12.8946C15.5777 14.9779 13.8204 16.6075 11.6795 16.6075C10.5338 16.6074 9.50405 16.1335 8.75958 15.38L7.96857 16.171L7.7547 16.3839L7.7547 13.5577L10.5818 13.5577L10.368 13.7706L9.50372 14.6358C10.0603 15.2013 10.8279 15.5576 11.6795 15.5577C13.2435 15.5577 14.5399 14.3815 14.7283 12.8673L14.742 12.7579L15.7977 12.7579L15.784 12.8946ZM7.56232 12.2071L7.57599 12.0704C7.78224 9.98727 9.53878 8.35775 11.6795 8.35751C12.8254 8.35751 13.8568 8.83042 14.6014 9.58408L15.3914 8.79404L15.6053 8.58115L15.6053 11.4073L12.7781 11.4073L12.992 11.1944L13.8572 10.3282C13.3005 9.76263 12.5313 9.40732 11.6795 9.40732C10.1157 9.40755 8.82002 10.5837 8.63165 12.0977L8.61798 12.2071L7.56232 12.2071Z" fill="#AFC628" stroke="#AFC628" stroke-width="0.25"/>
4
+ </svg>
@@ -66,6 +66,7 @@ const Modal = styled.div<{ size?: string; height?: number | string }>`
66
66
  border-radius: 4px;
67
67
  width: ${(p) => getWidth(p.size)};
68
68
  height: ${(p) => getModalHeight(p.height, p.size)};
69
+ min-height: ${(p) => getModalHeight(p.height, p.size)};
69
70
  max-height: 80vh;
70
71
  box-shadow: ${(p) => p.theme.shadow.shadowXL};
71
72
  top: 50%;
@@ -1,7 +1,7 @@
1
- import { useRef, useEffect } from "react";
1
+ import { useEffect, useRef } from "react";
2
+ import { createPortal } from "react-dom";
2
3
  import { useLocation } from "react-router-dom";
3
4
 
4
- import { createPortal } from "react-dom";
5
5
  import { Button, IconAction } from "@ax/components";
6
6
 
7
7
  import * as S from "./style";
@@ -815,6 +815,23 @@ function updateCurrentSearch(query: string): (dispatch: Dispatch) => Promise<voi
815
815
  };
816
816
  }
817
817
 
818
+ function republishSite(siteID: number): (dispatch: Dispatch) => Promise<boolean> {
819
+ return async (dispatch) => {
820
+ try {
821
+ const responseActions = {
822
+ handleSuccess: () => {},
823
+ handleError: (response: any) => appActions.handleError(response)(dispatch),
824
+ };
825
+ const callback = async () => await sites.republishSite(siteID);
826
+
827
+ return await handleRequest(callback, responseActions, [])(dispatch);
828
+ } catch (e) {
829
+ console.log(e);
830
+ return false;
831
+ }
832
+ };
833
+ }
834
+
818
835
  export {
819
836
  setCurrentSiteInfo,
820
837
  setCurrentSitePages,
@@ -849,4 +866,5 @@ export {
849
866
  updateCurrentSearch,
850
867
  setIsSitesLoading,
851
868
  getAllSites,
869
+ republishSite,
852
870
  };
@@ -6,13 +6,23 @@ import { sitesActions } from "@ax/containers/Sites";
6
6
  import { isDevelopment } from "@ax/helpers";
7
7
  import type { IGetSitesParams, ISite } from "@ax/types";
8
8
 
9
- import { ItemName, ItemThumbnail } from "./../../atoms";
9
+ import { ItemName, ItemThumbnail, RepublishSiteModal } from "./../../atoms";
10
10
  import { useSiteActions } from "./../../hooks";
11
11
 
12
12
  import * as S from "./style";
13
13
 
14
14
  const GridSiteItem = (props: IGridSiteItemProps): JSX.Element => {
15
- const { site, setSiteInfo, setHistoryPush, deleteSite, publishSite, unpublishSite, getParams } = props;
15
+ const {
16
+ site,
17
+ setSiteInfo,
18
+ setHistoryPush,
19
+ deleteSite,
20
+ publishSite,
21
+ unpublishSite,
22
+ republishSite,
23
+ getParams,
24
+ toggleToast,
25
+ } = props;
16
26
  const { hidden } = site;
17
27
 
18
28
  const {
@@ -29,7 +39,20 @@ const GridSiteItem = (props: IGridSiteItemProps): JSX.Element => {
29
39
  isOpenPublish,
30
40
  togglePublishModal,
31
41
  publishModal,
32
- } = useSiteActions({ site, setSiteInfo, setHistoryPush, deleteSite, publishSite, unpublishSite, getParams });
42
+ isOpenRepublish,
43
+ toggleRepublishModal,
44
+ onRepublish,
45
+ } = useSiteActions({
46
+ site,
47
+ setSiteInfo,
48
+ setHistoryPush,
49
+ deleteSite,
50
+ publishSite,
51
+ unpublishSite,
52
+ republishSite,
53
+ getParams,
54
+ toggleToast,
55
+ });
33
56
 
34
57
  const { title, mainAction, secondaryAction, content } = publishModal;
35
58
 
@@ -92,6 +115,7 @@ const GridSiteItem = (props: IGridSiteItemProps): JSX.Element => {
92
115
  >
93
116
  {isOpenPublish ? <S.ModalContent data-testid="publish-modal">{content}</S.ModalContent> : null}
94
117
  </Modal>
118
+ <RepublishSiteModal isOpen={isOpenRepublish} toggleModal={toggleRepublishModal} onRepublish={onRepublish} />
95
119
  </>
96
120
  );
97
121
  };
@@ -103,7 +127,9 @@ interface IGridSiteItemProps {
103
127
  deleteSite(siteID: number, params?: IGetSitesParams): Promise<void>;
104
128
  publishSite(siteID: number, params?: IGetSitesParams): Promise<void>;
105
129
  unpublishSite(siteID: number, params?: IGetSitesParams): Promise<void>;
130
+ republishSite(siteID: number): Promise<boolean>;
106
131
  getParams: () => IGetSitesParams;
132
+ toggleToast(message: string): void;
107
133
  }
108
134
 
109
135
  const mapDispatchToProps = {
@@ -112,6 +138,7 @@ const mapDispatchToProps = {
112
138
  deleteSite: sitesActions.deleteSite,
113
139
  publishSite: sitesActions.publishSite,
114
140
  unpublishSite: sitesActions.unpublishSite,
141
+ republishSite: sitesActions.republishSite,
115
142
  };
116
143
 
117
144
  export default connect(null, mapDispatchToProps)(GridSiteItem);
@@ -6,7 +6,7 @@ import { sitesActions } from "@ax/containers/Sites";
6
6
  import { getFormattedDateWithTimezone } from "@ax/helpers";
7
7
  import type { ICheck, IGetSitesParams, ISite } from "@ax/types";
8
8
 
9
- import { ItemName, ItemThumbnail } from "./../../atoms";
9
+ import { ItemName, ItemThumbnail, RepublishSiteModal } from "./../../atoms";
10
10
  import { useSiteActions } from "./../../hooks";
11
11
 
12
12
  import * as S from "./style";
@@ -19,10 +19,12 @@ const ListSiteItem = (props: IListSiteItemProps): JSX.Element => {
19
19
  deleteSite,
20
20
  publishSite,
21
21
  unpublishSite,
22
+ republishSite,
22
23
  isSelected,
23
24
  onCheck,
24
25
  getParams,
25
26
  hoverCheck,
27
+ toggleToast,
26
28
  } = props;
27
29
 
28
30
  const { hidden } = site;
@@ -41,14 +43,24 @@ const ListSiteItem = (props: IListSiteItemProps): JSX.Element => {
41
43
  isOpenPublish,
42
44
  togglePublishModal,
43
45
  publishModal,
44
- } = useSiteActions({ site, setSiteInfo, setHistoryPush, deleteSite, publishSite, unpublishSite, getParams });
46
+ isOpenRepublish,
47
+ toggleRepublishModal,
48
+ } = useSiteActions({
49
+ site,
50
+ setSiteInfo,
51
+ setHistoryPush,
52
+ deleteSite,
53
+ publishSite,
54
+ unpublishSite,
55
+ republishSite,
56
+ getParams,
57
+ toggleToast,
58
+ });
45
59
 
46
60
  const { title, mainAction, secondaryAction, content } = publishModal;
47
61
 
48
62
  const handleOnChange = (value: ICheck) => onCheck(value);
49
63
 
50
- console.log(site, "site");
51
-
52
64
  return (
53
65
  <>
54
66
  <S.SiteItemRow key={site.id} role="rowgroup" selected={isSelected} data-testid="list-site-item">
@@ -113,6 +125,7 @@ const ListSiteItem = (props: IListSiteItemProps): JSX.Element => {
113
125
  >
114
126
  {isOpenPublish ? <S.ModalContent data-testid="publish-modal">{content}</S.ModalContent> : null}
115
127
  </Modal>
128
+ <RepublishSiteModal isOpen={isOpenRepublish} toggleModal={toggleRepublishModal} onRepublish={() => null} />
116
129
  </>
117
130
  );
118
131
  };
@@ -126,8 +139,10 @@ interface IListSiteItemProps {
126
139
  deleteSite(siteID: number, params?: IGetSitesParams): Promise<void>;
127
140
  publishSite(siteID: number, params?: IGetSitesParams): Promise<void>;
128
141
  unpublishSite(siteID: number, params?: IGetSitesParams): Promise<void>;
142
+ republishSite(siteID: number): Promise<boolean>;
129
143
  onCheck: (e: ICheck) => void;
130
144
  getParams: () => IGetSitesParams;
145
+ toggleToast(message: string): void;
131
146
  }
132
147
 
133
148
  const mapDispatchToProps = {
@@ -136,6 +151,7 @@ const mapDispatchToProps = {
136
151
  deleteSite: sitesActions.deleteSite,
137
152
  publishSite: sitesActions.publishSite,
138
153
  unpublishSite: sitesActions.unpublishSite,
154
+ republishSite: sitesActions.republishSite,
139
155
  };
140
156
 
141
157
  export default connect(null, mapDispatchToProps)(ListSiteItem);
@@ -101,6 +101,7 @@ const SiteModal = (props: ISiteModalProps): JSX.Element => {
101
101
  entity="languages"
102
102
  onChange={setLangValue}
103
103
  mandatory={true}
104
+ menuInPortal={true}
104
105
  />
105
106
  </S.FieldsWrapper>
106
107
  <S.FieldsWrapper>
@@ -112,6 +113,7 @@ const SiteModal = (props: ISiteModalProps): JSX.Element => {
112
113
  value={domain}
113
114
  onChange={setDomainValue}
114
115
  mandatory={true}
116
+ menuInPortal={true}
115
117
  />
116
118
  <FieldsBehavior
117
119
  title="Path"
@@ -1,5 +1,6 @@
1
- import { Image, Tooltip } from "@ax/components";
1
+ import { Image, Modal, Tooltip } from "@ax/components";
2
2
  import { trimText } from "@ax/helpers";
3
+ import type { IModal } from "@ax/types";
3
4
 
4
5
  import * as S from "./style";
5
6
 
@@ -44,4 +45,37 @@ interface IItemThumbnailProps {
44
45
  borderRadius?: boolean;
45
46
  }
46
47
 
47
- export { ItemName, ItemThumbnail };
48
+ const RepublishSiteModal = (props: IRepublishSiteModal): JSX.Element => {
49
+ const { isOpen, toggleModal, onRepublish } = props;
50
+
51
+ const mainAction = { title: "Yes, republish now", onClick: onRepublish };
52
+
53
+ const secondaryAction = { title: "Cancel", onClick: toggleModal };
54
+
55
+ return (
56
+ <Modal
57
+ isOpen={isOpen}
58
+ hide={toggleModal}
59
+ size="S"
60
+ height="auto"
61
+ title="Republish site?"
62
+ mainAction={mainAction}
63
+ secondaryAction={secondaryAction}
64
+ >
65
+ {isOpen && (
66
+ <S.ModalContent data-testid="republish-site-modal">
67
+ <p>
68
+ This <strong>will refresh all currently public pages</strong> to reflect the latest global changes. Drafts
69
+ and unpublished content will remain hidden.
70
+ </p>
71
+ </S.ModalContent>
72
+ )}
73
+ </Modal>
74
+ );
75
+ };
76
+
77
+ interface IRepublishSiteModal extends IModal {
78
+ onRepublish: () => void;
79
+ }
80
+
81
+ export { ItemName, ItemThumbnail, RepublishSiteModal };
@@ -117,7 +117,9 @@ interface IUseSiteActionsParams {
117
117
  deleteSite(siteID: number, params?: IGetSitesParams): Promise<void>;
118
118
  publishSite(siteID: number, params?: IGetSitesParams): Promise<void>;
119
119
  unpublishSite(siteID: number, params?: IGetSitesParams): Promise<void>;
120
+ republishSite(siteID: number): Promise<boolean>;
120
121
  getParams: () => IGetSitesParams;
122
+ toggleToast(message: string): void;
121
123
  }
122
124
 
123
125
  const publishedTooltip: Record<string, string> = {
@@ -143,9 +145,11 @@ const useSiteActions = ({
143
145
  deleteSite,
144
146
  publishSite,
145
147
  unpublishSite,
148
+ republishSite,
146
149
  getParams,
150
+ toggleToast,
147
151
  }: IUseSiteActionsParams) => {
148
- const { isOpen, toggleModal } = useModals(["delete", "publish"]);
152
+ const { isOpen, toggleModal } = useModals(["delete", "publish", "republish"]);
149
153
  const [inputValue, setInputValue] = useState("");
150
154
 
151
155
  const { allowedToDeleteSite, allowedToPublishSite, allowedToUnpublishSite } = usePermissionsForSite(site.id, {
@@ -154,7 +158,7 @@ const useSiteActions = ({
154
158
  allowedToUnpublishSite: "general.unpublishSite",
155
159
  });
156
160
 
157
- const { updated, isPublished } = site;
161
+ const { updated, isPublished, home } = site;
158
162
  const publishedState = getPublishedState(isPublished, updated);
159
163
 
160
164
  const setSite = async () => {
@@ -163,21 +167,30 @@ const useSiteActions = ({
163
167
  };
164
168
 
165
169
  const deleteOption = allowedToDeleteSite
166
- ? { label: "delete", icon: "delete", action: () => toggleModal("delete") }
170
+ ? { label: "Delete site", icon: "delete", action: () => toggleModal("delete") }
167
171
  : undefined;
168
172
 
169
173
  const publishOptionProps =
170
174
  isPublished && allowedToUnpublishSite
171
- ? { label: "Unpublish", icon: "offlinePending" }
175
+ ? { label: "Unpublish site", icon: "offlinePending" }
172
176
  : !isPublished && allowedToPublishSite
173
- ? { label: "Publish", icon: "uploadPending" }
177
+ ? { label: "Publish site", icon: "uploadPending" }
174
178
  : null;
175
179
 
176
180
  const publishOption = publishOptionProps
177
181
  ? { label: publishOptionProps.label, icon: publishOptionProps.icon, action: () => toggleModal("publish") }
178
182
  : undefined;
179
183
 
180
- const menuOptions = [deleteOption, publishOption];
184
+ const republishOption =
185
+ isPublished && allowedToPublishSite
186
+ ? { label: "Republish site", icon: "republish", action: () => toggleModal("republish") }
187
+ : undefined;
188
+
189
+ const viewOption = isPublished
190
+ ? { label: "View online", icon: "view", action: () => goToPublishedSite() }
191
+ : undefined;
192
+
193
+ const menuOptions = [viewOption, publishOption, republishOption, deleteOption];
181
194
 
182
195
  const handleDeleteSite = async () => {
183
196
  const params = getParams();
@@ -190,6 +203,7 @@ const useSiteActions = ({
190
203
  onClick: handleDeleteSite,
191
204
  disabled: inputValue !== site.name.toUpperCase(),
192
205
  };
206
+
193
207
  const secondaryDeleteAction = { title: "Cancel", onClick: () => toggleModal("delete") };
194
208
 
195
209
  const handlePublishSite = async () => {
@@ -204,6 +218,20 @@ const useSiteActions = ({
204
218
  toggleModal("publish");
205
219
  };
206
220
 
221
+ const handleRepublishSite = async () => {
222
+ const isRepublished = await republishSite(site.id);
223
+ toggleModal("republish");
224
+ if (isRepublished) {
225
+ toggleToast("Republishing site. Changes will be live shortly.");
226
+ }
227
+ };
228
+
229
+ const goToPublishedSite = () => {
230
+ if (home) {
231
+ window.open(home, "_blank");
232
+ }
233
+ };
234
+
207
235
  const publishModal = isPublished
208
236
  ? {
209
237
  mainAction: { title: "Unpublish Site", onClick: handleUnpublishSite },
@@ -244,6 +272,9 @@ const useSiteActions = ({
244
272
  isOpenPublish: isOpen("publish"),
245
273
  togglePublishModal: () => toggleModal("publish"),
246
274
  publishModal,
275
+ isOpenRepublish: isOpen("republish"),
276
+ toggleRepublishModal: () => toggleModal("republish"),
277
+ onRepublish: handleRepublishSite,
247
278
  };
248
279
  };
249
280
 
@@ -10,11 +10,12 @@ import {
10
10
  Pagination,
11
11
  SearchTagsBar,
12
12
  TableList,
13
+ Toast,
13
14
  } from "@ax/components";
14
15
  import { appActions } from "@ax/containers/App";
15
16
  import type { IError } from "@ax/containers/App/reducer";
16
17
  import { sitesActions } from "@ax/containers/Sites";
17
- import { useBulkSelection, useModal, usePermission } from "@ax/hooks";
18
+ import { useBulkSelection, useModal, usePermission, useToast } from "@ax/hooks";
18
19
  import type { ICheck, IGetSitesParams, IQueryValue, IRootState, ISite, ISiteListConfig, IUser } from "@ax/types";
19
20
 
20
21
  import AllSitesHeader from "./AllSitesHeader";
@@ -83,6 +84,7 @@ const SitesList = (props: ISitesListProps) => {
83
84
  const errorRef = useRef<HTMLDivElement>(null);
84
85
  const { isOpen, toggleModal } = useModal();
85
86
  const { isOpen: isWelcomeOpen, toggleModal: toggleWelcomeModal } = useModal();
87
+ const { isVisible, toggleToast, setIsVisible, state: toastState } = useToast();
86
88
 
87
89
  const scrollToTop = useCallback(() => {
88
90
  if (tableRef.current) {
@@ -264,7 +266,7 @@ const SitesList = (props: ISitesListProps) => {
264
266
  sites.map((site: ISite) => {
265
267
  const isItemSelected = isSelected(site.id);
266
268
  return displayMode === "grid" ? (
267
- <GridSiteItem key={site.id} site={site} getParams={getParams} />
269
+ <GridSiteItem key={site.id} site={site} getParams={getParams} toggleToast={toggleToast} />
268
270
  ) : (
269
271
  <ListSiteItem
270
272
  key={site.id}
@@ -273,6 +275,7 @@ const SitesList = (props: ISitesListProps) => {
273
275
  onCheck={handleAddToBulk}
274
276
  getParams={getParams}
275
277
  hoverCheck={checkState.hoverCheck}
278
+ toggleToast={toggleToast}
276
279
  />
277
280
  );
278
281
  })
@@ -281,7 +284,7 @@ const SitesList = (props: ISitesListProps) => {
281
284
  <EmptyState icon="search" message="We couldn't find what you are looking for. Please, try another search." />
282
285
  </S.EmptyStateWrapper>
283
286
  ),
284
- [sites, displayMode, isSelected, handleAddToBulk, getParams, checkState.hoverCheck],
287
+ [sites, displayMode, isSelected, handleAddToBulk, getParams, checkState.hoverCheck, toggleToast],
285
288
  );
286
289
 
287
290
  const showPagination = totalItems > itemsPerPage;
@@ -374,6 +377,7 @@ const SitesList = (props: ISitesListProps) => {
374
377
  setHistoryPush={setHistoryPush}
375
378
  />
376
379
  <WelcomeModal isOpen={isWelcomeOpen} toggleModal={toggleWelcomeModal} />
380
+ {isVisible && <Toast setIsVisible={setIsVisible} message={toastState} />}
377
381
  </MainWrapper>
378
382
  );
379
383
  };
@@ -101,6 +101,14 @@ const SearchTagsGrid = styled.div`
101
101
  margin: ${(p) => `${p.theme.spacing.xs} ${p.theme.spacing.m}`};
102
102
  `;
103
103
 
104
+ const ModalContent = styled.div`
105
+ padding: ${(p) => p.theme.spacing.m};
106
+
107
+ p {
108
+ margin-bottom: ${(p) => p.theme.spacing.m};
109
+ }
110
+ `;
111
+
104
112
  export {
105
113
  SitesListWrapper,
106
114
  GridList,
@@ -112,4 +120,5 @@ export {
112
120
  AllSitesListWrapper,
113
121
  SearchTags,
114
122
  SearchTagsGrid,
123
+ ModalContent,
115
124
  };