@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 +0 -5
- package/package.json +2 -2
- package/src/__tests__/components/Browser/Browser.test.tsx +1 -87
- package/src/__tests__/helpers/shareToken.test.ts +91 -0
- package/src/api/index.tsx +4 -0
- package/src/api/pages.tsx +21 -18
- package/src/api/shareToken.tsx +62 -0
- package/src/api/utils.tsx +13 -0
- package/src/components/Browser/index.tsx +77 -31
- package/src/components/Browser/style.tsx +15 -8
- package/src/components/BrowserContent/index.tsx +20 -5
- package/src/components/ErrorPage/index.tsx +28 -14
- package/src/components/Fields/TextField/index.tsx +18 -4
- package/src/components/Icon/components/GriddoLogo.js +16 -0
- package/src/components/Icon/index.tsx +1 -0
- package/src/components/Icon/svgs/GriddoLogo.svg +9 -0
- package/src/components/Modal/index.tsx +3 -1
- package/src/components/Modal/style.tsx +14 -8
- package/src/components/PageInfoBanner/index.tsx +38 -0
- package/src/components/PageInfoBanner/styles.tsx +40 -0
- package/src/components/SharePageModal/index.tsx +187 -0
- package/src/components/SharePageModal/style.tsx +41 -0
- package/src/components/StatusTile/index.tsx +28 -0
- package/src/components/StatusTile/styles.ts +63 -0
- package/src/components/index.tsx +103 -102
- package/src/global.d.ts +8 -0
- package/src/helpers/dates.tsx +14 -8
- package/src/helpers/index.tsx +5 -0
- package/src/helpers/shareToken.ts +25 -0
- package/src/modules/PublicPreview/index.tsx +35 -13
- package/src/routes/publicRoutes.tsx +2 -2
- package/src/themes/theme.json +4 -0
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.
|
|
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": "
|
|
220
|
+
"gitHead": "8b7d5ecf70920b98364c6207ff6079a7af98b430"
|
|
221
221
|
}
|
|
@@ -56,93 +56,7 @@ describe("Browser component rendering", () => {
|
|
|
56
56
|
expect(browserContentWrapper).not.toBeTruthy();
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
|
|
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 {
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
if (format)
|
|
127
|
-
if (type)
|
|
128
|
-
if (filterPages)
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
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 {
|
|
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
|
|
12
|
+
import SharePageModal from "../SharePageModal";
|
|
9
13
|
import Toast from "../Toast";
|
|
10
|
-
import
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
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
|
-
|
|
35
|
-
:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
80
|
+
export { BrowserWrapper, FrameWrapper, IconWrapper, NavActions, NavBar, NavUrl, Wrapper };
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { useCallback, useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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;
|