Skip to content

Commit

Permalink
fix: support mapped tasks (#494)
Browse files Browse the repository at this point in the history
* fix: support mapped tasks #none

Signed-off-by: James <[email protected]>

* fix: fix comments #none

Signed-off-by: James <[email protected]>

* fix: fix unit test #none

Signed-off-by: James <[email protected]>

* fix: add string constants #none

Signed-off-by: James <[email protected]>

* fix: added test for mapInputHelper #none

Signed-off-by: James <[email protected]>

* fix: fix test for utils.test.ts #none

Signed-off-by: James <[email protected]>

* chore: trigger snyk re-run

* fix: multiple keys for mapped types; #none

Signed-off-by: James <[email protected]>

* chore: storybook item (#530)

Signed-off-by: Nastya Rusina <[email protected]>

* fix: fix validation for duplicate and fix focus issue

Signed-off-by: James <[email protected]>

* chore: provide previous run values on relaunch

Signed-off-by: Nastya Rusina <[email protected]>

Co-authored-by: Nastya <[email protected]>
Co-authored-by: Nastya Rusina <[email protected]>
  • Loading branch information
3 people authored Jul 1, 2022
1 parent d191445 commit 506b953
Show file tree
Hide file tree
Showing 14 changed files with 734 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BlobInput } from './BlobInput';
import { CollectionInput } from './CollectionInput';
import { formStrings, inputsDescription } from './constants';
import { LaunchState } from './launchMachine';
import { MapInput } from './MapInput';
import { NoInputsNeeded } from './NoInputsNeeded';
import { SimpleInput } from './SimpleInput';
import { StructInput } from './StructInput';
Expand All @@ -24,6 +25,7 @@ function getComponentForInput(input: InputProps, showErrors: boolean) {
case InputType.Struct:
return <StructInput {...props} />;
case InputType.Map:
return <MapInput {...props} />;
case InputType.Unknown:
case InputType.None:
return <UnsupportedInput {...props} />;
Expand Down
213 changes: 213 additions & 0 deletions packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { Button, IconButton, TextField, Typography } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import * as React from 'react';
import RemoveIcon from '@material-ui/icons/Remove';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import { requiredInputSuffix } from './constants';
import { InputProps, InputType, InputTypeDefinition, InputValue } from './types';
import { formatType, toMappedTypeValue } from './utils';

const useStyles = makeStyles((theme: Theme) => ({
formControl: {
width: '100%',
marginTop: theme.spacing(1),
},
controls: {
margin: theme.spacing(1),
width: '100%',
display: 'flex',
alignItems: 'flex-start',
flexDirection: 'row',
},
keyControl: {
marginRight: theme.spacing(1),
},
valueControl: {
flexGrow: 1,
},
addButton: {
display: 'flex',
justifyContent: 'flex-end',
marginTop: theme.spacing(1),
},
error: {
border: '1px solid #f44336',
},
}));

interface MapInputItemProps {
data: MapInputItem;
subtype?: InputTypeDefinition;
setKey: (key: string) => void;
setValue: (value: string) => void;
isValid: (value: string) => boolean;
onDeleteItem: () => void;
}

const MapSingleInputItem = (props: MapInputItemProps) => {
const classes = useStyles();
const { data, subtype, setKey, setValue, isValid, onDeleteItem } = props;
const [error, setError] = React.useState(false);

const isOneLineType = subtype?.type === InputType.String || subtype?.type === InputType.Integer;

return (
<div className={classes.controls}>
<TextField
label={`string${requiredInputSuffix}`}
onChange={({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setKey(value);
setError(!!value && !isValid(value));
}}
value={data.key}
error={error}
placeholder="key"
variant="outlined"
helperText={error ? 'This key already defined' : ''}
className={classes.keyControl}
/>
<TextField
label={subtype ? `${formatType(subtype)}${requiredInputSuffix}` : ''}
onChange={({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setValue(value);
}}
value={data.value}
variant="outlined"
className={classes.valueControl}
multiline={!isOneLineType}
type={subtype?.type === InputType.Integer ? 'number' : 'text'}
/>
<IconButton onClick={onDeleteItem}>
<RemoveIcon />
</IconButton>
</div>
);
};

type MapInputItem = {
id: number | null;
key: string;
value: string;
};

const getNewMapItem = (id, key = '', value = ''): MapInputItem => {
return { id, key, value };
};

function parseMappedTypeValue(value?: InputValue): MapInputItem[] {
const fallback = [getNewMapItem(0)];
if (!value) {
return fallback;
}
try {
const mapObj = JSON.parse(value.toString());
if (typeof mapObj === 'object') {
return Object.keys(mapObj).map((key, index) => getNewMapItem(index, key, mapObj[key]));
}
} catch (e) {
// do nothing
}

return fallback;
}

export const MapInput = (props: InputProps) => {
const {
value,
label,
onChange,
typeDefinition: { subtype },
} = props;
const classes = useStyles();

const [data, setData] = React.useState<MapInputItem[]>(parseMappedTypeValue(value));

const onAddItem = () => {
setData((data) => [...data, getNewMapItem(data.length)]);
};

const updateUpperStream = () => {
const newPairs = data
.filter((item) => {
// we filter out delted values and items with errors or empty keys/values
return item.id !== null && !!item.key && !!item.value;
})
.map((item) => {
return {
key: item.key,
value: item.value,
};
});
const newValue = toMappedTypeValue(newPairs);
onChange(newValue);
};

const onSetKey = (id: number | null, key: string) => {
if (id === null) return;
setData((data) => {
data[id].key = key;
return [...data];
});
updateUpperStream();
};

const onSetValue = (id: number | null, value: string) => {
if (id === null) return;
setData((data) => {
data[id].value = value;
return [...data];
});
updateUpperStream();
};

const onDeleteItem = (id: number | null) => {
if (id === null) return;
setData((data) => {
const dataIndex = data.findIndex((item) => item.id === id);
if (dataIndex >= 0 && dataIndex < data.length) {
data[dataIndex].id = null;
}
return [...data];
});
updateUpperStream();
};

const isValid = (id: number | null, value: string) => {
if (id === null) return true;
// findIndex returns -1 if value is not found, which means we can use that key
return (
data
.filter((item) => item.id !== null && item.id !== id)
.findIndex((item) => item.key === value) === -1
);
};

return (
<Card variant="outlined">
<CardContent>
<Typography variant="body1" component="label">
{label}
</Typography>
{data
.filter((item) => item.id !== null)
.map((item) => {
return (
<MapSingleInputItem
key={item.id}
data={item}
subtype={subtype}
setKey={(key) => onSetKey(item.id, key)}
setValue={(value) => onSetValue(item.id, value)}
isValid={(value) => isValid(item.id, value)}
onDeleteItem={() => onDeleteItem(item.id)}
/>
);
})}
<div className={classes.addButton}>
<Button onClick={onAddItem}>+ ADD ITEM</Button>
</div>
</CardContent>
</Card>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,31 @@ export function nestedCollectionInputTypeDefinition(
},
};
}

export function mapInputTypeDefinition(typeDefinition: InputTypeDefinition): InputTypeDefinition {
return {
literalType: {
mapValueType: typeDefinition.literalType,
},
type: InputType.Map,
subtype: typeDefinition,
};
}

export function nestedMapInputTypeDefinition(
typeDefinition: InputTypeDefinition,
): InputTypeDefinition {
return {
literalType: {
mapValueType: {
mapValueType: typeDefinition.literalType,
},
},
type: InputType.Map,
subtype: {
literalType: { mapValueType: typeDefinition.literalType },
type: InputType.Map,
subtype: typeDefinition,
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { MapInput } from '../MapInput';
import { InputValue } from '../types';
import { inputTypes } from '../inputHelpers/test/testCases';

const stories = storiesOf('Launch/MapInput', module);

stories.addDecorator((story) => {
return <div style={{ width: 600, height: '95vh' }}>{story()}</div>;
});

stories.add('Base', () => {
return (
<MapInput
description="Something"
name="Variable A"
label="Variable_A"
required={false}
typeDefinition={inputTypes.map}
onChange={(newValue: InputValue) => {
console.log('onChange', newValue);
}}
/>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { datetimeHelper } from './datetime';
import { durationHelper } from './duration';
import { floatHelper } from './float';
import { integerHelper } from './integer';
import { mapHelper } from './map';
import { noneHelper } from './none';
import { schemaHelper } from './schema';
import { stringHelper } from './string';
Expand All @@ -26,7 +27,7 @@ const inputHelpers: Record<InputType, InputHelper> = {
[InputType.Error]: unsupportedHelper,
[InputType.Float]: floatHelper,
[InputType.Integer]: integerHelper,
[InputType.Map]: unsupportedHelper,
[InputType.Map]: mapHelper,
[InputType.None]: noneHelper,
[InputType.Schema]: schemaHelper,
[InputType.String]: stringHelper,
Expand Down
Loading

0 comments on commit 506b953

Please sign in to comment.