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;