@griddo/ax 11.11.6 → 11.11.7-rc.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/README.md CHANGED
@@ -6,10 +6,6 @@
6
6
  - [AGENTS.md](#agentsmd)
7
7
  - [Centralización de textos de UI](docs/LOCALE-UI-TEXTS.md)
8
8
 
9
- ## AGENTS.md
10
-
11
- Este repositorio incluye un archivo llamado [AGENTS.md](docs/AGENTS.md) que contiene las directrices y reglas de arquitectura que la IA sigue al generar o modificar código. Este archivo define la estructura del proyecto, las convenciones tecnológicas (por ejemplo, el uso de React Router v5, estilo con styled-components, Redux manual), y recomendaciones específicas para mantener la coherencia y calidad del proyecto. Cada vez que la IA realiza cambios en el código, consulta este documento para asegurarse de respetar los patrones legacy y las mejores prácticas establecidas por el equipo de Griddo AX.
12
-
13
9
  ## Comandos de npm
14
10
 
15
11
  ### Desarrollo
@@ -45,7 +41,6 @@ biome lint . --only=<GROUP|RULE|DOMAIN>
45
41
  node_modules/.bin/biome lint . --only=noConsole
46
42
  ```
47
43
 
48
-
49
44
  ## Extras
50
45
 
51
46
  ### Biome organize imports constants group
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "11.11.6",
4
+ "version": "11.11.7-rc.0",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Diego M. Béjar <diego.bejar@secuoyas.com>",
@@ -217,5 +217,5 @@
217
217
  "publishConfig": {
218
218
  "access": "public"
219
219
  },
220
- "gitHead": "fed7cc70b03ba1e4d5e24f99e9a83259e7c5cd49"
220
+ "gitHead": "8b7d5ecf70920b98364c6207ff6079a7af98b430"
221
221
  }
@@ -56,93 +56,7 @@ describe("Browser component rendering", () => {
56
56
  expect(browserContentWrapper).not.toBeTruthy();
57
57
  });
58
58
 
59
- it("should do the copyUrl action and return console error", async () => {
60
- defaultProps.isPreview = true;
61
- defaultProps.showIframe = true;
62
- defaultProps.content = {
63
- id: 1,
64
- entity: 123,
65
- };
66
-
67
- document.execCommand = jest.fn();
68
- console.error = jest.fn();
69
-
70
- render(
71
- <ThemeProvider theme={parseTheme(globalTheme)}>
72
- <Browser {...defaultProps} />
73
- </ThemeProvider>,
74
- );
75
-
76
- const navActionsWrapper = screen.queryByTestId("nav-actions-wrapper");
77
- expect(navActionsWrapper).toBeTruthy();
78
- const browserWrapper = screen.getByTestId("browser-wrapper");
79
- expect(browserWrapper).toBeTruthy();
80
-
81
- const iconWrapperBrowser = screen.getAllByTestId("icon-wrapper-browser");
82
-
83
- fireEvent.click(iconWrapperBrowser[0]);
84
-
85
- await waitFor(() => expect(console.error).toHaveBeenCalledWith("Could not copy text: ", undefined));
86
- });
87
-
88
- it("should do the copyUrl action and return document.execCommand call", async () => {
89
- defaultProps.isPreview = true;
90
- defaultProps.showIframe = true;
91
-
92
- Object.assign(document, {
93
- execCommand: jest.fn().mockImplementation(() => Promise.resolve()),
94
- });
95
-
96
- render(
97
- <ThemeProvider theme={parseTheme(globalTheme)}>
98
- <Browser {...defaultProps} />
99
- </ThemeProvider>,
100
- { wrapper: BrowserRouter },
101
- );
102
- const browserWrapper = screen.getByTestId("browser-wrapper");
103
- expect(browserWrapper).toBeTruthy();
104
-
105
- const iconWrapperBrowser = screen.getAllByTestId("icon-wrapper-browser");
106
-
107
- fireEvent.click(iconWrapperBrowser[0]);
108
- await waitFor(() => expect(screen.getByText(/URL Copied/i)).toBeInTheDocument());
109
- });
110
-
111
- it("should do the copyUrl action and return clipboard call", async () => {
112
- defaultProps.isPreview = true;
113
- defaultProps.showIframe = true;
114
- defaultProps.content = {
115
- id: 2,
116
- entity: 456,
117
- };
118
-
119
- Object.assign(navigator, {
120
- clipboard: {
121
- writeText: jest.fn().mockImplementation(() => Promise.resolve()),
122
- },
123
- });
124
-
125
- Object.assign(window, {
126
- isSecureContext: {},
127
- });
128
-
129
- jest.spyOn(navigator.clipboard, "writeText");
130
-
131
- render(
132
- <ThemeProvider theme={parseTheme(globalTheme)}>
133
- <Browser {...defaultProps} />
134
- </ThemeProvider>,
135
- );
136
-
137
- const browserWrapper = screen.getByTestId("browser-wrapper");
138
- expect(browserWrapper).toBeTruthy();
139
-
140
- const iconWrapperBrowser = screen.getAllByTestId("icon-wrapper-browser");
141
- fireEvent.click(iconWrapperBrowser[0]);
142
- await waitFor(() => expect(navigator.clipboard.writeText).toBeCalledWith("http://localhost/page-preview/2/456"));
143
- });
144
-
145
- it("should render the browserContent", () => {
59
+ it("should render the browserContent", () => {
146
60
  defaultProps.isPreview = false;
147
61
  defaultProps.showIframe = false;
148
62
  const deleteModuleActionMock = defaultProps.actions?.deleteModuleAction as jest.MockedFunction<
@@ -0,0 +1,91 @@
1
+ import type { IShareData } from "@ax/api";
2
+
3
+ import { getShareTokenInfo } from "@ax/helpers";
4
+
5
+ const makeShareData = (overrides: Partial<IShareData> = {}): IShareData => ({
6
+ entity: "test-entity",
7
+ daysValid: 30,
8
+ startDate: "2026-03-01T00:00:00.000Z",
9
+ endDate: "2026-03-31T00:00:00.000Z",
10
+ visitCount: 5,
11
+ ...overrides,
12
+ });
13
+
14
+ describe("getShareTokenInfo", () => {
15
+ describe("token expiration", () => {
16
+ it("should detect an active token", () => {
17
+ const now = new Date("2026-03-15");
18
+ const result = getShareTokenInfo(makeShareData(), now);
19
+
20
+ expect(result.tokenHasExpired).toBe(false);
21
+ });
22
+
23
+ it("should detect an expired token", () => {
24
+ const now = new Date("2026-04-01");
25
+ const result = getShareTokenInfo(makeShareData(), now);
26
+
27
+ expect(result.tokenHasExpired).toBe(true);
28
+ });
29
+
30
+ it("should detect expiration on the exact end date", () => {
31
+ const now = new Date("2026-03-31T12:00:00.000Z");
32
+ const result = getShareTokenInfo(makeShareData(), now);
33
+
34
+ expect(result.tokenHasExpired).toBe(true);
35
+ });
36
+ });
37
+
38
+ describe("days until expiration", () => {
39
+ it("should return remaining days for an active token", () => {
40
+ const now = new Date("2026-03-21");
41
+ const result = getShareTokenInfo(makeShareData(), now);
42
+
43
+ expect(result.validTokenDaysUntilExpires).toBe(10);
44
+ });
45
+
46
+ it("should return 0 for an expired token", () => {
47
+ const now = new Date("2026-05-01");
48
+ const result = getShareTokenInfo(makeShareData(), now);
49
+
50
+ expect(result.validTokenDaysUntilExpires).toBe(0);
51
+ });
52
+
53
+ it("should return 0 on the exact end date", () => {
54
+ const now = new Date("2026-03-31");
55
+ const result = getShareTokenInfo(makeShareData(), now);
56
+
57
+ expect(result.validTokenDaysUntilExpires).toBe(0);
58
+ });
59
+ });
60
+
61
+ describe("token renewal", () => {
62
+ it("should allow renewal when less than 14 days remain", () => {
63
+ const now = new Date("2026-03-20"); // 11 days left
64
+ const result = getShareTokenInfo(makeShareData(), now);
65
+
66
+ expect(result.tokenCanBeRenewed).toBe(true);
67
+ });
68
+
69
+ it("should not allow renewal when 14 or more days remain", () => {
70
+ const now = new Date("2026-03-10"); // 21 days left
71
+ const result = getShareTokenInfo(makeShareData(), now);
72
+
73
+ expect(result.tokenCanBeRenewed).toBe(false);
74
+ });
75
+
76
+ it("should not allow renewal for an expired token", () => {
77
+ const now = new Date("2026-04-15");
78
+ const result = getShareTokenInfo(makeShareData(), now);
79
+
80
+ expect(result.tokenCanBeRenewed).toBe(false);
81
+ });
82
+ });
83
+
84
+ describe("formatted expiration date", () => {
85
+ it("should format the expiration date", () => {
86
+ const result = getShareTokenInfo(makeShareData());
87
+
88
+ expect(result.tokenExpirationDate).toBe("March 31, 2026");
89
+ });
90
+ });
91
+ });
package/src/api/index.tsx CHANGED
@@ -22,6 +22,9 @@ import forms from "./forms";
22
22
  import folders from "./folders";
23
23
  import logs from "./logs";
24
24
  import schemas from "./schemas";
25
+ import shareToken from "./shareToken";
26
+
27
+ export type { IShareData } from "./shareToken";
25
28
 
26
29
  export {
27
30
  sites,
@@ -48,4 +51,5 @@ export {
48
51
  folders,
49
52
  logs,
50
53
  schemas,
54
+ shareToken,
51
55
  };
package/src/api/pages.tsx CHANGED
@@ -1,9 +1,11 @@
1
- import { AxiosResponse } from "axios";
1
+ import type { IGetPagesParams } from "@ax/types";
2
+
3
+ import type { AxiosResponse } from "axios";
4
+
2
5
  import { template } from "./config";
3
- import { IServiceConfig, sendRequest } from "./utils";
4
- import { IGetPagesParams } from "@ax/types";
6
+ import { type IServiceConfig, sendRequest } from "./utils";
5
7
 
6
- const PUBLIC_BASE_PATH = process.env.REACT_APP_PUBLIC_API_ENDPOINT;
8
+ const PUBLIC_BASE_PATH = process.env.GRIDDO_PUBLIC_API_URL || process.env.REACT_APP_PUBLIC_API_ENDPOINT;
7
9
 
8
10
  const SERVICES: { [key: string]: IServiceConfig } = {
9
11
  GET_PAGES: {
@@ -116,20 +118,21 @@ const getPages = async (params: IGetPagesParams, filterQuery = ""): Promise<Axio
116
118
  type,
117
119
  } = params;
118
120
 
119
- SERVICES.GET_PAGES.dynamicUrl = `${host}${endpoint}?deleted=${deleted}${filterQuery}`;
120
-
121
- if (page && itemsPerPage)
122
- SERVICES.GET_PAGES.dynamicUrl = SERVICES.GET_PAGES.dynamicUrl + `&page=${page}&itemsPerPage=${itemsPerPage}`;
123
- if (query && query.trim() !== "") SERVICES.GET_PAGES.dynamicUrl = SERVICES.GET_PAGES.dynamicUrl + `&query=${query}`;
124
- if (filterStructuredData)
125
- SERVICES.GET_PAGES.dynamicUrl = SERVICES.GET_PAGES.dynamicUrl + `&filterStructuredData=${filterStructuredData}`;
126
- if (format) SERVICES.GET_PAGES.dynamicUrl = SERVICES.GET_PAGES.dynamicUrl + `&format=${format}`;
127
- if (type) SERVICES.GET_PAGES.dynamicUrl = SERVICES.GET_PAGES.dynamicUrl + `&type=${type}`;
128
- if (filterPages)
129
- SERVICES.GET_PAGES.dynamicUrl = SERVICES.GET_PAGES.dynamicUrl + `&filterPages=${filterPages.join(",")}`;
130
- if (filterSites)
131
- SERVICES.GET_PAGES.dynamicUrl = SERVICES.GET_PAGES.dynamicUrl + `&filterSites=${filterSites.join(",")}`;
132
- if (ignoreLang) SERVICES.GET_PAGES.dynamicUrl = SERVICES.GET_PAGES.dynamicUrl + `&ignoreLang=${ignoreLang}`;
121
+ // init
122
+ let dynamicUrl = `${host}${endpoint}?deleted=${deleted}${filterQuery}`;
123
+
124
+ // concatenate...
125
+ if (page && itemsPerPage) dynamicUrl += `&page=${page}&itemsPerPage=${itemsPerPage}`;
126
+ if (query?.trim()) dynamicUrl += `&query=${encodeURIComponent(query)}`;
127
+ if (filterStructuredData) dynamicUrl += `&filterStructuredData=${filterStructuredData}`;
128
+ if (format) dynamicUrl += `&format=${format}`;
129
+ if (type) dynamicUrl += `&type=${type}`;
130
+ if (filterPages?.length) dynamicUrl += `&filterPages=${filterPages.join(",")}`;
131
+ if (filterSites?.length) dynamicUrl += `&filterSites=${filterSites.join(",")}`;
132
+ if (ignoreLang) dynamicUrl += `&ignoreLang=${ignoreLang}`;
133
+
134
+ // mutate
135
+ SERVICES.GET_PAGES.dynamicUrl = dynamicUrl;
133
136
 
134
137
  const dataHeader = {
135
138
  ...(lang && { lang }),
@@ -0,0 +1,62 @@
1
+ import type { AxiosResponse } from "axios";
2
+
3
+ import { template } from "./config";
4
+ import { type IServiceConfig, sendRequest } from "./utils";
5
+
6
+ export interface IShareData {
7
+ entity: string;
8
+ daysValid: number;
9
+ startDate: string;
10
+ endDate: string;
11
+ visitCount: number;
12
+ }
13
+
14
+ const SERVICES: { [key: string]: IServiceConfig } = {
15
+ CREATE_SHARE: {
16
+ ...template,
17
+ endpoint: ["/page/", "/share"],
18
+ method: "POST",
19
+ },
20
+ GET_SHARE: {
21
+ ...template,
22
+ endpoint: ["/page/", "/share"],
23
+ method: "GET",
24
+ },
25
+ RENEW_SHARE: {
26
+ ...template,
27
+ endpoint: ["/page/", "/share"],
28
+ method: "PUT",
29
+ },
30
+ };
31
+
32
+ const createShare = async (pageID: number): Promise<AxiosResponse> => {
33
+ const {
34
+ host,
35
+ endpoint: [prefix, suffix],
36
+ } = SERVICES.CREATE_SHARE;
37
+ SERVICES.CREATE_SHARE.dynamicUrl = `${host}${prefix}${pageID}${suffix}`;
38
+
39
+ return sendRequest(SERVICES.CREATE_SHARE);
40
+ };
41
+
42
+ const getShare = async (pageID: number): Promise<AxiosResponse> => {
43
+ const {
44
+ host,
45
+ endpoint: [prefix, suffix],
46
+ } = SERVICES.GET_SHARE;
47
+ SERVICES.GET_SHARE.dynamicUrl = `${host}${prefix}${pageID}${suffix}`;
48
+
49
+ return sendRequest(SERVICES.GET_SHARE);
50
+ };
51
+
52
+ const renewShare = async (pageID: number): Promise<AxiosResponse> => {
53
+ const {
54
+ host,
55
+ endpoint: [prefix, suffix],
56
+ } = SERVICES.RENEW_SHARE;
57
+ SERVICES.RENEW_SHARE.dynamicUrl = `${host}${prefix}${pageID}${suffix}`;
58
+
59
+ return sendRequest(SERVICES.RENEW_SHARE);
60
+ };
61
+
62
+ export default { createShare, getShare, renewShare };
package/src/api/utils.tsx CHANGED
@@ -36,17 +36,30 @@ const getSite = (): Record<string, unknown> => {
36
36
  };
37
37
 
38
38
  const getHeaders = (headers: Record<string, unknown>, hasToken: boolean) => {
39
+ // Check if we're in preview mode and add preview share headers
40
+ const previewPageId = sessionStorage.getItem("previewPageId");
41
+ const previewEntity = sessionStorage.getItem("previewEntity");
42
+ const previewHeaders =
43
+ previewPageId && previewEntity
44
+ ? {
45
+ "x-preview-share-page-id": previewPageId,
46
+ "x-preview-share-entity": previewEntity,
47
+ }
48
+ : {};
49
+
39
50
  return hasToken
40
51
  ? {
41
52
  ...headers,
42
53
  ...getToken(),
43
54
  ...getLang(),
44
55
  ...getSite(),
56
+ ...previewHeaders,
45
57
  }
46
58
  : {
47
59
  ...headers,
48
60
  ...getLang(),
49
61
  ...getSite(),
62
+ ...previewHeaders,
50
63
  };
51
64
  };
52
65
 
@@ -1,13 +1,17 @@
1
1
  import { useEffect, useState } from "react";
2
2
 
3
+ import type { IShareData } from "@ax/api";
4
+ import { shareToken as shareTokenApi } from "@ax/api";
5
+ import { PageInfoBanner } from "@ax/components";
3
6
  import { findByEditorID } from "@ax/forms";
4
- import { copyTextToClipboard } from "@ax/helpers";
5
- import { useOnMessageReceivedFromIframe, useToast } from "@ax/hooks";
7
+ import { DEV_NOW, getShareTokenInfo } from "@ax/helpers";
8
+ import { useModal, useOnMessageReceivedFromIframe, useToast } from "@ax/hooks";
6
9
 
10
+ import BrowserContent from "../BrowserContent";
7
11
  import Icon from "../Icon";
8
- import Tooltip from "../Tooltip";
12
+ import SharePageModal from "../SharePageModal";
9
13
  import Toast from "../Toast";
10
- import BrowserContent from "../BrowserContent";
14
+ import Tooltip from "../Tooltip";
11
15
 
12
16
  import * as S from "./style";
13
17
 
@@ -31,7 +35,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
31
35
  editorType = "page",
32
36
  } = props;
33
37
 
34
- const { id, entity } = content;
38
+ const { id, entity, haveDraftPage } = content;
35
39
  const domain = window.location.origin;
36
40
  const urlPreview = `${domain}/editor/page-preview?preview=${!!isPreview}&disabled=${!!disabled}&type=${editorType}`;
37
41
  const isPageEditor = editorType === "page";
@@ -39,6 +43,10 @@ const Browser = (props: IBrowserProps): JSX.Element => {
39
43
 
40
44
  const [resolution, setResolution] = useState("desktop");
41
45
  const { isVisible, toggleToast, setIsVisible, state: toastState } = useToast();
46
+ const { isOpen: isShareOpen, toggleModal: toggleSharePageModal } = useModal(false, true);
47
+ const [shareData, setShareData] = useState<IShareData | null>(null);
48
+
49
+ const tokenInfo = shareData ? getShareTokenInfo(shareData, DEV_NOW) : null;
42
50
 
43
51
  useOnMessageReceivedFromIframe(actions);
44
52
 
@@ -47,9 +55,22 @@ const Browser = (props: IBrowserProps): JSX.Element => {
47
55
  (window as any).browserRef = null;
48
56
  }, []);
49
57
 
58
+ // Fetch share data when in preview mode
59
+ useEffect(() => {
60
+ if (isPreview && id) {
61
+ const fetchShare = async () => {
62
+ const response = await shareTokenApi.getShare(id);
63
+ if (response.status === 200) {
64
+ setShareData(response.data);
65
+ }
66
+ };
67
+ fetchShare();
68
+ }
69
+ }, [isPreview, id]);
70
+
50
71
  const selectEditorID = (
51
72
  selectedComponent: { editorID: number; component: any; type: string; parentEditorID: number },
52
- parentComponent: string | undefined | null,
73
+ _parentComponent: string | undefined | null,
53
74
  e: React.SyntheticEvent,
54
75
  ) => {
55
76
  const { element } = findByEditorID(content, selectedComponent.parentEditorID);
@@ -64,29 +85,6 @@ const Browser = (props: IBrowserProps): JSX.Element => {
64
85
  }
65
86
  };
66
87
 
67
- const getWidth = (res: string) => {
68
- switch (res) {
69
- case "tablet":
70
- return "768px";
71
- case "phone":
72
- return "425px";
73
- default:
74
- return "100%";
75
- }
76
- };
77
-
78
- const copyUrl = () => {
79
- const sharedUrl = `${domain}/page-preview/${id}/${entity}`;
80
- copyTextToClipboard(sharedUrl).then(
81
- () => {
82
- toggleToast("URL Copied");
83
- },
84
- (err) => {
85
- console.error("Could not copy text: ", err);
86
- },
87
- );
88
- };
89
-
90
88
  const deleteModuleSelected = (editorID: number) => {
91
89
  actions?.setSelectedContentAction(0);
92
90
  actions?.deleteModuleAction?.([editorID]);
@@ -115,8 +113,18 @@ const Browser = (props: IBrowserProps): JSX.Element => {
115
113
  <S.NavUrl>{url}</S.NavUrl>
116
114
  {isPreview && (
117
115
  <S.NavActions data-testid="nav-actions-wrapper">
118
- <S.IconWrapper data-testid="icon-wrapper-browser" onClick={copyUrl}>
119
- <Tooltip content="Copy url to share draft" bottom>
116
+ <S.IconWrapper
117
+ data-testid="icon-wrapper-browser"
118
+ onClick={haveDraftPage ? undefined : toggleSharePageModal}
119
+ active={!haveDraftPage}
120
+ >
121
+ <Tooltip
122
+ hideOnClick={!haveDraftPage}
123
+ content={
124
+ haveDraftPage ? "Only available for drafts. This page already has a public URL." : "Share draft"
125
+ }
126
+ bottom
127
+ >
120
128
  <Icon name="share" size="24" />
121
129
  </Tooltip>
122
130
  </S.IconWrapper>
@@ -147,6 +155,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
147
155
  )}
148
156
  </S.NavBar>
149
157
  )}
158
+
150
159
  {showIframe ? (
151
160
  <S.FrameWrapper hasBorder={isPageEditor} isFormEditor={isFormEditor} data-testid="navbar-iframe-wrapper">
152
161
  <iframe
@@ -161,6 +170,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
161
170
  ) : (
162
171
  <S.Wrapper
163
172
  data-testid="browser-content-wrapper"
173
+ // biome-ignore lint/suspicious/noAssignInExpressions: TODO: fix this
164
174
  ref={(ref: any) => ((window as any).browserRef = ref)}
165
175
  className="browser-content"
166
176
  >
@@ -182,11 +192,47 @@ const Browser = (props: IBrowserProps): JSX.Element => {
182
192
  />
183
193
  </S.Wrapper>
184
194
  )}
195
+
185
196
  {isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
197
+
198
+ {isPreview && shareData && tokenInfo && (
199
+ <PageInfoBanner
200
+ message={
201
+ tokenInfo.tokenHasExpired
202
+ ? `Access to this draft link expired on ${tokenInfo.tokenExpirationDate}. You need to generate a`
203
+ : `This draft link expires on ${tokenInfo.tokenExpirationDate}. ${tokenInfo.tokenCanBeRenewed ? "Renew it" : "Open details"}`
204
+ }
205
+ actionLabel={tokenInfo.tokenHasExpired ? "new one" : "here"}
206
+ icon={tokenInfo.tokenHasExpired ? "hide" : "view"}
207
+ onAction={toggleSharePageModal}
208
+ />
209
+ )}
210
+
211
+ {isPreview && (
212
+ <SharePageModal
213
+ pageTitle={content.title}
214
+ isOpen={isShareOpen}
215
+ hide={toggleSharePageModal}
216
+ pageID={id}
217
+ entity={entity}
218
+ shareData={shareData}
219
+ onShareChange={setShareData}
220
+ />
221
+ )}
186
222
  </S.BrowserWrapper>
187
223
  );
188
224
  };
189
225
 
226
+ function getWidth(res: string) {
227
+ switch (res) {
228
+ case "tablet":
229
+ return "768px";
230
+ case "phone":
231
+ return "425px";
232
+ default:
233
+ return "100%";
234
+ }
235
+ }
190
236
  export interface IBrowserProps {
191
237
  content: any;
192
238
  header?: any;
@@ -1,4 +1,4 @@
1
- import styled from "styled-components";
1
+ import styled, { css } from "styled-components";
2
2
 
3
3
  const BrowserWrapper = styled.div`
4
4
  background-color: ${(p) => p.theme.color.uiBackground01};
@@ -31,14 +31,21 @@ const NavActions = styled.div`
31
31
 
32
32
  const IconWrapper = styled.div<{ active?: boolean }>`
33
33
  margin-left: ${(p) => p.theme.spacing.m};
34
- cursor: pointer;
35
- :hover {
36
- svg {
37
- path {
38
- fill: ${(p) => p.theme.color.interactive01};
34
+
35
+ cursor: ${(p) => (p.active === false ? "default" : "pointer")};
36
+
37
+ ${(p) =>
38
+ p.active !== false &&
39
+ css`
40
+ :hover {
41
+ svg {
42
+ path {
43
+ fill: ${p.theme.color.interactive01};
44
+ }
39
45
  }
40
46
  }
41
- }
47
+ `}
48
+
42
49
  svg {
43
50
  path {
44
51
  fill: ${(p) => (p.active ? p.theme.color.interactive01 : p.theme.color.iconNonActive)};
@@ -70,4 +77,4 @@ const Wrapper = styled.div`
70
77
  }
71
78
  `;
72
79
 
73
- export { BrowserWrapper, NavBar, NavUrl, NavActions, IconWrapper, FrameWrapper, Wrapper };
80
+ export { BrowserWrapper, FrameWrapper, IconWrapper, NavActions, NavBar, NavUrl, Wrapper };
@@ -1,9 +1,13 @@
1
- import { useCallback, useEffect } from "react";
2
- import * as components from "components";
3
- import { builderSSR, ssrHelpers, SiteProvider } from "components";
4
- import { type Core, Preview } from "@griddo/core";
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ import { PageInfoBanner } from "@ax/components";
4
+ import { formatDate } from "@ax/helpers";
5
5
  import type { ILanguage, ISocialState } from "@ax/types";
6
6
 
7
+ import { type Core, Preview } from "@griddo/core";
8
+ import * as components from "components";
9
+ import { builderSSR, SiteProvider, ssrHelpers } from "components";
10
+
7
11
  const BrowserContent = (props: IProps) => {
8
12
  const {
9
13
  content,
@@ -21,6 +25,7 @@ const BrowserContent = (props: IProps) => {
21
25
  moduleActions,
22
26
  renderer,
23
27
  selectHoverEditorID,
28
+ shareEndDate,
24
29
  } = props;
25
30
 
26
31
  const API_URL = process.env.GRIDDO_API_URL || process.env.REACT_APP_API_ENDPOINT;
@@ -33,6 +38,8 @@ const BrowserContent = (props: IProps) => {
33
38
  }
34
39
  }, []);
35
40
 
41
+ const [showBanner, setShowBanner] = useState(true);
42
+
36
43
  // biome-ignore lint/correctness/useExhaustiveDependencies: fix
37
44
  useEffect(useInstanceExternalAssets, [useInstanceExternalAssets]);
38
45
 
@@ -51,6 +58,13 @@ const BrowserContent = (props: IProps) => {
51
58
  moduleActions={moduleActions}
52
59
  selectHoverEditorID={selectHoverEditorID}
53
60
  >
61
+ {renderer === "sharedPage" && showBanner && (
62
+ <PageInfoBanner
63
+ message={`This draft link expires on ${shareEndDate ? formatDate(new Date(shareEndDate)) : "..."}`}
64
+ icon="scheduled"
65
+ onDismiss={() => setShowBanner(false)}
66
+ />
67
+ )}
54
68
  <Preview
55
69
  isPage={isPage}
56
70
  apiUrl={API_URL}
@@ -77,7 +91,7 @@ interface IProps {
77
91
  footer?: any;
78
92
  languageID: number;
79
93
  pageLanguages: Core.Page["pageLanguages"];
80
- renderer: "editor" | "preview" | "forms";
94
+ renderer: "editor" | "preview" | "forms" | "sharedPage";
81
95
  selectEditorID?(
82
96
  selectedComponent: { editorID: number; component: any; type: string; parentEditorID: number },
83
97
  parentComponent: string | undefined | null,
@@ -89,6 +103,7 @@ interface IProps {
89
103
  duplicateModuleAction?(editorID: number): void;
90
104
  copyModuleAction?(editorID: number): void;
91
105
  };
106
+ shareEndDate?: string | null;
92
107
  }
93
108
 
94
109
  export default BrowserContent;