diff --git a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md index ce85b752b3..5617a3672b 100644 --- a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md +++ b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We made "Associated files/images" preconfigured with corresponding entities. +- We made it possible to configure custom buttons for file uploader entries. + ### Fixed - We fixed an issue with hover colors of the dropzone. diff --git a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts index 7bbad26d0b..7275c6973a 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts @@ -26,6 +26,20 @@ export function getProperties( ]); } + if (values.enableCustomButtons) { + values.customButtons.forEach((_button, index) => { + hideNestedPropertiesIn( + properties, + values, + "customButtons", + index, + values.uploadMode === "files" ? ["buttonActionImage"] : ["buttonActionFile"] + ); + }); + } else { + hidePropertiesIn(properties, values, ["customButtons"]); + } + return properties; } @@ -81,6 +95,23 @@ export function check(values: FileUploaderPreviewProps): Problem[] { message: "There must be at least one file per upload allowed." }); } + + if (values.enableCustomButtons) { + // check that at max one actions is default + const defaultIdx = new Set(); + values.customButtons.forEach((_button, index) => { + if (_button.buttonIsDefault) { + defaultIdx.add(index); + } + }); + + if (defaultIdx.size > 1) { + errors.push({ + property: `customButtons`, + message: `Only one default button is allowed.` + }); + } + } } return errors; diff --git a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml index a7c122d03a..3d17ac233e 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml @@ -194,6 +194,43 @@ Object creation timeout Consider uploads unsuccessful if the Action to create new files/images does not create new objects within the configured amount of seconds. + + Enable custom buttons + + + + Custom buttons + + + + + Caption + + + + Icon + + + + Action + + + + Action + + + + Is default + The action will be triggered by clicking on the file entry. + + + Is visible + The button will be hidden if false is returned. + + + + + diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/ActionButton.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/ActionButton.tsx new file mode 100644 index 0000000000..8ccf4572e8 --- /dev/null +++ b/packages/pluggableWidgets/file-uploader-web/src/components/ActionButton.tsx @@ -0,0 +1,55 @@ +import { createElement, MouseEvent, ReactElement, useCallback } from "react"; +import classNames from "classnames"; +import { ListActionValue } from "mendix"; +import { FileStore } from "../stores/FileStore"; + +interface ActionButtonProps { + icon: ReactElement; + title?: string; + action?: () => void; + isDisabled: boolean; +} + +export function ActionButton({ action, icon, title, isDisabled }: ActionButtonProps) { + const onClick = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + action?.(); + }, + [action] + ); + return ( + + ); +} + +interface FileActionButtonProps { + store: FileStore; + listAction?: ListActionValue; + title?: string; + icon: ReactElement; +} + +export function FileActionButton({ listAction, store, title, icon }: FileActionButtonProps) { + const action = useCallback(() => { + store.executeAction(listAction); + }, [store, listAction]); + + return ( + + ); +} diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx new file mode 100644 index 0000000000..1dc1ba4bc5 --- /dev/null +++ b/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx @@ -0,0 +1,80 @@ +import { createElement, ReactElement, useCallback } from "react"; +import { FileUploaderContainerProps } from "../../typings/FileUploaderProps"; +import { ActionButton, FileActionButton } from "./ActionButton"; +import { IconInternal } from "@mendix/widget-plugin-component-kit/IconInternal"; +import { FileStore } from "../stores/FileStore"; +import { useTranslationsStore } from "../utils/useTranslationsStore"; + +interface ButtonsBarProps { + actions?: FileUploaderContainerProps["customButtons"]; + store: FileStore; +} + +export const ActionsBar = ({ actions, store }: ButtonsBarProps): ReactElement | null => { + if (!actions) { + return ; + } + + if (actions && store.canExecuteActions) { + return ( +
+ {actions.map(a => { + if (!a.buttonIsVisible.value) { + return null; + } + const listAction = a.buttonActionImage ?? a.buttonActionFile; + + return ( + } + title={a.buttonCaption.value} + store={store} + listAction={listAction} + /> + ); + })} +
+ ); + } + + return null; +}; + +function DefaultActionsBar(props: ButtonsBarProps) { + const translations = useTranslationsStore(); + + const onRemove = useCallback(() => { + props.store.remove(); + }, [props.store]); + + const onViewClick = useCallback(() => { + onDownloadClick(props.store.downloadUrl); + }, [props.store.downloadUrl]); + + return ( +
+ } + title={translations.get("downloadButtonTextMessage")} + action={onViewClick} + isDisabled={!props.store.canDownload} + /> + } + title={translations.get("removeButtonTextMessage")} + action={onRemove} + isDisabled={!props.store.canRemove} + /> +
+ ); +} + +function onDownloadClick(fileUrl: string | undefined): void { + if (!fileUrl) { + return; + } + const url = new URL(fileUrl); + url.searchParams.append("target", "window"); + + window.open(url, "mendix_file"); +} diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/FileEntry.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/FileEntry.tsx index ec09676e89..fe8f42a199 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/components/FileEntry.tsx +++ b/packages/pluggableWidgets/file-uploader-web/src/components/FileEntry.tsx @@ -1,21 +1,33 @@ import classNames from "classnames"; import { ProgressBar } from "./ProgressBar"; import { UploadInfo } from "./UploadInfo"; -import { createElement, ReactElement, useCallback, MouseEvent, KeyboardEvent } from "react"; +import { createElement, ReactElement, useCallback, MouseEvent, KeyboardEvent, ReactNode } from "react"; import { FileStatus, FileStore } from "../stores/FileStore"; import { observer } from "mobx-react-lite"; import fileSize from "filesize.js"; import { FileIcon } from "./FileIcon"; -import { useTranslationsStore } from "../utils/useTranslationsStore"; +import { FileUploaderContainerProps } from "../../typings/FileUploaderProps"; +import { ActionsBar } from "./ActionsBar"; interface FileEntryContainerProps { store: FileStore; + actions?: FileUploaderContainerProps["customButtons"]; } -export const FileEntryContainer = observer(({ store }: FileEntryContainerProps): ReactElement => { - const onRemove = useCallback(() => { - store.remove(); - }, [store]); +export const FileEntryContainer = observer(({ store, actions }: FileEntryContainerProps): ReactElement | null => { + const defaultAction = actions?.find(a => { + return a.buttonIsDefault; + }); + + const defaultListAction = defaultAction?.buttonActionFile ?? defaultAction?.buttonActionImage; + + const onDefaultAction = useCallback(() => { + store.executeAction(defaultListAction); + }, [store, defaultListAction]); + + if (store.fileStatus === "missing") { + return null; + } return ( } /> ); }); @@ -42,25 +52,21 @@ interface FileEntryProps { fileStatus: FileStatus; errorMessage?: string; - canRemove: boolean; - onRemove: () => void; + defaultAction?: () => void; - canDownload: boolean; - downloadUrl?: string; + actions?: ReactNode; } function FileEntry(props: FileEntryProps): ReactElement { - const translations = useTranslationsStore(); + const { defaultAction } = props; - const { canDownload, downloadUrl, onRemove } = props; - - const onViewClick = useCallback( + const onClick = useCallback( (e: MouseEvent) => { e.stopPropagation(); e.preventDefault(); - onDownloadClick(downloadUrl); + defaultAction?.(); }, - [downloadUrl] + [defaultAction] ); const onKeyDown = useCallback( @@ -68,18 +74,10 @@ function FileEntry(props: FileEntryProps): ReactElement { if (e.code === "Enter" || e.code === "Space") { e.stopPropagation(); e.preventDefault(); - onDownloadClick(downloadUrl); + defaultAction?.(); } }, - [downloadUrl] - ); - - const onRemoveClick = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - onRemove(); - }, - [onRemove] + [defaultAction] ); return ( @@ -89,9 +87,9 @@ function FileEntry(props: FileEntryProps): ReactElement { invalid: props.fileStatus === "validationError" })} title={props.title} - tabIndex={canDownload ? 0 : undefined} - onClick={canDownload ? onViewClick : undefined} - onKeyDown={canDownload ? onKeyDown : undefined} + tabIndex={!!defaultAction ? 0 : undefined} + onClick={!!defaultAction ? onClick : undefined} + onKeyDown={!!defaultAction ? onKeyDown : undefined} >
{props.size !== -1 && fileSize(props.size)}
-
- {downloadUrl &&
} - -
+ {props.actions}
@@ -134,11 +120,3 @@ function FileEntry(props: FileEntryProps): ReactElement {
); } - -function onDownloadClick(fileUrl: string | undefined): void { - if (!fileUrl) { - return; - } - const url = `${fileUrl}&target=window`; - window.open(url, "mendix_file"); -} diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx index 21aa591be4..c177d38dd5 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx +++ b/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx @@ -35,7 +35,13 @@ export const FileUploaderRoot = observer((props: FileUploaderContainerProps): Re
{(rootStore.files ?? []).map(fileStore => { - return ; + return ( + + ); })}
diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts index bfd72b36df..8d3ce414b8 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts @@ -1,5 +1,5 @@ import { Big } from "big.js"; -import { ObjectItem } from "mendix"; +import { ListActionValue, ObjectItem } from "mendix"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import mimeTypes from "mime-types"; @@ -13,9 +13,11 @@ import { removeObject, saveFile } from "../utils/mx-data"; +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; export type FileStatus = | "existingFile" + | "missing" | "new" | "uploading" | "done" @@ -57,10 +59,31 @@ export class FileStore { canRemove: computed, imagePreviewUrl: computed, upload: action, - fetchMxObject: action + fetchMxObject: action, + markMissing: action }); } + markMissing() { + this.fileStatus = "missing"; + this._mxObject = undefined; + this._objectItem = undefined; + } + + canExecute(listAction: ListActionValue): boolean { + if (!this._objectItem) { + return false; + } + + return listAction.get(this._objectItem).canExecute; + } + + executeAction(listAction?: ListActionValue): void { + if (listAction && this._objectItem) { + executeAction(listAction.get(this._objectItem)); + } + } + validate(): boolean { return !(this.fileStatus !== "new" || !this._file); } @@ -93,7 +116,7 @@ export class FileStore { get title(): string { if (this._mxObject) { - return this._mxObject?.get2("Name").toString(); + return this._mxObject.get2("Name")?.toString(); } return this._file?.name ?? "..."; @@ -101,7 +124,7 @@ export class FileStore { get size(): number { if (this._mxObject) { - return (this._mxObject.get2("Size") as Big).toNumber(); + return (this._mxObject.get2("Size") as Big)?.toNumber(); } return this._file?.size ?? -1; @@ -119,6 +142,10 @@ export class FileStore { return this.fileStatus === "done" || this.fileStatus === "existingFile"; } + get canExecuteActions(): boolean { + return !!this._objectItem; + } + async remove(): Promise { if (!this.canRemove || !this._objectItem) { return; diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts index 40240e8d72..e54117f0ff 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts @@ -3,17 +3,18 @@ import { FileUploaderContainerProps, UploadModeEnum } from "../../typings/FileUp import { action, computed, makeObservable, observable } from "mobx"; import { getImageUploaderFormats, parseAllowedFormats } from "../utils/parseAllowedFormats"; import { FileStore } from "./FileStore"; -import { fileHasContents } from "../utils/mx-data"; import { FileRejection } from "react-dropzone"; import { FileCheckFormat } from "../utils/predefinedFormats"; import { TranslationsStore } from "./TranslationsStore"; import { ObjectCreationHelper } from "../utils/ObjectCreationHelper"; +import { DatasourceUpdateProcessor } from "../utils/DatasourceUpdateProcessor"; export class FileUploaderStore { files: FileStore[] = []; lastSeenItems: Set = new Set(); objectCreationHelper: ObjectCreationHelper; + updateProcessor: DatasourceUpdateProcessor; existingItemsLoaded = false; isReadOnly: boolean; @@ -39,6 +40,29 @@ export class FileUploaderStore { this._uploadMode = props.uploadMode; this.objectCreationHelper = new ObjectCreationHelper(this._widgetName, props.objectCreationTimeout); + this.updateProcessor = new DatasourceUpdateProcessor({ + loaded: () => { + this.objectCreationHelper.enable(); + }, + processNew: (newItem: ObjectItem) => { + this.objectCreationHelper.processEmptyObjectItem(newItem); + }, + processExisting: (existingItem: ObjectItem) => { + this.processExistingFileItem(existingItem); + }, + processMissing: (missingItem: ObjectItem) => { + const missingFile = this.files.find(f => { + return f._objectItem?.id === missingItem.id; + }); + + if (!missingFile) { + console.warn(`Object ${missingItem.id} is not found in file stores.`); + return; + } + + missingFile?.markMissing(); + } + }); this.isReadOnly = props.readOnlyMode; @@ -71,28 +95,7 @@ export class FileUploaderStore { } this.translations.updateProps(props); - - const itemsDs = this._ds; - if (!this.existingItemsLoaded) { - if (itemsDs.status === "available" && itemsDs.items) { - for (const item of itemsDs.items) { - this.processExistingFileItem(item); - } - - this.existingItemsLoaded = true; - this.objectCreationHelper.enable(); - } - } else { - for (const newItem of findNewItems(this.lastSeenItems, itemsDs.items || [])) { - if (!fileHasContents(newItem)) { - this.lastSeenItems.add(newItem.id); - this.objectCreationHelper.processEmptyObjectItem(newItem); - } else { - // adding this file to the list as is as this file is not empty and probably created externally - this.processExistingFileItem(newItem); - } - } - } + this.updateProcessor.processUpdate(this._ds); } processExistingFileItem(item: ObjectItem): void { @@ -169,7 +172,3 @@ export class FileUploaderStore { } } } - -function findNewItems(lastSeenItems: Set, currentItems: ObjectItem[]): ObjectItem[] { - return currentItems.filter(i => !lastSeenItems.has(i.id)); -} diff --git a/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss b/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss index f49296708a..c5e1cdc020 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss +++ b/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss @@ -7,8 +7,14 @@ $file-brand-warning: #eca51c; $file-color-danger-lighter: #f9d9dc; $file-brand-danger: #e33f4e; $file-gray-light: #6c7180; +$file-gray-darker: #3b4251; $file-brand-success: #3cb33d; +$file-action-icon-hover: #2a39b8; +$file-action-icon-active: #264ae5; +$file-dropzone-color: #8f620b; +$file-progress-background-color: #f0f1f2; + $file-icon: url(../assets/file-icon.svg); $file-warning-icon: url(../assets/warning-icon.svg); $file-view-icon: url(../assets/view-icon.svg); @@ -89,7 +95,7 @@ Place your custom CSS here .dropzone-message { font-size: 12px; line-height: 17px; - color: #8f620b; + color: var(--file-dropzone-color, $file-dropzone-color); padding-left: 18px; margin-bottom: 1em; @@ -184,7 +190,7 @@ Place your custom CSS here .entry-details-main-name { flex: 1; font-weight: 600; - color: #3b4251; + color: var(--gray-darker, $file-gray-darker); text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -218,12 +224,18 @@ Place your custom CSS here .remove-icon { background-image: var(--file-remove-icon-hover, $file-remove-icon-hover); } + .file-action-icon { + color: var(--file-action-icon-hover, $file-action-icon-hover); + } } &:active { .remove-icon { background-image: var(--file-remove-icon-active, $file-remove-icon-active); } + .file-action-icon { + color: var(--file-action-icon-active, $file-action-icon-active); + } } &.disabled { @@ -231,6 +243,11 @@ Place your custom CSS here } } + .file-action-icon { + font-size: 24px; + color: var(--brand-primary, $file-brand-primary); + } + .remove-icon { height: 24px; width: 24px; @@ -248,7 +265,7 @@ Place your custom CSS here height: 4px; border-radius: 2px; - background-color: #f0f1f2; + background-color: var(--file-progress-background-color, $file-progress-background-color); margin: 5px 0; width: 100%; overflow: hidden; diff --git a/packages/pluggableWidgets/file-uploader-web/src/utils/DatasourceUpdateProcessor.ts b/packages/pluggableWidgets/file-uploader-web/src/utils/DatasourceUpdateProcessor.ts new file mode 100644 index 0000000000..7de8431d49 --- /dev/null +++ b/packages/pluggableWidgets/file-uploader-web/src/utils/DatasourceUpdateProcessor.ts @@ -0,0 +1,68 @@ +import { ListValue, ObjectItem } from "mendix"; +import { fileHasContents } from "./mx-data"; + +export interface DatasourceUpdateProcessorCallbacks { + loaded(): void; + processExisting(item: ObjectItem): void; + processNew(item: ObjectItem): void; + processMissing(item: ObjectItem): void; +} + +export class DatasourceUpdateProcessor { + existingItemsLoaded = false; + + seenItems = new Set(); + guidToObject = new Map(); + + constructor(private readonly callbacks: DatasourceUpdateProcessorCallbacks) {} + + addToMap(obj: ObjectItem): void { + this.guidToObject.set(obj.id, obj); + } + + processUpdate(itemsDs: ListValue): void { + if (!this.existingItemsLoaded) { + if (itemsDs.status === "available" && itemsDs.items) { + for (const item of itemsDs.items) { + this.seenItems.add(item.id); + this.callbacks.processExisting(item); + } + + this.existingItemsLoaded = true; + this.callbacks.loaded(); + } + } + + const currentItems = itemsDs.items; + + if (!currentItems) { + return; + } + + const currentItemsSet = new Set(); + currentItems.forEach(item => { + this.addToMap(item); + currentItemsSet.add(item.id); + }); + + let newItems = new Set([...currentItemsSet].filter(x => !this.seenItems.has(x))); + let missingItems = new Set([...this.seenItems].filter(x => !currentItemsSet.has(x))); + + // missing + for (const missingItem of missingItems) { + this.seenItems.delete(missingItem); + this.callbacks.processMissing(this.guidToObject.get(missingItem)!); + } + + // new + for (const newItem of newItems) { + const obj = this.guidToObject.get(newItem)!; + this.seenItems.add(newItem); + if (fileHasContents(obj)) { + this.callbacks.processExisting(obj); + } else { + this.callbacks.processNew(obj); + } + } + } +} diff --git a/packages/pluggableWidgets/file-uploader-web/src/utils/__tests__/DatasourceUpdateProcessor.spec.ts b/packages/pluggableWidgets/file-uploader-web/src/utils/__tests__/DatasourceUpdateProcessor.spec.ts new file mode 100644 index 0000000000..70bcbda22c --- /dev/null +++ b/packages/pluggableWidgets/file-uploader-web/src/utils/__tests__/DatasourceUpdateProcessor.spec.ts @@ -0,0 +1,90 @@ +import { DatasourceUpdateProcessor, DatasourceUpdateProcessorCallbacks } from "../DatasourceUpdateProcessor"; +import { ListValueBuilder, obj } from "@mendix/widget-plugin-test-utils"; +import { ObjectItem } from "mendix"; + +let fileHasContentsMock = jest.fn(); +jest.mock("../mx-data", () => ({ + fileHasContents: (...args: any[]) => fileHasContentsMock(...args) +})); + +describe("DatasourceUpdateProcessor", () => { + let dsUpdater: DatasourceUpdateProcessor; + let callbacks: DatasourceUpdateProcessorCallbacks; + beforeEach(() => { + fileHasContentsMock.mockImplementation(() => true); + callbacks = { + loaded: jest.fn(), + processExisting: jest.fn(), + processNew: jest.fn(), + processMissing: jest.fn() + }; + + dsUpdater = new DatasourceUpdateProcessor(callbacks); + }); + describe("loaded callback", () => { + describe("when updated with loading ds", () => { + beforeEach(() => { + dsUpdater.processUpdate(new ListValueBuilder().isLoading().build()); + }); + }); + test("'loaded' is not yet called", () => { + expect(callbacks.loaded).not.toHaveBeenCalled(); + }); + + describe("when called with loaded ds", () => { + beforeEach(() => { + dsUpdater.processUpdate(new ListValueBuilder().withItems([obj("A"), obj("B"), obj("C")]).build()); + }); + + test("'loaded' is called", () => { + expect(callbacks.loaded).toHaveBeenCalledTimes(1); + }); + + test("'processExisting' is called", () => { + expect(callbacks.processExisting).toHaveBeenCalledTimes(3); + expect((callbacks.processExisting as jest.Mock).mock.calls).toEqual([ + [obj("A")], + [obj("B")], + [obj("C")] + ]); + }); + + describe("when called with new empty, new non-empty object and missing object", () => { + beforeEach(() => { + fileHasContentsMock.mockImplementation((obj: ObjectItem) => { + // Simulate that D is an empty object + return obj.id !== "obj_D"; + }); + + (callbacks.processExisting as jest.Mock).mockClear(); + (callbacks.processNew as jest.Mock).mockClear(); + (callbacks.processMissing as jest.Mock).mockClear(); + (callbacks.loaded as jest.Mock).mockClear(); + + dsUpdater.processUpdate( + new ListValueBuilder().withItems([obj("A"), obj("C"), obj("D"), obj("E")]).build() + ); + }); + + test("'loaded' is not called again", () => { + expect(callbacks.loaded).not.toHaveBeenCalled(); + }); + + test("'processMissing' is not called", () => { + expect(callbacks.processMissing).toHaveBeenCalledTimes(1); + expect(callbacks.processMissing).toHaveBeenCalledWith(obj("B")); + }); + + test("'processExisting' is not called", () => { + expect(callbacks.processMissing).toHaveBeenCalledTimes(1); + expect(callbacks.processExisting).toHaveBeenCalledWith(obj("E")); + }); + + test("'processNew' is called", () => { + expect(callbacks.processMissing).toHaveBeenCalledTimes(1); + expect(callbacks.processNew).toHaveBeenCalledWith(obj("D")); + }); + }); + }); + }); +}); diff --git a/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts b/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts index d5000ae57d..ff42ba5a5d 100644 --- a/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts +++ b/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts @@ -4,7 +4,7 @@ * @author Mendix Widgets Framework Team */ import { CSSProperties } from "react"; -import { ActionValue, DynamicValue, ListValue } from "mendix"; +import { ActionValue, DynamicValue, ListValue, ListActionValue, WebIcon } from "mendix"; export type UploadModeEnum = "files" | "images"; @@ -20,6 +20,15 @@ export interface AllowedFileFormatsType { typeFormatDescription: DynamicValue; } +export interface CustomButtonsType { + buttonCaption: DynamicValue; + buttonIcon: DynamicValue; + buttonActionFile?: ListActionValue; + buttonActionImage?: ListActionValue; + buttonIsDefault: boolean; + buttonIsVisible: DynamicValue; +} + export interface AllowedFileFormatsPreviewType { configMode: ConfigModeEnum; predefinedType: PredefinedTypeEnum; @@ -28,6 +37,15 @@ export interface AllowedFileFormatsPreviewType { typeFormatDescription: string; } +export interface CustomButtonsPreviewType { + buttonCaption: string; + buttonIcon: { type: "glyph"; iconClass: string; } | { type: "image"; imageUrl: string; iconUrl: string; } | { type: "icon"; iconClass: string; } | undefined; + buttonActionFile: {} | null; + buttonActionImage: {} | null; + buttonIsDefault: boolean; + buttonIsVisible: string; +} + export interface FileUploaderContainerProps { name: string; class: string; @@ -57,6 +75,8 @@ export interface FileUploaderContainerProps { removeSuccessMessage: DynamicValue; removeErrorMessage: DynamicValue; objectCreationTimeout: number; + enableCustomButtons: boolean; + customButtons: CustomButtonsType[]; } export interface FileUploaderPreviewProps { @@ -94,4 +114,6 @@ export interface FileUploaderPreviewProps { removeSuccessMessage: string; removeErrorMessage: string; objectCreationTimeout: number | null; + enableCustomButtons: boolean; + customButtons: CustomButtonsPreviewType[]; }