Skip to content

Commit

Permalink
v1.3.0
Browse files Browse the repository at this point in the history
- Added support for multiple image types
- Added support for D365 mobile apps
  • Loading branch information
cathalnoonan committed May 8, 2021
1 parent 2850cd7 commit 748a443
Show file tree
Hide file tree
Showing 20 changed files with 5,114 additions and 14,442 deletions.
11 changes: 11 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@echo off

call yarn --cwd ./control

SET msbuild="C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe"

if exist ./solution/bin/Release/ImageControl.zip (
%msbuild% ./solution/ImageControl.cdsproj /t:build /p:Configuration=Release
) else (
%msbuild% ./solution/ImageControl.cdsproj /t:build /p:Configuration=Release /restore
)
8 changes: 8 additions & 0 deletions clean.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@echo off

call npx rimraf ./control/node_modules
call npx rimraf ./control/obj
call npx rimraf ./control/out

call npx rimraf ./solution/bin
call npx rimraf ./solution/obj
10 changes: 5 additions & 5 deletions control/ImageControl/ControlManifest.Input.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="Cathal" constructor="ImageControl" display-name-key="ImageControl_Display_Key" description-key="ImageControl_Desc_Key" control-type="standard"
version="1.2.1" >
version="1.3.0">

<property name="field" display-name-key="Field_Display_Key" description-key="Field_Desc_Key" of-type="Multiple" usage="bound" required="true" />

Expand All @@ -15,12 +15,12 @@
</property>

<resources>
<code path="index.ts" order="1"/>
<code path="index.ts" order="1" />
<css path="styles.css" order="1" />

<resx path="strings/ImageControl.1043.resx" version="1.2.1" />
<resx path="strings/ImageControl.1040.resx" version="1.2.1" />
<resx path="strings/ImageControl.1033.resx" version="1.2.1" />
<resx path="strings/ImageControl.1043.resx" version="1.3.0" />
<resx path="strings/ImageControl.1040.resx" version="1.3.0" />
<resx path="strings/ImageControl.1033.resx" version="1.3.0" />
</resources>

<feature-usage>
Expand Down
220 changes: 105 additions & 115 deletions control/ImageControl/components/imagecontrol.tsx
Original file line number Diff line number Diff line change
@@ -1,129 +1,119 @@
import * as React from 'react';
import { ResourceStringUtility, FieldLengthValidator, addDataImage, removeDataImage } from '../utilities';
import * as React from 'react'
import classNames from 'classnames'

export interface ImageControlComponentProps {
value: string | null;
fieldLength: number;
maxFieldLength: number;
resourceStrings: ResourceStringUtility;
displayBorder: boolean;
isDisabled: boolean;
editable: boolean;
toBase64: (file: File) => Promise<string>;
alertError: (message: string) => void;
notifyOutputChanged: () => void;
pickFile: () => Promise<ComponentFramework.FileObject[]>;
}
import { ResourceStrings } from '../strings'
import { FieldLengthValidator, addDataImage, removeDataImage, toBase64 } from '../utilities'

interface IState {
value: string | null;
export interface ImageControlComponentProps {
value: string | null
displayBorder: boolean
resourceStrings: ResourceStrings
attribute: {
fieldLength: number
maxFieldLength: number
isDisabled: boolean
isEditable: boolean
}
updateValue: (value: string | null) => void
pickFile: () => Promise<ComponentFramework.FileObject[]>
alertError: (message: string) => void
}

export class ImageControlComponent extends React.Component<ImageControlComponentProps, IState> {
export function ImageControlComponent(props: ImageControlComponentProps) {
const fieldLengthValidator = new FieldLengthValidator({
fieldLength: props.attribute.fieldLength,
maxFieldLength: props.attribute.maxFieldLength,
resourceStrings: props.resourceStrings,
})

private fieldLengthValidator: FieldLengthValidator;

constructor(props: ImageControlComponentProps) {
super(props);
this.fieldLengthValidator = new FieldLengthValidator({ ...props });
this.state = {
value: this.props.value,
// State
const [value, setValueInternal] = React.useState<string | null>(props.value)
const isEmpty = !value || value === 'val' // 'val' is the default text in the test harness
const isEditable = !props.attribute.isDisabled && props.attribute.isEditable
const imageSrc = isEmpty ? '' : addDataImage(value!)
const updateValue = (value: string | null) => {
if (!isEditable) return

if (!!value) {
value = removeDataImage(value)
}
props.updateValue(value)
setValueInternal(value)
}

render() {
return (
<div
className='ImageControl'
onDragOver={this.onDragOver}
onDrop={this.onDrop}
>
<img
className={this.getImageClass()}
src={this.getImageSource()}
/>
<p
className={this.getLabelClass()}
>
{this.props.resourceStrings.dragImageHere}
</p>
<div className='button-container'>
<button
className={this.getClickToClearButtonClass()}
onClick={this.onClickClearButton}
>
{this.props.resourceStrings.clickToClear}
</button>
<button
className={this.getPickFileButtonClass()}
onClick={this.onClickPickFileButton}
>
{this.props.resourceStrings.pickFile}
</button>
</div>
</div>
);
}
// Styles
const labelClass = classNames({ 'label': true, 'hidden': !isEmpty })
const imageClass = classNames({ 'hidden': isEmpty, 'image-border': props.displayBorder })
const buttonClassClickToClear = classNames({ 'hidden': isEmpty })
const buttonClassPickFile = classNames({ 'hidden': !isEmpty })

// State
getValue = (): string | null => this.state.value;
setValue = (value: string | null): void => {
if (value) value = removeDataImage(value);
if (this.state.value === value) return;
this.setState({ value }, this.props.notifyOutputChanged);
// Events
const onDragOver = (ev: React.DragEvent<HTMLDivElement>) => {
ev.preventDefault()
}
private getImageSource = (): string => this.state.value && addDataImage(this.state.value) || '';
private isEmpty = (): boolean => !this.state.value || this.state.value === 'val';
private isEditable = (): boolean => !this.props.isDisabled && this.props.editable;
const onDrop = async (ev: React.DragEvent<HTMLDivElement>) => {
ev.preventDefault()

if (!isEditable) return
if (!ev.dataTransfer || !ev.dataTransfer.files) return // Don't alert as this is not an error condition

// Styles
private getLabelClass = (): string => 'label' + (this.isEmpty() ? '' : ' hidden');
private getImageClass = (): string => (this.isEmpty() ? ' hidden' : '') + (this.props.displayBorder ? ' image-border' : '');
private getClickToClearButtonClass = (): string => this.isEmpty() ? ' hidden' : '';
private getPickFileButtonClass = (): string => this.isEmpty() ? '' : ' hidden';

// Event handlers
private onDragOver = (ev: React.DragEvent): void => ev.preventDefault();
private onDropSuccess = (value: string): void => this.setValue(value);
private onDrop = (ev: React.DragEvent): void => {
ev.preventDefault();

if (!this.isEditable()) return;
if (!ev.dataTransfer || !ev.dataTransfer.files) return; // Don't alert as this is not an error condition
const files = ev.dataTransfer.files;
if (files.length === 0) return this.props.alertError(this.props.resourceStrings.noFileError);
if (files.length > 1) return this.props.alertError(this.props.resourceStrings.multipleFileError);

const file = files[0];
if (file.type !== 'image/png') return this.props.alertError(this.props.resourceStrings.fileTypeError);

this.props.toBase64(file)
.then(this.fieldLengthValidator.validate)
.then(this.onDropSuccess)
.catch(this.props.alertError)
try {
const files = ev.dataTransfer.files

// Guard: Exactly one file
if (files.length === 0) throw props.resourceStrings.noFileError
if (files.length > 1) throw props.resourceStrings.multipleFileError

const file = files[0]

const base64 = await toBase64(file)
await fieldLengthValidator.validate(base64)
updateValue(base64)

} catch (errorMessage) {
props.alertError(errorMessage)
}
}
private onClickClearButton = (ev: React.MouseEvent): void => {
if (!this.isEditable()) return;
this.setValue(null);
const onClickClear = (ev: React.MouseEvent) => {
updateValue(null)
}
private onClickPickFileButton = (ev: React.MouseEvent): void => {
if (!this.isEditable()) return;

const { alertError, resourceStrings } = this.props;

this.props.pickFile()
.then(response => {
return new Promise<string>((resolve, reject) => {
if (response == null || response.length === 0) return reject();
if (response.length !== 1) return reject(resourceStrings.onlyPickOneFileError);
const { mimeType, fileContent } = response[0];
if (mimeType !== 'image/png') return reject(resourceStrings.onlyPngImagesSupported);

resolve(fileContent);
});
})
.then(fileContent => this.fieldLengthValidator.validate(fileContent))
.then(result => this.onDropSuccess(result))
.catch(message => alertError(message));
const onClickPickFile = async (ev: React.MouseEvent): Promise<void> => {
if (!isEditable) return

try {
const response = await props.pickFile()

// Guard: Exactly one file
if (response == null || response.length === 0) return//throw props.resourceStrings.noFileError
if (response.length !== 1) throw props.resourceStrings.onlyPickOneFileError

const fileContent = response[0].fileContent
await fieldLengthValidator.validate(fileContent)
updateValue(fileContent)

} catch (errorMessage) {
props.alertError(errorMessage)
}
}
}

// Render
return (
<div className='ImageControl' onDragOver={onDragOver} onDrop={onDrop}>
<img className={imageClass} src={imageSrc} />

<p className={labelClass}>
{props.resourceStrings.dragImageHere}
</p>

<div className='button-container'>
<button className={buttonClassClickToClear} onClick={onClickClear}>
{props.resourceStrings.clickToClear}
</button>

<button className={buttonClassPickFile} onClick={onClickPickFile}>
{props.resourceStrings.pickFile}
</button>
</div>
</div>
)
}
1 change: 0 additions & 1 deletion control/ImageControl/components/index.ts

This file was deleted.

Loading

0 comments on commit 748a443

Please sign in to comment.