Skip to content

[WC-2887] Preconfigured associations for File Uploader and color fix and custom buttons #1500

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/modules/file-uploader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"moduleFolderNameInModeler": "fileuploader",
"marketplace": {
"minimumMXVersion": "10.21.0.64362",
"minimumMXVersion": "10.22.0.68245",
"appNumber": 235351,
"appName": "File Uploader"
},
Expand Down
10 changes: 10 additions & 0 deletions packages/pluggableWidgets/file-uploader-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- 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

## [2.1.0] - 2025-04-16

### Changed
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/file-uploader-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"packagePath": "com.mendix.widget.web",
"marketplace": {
"minimumMXVersion": "10.21.0.64362",
"minimumMXVersion": "10.22.0.68245",
"appName": "File Uploader"
},
"testProject": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<number>();
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
<enumerationValue key="images">Images</enumerationValue>
</enumerationValues>
</property>
<property key="associatedFiles" type="datasource" isList="true">
<property key="associatedFiles" type="datasource" isList="true" defaultType="Association" defaultValue="FileUploader.FileUploadContext/FileUploader.UploadedFile_FileUploadContext/FileUploader.UploadedFile">
<caption>Associated files</caption>
<description />
</property>
<property key="associatedImages" type="datasource" isList="true">
<property key="associatedImages" type="datasource" isList="true" defaultType="Association" defaultValue="FileUploader.FileUploadContext/FileUploader.UploadedImage_FileUploadContext/FileUploader.UploadedImage">
<caption>Associated images</caption>
<description />
</property>
Expand Down Expand Up @@ -194,6 +194,43 @@
<caption>Object creation timeout</caption>
<description>Consider uploads unsuccessful if the Action to create new files/images does not create new objects within the configured amount of seconds.</description>
</property>
<property key="enableCustomButtons" type="boolean" defaultValue="false">
<caption>Enable custom buttons</caption>
<description />
</property>
<property key="customButtons" type="object" isList="true" required="false">
<caption>Custom buttons</caption>
<description />
<properties>
<propertyGroup caption="General">
<property key="buttonCaption" type="textTemplate">
<caption>Caption</caption>
<description />
</property>
<property key="buttonIcon" type="icon">
<caption>Icon</caption>
<description />
</property>
<property key="buttonActionFile" type="action" dataSource="../associatedFiles">
<caption>Action</caption>
<description />
</property>
<property key="buttonActionImage" type="action" dataSource="../associatedImages">
<caption>Action</caption>
<description />
</property>
<property key="buttonIsDefault" type="boolean" defaultValue="false">
<caption>Default file action</caption>
<description>When set to Yes, the action will be triggered by clicking on the file entry.</description>
</property>
<property key="buttonIsVisible" type="expression" defaultValue="true">
<caption>Visible</caption>
<description>The button will be hidden when false is returned.</description>
<returnType type="Boolean" />
</property>
</propertyGroup>
</properties>
</property>
</propertyGroup>
</properties>
</widget>
Original file line number Diff line number Diff line change
@@ -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): ReactElement {
const onClick = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
action?.();
},
[action]
);
return (
<button
role={"button"}
className={classNames("action-button", {
disabled: isDisabled
})}
onClick={onClick}
title={title}
>
{icon}
</button>
);
}

interface FileActionButtonProps {
store: FileStore;
listAction?: ListActionValue;
title?: string;
icon: ReactElement;
}

export function FileActionButton({ listAction, store, title, icon }: FileActionButtonProps): ReactElement {
const action = useCallback(() => {
store.executeAction(listAction);
}, [store, listAction]);

return (
<ActionButton
icon={icon}
title={title}
action={action}
isDisabled={!(!listAction || store.canExecute(listAction))}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 <DefaultActionsBar store={store} />;
}

if (actions && store.canExecuteActions) {
return (
<div className={"entry-details-actions"}>
{actions.map((a, i) => {
if (!a.buttonIsVisible.value) {
return null;
}
const listAction = a.buttonActionImage ?? a.buttonActionFile;

return (
<FileActionButton
key={i}
icon={<IconInternal icon={a.buttonIcon.value} className={"file-action-icon"} />}
title={a.buttonCaption.value}
store={store}
listAction={listAction}
/>
);
})}
</div>
);
}

return null;
};

function DefaultActionsBar(props: ButtonsBarProps): ReactElement {
const translations = useTranslationsStore();

const onRemove = useCallback(() => {
props.store.remove();
}, [props.store]);

const onViewClick = useCallback(() => {
onDownloadClick(props.store.downloadUrl);
}, [props.store.downloadUrl]);

return (
<div className={"entry-details-actions"}>
<ActionButton
icon={<span className={"download-icon"} aria-hidden />}
title={translations.get("downloadButtonTextMessage")}
action={onViewClick}
isDisabled={!props.store.canDownload}
/>
<ActionButton
icon={<span className={"remove-icon"} aria-hidden />}
title={translations.get("removeButtonTextMessage")}
action={onRemove}
isDisabled={!props.store.canRemove}
/>
</div>
);
}

function onDownloadClick(fileUrl: string | undefined): void {
if (!fileUrl) {
return;
}
const url = new URL(fileUrl);
url.searchParams.append("target", "window");

window.open(url, "mendix_file");
}
Loading
Loading