Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.

Commit

Permalink
feat: asset properties (fixes #274)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheReincarnator authored and lkuechler committed Apr 4, 2018
1 parent ab6d5dd commit 243a4d2
Show file tree
Hide file tree
Showing 17 changed files with 351 additions and 55 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@
"@types/deep-assign": "^0.1.1",
"@types/electron-devtools-installer": "^2.0.2",
"@types/fs-extra": "^5.0.1",
"@types/isomorphic-fetch": "0.0.34",
"@types/js-yaml": "^3.10.1",
"@types/lodash": "^4.14.104",
"@types/mime-types": "^2.1.0",
"@types/node": "^9.4.0",
"@types/object-path": "^0.9.29",
"@types/react": "^16.0.0",
Expand All @@ -120,8 +122,10 @@
"electron-log": "2.2.14",
"electron-updater": "2.21.0",
"fs-extra": "^5.0.0",
"isomorphic-fetch": "^2.2.1",
"js-yaml": "3.11.0",
"lodash": "^4.17.5",
"mime-types": "^2.1.18",
"mobx": "3.4.1",
"mobx-react": "4.4.3",
"object-path": "^0.11.4",
Expand Down
34 changes: 34 additions & 0 deletions src/component/container/property-list.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { AssetItem } from '../../lsg/patterns/property-items/asset-item';
import { AssetProperty } from '../../store/styleguide/property/asset-property';
import { BooleanItem } from '../../lsg/patterns/property-items/boolean-item';
import { remote } from 'electron';
import Element from '../../lsg/patterns/element';
import { EnumItem, Values } from '../../lsg/patterns/property-items/enum-item';
import { EnumProperty, Option } from '../../store/styleguide/property/enum-property';
Expand Down Expand Up @@ -66,6 +69,21 @@ class PropertyTree extends React.Component<PropertyTreeProps> {
Store.getInstance().execute(this.lastCommand);
}

protected handleChooseAsset(id: string, context?: ObjectContext): void {
remote.dialog.showOpenDialog(
{
title: 'Select an image',
properties: ['openFile']
},
filePaths => {
if (filePaths && filePaths.length) {
const dataUrl = AssetProperty.getValueFromFile(filePaths[0]);
this.handleChange(id, dataUrl, context);
}
}
);
}

@action
protected handleClick(): void {
this.isOpen = !this.isOpen;
Expand Down Expand Up @@ -149,6 +167,22 @@ class PropertyTree extends React.Component<PropertyTreeProps> {
/>
);

case 'asset':
const src = value as string | undefined;
return (
<AssetItem
key={id}
label={name}
inputValue={src && !src.startsWith('data:') ? src : ''}
imageSrc={src}
handleInputChange={event =>
this.handleChange(id, event.currentTarget.value, context)
}
handleChooseClick={event => this.handleChooseAsset(id, context)}
handleClearClick={event => this.handleChange(id, undefined, context)}
/>
);

case 'object':
const objectProperty = property as ObjectProperty;
const newPath = (context && `${context.path}.${id}`) || id;
Expand Down
38 changes: 38 additions & 0 deletions src/lsg/patterns/property-items/asset-item/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import AssetItem from './index';
import * as React from 'react';
import styled from 'styled-components';

const NOOP = () => {}; // tslint:disable-line no-empty

const StyledDemo = styled.div`
width: 200px;
margin-bottom: 20px;
`;

const AssetItemDemo: React.StatelessComponent<void> = (): JSX.Element => (
<div>
<StyledDemo>
<AssetItem label="Empty" />
</StyledDemo>
<StyledDemo>
<AssetItem
handleChooseClick={NOOP}
handleClearClick={NOOP}
handleInputChange={NOOP}
label="Internal"
imageSrc="http://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/light-bulb-icon.png"
/>
</StyledDemo>
<StyledDemo>
<AssetItem
handleChooseClick={NOOP}
handleClearClick={NOOP}
handleInputChange={NOOP}
label="External"
inputValue="http://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/light-bulb-icon.png"
/>
</StyledDemo>
</div>
);

export default AssetItemDemo;
111 changes: 111 additions & 0 deletions src/lsg/patterns/property-items/asset-item/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { colors } from '../../colors';
import { fonts } from '../../fonts';
import * as React from 'react';
import { getSpace, Size } from '../../space';
import styled from 'styled-components';

export interface AssetItemProps {
className?: string;
handleChooseClick?: React.MouseEventHandler<HTMLButtonElement>;
handleClearClick?: React.MouseEventHandler<HTMLButtonElement>;
handleInputChange?: React.ChangeEventHandler<HTMLInputElement>;
imageSrc?: string;
inputValue?: string;
label: string;
}

const StyledAssetItem = styled.div`
width: 100%;
`;

const StyledPreview = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: ${getSpace(Size.XS)}px;
`;

const StyledLabel = styled.span`
display: block;
margin-bottom: ${getSpace(Size.XS)}px;
font-size: 12px;
font-family: ${fonts().NORMAL_FONT};
color: ${colors.grey36.toString()};
`;

const StyledInput = styled.input`
display: inline-block;
box-sizing: border-box;
max-width: 75%;
text-overflow: ellipsis;
border: none;
border-bottom: 1px solid transparent;
background: transparent;
font-family: ${fonts().NORMAL_FONT};
font-size: 15px;
color: ${colors.grey36.toString()};
transition: all 0.2s;
::-webkit-input-placeholder {
color: ${colors.grey60.toString()};
}
&:hover {
color: ${colors.black.toString()};
border-color: ${colors.grey60.toString()};
}
&:focus {
outline: none;
border-color: ${colors.blue40.toString()};
color: ${colors.black.toString()};
}
`;

const StyledImageBox = styled.div`
box-sizing: border-box;
border-radius: 3px;
width: 42px;
height: 42px;
background-color: ${colors.white.toString()};
border: 0.5px solid ${colors.grey90.toString()};
padding: 3px;
margin-right: 6px;
flex-shrink: 0;
`;

const StyledImage = styled.img`
width: 100%;
`;

const StyledButton = styled.button`
max-width: 50%;
margin-right: 3px;
border: 0.5px solid ${colors.grey90.toString()};
border-radius: 3px;
background-color: ${colors.white.toString()};
padding: ${getSpace(Size.XS)}px ${getSpace(Size.S)}px;
`;

export const AssetItem: React.StatelessComponent<AssetItemProps> = props => (
<StyledAssetItem className={props.className}>
<label>
<StyledLabel>{props.label}</StyledLabel>
<StyledPreview>
<StyledImageBox>
<StyledImage src={props.imageSrc} />
</StyledImageBox>
<StyledInput
onChange={props.handleInputChange}
type="textarea"
value={props.inputValue}
placeholder="Enter external URL"
/>
</StyledPreview>
</label>
<StyledButton onClick={props.handleChooseClick}>Open</StyledButton>
<StyledButton onClick={props.handleClearClick}>Clear</StyledButton>
</StyledAssetItem>
);

export default AssetItem;
8 changes: 8 additions & 0 deletions src/lsg/patterns/property-items/asset-item/pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "asset-item",
"displayName": "Asset Item",
"version": "1.0.0",
"tags": [
"property"
]
}
24 changes: 14 additions & 10 deletions src/store/page/page-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,16 +408,20 @@ export class PageElement {
return;
}

value = property.coerceValue(value);

if (!path) {
this.propertyValues.set(id, value);
return;
}

const rootPropertyValue = this.propertyValues.get(id) || {};
ObjectPath.set<{}, PropertyValue>(rootPropertyValue, path, value);
this.propertyValues.set(id, deepAssign({}, rootPropertyValue));
(async () => {
const coercedValue: string = await property.coerceValue(value);
if (path) {
const rootPropertyValue = this.propertyValues.get(id) || {};
ObjectPath.set<{}, PropertyValue>(rootPropertyValue, path, coercedValue);
this.propertyValues.set(id, deepAssign({}, rootPropertyValue));
} else {
this.propertyValues.set(id, coercedValue);
}
})().catch(reason => {
console.log(
`Failed to coerce property value of property ${this.getId()} of pattern ${this.getPattern()}: ${reason}`
);
});
}

/**
Expand Down
102 changes: 102 additions & 0 deletions src/store/styleguide/property/asset-property.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as FileUtils from 'fs';
import * as fetch from 'isomorphic-fetch';
import * as MimeTypes from 'mime-types';
import * as PathUtils from 'path';
import { Property } from './property';

/**
* An asset property is a property that takes an uploaded file (e.g. an image)
* as a data-URL string to output it as src of an img tag or alike.
* As designer content value (raw value), the asset property accepts data-URL strings and
* undefined (as empty src) only. All other values are invalid and converted into undefined.
* To convert a given file, Buffer, or HTTP URL into a data-URL string,
* use getValueFromFile, getValueFromBuffer, or getValueFromUrl.
* @see Property
* @see AssetProperty.getValueFromFile
* @see AssetProperty.getValueFromBuffer
* @see AssetProperty.getValueFromUrl
*/
export class AssetProperty extends Property {
/**
* Creates a new asset property.
* @param id The technical ID of this property (e.g. the property name
* in the TypeScript props interface).
*/
public constructor(id: string) {
super(id);
}

/**
* Converts a given buffer and mime type into a data-URL string, a valid value for this property.
* @param buffer The buffer to read.
* @param mimeType The mime type of the buffer content.
* @return The data-URL string.
*/
public static getValueFromBuffer(buffer: Buffer, mimeType: string): string {
return `data:${mimeType};base64,${buffer.toString('base64')}`;
}

/**
* Reads a given file and converts it into a data-URL string, a valid value for this property.
* @param path The OS-specific path to the file to read.
* @return The data-URL string.
*/
public static getValueFromFile(path: string): string {
const fileName: string | undefined = path.split(PathUtils.sep).pop();
if (fileName === undefined) {
throw new Error(`Invalid asset path ${path}`);
}

const mimeType = MimeTypes.lookup(fileName) || 'application/octet-stream';
const buffer: Buffer = FileUtils.readFileSync(path);
return this.getValueFromBuffer(buffer, mimeType);
}

/**
* Downloads a given file from HTTP and converts it into a data-URL string,
* a valid value for this property.
* @param url The URL of the file to download.
* @param callback A callback that gets the downloaded file as data-URL.
*/
public static async getValueFromUrl(url: string): Promise<string> {
const response = await fetch(url);
if (response.status >= 400) {
throw new Error(`Failed to load file from server: HTTP ${response.status}`);
}
if (!response.body) {
throw new Error('Server returned no content');
}

const mimeType = response.headers.get('Content-Type') || 'application/octet-stream';

// tslint:disable-next-line:no-any
const buffer: Buffer = await (response as any).buffer();
return AssetProperty.getValueFromBuffer(buffer, mimeType);
}

/**
* @inheritdoc
*/
// tslint:disable-next-line:no-any
public async coerceValue(value: any): Promise<any> {
if (typeof value === 'string') {
return value;
}

return undefined;
}

/**
* @inheritdoc
*/
public getType(): string {
return 'asset';
}

/**
* @inheritdoc
*/
public toString(): string {
return `AssetProperty(${super.toString()})`;
}
}
2 changes: 1 addition & 1 deletion src/store/styleguide/property/boolean-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class BooleanProperty extends Property {
* @inheritdoc
*/
// tslint:disable-next-line:no-any
public coerceValue(value: any): any {
public async coerceValue(value: any): Promise<any> {
return value === true || value === 'true' || value === 1;
}

Expand Down
6 changes: 3 additions & 3 deletions src/store/styleguide/property/enum-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ export class EnumProperty extends Property {
* @inheritdoc
*/
// tslint:disable-next-line:no-any
public coerceValue(value: any): any {
public async coerceValue(value: any): Promise<any> {
if (value === null || value === undefined || value === '') {
return undefined;
return;
}

for (const option of this.options) {
Expand Down Expand Up @@ -80,7 +80,7 @@ export class EnumProperty extends Property {
}
}

return undefined;
return;
}

/**
Expand Down
Loading

0 comments on commit 243a4d2

Please sign in to comment.