From 864d4effb8b01b4f834798c463ac6f6b167eb73a Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Thu, 16 May 2024 20:46:03 +0100 Subject: [PATCH 01/24] feat: add setting for favicon --- .../LandingNodeDefaultRepository.ts | 1 + src/domain/entities/LandingNode.ts | 2 ++ .../usecases/__tests__/landingNodeFixtures.ts | 9 ++++++++ .../LandingPageEditDialog.tsx | 21 ++++++++++++++++--- src/webapp/pages/home/HomePage.tsx | 4 +++- 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/data/repositories/LandingNodeDefaultRepository.ts b/src/data/repositories/LandingNodeDefaultRepository.ts index 0efc6db..ddc231d 100644 --- a/src/data/repositories/LandingNodeDefaultRepository.ts +++ b/src/data/repositories/LandingNodeDefaultRepository.ts @@ -46,6 +46,7 @@ export class LandingNodeDefaultRepository implements LandingNodeRepository { type: "root" as const, icon: "img/logo-eyeseetea.png", iconLocation: "top", + favicon: "img/logo-eyeseetea.png", pageRendering: "multiple", order: undefined, name: { diff --git a/src/domain/entities/LandingNode.ts b/src/domain/entities/LandingNode.ts index ce53c3e..99002ef 100644 --- a/src/domain/entities/LandingNode.ts +++ b/src/domain/entities/LandingNode.ts @@ -24,6 +24,7 @@ export interface LandingNode { type: LandingNodeType; icon: string; iconLocation: string; + favicon: string; pageRendering: LandingNodePageRendering | undefined; order: number | undefined; name: TranslatableText; @@ -42,6 +43,7 @@ export const LandingNodeModel: Codec = Schema.object({ type: LandingPageNodeTypeModel, icon: Schema.optionalSafe(Schema.string, ""), iconLocation: Schema.optionalSafe(Schema.string, ""), + favicon: Schema.optionalSafe(Schema.string, ""), pageRendering: Schema.optional(LandingPageNodePageRenderingModel), order: Schema.optional(Schema.integer), name: TranslatableTextModel, diff --git a/src/domain/usecases/__tests__/landingNodeFixtures.ts b/src/domain/usecases/__tests__/landingNodeFixtures.ts index 720b07a..d849685 100644 --- a/src/domain/usecases/__tests__/landingNodeFixtures.ts +++ b/src/domain/usecases/__tests__/landingNodeFixtures.ts @@ -32,6 +32,7 @@ export const sectionNode: LandingNode = { executeOnInit: true, icon: "", iconLocation: "", + favicon: "", id: "XusobLebMel", name: { key: "XusobLebMel-name", @@ -55,6 +56,7 @@ export const sectionNode: LandingNode = { executeOnInit: true, icon: "", iconLocation: "", + favicon: "", id: "ycAtL2slUDJ", name: { key: "ycAtL2slUDJ-name", @@ -90,6 +92,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "img/logo-eyeseetea.png", iconLocation: "top", + favicon: "img/logo-eyeseetea.png", id: "pAfyLmQmCU6", name: { key: "pAfyLmQmCU6-name", @@ -118,6 +121,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + favicon: "", id: "XusobLebMel", name: { key: "XusobLebMel-name", @@ -141,6 +145,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + favicon: "", id: "ycAtL2slUDJ", name: { key: "ycAtL2slUDJ-name", @@ -173,6 +178,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + favicon: "", id: "SEuEePxkvE7", name: { key: "SEuEePxkvE7-name", @@ -201,6 +207,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + favicon: "", id: "MmG1BCLXE1Q", name: { key: "MmG1BCLXE1Q-name", @@ -229,6 +236,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + favicon: "", id: "Jqdtr2lTgNP", name: { key: "Jqdtr2lTgNP-name", @@ -257,6 +265,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + favicon: "", id: "jFYOc9eUhVd", name: { key: "jFYOc9eUhVd-name", diff --git a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx index 132d20e..5dfcba8 100644 --- a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx +++ b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx @@ -29,6 +29,7 @@ const buildDefaultNode = ( parent, icon: "", iconLocation: "", + favicon: "", pageRendering, order, name: { key: "", referenceValue: "", translations: {} }, @@ -111,11 +112,11 @@ export const LandingPageEditDialog: React.FC = props }; const handleFileUpload = useCallback( - (event: ChangeEvent) => { + (event: ChangeEvent, fileType: keyof LandingNode) => { const file = event.target.files ? event.target.files[0] : undefined; file?.arrayBuffer().then(async data => { const icon = await compositionRoot.instance.uploadFile(data, file.name); - setValue(node => ({ ...node, icon })); + setValue(node => ({ ...node, [fileType]: icon })); }); }, [compositionRoot] @@ -186,7 +187,7 @@ export const LandingPageEditDialog: React.FC = props ) : null} - + handleFileUpload(event, "icon")} />
@@ -204,6 +205,20 @@ export const LandingPageEditDialog: React.FC = props
+ +

{i18n.t("Favicon")}

+ + + {value.favicon ? ( + + {`Page + + ) : null} + + handleFileUpload(event, "favicon")} /> + +
+ {type === "root" && (

{i18n.t("Style")}

diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index 6d150b1..b2327d5 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -102,7 +102,9 @@ export const HomePage: React.FC = React.memo(() => { useEffect(() => { const icon = favicon.current; - icon?.setAttribute("href", (pageType === "singleLanding" && currentPage?.icon) || defaultIcon); + const pageFavicon = currentPage?.favicon || currentPage?.icon; + + icon?.setAttribute("href", (pageType === "singleLanding" && pageFavicon) || defaultIcon); document.title = (pageType === "singleLanding" && currentPage && translate(currentPage.name)) || defaultTitle; return () => { icon?.setAttribute("href", defaultIcon); From c4da6064f310f9c716cb2705e3b1d013674bb1da Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 20 May 2024 11:16:42 +0100 Subject: [PATCH 02/24] feat: add warnings for favicon --- .../LandingPageEditDialog.tsx | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx index 5dfcba8..9d967f9 100644 --- a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx +++ b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx @@ -52,8 +52,9 @@ export const LandingPageEditDialog: React.FC = props const [value, setValue] = useState( initialNode ?? buildDefaultNode(type, parent, order, "multiple", true) ); - const [iconLocation, setIconLocation] = React.useState(value.iconLocation === "bottom"); - const [pageRendering, setPageRendering] = React.useState(value.pageRendering === "single"); + const [iconLocation, setIconLocation] = useState(value.iconLocation === "bottom"); + const [pageRendering, setPageRendering] = useState(value.pageRendering === "single"); + const [warnings, setWarnings] = useState([]); const items = useMemo( () => @@ -114,7 +115,36 @@ export const LandingPageEditDialog: React.FC = props const handleFileUpload = useCallback( (event: ChangeEvent, fileType: keyof LandingNode) => { const file = event.target.files ? event.target.files[0] : undefined; + file?.arrayBuffer().then(async data => { + const reader = new FileReader(); + reader.onload = e => { + const img = new Image(); + img.onload = () => { + const width = img.width; + const height = img.height; + const aspectRatio = width / height; + + setWarnings([]); + const newWarnings: string[] = []; + + if (fileType === "favicon") { + if (aspectRatio !== FAVICON_ASPECT_RATIO) { + newWarnings.push("Please ensure that your favicon has a 1:1 aspect ratio."); + } + + if (width > FAVICON_MAX_SIZE || height > FAVICON_MAX_SIZE) { + newWarnings.push("Please use an icon of 128x128 pixels or smaller."); + } + + newWarnings.length !== 0 && setWarnings(newWarnings); + } + }; + if (e.target?.result) { + img.src = e.target.result as string; + } + }; + reader.readAsDataURL(file); const icon = await compositionRoot.instance.uploadFile(data, file.name); setValue(node => ({ ...node, [fileType]: icon })); }); @@ -205,19 +235,29 @@ export const LandingPageEditDialog: React.FC = props
- -

{i18n.t("Favicon")}

- - - {value.favicon ? ( - - {`Page - - ) : null} - - handleFileUpload(event, "favicon")} /> - -
+ {type === "root" && ( + +

{i18n.t("Favicon")}

+ + + {value.favicon ? ( + + {`Page + + ) : null} + + handleFileUpload(event, "favicon")} /> + + + {warnings.length > 0 && ( + + {warnings.map(warning => ( +

{warning}

+ ))} +
+ )} +
+ )} {type === "root" && ( @@ -293,6 +333,9 @@ export const LandingPageEditDialog: React.FC = props ); }; +const FAVICON_ASPECT_RATIO = 1; +const FAVICON_MAX_SIZE = 128; + export interface LandingPageEditDialogProps extends Omit { initialNode?: LandingNode; type: LandingNodeType; @@ -349,6 +392,13 @@ const StyledLandingBody = styled(LandingBody)` max-width: 600px; `; +const WarningText = styled.p` + font-size: 12px; + line-height: 0.1; + font-style: italic; + color: red; +`; + const StepPreview: React.FC<{ className?: string; value?: string; From 63cb05e4c83f49935abc9ca8db498895a83921dd Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 20 May 2024 16:03:11 +0100 Subject: [PATCH 03/24] feat: add icon size settings --- .../LandingNodeDefaultRepository.ts | 1 + src/domain/entities/LandingNode.ts | 2 ++ .../usecases/__tests__/landingNodeFixtures.ts | 9 ++++++ .../AdditionalComponents.tsx | 1 + src/webapp/components/card-board/BigCard.tsx | 25 +++++----------- .../components/card-board/BigCardIcon.tsx | 23 ++++++++++++++ src/webapp/components/item-root/ItemRoot.tsx | 1 + .../LandingPageEditDialog.tsx | 30 +++++++++++++++++-- 8 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 src/webapp/components/card-board/BigCardIcon.tsx diff --git a/src/data/repositories/LandingNodeDefaultRepository.ts b/src/data/repositories/LandingNodeDefaultRepository.ts index ddc231d..7ce7afc 100644 --- a/src/data/repositories/LandingNodeDefaultRepository.ts +++ b/src/data/repositories/LandingNodeDefaultRepository.ts @@ -46,6 +46,7 @@ export class LandingNodeDefaultRepository implements LandingNodeRepository { type: "root" as const, icon: "img/logo-eyeseetea.png", iconLocation: "top", + iconSize: "small", favicon: "img/logo-eyeseetea.png", pageRendering: "multiple", order: undefined, diff --git a/src/domain/entities/LandingNode.ts b/src/domain/entities/LandingNode.ts index 99002ef..3895553 100644 --- a/src/domain/entities/LandingNode.ts +++ b/src/domain/entities/LandingNode.ts @@ -24,6 +24,7 @@ export interface LandingNode { type: LandingNodeType; icon: string; iconLocation: string; + iconSize: string; favicon: string; pageRendering: LandingNodePageRendering | undefined; order: number | undefined; @@ -43,6 +44,7 @@ export const LandingNodeModel: Codec = Schema.object({ type: LandingPageNodeTypeModel, icon: Schema.optionalSafe(Schema.string, ""), iconLocation: Schema.optionalSafe(Schema.string, ""), + iconSize: Schema.optionalSafe(Schema.string, ""), favicon: Schema.optionalSafe(Schema.string, ""), pageRendering: Schema.optional(LandingPageNodePageRenderingModel), order: Schema.optional(Schema.integer), diff --git a/src/domain/usecases/__tests__/landingNodeFixtures.ts b/src/domain/usecases/__tests__/landingNodeFixtures.ts index d849685..38939f3 100644 --- a/src/domain/usecases/__tests__/landingNodeFixtures.ts +++ b/src/domain/usecases/__tests__/landingNodeFixtures.ts @@ -32,6 +32,7 @@ export const sectionNode: LandingNode = { executeOnInit: true, icon: "", iconLocation: "", + iconSize: "", favicon: "", id: "XusobLebMel", name: { @@ -56,6 +57,7 @@ export const sectionNode: LandingNode = { executeOnInit: true, icon: "", iconLocation: "", + iconSize: "", favicon: "", id: "ycAtL2slUDJ", name: { @@ -92,6 +94,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "img/logo-eyeseetea.png", iconLocation: "top", + iconSize: "small", favicon: "img/logo-eyeseetea.png", id: "pAfyLmQmCU6", name: { @@ -121,6 +124,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + iconSize: "", favicon: "", id: "XusobLebMel", name: { @@ -145,6 +149,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + iconSize: "", favicon: "", id: "ycAtL2slUDJ", name: { @@ -178,6 +183,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + iconSize: "", favicon: "", id: "SEuEePxkvE7", name: { @@ -207,6 +213,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + iconSize: "", favicon: "", id: "MmG1BCLXE1Q", name: { @@ -236,6 +243,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + iconSize: "", favicon: "", id: "Jqdtr2lTgNP", name: { @@ -265,6 +273,7 @@ export const validLandingPagesTree: PersistedLandingPage[] = [ executeOnInit: true, icon: "", iconLocation: "", + iconSize: "", favicon: "", id: "jFYOc9eUhVd", name: { diff --git a/src/webapp/components/additional-components/AdditionalComponents.tsx b/src/webapp/components/additional-components/AdditionalComponents.tsx index 806fe1e..151b3f4 100644 --- a/src/webapp/components/additional-components/AdditionalComponents.tsx +++ b/src/webapp/components/additional-components/AdditionalComponents.tsx @@ -97,6 +97,7 @@ export const AdditionalComponents: React.FC<{ disabled={action?.disabled} icon={action?.icon ? {`Icon : undefined} iconLocation={action?.iconLocation} + iconSize={currentPage.iconSize} description={description} backgroundColor={action?.backgroundColor} fontColor={action?.fontColor} diff --git a/src/webapp/components/card-board/BigCard.tsx b/src/webapp/components/card-board/BigCard.tsx index 6f208ab..5927679 100644 --- a/src/webapp/components/card-board/BigCard.tsx +++ b/src/webapp/components/card-board/BigCard.tsx @@ -2,6 +2,7 @@ import React, { ReactNode } from "react"; import styled from "styled-components"; import { CardTitleIcon } from "./CardTitleIcon"; import { CardProgress, CardProgressText } from "./CardProgress"; +import { BigCardIcon } from "./BigCardIcon"; const BaseCard: React.FC = ({ className, @@ -9,6 +10,7 @@ const BaseCard: React.FC = ({ description, icon, iconLocation, + iconSize, backgroundColor, fontColor, textAlignment, @@ -31,9 +33,11 @@ const BaseCard: React.FC = ({ onContextMenu={onContextMenu} > {progress && progress >= 100 ? done : null} - {icon && iconLocation === "top" ? {icon} : null} + {icon && iconLocation === "top" ? {icon} : null} {label} - {icon && (!iconLocation || iconLocation === "bottom") ? {icon} : null} + {icon && (!iconLocation || iconLocation === "bottom") ? ( + {icon} + ) : null} {description ?

{description}

: null} {progress !== undefined ? {`${normalizedProgress}%`} : null} {progress !== undefined ? : null} @@ -47,6 +51,7 @@ export const BigCard = styled(BaseCard)` text-align: left; margin: 10px 10px 10px; user-select: none; + height: 305px; cursor: ${({ onClick, disabled }) => (onClick && !disabled ? "pointer" : "inherit")}; `; @@ -65,6 +70,7 @@ export interface BigCardProps { description?: string; icon?: ReactNode; iconLocation?: string; + iconSize?: string; backgroundColor?: string; fontColor?: string; textAlignment?: any; @@ -77,18 +83,3 @@ const BigCardTitle = styled.span` font-weight: 700; display: block; `; - -const BigCardIcon = styled.span` - display: flex; - place-content: center; - margin: 20px 0px; - - img, - svg { - height: 72px; - max-height: 10vw; - // max-width: 18vh; - margin: 0; - user-drag: none; - } -`; diff --git a/src/webapp/components/card-board/BigCardIcon.tsx b/src/webapp/components/card-board/BigCardIcon.tsx new file mode 100644 index 0000000..c353b83 --- /dev/null +++ b/src/webapp/components/card-board/BigCardIcon.tsx @@ -0,0 +1,23 @@ +import styled from "styled-components"; + +type BigCardIconProps = { + iconSize: string | undefined; +}; + +export const BigCardIcon = styled.span` + display: flex; + place-content: center; + margin: 20px 0px; + img, + svg { + height: ${props => (props.iconSize ? iconSizeValues[props.iconSize] : iconSizeValues.small)}; + margin: 0; + user-drag: none; + } +`; + +const iconSizeValues: Record = { + small: "72px", + medium: "96px", + large: "128px", +}; diff --git a/src/webapp/components/item-root/ItemRoot.tsx b/src/webapp/components/item-root/ItemRoot.tsx index 71e9522..26c594d 100644 --- a/src/webapp/components/item-root/ItemRoot.tsx +++ b/src/webapp/components/item-root/ItemRoot.tsx @@ -51,6 +51,7 @@ export const ItemRoot: React.FC<{ {`Icon ) : undefined } + iconSize={item.iconSize} /> ))} diff --git a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx index 9d967f9..4055040 100644 --- a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx +++ b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx @@ -4,7 +4,7 @@ import { MultipleDropdown, useSnackbar, } from "@eyeseetea/d2-ui-components"; -import { Switch, TextField } from "@material-ui/core"; +import { Button, Switch, TextField } from "@material-ui/core"; import React, { ChangeEvent, useCallback, useMemo, useState } from "react"; import styled from "styled-components"; import { generateUid } from "../../../data/utils/uid"; @@ -29,6 +29,7 @@ const buildDefaultNode = ( parent, icon: "", iconLocation: "", + iconSize: "", favicon: "", pageRendering, order, @@ -233,6 +234,23 @@ export const LandingPageEditDialog: React.FC = props

{i18n.t("Bottom")}

+ + + + {["small", "medium", "large"].map((size, i) => ( + + ))} +
{type === "root" && ( @@ -252,7 +270,7 @@ export const LandingPageEditDialog: React.FC = props {warnings.length > 0 && ( {warnings.map(warning => ( -

{warning}

+

{i18n.t(warning)}

))}
)} @@ -399,6 +417,14 @@ const WarningText = styled.p` color: red; `; +const OptionContainer = styled.div` + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + width: 50%; + gap: 10px; + margin: 20px 0; +`; + const StepPreview: React.FC<{ className?: string; value?: string; From 56b18ec4527df81ac6e8c0f7635a7f791a4f6ac6 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Wed, 22 May 2024 15:22:33 +0100 Subject: [PATCH 04/24] fix: back navigation to dashboard from sub actionv --- src/webapp/pages/home/HomePage.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index b2327d5..bd394d7 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -18,6 +18,7 @@ import { goTo } from "../../utils/routes"; import { defaultIcon, defaultTitle } from "../../router/Router"; import { useAnalytics } from "../../hooks/useAnalytics"; import { Maybe } from "../../../types/utils"; +import _ from "lodash"; export const HomePage: React.FC = React.memo(() => { const { hasSettingsAccess, landings, reload, isLoading, launchAppBaseUrl, translate, compositionRoot } = @@ -66,9 +67,17 @@ export const HomePage: React.FC = React.memo(() => { ); const goBack = useCallback(() => { - if (initLandings?.length === 1 || currentPage?.type !== "root") updateHistory(history => history.slice(1)); - else setPageType("userLandings"); - }, [currentPage, initLandings]); + if (currentPage?.type === "root" && _.every(history, landing => landing.id === currentPage.id)) { + updateHistory([]); + setPageType("userLandings"); + } else if ( + initLandings?.length === 1 || + currentPage?.type !== "root" || + (currentPage?.type === "root" && !_.isEmpty(history)) + ) { + updateHistory(history => history.slice(1)); + } else setPageType("userLandings"); + }, [currentPage, history, initLandings?.length]); const goHome = useCallback(() => { if (initLandings?.length === 1) updateHistory([]); From ab4c7a5f705a58aebf96a0060b88dd4f4687ecd7 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Wed, 22 May 2024 15:24:02 +0100 Subject: [PATCH 05/24] feat: update translation files --- i18n/en.pot | 10 ++++++++-- i18n/es.po | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 8ae5fb8..36a0464 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-04-23T08:25:26.968Z\n" -"PO-Revision-Date: 2024-04-23T08:25:26.968Z\n" +"POT-Creation-Date: 2024-05-22T14:23:25.170Z\n" +"PO-Revision-Date: 2024-05-22T14:23:25.170Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -259,6 +259,12 @@ msgstr "" msgid "Bottom" msgstr "" +msgid "Icon Size" +msgstr "" + +msgid "Favicon" +msgstr "" + msgid "Page Rendering" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index fa2d171..8960134 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2024-04-23T08:25:26.968Z\n" +"POT-Creation-Date: 2024-05-22T14:23:25.170Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -259,6 +259,12 @@ msgstr "" msgid "Bottom" msgstr "" +msgid "Icon Size" +msgstr "" + +msgid "Favicon" +msgstr "" + msgid "Page Rendering" msgstr "" From ed43ae47236d0445c525715ac0c8f4b199e08371 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Thu, 23 May 2024 10:05:10 +0100 Subject: [PATCH 06/24] fix: use max height for cards --- src/webapp/components/card-board/BigCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webapp/components/card-board/BigCard.tsx b/src/webapp/components/card-board/BigCard.tsx index 5927679..7436c85 100644 --- a/src/webapp/components/card-board/BigCard.tsx +++ b/src/webapp/components/card-board/BigCard.tsx @@ -51,7 +51,7 @@ export const BigCard = styled(BaseCard)` text-align: left; margin: 10px 10px 10px; user-select: none; - height: 305px; + max-height: 305px; cursor: ${({ onClick, disabled }) => (onClick && !disabled ? "pointer" : "inherit")}; `; From 5a06446a2e5622618b735d748dca5d941880e438 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Thu, 23 May 2024 16:30:03 +0100 Subject: [PATCH 07/24] fix: users see actions they do not have permissions to --- src/domain/entities/Action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/entities/Action.ts b/src/domain/entities/Action.ts index 4c20742..ab648be 100644 --- a/src/domain/entities/Action.ts +++ b/src/domain/entities/Action.ts @@ -118,7 +118,7 @@ export const getPageActions = ( const hasUserAccess = actionUsers.includes(user.id); const hasUserGroupAccess = _.intersection(actionUserGroups, userGroupIds).length > 0; - const hasPublicAccess = action.publicAccess !== "--------"; + const hasPublicAccess = action.publicAccess && action.publicAccess !== "--------"; return hasUserAccess || hasUserGroupAccess || hasPublicAccess; }) From 35b6dcd18599b771ed2a1705db0415192a3885e9 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 27 May 2024 09:00:07 +0100 Subject: [PATCH 08/24] feat: spread favicon to children nodes --- src/domain/entities/LandingNode.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/domain/entities/LandingNode.ts b/src/domain/entities/LandingNode.ts index 9bf7945..9967c33 100644 --- a/src/domain/entities/LandingNode.ts +++ b/src/domain/entities/LandingNode.ts @@ -75,7 +75,7 @@ export const updateLandingNodes = ( permissions: LandingPagePermission[], user: User ): LandingNode[] => { - return _(nodes) + const updatedNodes = _(nodes) .map(node => { const pagePermission = permissions?.find(permission => permission.id === node.id); @@ -96,6 +96,25 @@ export const updateLandingNodes = ( }) .compact() .value(); + + return updatedNodes.map(node => applyFavicon(node)); +}; + +const applyFavicon = (parent: LandingNode): LandingNode => { + const spreadFaviconToChildren = (children: LandingNode[], favicon: string): LandingNode[] => { + return _.map(children, child => { + const updatedChild: LandingNode = { ...child, favicon }; + if (child.children) { + updatedChild.children = spreadFaviconToChildren(child.children, favicon); + } + return updatedChild; + }); + }; + + return { + ...parent, + children: parent.children ? spreadFaviconToChildren(parent.children, parent.favicon) : [], + }; }; /* Return a redirect URL if there is only one visible action on primary nodes */ From 5827b38848214622e4a2be6e5ef459ee1362220b Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 27 May 2024 10:03:48 +0100 Subject: [PATCH 09/24] feat: add iconSize feature to available landing pages cards --- src/webapp/pages/home/HomePage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index 920175d..e3ddc38 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -176,6 +176,7 @@ export const HomePage: React.FC = React.memo(() => { {`Icon ) : undefined } + iconSize={landing.iconSize} /> ); })} From 204d82618e131b5ace2c111d11b610ab6971f73c Mon Sep 17 00:00:00 2001 From: p3rcypj Date: Mon, 27 May 2024 11:25:56 +0000 Subject: [PATCH 10/24] Remove Icon as fallback --- src/webapp/pages/home/HomePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index e3ddc38..79a441c 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -111,7 +111,7 @@ export const HomePage: React.FC = React.memo(() => { useEffect(() => { const icon = favicon.current; - const pageFavicon = currentPage?.favicon || currentPage?.icon; + const pageFavicon = currentPage?.favicon; icon?.setAttribute("href", (pageType === "singleLanding" && pageFavicon) || defaultIcon); document.title = (pageType === "singleLanding" && currentPage && translate(currentPage.name)) || defaultTitle; From a82d9b60948882e38a8ae025b126f813919d3717 Mon Sep 17 00:00:00 2001 From: Adrian Quintana Date: Mon, 27 May 2024 12:47:00 +0100 Subject: [PATCH 11/24] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f835d7a..776811b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "home-page", "description": "Home Page App", - "version": "1.7.1", + "version": "1.8.0", "license": "GPL-3.0", "author": "EyeSeeTea team", "homepage": ".", From 865fc9e64e64584e5fbd44d8021e1b6e4c73068f Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 27 May 2024 15:19:52 +0100 Subject: [PATCH 12/24] refactor: image file upload --- src/domain/entities/Action.ts | 2 +- src/domain/entities/LandingNode.ts | 12 +-- .../LandingPageEditDialog.tsx | 61 +++--------- .../useImageFileUpload.tsx | 93 +++++++++++++++++++ 4 files changed, 112 insertions(+), 56 deletions(-) create mode 100644 src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx diff --git a/src/domain/entities/Action.ts b/src/domain/entities/Action.ts index ab648be..2672658 100644 --- a/src/domain/entities/Action.ts +++ b/src/domain/entities/Action.ts @@ -118,7 +118,7 @@ export const getPageActions = ( const hasUserAccess = actionUsers.includes(user.id); const hasUserGroupAccess = _.intersection(actionUserGroups, userGroupIds).length > 0; - const hasPublicAccess = action.publicAccess && action.publicAccess !== "--------"; + const hasPublicAccess = Boolean(action.publicAccess) && action.publicAccess !== "--------"; return hasUserAccess || hasUserGroupAccess || hasPublicAccess; }) diff --git a/src/domain/entities/LandingNode.ts b/src/domain/entities/LandingNode.ts index 9967c33..2bebba0 100644 --- a/src/domain/entities/LandingNode.ts +++ b/src/domain/entities/LandingNode.ts @@ -103,17 +103,17 @@ export const updateLandingNodes = ( const applyFavicon = (parent: LandingNode): LandingNode => { const spreadFaviconToChildren = (children: LandingNode[], favicon: string): LandingNode[] => { return _.map(children, child => { - const updatedChild: LandingNode = { ...child, favicon }; - if (child.children) { - updatedChild.children = spreadFaviconToChildren(child.children, favicon); - } - return updatedChild; + return { + ...child, + favicon: favicon, + children: spreadFaviconToChildren(child.children, favicon), + }; }); }; return { ...parent, - children: parent.children ? spreadFaviconToChildren(parent.children, parent.favicon) : [], + children: spreadFaviconToChildren(parent.children, parent.favicon), }; }; diff --git a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx index 4055040..e53d10d 100644 --- a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx +++ b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx @@ -5,7 +5,7 @@ import { useSnackbar, } from "@eyeseetea/d2-ui-components"; import { Button, Switch, TextField } from "@material-ui/core"; -import React, { ChangeEvent, useCallback, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import styled from "styled-components"; import { generateUid } from "../../../data/utils/uid"; import { LandingNode, LandingNodePageRendering, LandingNodeType } from "../../../domain/entities/LandingNode"; @@ -15,6 +15,8 @@ import { MarkdownEditor } from "../markdown-editor/MarkdownEditor"; import { MarkdownViewer } from "../markdown-viewer/MarkdownViewer"; import { LandingBody } from "../landing-layout"; import { ColorPicker } from "../color-picker/ColorPicker"; +import _ from "lodash"; +import useImageFileUpload from "./useImageFileUpload"; const buildDefaultNode = ( type: LandingNodeType, @@ -55,8 +57,8 @@ export const LandingPageEditDialog: React.FC = props ); const [iconLocation, setIconLocation] = useState(value.iconLocation === "bottom"); const [pageRendering, setPageRendering] = useState(value.pageRendering === "single"); - const [warnings, setWarnings] = useState([]); + const { faviconWarnings, uploadFavicon, uploadIcon } = useImageFileUpload(setValue); const items = useMemo( () => actions @@ -113,45 +115,9 @@ export const LandingPageEditDialog: React.FC = props setValue(value => ({ ...value, pageRendering: event.target.checked ? "single" : "multiple" })); }; - const handleFileUpload = useCallback( - (event: ChangeEvent, fileType: keyof LandingNode) => { - const file = event.target.files ? event.target.files[0] : undefined; - - file?.arrayBuffer().then(async data => { - const reader = new FileReader(); - reader.onload = e => { - const img = new Image(); - img.onload = () => { - const width = img.width; - const height = img.height; - const aspectRatio = width / height; - - setWarnings([]); - const newWarnings: string[] = []; - - if (fileType === "favicon") { - if (aspectRatio !== FAVICON_ASPECT_RATIO) { - newWarnings.push("Please ensure that your favicon has a 1:1 aspect ratio."); - } - - if (width > FAVICON_MAX_SIZE || height > FAVICON_MAX_SIZE) { - newWarnings.push("Please use an icon of 128x128 pixels or smaller."); - } - - newWarnings.length !== 0 && setWarnings(newWarnings); - } - }; - if (e.target?.result) { - img.src = e.target.result as string; - } - }; - reader.readAsDataURL(file); - const icon = await compositionRoot.instance.uploadFile(data, file.name); - setValue(node => ({ ...node, [fileType]: icon })); - }); - }, - [compositionRoot] - ); + const onChangeIconSize = useCallback(size => { + setValue(value => ({ ...value, iconSize: size })); + }, []); return ( @@ -218,7 +184,7 @@ export const LandingPageEditDialog: React.FC = props ) : null} - handleFileUpload(event, "icon")} /> +
@@ -245,7 +211,7 @@ export const LandingPageEditDialog: React.FC = props } variant="contained" value={size} - onClick={() => setValue(value => ({ ...value, iconSize: size }))} + onClick={() => onChangeIconSize(size)} > {size} @@ -264,12 +230,12 @@ export const LandingPageEditDialog: React.FC = props ) : null} - handleFileUpload(event, "favicon")} /> + - {warnings.length > 0 && ( + {!_.isEmpty(faviconWarnings) && ( - {warnings.map(warning => ( + {faviconWarnings.map(warning => (

{i18n.t(warning)}

))}
@@ -351,9 +317,6 @@ export const LandingPageEditDialog: React.FC = props ); }; -const FAVICON_ASPECT_RATIO = 1; -const FAVICON_MAX_SIZE = 128; - export interface LandingPageEditDialogProps extends Omit { initialNode?: LandingNode; type: LandingNodeType; diff --git a/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx b/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx new file mode 100644 index 0000000..faba258 --- /dev/null +++ b/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx @@ -0,0 +1,93 @@ +import { ChangeEvent, useCallback, useState } from "react"; +import { useAppContext } from "../../contexts/app-context"; +import { LandingNode } from "../../../domain/entities/LandingNode"; +import i18n from "../../../locales"; + +export default function useImageFileUpload(updateNode: (value: React.SetStateAction) => void) { + const { compositionRoot } = useAppContext(); + + const [faviconWarnings, setFaviconWarnings] = useState([]); + + const processImageFile = (file: File, fileType: keyof LandingNode) => { + const reader = new FileReader(); + reader.onload = e => { + const img = new Image(); + img.onload = () => { + const width = img.width; + const height = img.height; + const aspectRatio = width / height; + + setFaviconWarnings([]); + if (fileType === "favicon") { + const aspectRatioWarnings = [ + ...(!isAspectRatioWithinMargin(aspectRatio) + ? [ + i18n.t("Please ensure that your favicon has a {{aspectRatio}} aspect ratio.", { + aspectRatio: FAVICON_ASPECT_RATIO.fraction, + }), + ] + : []), + ]; + + const imageDimensionWarnings = [ + ...(!isWithinMargin(width) || !isWithinMargin(height) + ? [ + i18n.t("Please use an icon of {{max}}x{{max}} pixels or smaller.", { + max: FAVICON_MAX_SIZE, + }), + ] + : []), + ]; + + const warnings = [...aspectRatioWarnings, ...imageDimensionWarnings]; + setFaviconWarnings(warnings); + } + }; + if (e.target?.result) { + img.src = e.target.result as string; + } + }; + reader.readAsDataURL(file); + }; + + const handleImageFileUpload = useCallback( + (event: ChangeEvent, fileType: keyof LandingNode) => { + const file = event.target.files ? event.target.files[0] : undefined; + + file?.arrayBuffer().then(async data => { + const icon = await compositionRoot.instance.uploadFile(data, file.name); + processImageFile(file, fileType); + updateNode(node => ({ ...node, [fileType]: icon })); + }); + }, + [compositionRoot.instance, updateNode] + ); + + const uploadIcon = useCallback( + (event: ChangeEvent) => handleImageFileUpload(event, "icon"), + [handleImageFileUpload] + ); + const uploadFavicon = useCallback( + (event: ChangeEvent) => handleImageFileUpload(event, "favicon"), + [handleImageFileUpload] + ); + + return { + faviconWarnings, + uploadFavicon, + uploadIcon, + }; +} + +const FAVICON_ASPECT_RATIO = { fraction: "1:1", value: 1 }; +const FAVICON_MAX_SIZE = 128; + +const ALLOWED_MARGIN = 5; +const ASPECT_RATIO_MARGIN = 0.015; + +const isWithinMargin = (dimension: number): boolean => { + return dimension >= FAVICON_MAX_SIZE - ALLOWED_MARGIN && dimension <= FAVICON_MAX_SIZE + ALLOWED_MARGIN; +}; +const isAspectRatioWithinMargin = (aspectRatio: number): boolean => + aspectRatio >= FAVICON_ASPECT_RATIO.value - ASPECT_RATIO_MARGIN && + aspectRatio <= FAVICON_ASPECT_RATIO.value + ASPECT_RATIO_MARGIN; From 7a8cc18de537f59c5ad40dce575d0e18e1a8fda2 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 27 May 2024 15:21:10 +0100 Subject: [PATCH 13/24] feat: update translation files --- i18n/en.pot | 10 ++++++++-- i18n/es.po | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0ec5cf5..02aa434 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-05-22T14:23:25.170Z\n" -"PO-Revision-Date: 2024-05-22T14:23:25.170Z\n" +"POT-Creation-Date: 2024-05-27T14:20:48.393Z\n" +"PO-Revision-Date: 2024-05-27T14:20:48.393Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -292,6 +292,12 @@ msgstr "" msgid "Contents" msgstr "" +msgid "Please ensure that your favicon has a {{aspectRatio}} aspect ratio." +msgstr "" + +msgid "Please use an icon of {{max}}x{{max}} pixels or smaller." +msgstr "" + msgid "Importing landing pages(s)" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 3179ba2..be3d534 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2024-05-22T14:23:25.170Z\n" +"POT-Creation-Date: 2024-05-27T14:20:48.393Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -292,6 +292,12 @@ msgstr "" msgid "Contents" msgstr "" +msgid "Please ensure that your favicon has a {{aspectRatio}} aspect ratio." +msgstr "" + +msgid "Please use an icon of {{max}}x{{max}} pixels or smaller." +msgstr "" + msgid "Importing landing pages(s)" msgstr "" From d4f78f973339c6153c09fe458cba097e68859aa1 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Wed, 29 May 2024 13:53:37 +0100 Subject: [PATCH 14/24] fix: go back for singleLanding with sublandings --- src/webapp/pages/home/HomePage.tsx | 52 +++++++++++++++++++----------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index 79a441c..4a249dc 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -42,7 +42,10 @@ export const HomePage: React.FC = React.memo(() => { return history[0] ?? initLandings?.[0]; }, [history, initLandings]); - const isRoot = history.length === 0; + const isRoot = _.isEmpty(history); + const isRootPage = currentPage?.type === "root"; + const isSingleLanding = pageType === "singleLanding"; + const hasSingleInitLanding = initLandings?.length === 1; const currentHistory = history[0]; const openSettings = useCallback(() => { @@ -67,17 +70,24 @@ export const HomePage: React.FC = React.memo(() => { ); const goBack = useCallback(() => { - if (currentPage?.type === "root" && _.every(history, landing => landing.id === currentPage.id)) { + const allHistoryMatchesCurrentPage = _.every(history, landing => landing.id === currentPage?.id); + + if (isRootPage && allHistoryMatchesCurrentPage) { updateHistory([]); setPageType("userLandings"); - } else if ( - initLandings?.length === 1 || - currentPage?.type !== "root" || - (currentPage?.type === "root" && !_.isEmpty(history)) - ) { + } else if (hasSingleInitLanding || !isRootPage || !isRoot) { updateHistory(history => history.slice(1)); - } else setPageType("userLandings"); - }, [currentPage, history, initLandings?.length]); + } else { + setPageType("userLandings"); + } + }, [currentPage, hasSingleInitLanding, history, isRoot, isRootPage]); + + const allowBackNavigation = useMemo(() => { + const isMultipleLandingSubPage = !isRoot && initLandings !== undefined && initLandings.length > 1; + const isSingleLandingSubPage = initLandings?.length === 1 && history.length > 1; + + return isSingleLanding && (isMultipleLandingSubPage || isSingleLandingSubPage); + }, [history, initLandings, isRoot, isSingleLanding]); const goHome = useCallback(() => { if (initLandings?.length === 1) updateHistory([]); @@ -98,6 +108,12 @@ export const HomePage: React.FC = React.memo(() => { }, 8000); }, [compositionRoot]); + useEffect(() => { + if (isSingleLanding && hasSingleInitLanding && isRootPage && isRoot) { + updateHistory(history => [currentPage, ...history]); + } + }, [currentPage, hasSingleInitLanding, isRoot, isRootPage, isSingleLanding]); + useEffect(() => { if (initLandings?.length === 0) { window.location.href = !defaultApplication @@ -113,13 +129,13 @@ export const HomePage: React.FC = React.memo(() => { const icon = favicon.current; const pageFavicon = currentPage?.favicon; - icon?.setAttribute("href", (pageType === "singleLanding" && pageFavicon) || defaultIcon); - document.title = (pageType === "singleLanding" && currentPage && translate(currentPage.name)) || defaultTitle; + icon?.setAttribute("href", (isSingleLanding && pageFavicon) || defaultIcon); + document.title = (isSingleLanding && currentPage && translate(currentPage.name)) || defaultTitle; return () => { icon?.setAttribute("href", defaultIcon); document.title = defaultTitle; }; - }, [reload, currentPage, pageType, translate]); + }, [reload, currentPage, isSingleLanding, translate]); useEffect(() => { if (userLandings && userLandings?.length > 1 && pageType === "userLandings") { @@ -127,14 +143,14 @@ export const HomePage: React.FC = React.memo(() => { title: "Homepage - Available Home Pages", location: `${window.location.hash.split("?")[0]}home-page-app/available-landings`, }); - } else if (currentPage && pageType === "singleLanding" && currentHistory) { - const type = currentPage.type === "root" ? "landing" : currentPage.type; + } else if (currentPage && isSingleLanding && currentHistory) { + const type = isRootPage ? "landing" : currentPage.type; analytics.sendPageView({ title: `Homepage - ${currentPage.name.referenceValue}`, location: `${window.location.hash.split("?")[0]}home-page-app/${type}/${currentPage.id}`, }); } - }, [currentPage, analytics, pageType, userLandings, currentHistory]); + }, [analytics, currentHistory, currentPage, isRootPage, isSingleLanding, pageType, userLandings]); const redirect = useRedirectOnSinglePrimaryAction(currentPage, userLandings); @@ -143,8 +159,8 @@ export const HomePage: React.FC = React.memo(() => { backgroundColor={currentPage?.backgroundColor} onSettings={hasSettingsAccess ? openSettings : undefined} onAbout={openAbout} - onGoBack={!isRoot && pageType === "singleLanding" ? goBack : undefined} - onGoHome={!isRoot && pageType === "singleLanding" ? goHome : undefined} + onGoBack={allowBackNavigation ? goBack : undefined} + onGoHome={!isRoot && isSingleLanding ? goHome : undefined} onLogout={logout} centerChildren={true} > @@ -182,7 +198,7 @@ export const HomePage: React.FC = React.memo(() => { })} - ) : currentPage && pageType === "singleLanding" ? ( + ) : currentPage && isSingleLanding ? ( ) : null} From c51953d84c92a0df3754a05f73345ef23fc4a728 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Wed, 29 May 2024 19:18:39 +0100 Subject: [PATCH 15/24] feat: primary redirect landing page --- src/domain/entities/LandingNode.ts | 27 +++++++++++++-------- src/webapp/pages/home/HomePage.tsx | 38 ++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/domain/entities/LandingNode.ts b/src/domain/entities/LandingNode.ts index 2bebba0..1890ea2 100644 --- a/src/domain/entities/LandingNode.ts +++ b/src/domain/entities/LandingNode.ts @@ -4,6 +4,7 @@ import { TranslatableText, TranslatableTextModel } from "./TranslatableText"; import { LandingPagePermission } from "./Permission"; import { User } from "./User"; import { Action, getPageActions } from "./Action"; +import { Maybe } from "../../types/utils"; export const LandingPageNodeTypeModel = Schema.oneOf([ Schema.exact("root"), @@ -117,42 +118,48 @@ const applyFavicon = (parent: LandingNode): LandingNode => { }; }; -/* Return a redirect URL if there is only one visible action on primary nodes */ -export function getPrimaryRedirectUrl( +// Return +// a redirect URL if there is only one visible action on primary nodes +// a redirect page id when there is only one visible action on primary nodes +export function getPrimaryRedirectNodes( landingNode: LandingNode, options: { actions: Action[]; user: User } -): Url | undefined { +): { redirectUrl: Maybe; redirectPageId: Maybe } { const { actions, user } = options; const actionsById = _.keyBy(actions, action => action.id); const showAllActions = false; const isRoot = true; - const primaryUrls = _(landingNode.children) + const pageActions = _(landingNode.children) .reject(node => Boolean(node.secondary)) - .flatMap((node): Url[] => { + .flatMap((node): Action[] => { const nodeActions = actions.filter(action => node.actions.includes(action.id)); const actionIds = user && getPageActions(isRoot, showAllActions, actions, user, nodeActions); return _(actionIds) .map(actionId => actionsById[actionId]) .compact() - .map(action => action.dhisLaunchUrl) - .compact() .value(); }) .value(); - const redirectUrl = primaryUrls.length === 1 ? primaryUrls[0] : undefined; + const launchUrls = _.map(pageActions, action => action.dhisLaunchUrl); + const launchPageIds = _.map(pageActions, action => action.launchPageId); + + const redirectUrl = launchUrls.length === 1 ? launchUrls[0] : undefined; + const redirectPageId = launchPageIds.length === 1 ? launchPageIds[0] : undefined; const message = [ - `Primary URLs [${primaryUrls.length}]: ${primaryUrls.join(", ")}`, + `Primary URLs [${launchUrls.length}]: ${launchUrls.join(", ")}`, `Redirect URL: ${redirectUrl || "-"}`, + `Primary Page IDs [${launchPageIds.length}]: ${launchPageIds.join(", ")}`, + `Redirect Page ID: ${redirectPageId || "-"}`, ].join("\n"); console.debug(message); - return redirectUrl; + return { redirectUrl, redirectPageId }; } export function flattenLandingNodes(nodes: LandingNode[]): LandingNode[] { diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index 4a249dc..4f97e61 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -5,7 +5,7 @@ import { useSnackbar } from "@eyeseetea/d2-ui-components"; import { LandingNode, flattenLandingNodes, - getPrimaryRedirectUrl as getPrimaryActionUrl, + getPrimaryRedirectNodes as getPrimaryActionNodes, } from "../../../domain/entities/LandingNode"; import { LandingLayout, LandingContent } from "../../components/landing-layout"; import { useAppContext } from "../../contexts/app-context"; @@ -152,7 +152,8 @@ export const HomePage: React.FC = React.memo(() => { } }, [analytics, currentHistory, currentPage, isRootPage, isSingleLanding, pageType, userLandings]); - const redirect = useRedirectOnSinglePrimaryAction(currentPage, userLandings); + const redirect = useRedirectOnSinglePrimaryNode(currentPage, userLandings); + const pageToRender = redirect.currentPage || (currentPage && isSingleLanding ? currentPage : undefined); return ( { })} - ) : currentPage && isSingleLanding ? ( - + ) : pageToRender ? ( + ) : null} @@ -226,25 +227,38 @@ const ContentWrapper = styled.div` min-height: 100vh; `; -function useRedirectOnSinglePrimaryAction( +function useRedirectOnSinglePrimaryNode( landingNode: Maybe, userLandings: Maybe -): { isActive: boolean } { +): { isActive: boolean; currentPage: Maybe } { const { actions, launchAppBaseUrl } = useAppContext(); const { user } = useConfig(); const url = - user && landingNode && userLandings?.length === 1 - ? getPrimaryActionUrl(landingNode, { actions, user }) + user && landingNode && userLandings?.length === 2 + ? getPrimaryActionNodes(landingNode, { actions, user }) : undefined; const [isActive, setIsActive] = React.useState(false); + const [currentPage, setCurrentPage] = React.useState(); React.useEffect(() => { if (url) { - goTo(url, { baseUrl: launchAppBaseUrl }); - setIsActive(true); + const { redirectPageId, redirectUrl } = url; + if (redirectUrl && redirectPageId) { + return; + } + if (redirectUrl) { + goTo(redirectUrl, { baseUrl: launchAppBaseUrl }); + setIsActive(true); + } + if (redirectPageId) { + const page = userLandings?.find(landing => landing.id === redirectPageId); + if (page) { + setCurrentPage(page); + } + } } - }, [url, launchAppBaseUrl]); + }, [url, launchAppBaseUrl, userLandings]); - return { isActive }; + return { isActive: isActive, currentPage: currentPage }; } From a8c423f3f322a5ff00f398d38ded8b8f01b79807 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Thu, 30 May 2024 11:15:41 +0100 Subject: [PATCH 16/24] fix: redirect for single landing page --- src/webapp/pages/home/HomePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index 4f97e61..0c0d362 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -234,7 +234,7 @@ function useRedirectOnSinglePrimaryNode( const { actions, launchAppBaseUrl } = useAppContext(); const { user } = useConfig(); const url = - user && landingNode && userLandings?.length === 2 + user && landingNode && userLandings?.length === 1 ? getPrimaryActionNodes(landingNode, { actions, user }) : undefined; From b23f3a89823e3b9a24b9d3538b8ff03b6f6d2942 Mon Sep 17 00:00:00 2001 From: p3rcypj Date: Sun, 2 Jun 2024 22:57:14 +0000 Subject: [PATCH 17/24] Add TODO comments --- .../landing-page-edit-dialog/LandingPageEditDialog.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx index e53d10d..fa13b77 100644 --- a/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx +++ b/src/webapp/components/landing-page-edit-dialog/LandingPageEditDialog.tsx @@ -52,6 +52,7 @@ export const LandingPageEditDialog: React.FC = props const { actions, translate, compositionRoot } = useAppContext(); const snackbar = useSnackbar(); + /* TODO: File is increasing, move to useHook */ const [value, setValue] = useState( initialNode ?? buildDefaultNode(type, parent, order, "multiple", true) ); @@ -119,6 +120,7 @@ export const LandingPageEditDialog: React.FC = props setValue(value => ({ ...value, iconSize: size })); }, []); + /* TODO: Move to separate components to make it more readable */ return ( From 11d9b019afe8427de64df6ea78cfd780df311c0c Mon Sep 17 00:00:00 2001 From: p3rcypj Date: Sun, 2 Jun 2024 23:03:08 +0000 Subject: [PATCH 18/24] External imports to top --- src/webapp/pages/home/HomePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index 0c0d362..80be372 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -1,7 +1,9 @@ +import _ from "lodash"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import CircularProgress from "material-ui/CircularProgress"; import styled from "styled-components"; import { useSnackbar } from "@eyeseetea/d2-ui-components"; +import { useNavigate } from "react-router-dom"; import { LandingNode, flattenLandingNodes, @@ -9,7 +11,6 @@ import { } from "../../../domain/entities/LandingNode"; import { LandingLayout, LandingContent } from "../../components/landing-layout"; import { useAppContext } from "../../contexts/app-context"; -import { useNavigate } from "react-router-dom"; import { Item } from "../../components/item/Item"; import { useConfig } from "../settings/useConfig"; import { Cardboard } from "../../components/card-board/Cardboard"; @@ -19,7 +20,6 @@ import { defaultIcon, defaultTitle } from "../../router/Router"; import { useAnalytics } from "../../hooks/useAnalytics"; import { Maybe } from "../../../types/utils"; import i18n from "../../../locales"; -import _ from "lodash"; export const HomePage: React.FC = React.memo(() => { const { hasSettingsAccess, reload, isLoading, launchAppBaseUrl, translate, compositionRoot } = useAppContext(); From addc4798c9ed0cd8e38ca017bb489df09ecf0963 Mon Sep 17 00:00:00 2001 From: p3rcypj Date: Mon, 3 Jun 2024 00:47:21 +0000 Subject: [PATCH 19/24] Extract the pattern from recursive function --- src/domain/entities/LandingNode.ts | 39 +++++++++++--------------- src/webapp/pages/settings/useConfig.ts | 4 +-- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/domain/entities/LandingNode.ts b/src/domain/entities/LandingNode.ts index 1890ea2..c97e67e 100644 --- a/src/domain/entities/LandingNode.ts +++ b/src/domain/entities/LandingNode.ts @@ -71,11 +71,7 @@ export const buildOrderedLandingNodes = (nodes: LandingNode[]): OrderedLandingNo })); }; -export const updateLandingNodes = ( - nodes: LandingNode[], - permissions: LandingPagePermission[], - user: User -): LandingNode[] => { +function updateLandingNodes(nodes: LandingNode[], permissions: LandingPagePermission[], user: User): LandingNode[] { const updatedNodes = _(nodes) .map(node => { const pagePermission = permissions?.find(permission => permission.id === node.id); @@ -98,25 +94,24 @@ export const updateLandingNodes = ( .compact() .value(); - return updatedNodes.map(node => applyFavicon(node)); -}; + return updatedNodes; +} -const applyFavicon = (parent: LandingNode): LandingNode => { - const spreadFaviconToChildren = (children: LandingNode[], favicon: string): LandingNode[] => { - return _.map(children, child => { - return { - ...child, - favicon: favicon, - children: spreadFaviconToChildren(child.children, favicon), - }; - }); - }; +function applyFavicon(children: LandingNode[], favicon: string): LandingNode[] { + return _.map(children, child => { + return { + ...child, + favicon: favicon, + children: applyFavicon(child.children, favicon), + }; + }); +} - return { - ...parent, - children: spreadFaviconToChildren(parent.children, parent.favicon), - }; -}; +export function updateLandings(nodes: LandingNode[], permissions: LandingPagePermission[], user: User): LandingNode[] { + const landings = updateLandingNodes(nodes, permissions, user); + + return landings.map(landing => ({ ...landing, children: applyFavicon(landing.children, landing.favicon) })); +} // Return // a redirect URL if there is only one visible action on primary nodes diff --git a/src/webapp/pages/settings/useConfig.ts b/src/webapp/pages/settings/useConfig.ts index 4f92c77..c25bdd7 100644 --- a/src/webapp/pages/settings/useConfig.ts +++ b/src/webapp/pages/settings/useConfig.ts @@ -4,7 +4,7 @@ import { SharedUpdate } from "../../components/permissions-dialog/PermissionsDia import { useAppContext } from "../../contexts/app-context"; import { User } from "../../../domain/entities/User"; import { Maybe } from "../../../types/utils"; -import { LandingNode, updateLandingNodes } from "../../../domain/entities/LandingNode"; +import { LandingNode, updateLandings } from "../../../domain/entities/LandingNode"; export function useConfig(): useConfigPloc { const { compositionRoot, landings } = useAppContext(); @@ -17,7 +17,7 @@ export function useConfig(): useConfigPloc { const userLandings = useMemo(() => { if (!(landings && landingPagePermissions && user)) return undefined; - return updateLandingNodes(landings, landingPagePermissions, user); + return updateLandings(landings, landingPagePermissions, user); }, [landingPagePermissions, landings, user]); useEffect(() => { From 16abd9a8f7c188f4461aa1833740187a3c683068 Mon Sep 17 00:00:00 2001 From: p3rcypj Date: Mon, 3 Jun 2024 00:50:15 +0000 Subject: [PATCH 20/24] Rename function --- src/domain/entities/LandingNode.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/entities/LandingNode.ts b/src/domain/entities/LandingNode.ts index c97e67e..5263704 100644 --- a/src/domain/entities/LandingNode.ts +++ b/src/domain/entities/LandingNode.ts @@ -97,12 +97,12 @@ function updateLandingNodes(nodes: LandingNode[], permissions: LandingPagePermis return updatedNodes; } -function applyFavicon(children: LandingNode[], favicon: string): LandingNode[] { +function spreadFavicon(children: LandingNode[], favicon: string): LandingNode[] { return _.map(children, child => { return { ...child, favicon: favicon, - children: applyFavicon(child.children, favicon), + children: spreadFavicon(child.children, favicon), }; }); } @@ -110,7 +110,7 @@ function applyFavicon(children: LandingNode[], favicon: string): LandingNode[] { export function updateLandings(nodes: LandingNode[], permissions: LandingPagePermission[], user: User): LandingNode[] { const landings = updateLandingNodes(nodes, permissions, user); - return landings.map(landing => ({ ...landing, children: applyFavicon(landing.children, landing.favicon) })); + return landings.map(landing => ({ ...landing, children: spreadFavicon(landing.children, landing.favicon) })); } // Return From 58bdfbc3b7e18daba6fc442eebc9d24e411b3528 Mon Sep 17 00:00:00 2001 From: p3rcypj Date: Mon, 3 Jun 2024 02:03:31 +0000 Subject: [PATCH 21/24] _.map() on children --- src/domain/entities/LandingNode.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/domain/entities/LandingNode.ts b/src/domain/entities/LandingNode.ts index 5263704..3e3341e 100644 --- a/src/domain/entities/LandingNode.ts +++ b/src/domain/entities/LandingNode.ts @@ -71,7 +71,7 @@ export const buildOrderedLandingNodes = (nodes: LandingNode[]): OrderedLandingNo })); }; -function updateLandingNodes(nodes: LandingNode[], permissions: LandingPagePermission[], user: User): LandingNode[] { +function updateNodesPermissions(nodes: LandingNode[], permissions: LandingPagePermission[], user: User): LandingNode[] { const updatedNodes = _(nodes) .map(node => { const pagePermission = permissions?.find(permission => permission.id === node.id); @@ -88,7 +88,7 @@ function updateLandingNodes(nodes: LandingNode[], permissions: LandingPagePermis return { ...node, - children: updateLandingNodes(node.children, permissions, user), + children: updateNodesPermissions(node.children, permissions, user), }; }) .compact() @@ -97,20 +97,22 @@ function updateLandingNodes(nodes: LandingNode[], permissions: LandingPagePermis return updatedNodes; } -function spreadFavicon(children: LandingNode[], favicon: string): LandingNode[] { - return _.map(children, child => { - return { - ...child, - favicon: favicon, - children: spreadFavicon(child.children, favicon), - }; - }); +function spreadFavicon(node: LandingNode, favicon: string): LandingNode { + return { + ...node, + favicon: favicon, + children: node.children.map(child => spreadFavicon(child, favicon)), + }; } -export function updateLandings(nodes: LandingNode[], permissions: LandingPagePermission[], user: User): LandingNode[] { - const landings = updateLandingNodes(nodes, permissions, user); +export function updateLandings( + landings: LandingNode[], + permissions: LandingPagePermission[], + user: User +): LandingNode[] { + const landingsWithPermissions = updateNodesPermissions(landings, permissions, user); - return landings.map(landing => ({ ...landing, children: spreadFavicon(landing.children, landing.favicon) })); + return landingsWithPermissions.map(landing => spreadFavicon(landing, landing.favicon)); } // Return From 2a85fe8c5257b5eeb743d6c2b09c6a0f61dc2320 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 3 Jun 2024 09:38:54 +0100 Subject: [PATCH 22/24] fix: use initLandings for single primary action redirect --- src/webapp/pages/home/HomePage.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/webapp/pages/home/HomePage.tsx b/src/webapp/pages/home/HomePage.tsx index 80be372..89b1ebb 100644 --- a/src/webapp/pages/home/HomePage.tsx +++ b/src/webapp/pages/home/HomePage.tsx @@ -152,7 +152,7 @@ export const HomePage: React.FC = React.memo(() => { } }, [analytics, currentHistory, currentPage, isRootPage, isSingleLanding, pageType, userLandings]); - const redirect = useRedirectOnSinglePrimaryNode(currentPage, userLandings); + const redirect = useRedirectOnSinglePrimaryNode(currentPage, userLandings, initLandings); const pageToRender = redirect.currentPage || (currentPage && isSingleLanding ? currentPage : undefined); return ( @@ -229,12 +229,13 @@ const ContentWrapper = styled.div` function useRedirectOnSinglePrimaryNode( landingNode: Maybe, - userLandings: Maybe + userLandings: Maybe, + initLandings: Maybe ): { isActive: boolean; currentPage: Maybe } { const { actions, launchAppBaseUrl } = useAppContext(); const { user } = useConfig(); const url = - user && landingNode && userLandings?.length === 1 + user && landingNode && initLandings?.length === 1 ? getPrimaryActionNodes(landingNode, { actions, user }) : undefined; From 9e85c251fa7ee2bee139c61ae612ba92cc2aa6b7 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 3 Jun 2024 10:05:30 +0100 Subject: [PATCH 23/24] chore: increase readabilty --- .../landing-page-edit-dialog/useImageFileUpload.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx b/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx index faba258..35c7f4b 100644 --- a/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx +++ b/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx @@ -1,3 +1,4 @@ +import _ from "lodash"; import { ChangeEvent, useCallback, useState } from "react"; import { useAppContext } from "../../contexts/app-context"; import { LandingNode } from "../../../domain/entities/LandingNode"; @@ -30,7 +31,7 @@ export default function useImageFileUpload(updateNode: (value: React.SetStateAct ]; const imageDimensionWarnings = [ - ...(!isWithinMargin(width) || !isWithinMargin(height) + ...(!(isWithinMargin(width) && isWithinMargin(height)) ? [ i18n.t("Please use an icon of {{max}}x{{max}} pixels or smaller.", { max: FAVICON_MAX_SIZE, @@ -52,7 +53,7 @@ export default function useImageFileUpload(updateNode: (value: React.SetStateAct const handleImageFileUpload = useCallback( (event: ChangeEvent, fileType: keyof LandingNode) => { - const file = event.target.files ? event.target.files[0] : undefined; + const file = _.first(event.target.files); file?.arrayBuffer().then(async data => { const icon = await compositionRoot.instance.uploadFile(data, file.name); @@ -67,6 +68,7 @@ export default function useImageFileUpload(updateNode: (value: React.SetStateAct (event: ChangeEvent) => handleImageFileUpload(event, "icon"), [handleImageFileUpload] ); + const uploadFavicon = useCallback( (event: ChangeEvent) => handleImageFileUpload(event, "favicon"), [handleImageFileUpload] @@ -88,6 +90,7 @@ const ASPECT_RATIO_MARGIN = 0.015; const isWithinMargin = (dimension: number): boolean => { return dimension >= FAVICON_MAX_SIZE - ALLOWED_MARGIN && dimension <= FAVICON_MAX_SIZE + ALLOWED_MARGIN; }; + const isAspectRatioWithinMargin = (aspectRatio: number): boolean => aspectRatio >= FAVICON_ASPECT_RATIO.value - ASPECT_RATIO_MARGIN && aspectRatio <= FAVICON_ASPECT_RATIO.value + ASPECT_RATIO_MARGIN; From fe558231ed571081ad94e2942a8d67128b1b7260 Mon Sep 17 00:00:00 2001 From: deeonwuli Date: Mon, 3 Jun 2024 12:43:16 +0100 Subject: [PATCH 24/24] chore: increase favicon max size --- .../components/landing-page-edit-dialog/useImageFileUpload.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx b/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx index 35c7f4b..335add1 100644 --- a/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx +++ b/src/webapp/components/landing-page-edit-dialog/useImageFileUpload.tsx @@ -82,7 +82,7 @@ export default function useImageFileUpload(updateNode: (value: React.SetStateAct } const FAVICON_ASPECT_RATIO = { fraction: "1:1", value: 1 }; -const FAVICON_MAX_SIZE = 128; +const FAVICON_MAX_SIZE = 512; const ALLOWED_MARGIN = 5; const ASPECT_RATIO_MARGIN = 0.015;