From c84e2e781c21fa7036e31f690c95ded06aa47a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Mendes?= Date: Sun, 26 May 2024 15:33:49 +0100 Subject: [PATCH 1/3] commit changes new imagePicker --- .hintrc | 15 + package-lock.json | 41 +++ package.json | 1 + src/DynamicForm.ts | 3 +- src/ImagePicker.ts | 1 + src/controls/imagePicker/IFilePickerResult.ts | 35 ++ src/controls/imagePicker/IImagePickerProps.ts | 8 + src/controls/imagePicker/ImagePicker.tsx | 141 ++++++++ .../RenderSpninner/RenderSpinner.tsx | 49 +++ src/controls/imagePicker/SelectStokImage.tsx | 105 ++++++ src/controls/imagePicker/StockImagesModel.ts | 35 ++ src/controls/imagePicker/Upload.tsx | 30 ++ .../imagePicker/atoms/contextState.ts | 12 + .../imagePicker/constants/EMessageTypes.ts | 5 + .../imagePicker/constants/EUploadLocations.ts | 4 + .../imagePicker/constants/constants.ts | 11 + .../imagePicker/hooks/useDeleteFile.ts | 27 ++ src/controls/imagePicker/hooks/useGrapAPI.ts | 127 ++++++++ src/controls/imagePicker/hooks/useSpAPI.ts | 91 ++++++ .../imagePicker/hooks/useUploadFile.ts | 127 ++++++++ src/controls/imagePicker/hooks/useUtils.ts | 303 ++++++++++++++++++ src/controls/imagePicker/index.ts | 2 + .../imagePicker/models/IFileExtended.ts | 3 + src/controls/imagePicker/models/IImageFile.ts | 7 + .../imagePicker/models/ISearchImagesResult.ts | 26 ++ .../imagePicker/models/ISearchResult.ts | 7 + .../models/ISearchSiteAssetsResult.ts | 17 + .../imagePicker/models/IUploadFileResult.ts | 64 ++++ .../imagePicker/renderHeader/RenderHeader.tsx | 58 ++++ .../renderHeader/useRenderHeaderStyles.ts | 95 ++++++ .../imagePicker/selectFromSharePoint.tsx | 276 ++++++++++++++++ .../imagePicker/uploadFiles/DeleteFile.tsx | 51 +++ .../imagePicker/uploadFiles/FileError.tsx | 54 ++++ .../imagePicker/uploadFiles/FileProgress.tsx | 44 +++ .../imagePicker/uploadFiles/RetryUpload.tsx | 51 +++ .../uploadFiles/SelectUploadLocation.tsx | 71 ++++ .../imagePicker/uploadFiles/UploadFile.tsx | 186 +++++++++++ .../imagePicker/uploadFiles/UploadFiles.tsx | 181 +++++++++++ .../uploadFiles/useUploadFilesStyles.ts | 101 ++++++ .../imagePicker/useImagePickerStyles.ts | 85 +++++ src/loc/en-us.ts | 208 ++++++------ src/loc/mystrings.d.ts | 26 +- .../controlsTest/components/ControlsTest.tsx | 192 ++++++----- .../components/TestControl copy.tsx | 105 ++++++ .../controlsTest/components/TestControl.tsx | 67 ++-- 45 files changed, 2913 insertions(+), 235 deletions(-) create mode 100644 .hintrc create mode 100644 src/ImagePicker.ts create mode 100644 src/controls/imagePicker/IFilePickerResult.ts create mode 100644 src/controls/imagePicker/IImagePickerProps.ts create mode 100644 src/controls/imagePicker/ImagePicker.tsx create mode 100644 src/controls/imagePicker/RenderSpninner/RenderSpinner.tsx create mode 100644 src/controls/imagePicker/SelectStokImage.tsx create mode 100644 src/controls/imagePicker/StockImagesModel.ts create mode 100644 src/controls/imagePicker/Upload.tsx create mode 100644 src/controls/imagePicker/atoms/contextState.ts create mode 100644 src/controls/imagePicker/constants/EMessageTypes.ts create mode 100644 src/controls/imagePicker/constants/EUploadLocations.ts create mode 100644 src/controls/imagePicker/constants/constants.ts create mode 100644 src/controls/imagePicker/hooks/useDeleteFile.ts create mode 100644 src/controls/imagePicker/hooks/useGrapAPI.ts create mode 100644 src/controls/imagePicker/hooks/useSpAPI.ts create mode 100644 src/controls/imagePicker/hooks/useUploadFile.ts create mode 100644 src/controls/imagePicker/hooks/useUtils.ts create mode 100644 src/controls/imagePicker/index.ts create mode 100644 src/controls/imagePicker/models/IFileExtended.ts create mode 100644 src/controls/imagePicker/models/IImageFile.ts create mode 100644 src/controls/imagePicker/models/ISearchImagesResult.ts create mode 100644 src/controls/imagePicker/models/ISearchResult.ts create mode 100644 src/controls/imagePicker/models/ISearchSiteAssetsResult.ts create mode 100644 src/controls/imagePicker/models/IUploadFileResult.ts create mode 100644 src/controls/imagePicker/renderHeader/RenderHeader.tsx create mode 100644 src/controls/imagePicker/renderHeader/useRenderHeaderStyles.ts create mode 100644 src/controls/imagePicker/selectFromSharePoint.tsx create mode 100644 src/controls/imagePicker/uploadFiles/DeleteFile.tsx create mode 100644 src/controls/imagePicker/uploadFiles/FileError.tsx create mode 100644 src/controls/imagePicker/uploadFiles/FileProgress.tsx create mode 100644 src/controls/imagePicker/uploadFiles/RetryUpload.tsx create mode 100644 src/controls/imagePicker/uploadFiles/SelectUploadLocation.tsx create mode 100644 src/controls/imagePicker/uploadFiles/UploadFile.tsx create mode 100644 src/controls/imagePicker/uploadFiles/UploadFiles.tsx create mode 100644 src/controls/imagePicker/uploadFiles/useUploadFilesStyles.ts create mode 100644 src/controls/imagePicker/useImagePickerStyles.ts create mode 100644 src/webparts/controlsTest/components/TestControl copy.tsx diff --git a/.hintrc b/.hintrc new file mode 100644 index 000000000..c3989a414 --- /dev/null +++ b/.hintrc @@ -0,0 +1,15 @@ +{ + "extends": [ + "development" + ], + "hints": { + "axe/aria": "off", + "no-inline-styles": "off", + "axe/text-alternatives": [ + "default", + { + "frame-title": "off" + } + ] + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c2342d70a..4d7c526ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "react": "17.0.1", "react-accessible-accordion": "^5.0.0", "react-dom": "17.0.1", + "react-dropzone": "^14.2.3", "react-mentions": "^4.3.0", "react-quill": "2.0.0", "regexify-string": "^1.0.16", @@ -14441,6 +14442,14 @@ "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==" }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" + } + }, "node_modules/autoprefixer": { "version": "9.8.8", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", @@ -21522,6 +21531,22 @@ "node": ">=8.9.0" } }, + "node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/file-selector/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/file-type": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", @@ -35893,6 +35918,22 @@ "react": "17.0.1" } }, + "node_modules/react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 855eba825..b1375277a 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "react": "17.0.1", "react-accessible-accordion": "^5.0.0", "react-dom": "17.0.1", + "react-dropzone": "^14.2.3", "react-mentions": "^4.3.0", "react-quill": "2.0.0", "regexify-string": "^1.0.16", diff --git a/src/DynamicForm.ts b/src/DynamicForm.ts index c1d45cf8a..6b3fea92c 100644 --- a/src/DynamicForm.ts +++ b/src/DynamicForm.ts @@ -1 +1,2 @@ -export * from './controls/dynamicForm/index'; \ No newline at end of file +export * from './controls/dynamicForm/index'; +export * from './controls/dynamicForm/index'; diff --git a/src/ImagePicker.ts b/src/ImagePicker.ts new file mode 100644 index 000000000..f974fc3f4 --- /dev/null +++ b/src/ImagePicker.ts @@ -0,0 +1 @@ +export * from './controls/imagePicker/index'; diff --git a/src/controls/imagePicker/IFilePickerResult.ts b/src/controls/imagePicker/IFilePickerResult.ts new file mode 100644 index 000000000..2f53c81eb --- /dev/null +++ b/src/controls/imagePicker/IFilePickerResult.ts @@ -0,0 +1,35 @@ +export interface IFilePickerResult { + /** + * Selected file name with extension. + */ + fileName: string; + /** + * Selected file name without extension. + */ + fileNameWithoutExtension: string; + /** + * Absolute file URL. Undefined in case of file upload. + */ + fileAbsoluteUrl: string; + + /** + * Size of a selected file (in bytes). Undefined in all cases but file upload + */ + fileSize?: number; + + /** + * Absolute not modified file SharePoint URL. + */ + spItemUrl?: string; + + /** + * Downloads file picker result content. + */ + downloadFileContent: () => Promise; + + /** + * Preview + */ + previewDataUrl?: string; + } + \ No newline at end of file diff --git a/src/controls/imagePicker/IImagePickerProps.ts b/src/controls/imagePicker/IImagePickerProps.ts new file mode 100644 index 000000000..6fccfc73b --- /dev/null +++ b/src/controls/imagePicker/IImagePickerProps.ts @@ -0,0 +1,8 @@ +import { IImageFile } from './models/IImageFile'; + +export interface IImagePickerProps { +onImageSelected: (image: IImageFile) => void; +isOpen: boolean; +onDismiss: () => void; + +} \ No newline at end of file diff --git a/src/controls/imagePicker/ImagePicker.tsx b/src/controls/imagePicker/ImagePicker.tsx new file mode 100644 index 000000000..01741a933 --- /dev/null +++ b/src/controls/imagePicker/ImagePicker.tsx @@ -0,0 +1,141 @@ +import * as React from "react"; + +import strings from "ControlStrings"; +import { useAtom } from "jotai"; + +import { + Button, + Image, +} from "@fluentui/react-components"; +import { + Delete16Regular, + Image20Regular, +} from "@fluentui/react-icons"; +import { BaseComponentContext } from "@microsoft/sp-component-base"; + +import { contextState } from "./atoms/contextState"; +import { IFilePickerResult } from "./IFilePickerResult"; +/* import { RenderSpinner } from "./RenderSpninner/RenderSpinner"; */ +import { SelectFromSharePoint } from "./selectFromSharePoint"; +import { useImagePickerStyles } from "./useImagePickerStyles"; + +export interface IImagePickerProps { + onFileSelected: (file: IFilePickerResult) => void; + onDeleteFile: () => void; + selectedFileUrl: string; + context: BaseComponentContext; +} + +/** + * Renders the preview image component. + * + * @param props - The component props. + * @param props.selectedImageFileUrl - The URL of the selected image file. + * @returns The JSX element representing the preview image component. + */ +const RenderPreviewImage = (props: { selectedImageFileUrl: string }): JSX.Element => { + const { selectedImageFileUrl } = props; + + const maxWidth = 200; + const maxHeight = 200; + const styles = useImagePickerStyles(); + + if (!selectedImageFileUrl) { + return null; + } + + return ( + <> +
+ Selected Image +
+ + ); +}; + +/** + * Renders an image picker component. + * + * @component + * @example + * ```tsx + * + * ``` + */ + +export const ImagePicker: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const { onFileSelected, onDeleteFile, selectedFileUrl, context } = props; + const [isOpen, setIsOpen] = React.useState(false); + const styles = useImagePickerStyles(); + const ref = React.useRef(null); + const [appContext, setAppContext] = useAtom(contextState); + + React.useEffect(() => { + setAppContext({ + ...appContext, + context: context, + }); + }, []); + + const [selectedImageFileUrl, setSelectedImageFileUrl] = React.useState(selectedFileUrl); + + const onDismiss = (): void => { + setIsOpen(false); + }; + + const isFileSelected = React.useMemo(() => { + return !!selectedImageFileUrl; + }, [selectedImageFileUrl]); + + const onDeleteFileCLick = React.useCallback(() => { + setSelectedImageFileUrl(undefined); + onDeleteFile(); + }, []); + + const styleButtonDelete: React.CSSProperties = { display: !isFileSelected ? "none" : "inline-flex" }; + + if (!context) return null; + + return ( + <> +
+
+ + +
+ { + onFileSelected(file); + setSelectedImageFileUrl(file.previewDataUrl); + + onDismiss(); + }} + /> + {} +
+ + ); +}; diff --git a/src/controls/imagePicker/RenderSpninner/RenderSpinner.tsx b/src/controls/imagePicker/RenderSpninner/RenderSpinner.tsx new file mode 100644 index 000000000..2751f3117 --- /dev/null +++ b/src/controls/imagePicker/RenderSpninner/RenderSpinner.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import { + makeStyles, + mergeClasses, + Spinner, +} from '@fluentui/react-components'; + +const useStyles = makeStyles({ + root: { + display: "flex", + justifyContent: "center", + alignItems: "center", + + height: "100%", + width: "100%", + }, + spinner: { + width: "100px", + height: "100px", + }, + }); + +export interface IRenderSpinnerProps { + size: "medium" | "small" | "extra-tiny" | "tiny" | "extra-small" | "large" | "extra-large" | "huge"; + label?: string; + labelPosition?: "above" | "below" | "before" | "after"; + style?: React.CSSProperties; + className?: string; +} + +export const RenderSpinner: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const { size, label, labelPosition, style, className } = props; + + const styles = useStyles(); + return ( +
+ +
+ ); +}; diff --git a/src/controls/imagePicker/SelectStokImage.tsx b/src/controls/imagePicker/SelectStokImage.tsx new file mode 100644 index 000000000..88259975d --- /dev/null +++ b/src/controls/imagePicker/SelectStokImage.tsx @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import * as React from "react"; + +import { ApplicationCustomizerContext } from "@microsoft/sp-application-base"; +import { BaseComponentContext } from "@microsoft/sp-component-base"; + +import { + CONTENT_IMAGE_STOCK_URL, + CONTENT_URL, +} from "./constants/constants"; +import { useSpAPI } from "./hooks/useSpAPI"; +import { useUtils } from "./hooks/useUtils"; +import { IFilePickerResult } from "./IFilePickerResult"; +import { + StockImagesEvent, + SubmitValue, +} from "./StockImagesModel"; +import { useImagePickerStyles } from "./useImagePickerStyles"; + +export interface ISelectStockImageProps { + onFileSelected: (file: any) => void; + onCancel: () => void; + context: ApplicationCustomizerContext | BaseComponentContext | undefined; +} + +/** + * Renders a component that allows the user to select a stock image. + * + * @component + * @example + * ```tsx + * + * ``` + */ + +export const SelectStockImage: React.FunctionComponent = ( + props: React.PropsWithChildren +) => { + const { context, onFileSelected, onCancel } = props; + + const { getFileNameFromUrl, getFileNameWithoutExtension } = useUtils(); + const { downloadBingContent } = useSpAPI(context); + const styles = useImagePickerStyles(); + + const handleSave = (event: StockImagesEvent): void => { + let filePickerResult: IFilePickerResult = null; + const cdnFileInfo: SubmitValue = + event.Values && (event.Values as SubmitValue[]).length > 0 ? (event.Values as SubmitValue[])[0] : null; + if (cdnFileInfo) { + filePickerResult = { + downloadFileContent: () => { + return downloadBingContent( + cdnFileInfo.sourceUrl, + getFileNameFromUrl(getFileNameFromUrl(cdnFileInfo.sourceUrl)) + ); + }, + fileAbsoluteUrl: cdnFileInfo.sourceUrl, + fileName: getFileNameFromUrl(cdnFileInfo.sourceUrl), + fileNameWithoutExtension: getFileNameWithoutExtension(cdnFileInfo.sourceUrl), + previewDataUrl: cdnFileInfo.sourceUrl, + }; + } + onFileSelected(filePickerResult); + }; + + const handleImageIframeEvent = (event: MessageEvent) => { + if (!event || !event.origin || event.origin.indexOf(CONTENT_URL) !== 0) { + return; + } + + const eventData: StockImagesEvent = JSON.parse(event.data); + + if (eventData.MessageId === "AddItem") { + handleSave(eventData); + } else if (eventData.MessageId === "CancelDialog") { + onCancel(); + } + }; + + React.useLayoutEffect(() => { + window.addEventListener("message", handleImageIframeEvent); + return () => { + window.removeEventListener("message", handleImageIframeEvent); + }; + }, []); + + return ( + <> +
+