Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/pluggableWidgets/image-cropper-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Initial release of Image cropper widget.
- Custom aspect ratio width and height can be set with an expression or bound to an attribute.
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@ function StaticCropPreview(props: { imageUrl: string; values: ImageCropperPrevie
const [crop, setCrop] = useState<Crop | undefined>(undefined);
const imageRef = createRef<HTMLImageElement>();

// Preview only has the expression *text* (no runtime data). Numeric literals render a real
// ratio; an attribute/expression path can't be evaluated here, so it falls back to free aspect.
const toNumber = (v: string | null): number | undefined => {
const n = Number(v);
return v != null && v !== "" && Number.isFinite(n) ? n : undefined;
};
const aspect = resolveAspectRatio(
values.aspectRatio,
values.customAspectWidth ?? 0,
values.customAspectHeight ?? 0
toNumber(values.customAspectWidth),
toNumber(values.customAspectHeight)
);

const handleImageLoad = (percentCrop: Crop): void => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
<enumerationValue key="custom">Custom</enumerationValue>
</enumerationValues>
</property>
<property key="customAspectWidth" type="integer" defaultValue="1" required="true">
<property key="customAspectWidth" type="expression" defaultValue="1" required="true">
<caption>Custom aspect width</caption>
<description>Width side of the ratio (e.g. 3 in 3:2). Used when Aspect ratio is Custom.</description>
<description>Width side of the ratio (e.g. 3 in 3:2). Used when Aspect ratio is Custom. Can be bound to an attribute or expression.</description>
<returnType type="Integer" />
</property>
<property key="customAspectHeight" type="integer" defaultValue="1" required="true">
<property key="customAspectHeight" type="expression" defaultValue="1" required="true">
<caption>Custom aspect height</caption>
<description>Height side of the ratio (e.g. 2 in 3:2). Used when Aspect ratio is Custom.</description>
<description>Height side of the ratio (e.g. 2 in 3:2). Used when Aspect ratio is Custom. Can be bound to an attribute or expression.</description>
<returnType type="Integer" />
</property>
</propertyGroup>
<propertyGroup caption="Events">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ function makePreviewProps(overrides: Partial<ImageCropperPreviewProps> = {}): Im
image: null,
cropShape: "rect",
aspectRatio: "free",
customAspectWidth: null,
customAspectHeight: null,
customAspectWidth: "1",
customAspectHeight: "1",
onCropAction: null,
boundaryWidth: null,
boundaryHeight: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ function makeProps(overrides: Partial<ImageCropperContainerProps> = {}): ImageCr
image: makeImageProp(),
cropShape: "rect",
aspectRatio: "free",
customAspectWidth: 1,
customAspectHeight: 1,
customAspectWidth: dynamic.available(new Big(1)),
customAspectHeight: dynamic.available(new Big(1)),
boundaryWidth: 300,
boundaryHeight: 300,
resizableEnabled: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Big } from "big.js";
import { ValueStatus } from "mendix";
import { Ref } from "react";
import type { Crop, PixelCrop } from "react-image-crop";
import { actionValue } from "@mendix/widget-plugin-test-utils";
import { actionValue, dynamic } from "@mendix/widget-plugin-test-utils";
import type { ImageCropperContainerProps } from "../../typings/ImageCropperProps";

// Integration test: proves grayscale reversibility after rotate.
Expand Down Expand Up @@ -96,8 +96,8 @@ function makeProps(overrides: Partial<ImageCropperContainerProps> = {}): ImageCr
image: makeImageProp(),
cropShape: "rect",
aspectRatio: "free",
customAspectWidth: 1,
customAspectHeight: 1,
customAspectWidth: dynamic.available(new Big(1)),
customAspectHeight: dynamic.available(new Big(1)),
boundaryWidth: 300,
boundaryHeight: 300,
resizableEnabled: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Big } from "big.js";
import { ValueStatus } from "mendix";
import { Ref } from "react";
import type { Crop, PixelCrop } from "react-image-crop";
import { actionValue } from "@mendix/widget-plugin-test-utils";
import { actionValue, dynamic } from "@mendix/widget-plugin-test-utils";
import type { ImageCropperContainerProps } from "../../typings/ImageCropperProps";

// Multi-instance integration test: verifies that auto-commit only fires for the
Expand Down Expand Up @@ -88,8 +88,8 @@ function makeProps(overrides: Partial<ImageCropperContainerProps> = {}): ImageCr
image: makeImageProp(),
cropShape: "rect",
aspectRatio: "free",
customAspectWidth: 1,
customAspectHeight: 1,
customAspectWidth: dynamic.available(new Big(1)),
customAspectHeight: dynamic.available(new Big(1)),
boundaryWidth: 300,
boundaryHeight: 300,
resizableEnabled: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Big } from "big.js";
import { ValueStatus } from "mendix";
import { Ref } from "react";
import type { Crop, PixelCrop } from "react-image-crop";
import { actionValue } from "@mendix/widget-plugin-test-utils";
import { actionValue, dynamic } from "@mendix/widget-plugin-test-utils";
import type { ImageCropperContainerProps } from "../../typings/ImageCropperProps";

// Integration test: proves the rotate/grayscale actions reach the right util with the right args.
Expand Down Expand Up @@ -105,8 +105,8 @@ function makeProps(overrides: Partial<ImageCropperContainerProps> = {}): ImageCr
image: makeImageProp(),
cropShape: "rect",
aspectRatio: "free",
customAspectWidth: 1,
customAspectHeight: 1,
customAspectWidth: dynamic.available(new Big(1)),
customAspectHeight: dynamic.available(new Big(1)),
boundaryWidth: 300,
boundaryHeight: 300,
resizableEnabled: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ValueStatus } from "mendix";
import { Big } from "big.js";
import { DynamicValue, ValueStatus } from "mendix";
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
import { type SetStateAction } from "react";
import { type Crop, type PixelCrop } from "react-image-crop";
Expand Down Expand Up @@ -124,7 +125,13 @@ export class ImageCropperStore implements SetupComponent {
}

get aspect(): number | undefined {
return resolveAspectRatio(this.props.aspectRatio, this.props.customAspectWidth, this.props.customAspectHeight);
const toNumber = (p: DynamicValue<Big>): number | undefined =>
p.status === ValueStatus.Available && p.value ? p.value.toNumber() : undefined;
return resolveAspectRatio(
this.props.aspectRatio,
toNumber(this.props.customAspectWidth),
toNumber(this.props.customAspectHeight)
);
}

setup(): () => void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ValueStatus } from "mendix";
import { action, makeObservable, observable } from "mobx";
import type { Crop, PixelCrop } from "react-image-crop";
import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main";
import { dynamic } from "@mendix/widget-plugin-test-utils";
import type { ImageCropperContainerProps } from "../../../typings/ImageCropperProps";

// The store calls cropImage/rotateImage (async canvas work). Mock them so the spec asserts
Expand Down Expand Up @@ -52,8 +53,8 @@ function makeProps(overrides: Partial<ImageCropperContainerProps> = {}): ImageCr
image: makeImageProp(),
cropShape: "rect",
aspectRatio: "square",
customAspectWidth: 1,
customAspectHeight: 1,
customAspectWidth: dynamic.available(new Big(1)),
customAspectHeight: dynamic.available(new Big(1)),
boundaryWidth: 300,
boundaryHeight: 300,
resizableEnabled: true,
Expand Down Expand Up @@ -175,6 +176,34 @@ describe("ImageCropperStore", () => {
expect(store.aspect).toBeCloseTo(16 / 9);
dispose();
});

it("derives a custom ratio from the width/height expression values", () => {
const { store, gate, dispose } = makeStore({
aspectRatio: "custom",
customAspectWidth: dynamic.available(new Big(16)),
customAspectHeight: dynamic.available(new Big(9))
});
expect(store.aspect).toBeCloseTo(16 / 9);
gate.setProps(
makeProps({
aspectRatio: "custom",
customAspectWidth: dynamic.available(new Big(3)),
customAspectHeight: dynamic.available(new Big(2))
})
);
expect(store.aspect).toBeCloseTo(3 / 2);
dispose();
});

it("falls back to free aspect when a custom expression value is unavailable", () => {
const { store, dispose } = makeStore({
aspectRatio: "custom",
customAspectWidth: dynamic.unavailable(),
customAspectHeight: dynamic.available(new Big(9))
});
expect(store.aspect).toBeUndefined();
dispose();
});
});

describe("commitCrop gate (user-drag vs programmatic)", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { AspectRatioEnum } from "../../typings/ImageCropperProps";

export function resolveAspectRatio(
aspect: AspectRatioEnum,
customWidth: number,
customHeight: number
customWidth: number | undefined,
customHeight: number | undefined
): number | undefined {
switch (aspect) {
case "free":
Expand All @@ -17,7 +17,7 @@ export function resolveAspectRatio(
case "portrait3x4":
return 3 / 4;
case "custom":
if (customWidth > 0 && customHeight > 0) {
if (customWidth != null && customHeight != null && customWidth > 0 && customHeight > 0) {
return customWidth / customHeight;
}
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export interface ImageCropperContainerProps {
image: EditableImageValue<WebImage>;
cropShape: CropShapeEnum;
aspectRatio: AspectRatioEnum;
customAspectWidth: number;
customAspectHeight: number;
customAspectWidth: DynamicValue<Big>;
customAspectHeight: DynamicValue<Big>;
onCropAction?: ActionValue;
boundaryWidth: number;
boundaryHeight: number;
Expand Down Expand Up @@ -67,8 +67,8 @@ export interface ImageCropperPreviewProps {
image: { type: "static"; imageUrl: string; } | { type: "dynamic"; entity: string; } | null;
cropShape: CropShapeEnum;
aspectRatio: AspectRatioEnum;
customAspectWidth: number | null;
customAspectHeight: number | null;
customAspectWidth: string;
customAspectHeight: string;
onCropAction: {} | null;
boundaryWidth: number | null;
boundaryHeight: number | null;
Expand Down
Loading