Skip to content

Commit

Permalink
Feature/concept dnd select (#134)
Browse files Browse the repository at this point in the history
* WIP: initial

* refactoring and changes

* changes

* changes

* fix tests

* changes

* fix modal layer

* changes

* mock api added

* fix test

* demo data changes

* drag and drop file into forms

* change placeholder

* fix missing filter values

* merge develop

* set initial test parameters

* refactoring

* renamed label: uploadConceptListModal

* added maxWidth to Dropzone

* run script build-app for publish

* fix version

* update actions

* version to 1.9.0-rc.1 changed

* enable drag&drop files for InputMultiSelect

* changes
  • Loading branch information
MarcusBaitz committed Jul 17, 2018
1 parent 37b9f46 commit 48cb2a3
Show file tree
Hide file tree
Showing 34 changed files with 539 additions and 252 deletions.
10 changes: 10 additions & 0 deletions frontend/app/api/dnd/countries.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Afghanistan
Albania
Algeria
America
Andorra
Angola
Antigua
Argentina
Armenia
Australia
28 changes: 28 additions & 0 deletions frontend/app/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,34 @@ module.exports = function (app, port) {
})
});

/*
For DND File see ./app/api/dnd
*/
app.post(
'/api/datasets/:datasetId/concepts/:conceptId/tables/:tableId/filters/:filterId/resolve',
function response (req, res) {
setTimeout(() => {
res.setHeader('Content-Type', 'application/json');

if (req.params.filterId !== 'production_country') return null;

const countries = require('./autocomplete/countries');
const unknownCodes = req.body.values.filter(val => !countries.includes(val));
const values = req.body.values.filter(val => countries.includes(val));


res.send({
unknownCodes: unknownCodes,
resolvedFilter: {
tableId: req.params.tableId,
filterId: req.params.filterId,
value: values.map(val => ({label: val, value: val}))
}
});
}, 500);
}
);

/*
SEARCH
*/
Expand Down
18 changes: 18 additions & 0 deletions frontend/lib/js/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,24 @@ export function postConceptsListToResolve(
});
};

export function postConceptFilterValuesResolve(
datasetId: DatasetIdType,
conceptId: string,
tableId: string,
filterId: string,
values: string[],
) {
return fetchJson(
apiUrl() +
`/datasets/${datasetId}/concepts/${conceptId}` +
`/tables/${tableId}/filters/${filterId}/resolve`,
{
method: 'POST',
body: { values },
}
);
};

export const searchConcepts = (datasetId: DatasetIdType, query: string, limit?: number) => {
return fetchJson(apiUrl() + `/datasets/${datasetId}/concepts/search`, {
method: 'POST',
Expand Down
28 changes: 28 additions & 0 deletions frontend/lib/js/common/helpers/fileHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// @flow

export const readFileAsText = (file: File) => new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = (evt) => resolve(evt.target.result);
reader.onerror = (err) => reject(err);

reader.readAsText(file);
});

/**
* Split by \n, trim rows and filter empty rows
* @param fileContent
*/
export const cleanFileContent = (fileContent: string) => {
return fileContent.split('\n')
.map(row => row.trim())
.filter(row => row.length > 0);
};

export const checkFileType = (file: File, type?: string) => {
return file.type === type || "text/plain";
};

export const stripFileName = (fileName: string) => {
return fileName.replace(/\.[^/.]+$/, "");
}
1 change: 1 addition & 0 deletions frontend/lib/js/common/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

export * from './commonHelper';
export * from './dateHelper';
export * from './fileHelper';
5 changes: 5 additions & 0 deletions frontend/lib/js/file-upload/actionTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @flow

export const DROP_FILES_START = "file-dnd/DROP_FILES_START";
export const DROP_FILES_SUCCESS = "file-dnd/DROP_FILES_SUCCESS";
export const DROP_FILES_ERROR = "file-dnd/DROP_FILES_ERROR";
76 changes: 76 additions & 0 deletions frontend/lib/js/file-upload/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// @flow

import type { Dispatch } from 'redux';

import {
checkFileType,
readFileAsText,
cleanFileContent
} from "../common/helpers";
import {
defaultSuccess,
defaultError
} from "../common/actions";
import type { DateRangeType } from "../common/types/backend";
import {
uploadConceptListModalOpen
} from "../upload-concept-list-modal/actions";
import {
DROP_FILES_START,
DROP_FILES_SUCCESS,
DROP_FILES_ERROR
} from "./actionTypes";
import type
{
DraggedFileType,
GenericFileType
} from "./types";

const initialGenericFileType = (type?: GenericFileType) => ({
parameters: type ? type.parameters : {},
callback: uploadConceptListModalOpen
})

export const loadFilesStart = () =>
({ type: DROP_FILES_START });
export const loadFilesSuccess = (res: any) =>
defaultSuccess(DROP_FILES_SUCCESS, res);
export const loadFilesError = (err: any) =>
defaultError(DROP_FILES_ERROR, err);

export const dropFiles = (item: DraggedFileType, type?: GenericFileType) => {
return (dispatch: Dispatch) => {
dispatch(loadFilesStart());

type = !type || !type.callback ? initialGenericFileType(type) : type;

// Ignore all dropped files except the first
const file = item[0] || item.files[0];

if (!checkFileType(file))
return dispatch(loadFilesError(new Error("Invalid concept list file")));

return readFileAsText(file).then(
r => {
const values = cleanFileContent(r);
type.parameters.values = values;
type.parameters.fileName = file.name;

if (values.length)
return dispatch([
loadFilesSuccess(r),
type.callback(type)
]);

return dispatch(loadFilesError(new Error('An empty file was dropped')));
},
e => dispatch(loadFilesError(e))
);
}
};

export const dropFilesAndIdx = (item: DraggedFileType, andIdx: number) =>
dropFiles(item, { parameters: { andIdx }, callback: uploadConceptListModalOpen });

export const dropFilesDateRangeType = (item: DraggedFileType, dateRange: DateRangeType) =>
dropFiles(item, { parameters: { dateRange }, callback: uploadConceptListModalOpen });
11 changes: 11 additions & 0 deletions frontend/lib/js/file-upload/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @flow

export type DraggedFileType = {
files: File[],
isPreviousQuery?: void,
}

export type GenericFileType = {
parameters: Object,
callback?: Function
}
3 changes: 3 additions & 0 deletions frontend/lib/js/form-components/AsyncInputMultiSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type PropsType = FieldPropsType & {
startLoadingThreshold: number,
tooltip?: string,
onLoad: Function,
onDropFiles: Function,
};

const AsyncInputMultiSelect = ({
Expand All @@ -25,6 +26,7 @@ const AsyncInputMultiSelect = ({
onLoad,
isLoading,
input,
onDropFiles
}: PropsType) => (
<InputMultiSelect
label={label}
Expand All @@ -42,6 +44,7 @@ const AsyncInputMultiSelect = ({
return value
}
}
onDropFiles={onDropFiles}
/>
);

Expand Down
60 changes: 35 additions & 25 deletions frontend/lib/js/form-components/InputMultiSelect.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// @flow

import React from 'react';
import T from 'i18n-react';
import Select from 'react-select';
import classnames from 'classnames';
import { type FieldPropsType } from 'redux-form';
import React from 'react';
import T from 'i18n-react';
import Select from 'react-select';
import classnames from 'classnames';
import { type FieldPropsType } from 'redux-form';
import Dropzone from 'react-dropzone'

import { type SelectOptionsType } from '../common/types/backend';
import { isEmpty } from '../common/helpers';

import InfoTooltip from '../tooltip/InfoTooltip';
import InfoTooltip from '../tooltip/InfoTooltip';

type PropsType = FieldPropsType & {
label: string,
Expand All @@ -19,6 +19,8 @@ type PropsType = FieldPropsType & {
onInputChange?: Function,
isLoading?: boolean,
className?: string,
onDropFiles?: Function,
isOver: boolean
};

const InputMultiSelect = (props: PropsType) => (
Expand All @@ -36,24 +38,32 @@ const InputMultiSelect = (props: PropsType) => (
{ props.label }
{ props.tooltip && <InfoTooltip text={props.tooltip} /> }
</p>
<Select
name="form-field"
options={props.options}
value={props.input.value}
onChange={(values) => props.input.onChange(values.map(v => v.value))}
disabled={props.disabled}
searchable
multi
placeholder={T.translate('reactSelect.placeholder')}
backspaceToRemoveMessage={T.translate('reactSelect.backspaceToRemove')}
clearAllText={T.translate('reactSelect.clearAll')}
clearValueText={T.translate('reactSelect.clearValue')}
noResultsText={T.translate('reactSelect.noResults')}
onInputChange={props.onInputChange || function(value) { return value; }}
isLoading={props.isLoading}
className={props.className}
matchPos="start"
/>
<Dropzone
disableClick
style={{position: "relative", display: "block", maxWidth: "300px"}}
activeClassName={'dropzone--over'}
className={'dropzone'}
onDrop={props.onDropFiles}
>
<Select
name="form-field"
options={props.options}
value={props.input.value}
onChange={(values) => props.input.onChange(values.map(v => v.value))}
disabled={props.disabled}
searchable
multi
placeholder={T.translate('reactSelect.dndPlaceholder')}
backspaceToRemoveMessage={T.translate('reactSelect.backspaceToRemove')}
clearAllText={T.translate('reactSelect.clearAll')}
clearValueText={T.translate('reactSelect.clearValue')}
noResultsText={T.translate('reactSelect.noResults')}
onInputChange={props.onInputChange || function(value) { return value; }}
isLoading={props.isLoading}
className={props.className}
matchPos="start"
/>
</Dropzone>
</label>
);

Expand Down
5 changes: 4 additions & 1 deletion frontend/lib/js/query-group-modal/QueryGroupModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import classnames from 'classnames';
import T from 'i18n-react';
import { connect } from 'react-redux';
import DatePicker from 'react-datepicker';
import ie from 'ie-version';
import moment from 'moment';

import { dateTypes } from '../common/constants';
Expand Down Expand Up @@ -80,7 +81,9 @@ const QueryGroupModal = (props) => {
</span>
}
</p>
<div className="query-group-modal__dates">
<div className={
`query-group-modal__dates ${ie.version && ie.version === 11 ? ' ie11' : ''}`
}>
<div className="query-group-modal__input-group">
<label className="input-label" htmlFor="datepicker-min">
{T.translate('queryGroupModal.dateMinLabel')}
Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/js/query-node-editor/ParameterTableFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type PropsType = {
onLoadFilterSuggestions: Function,
onShowDescription: Function,
suggestions: ?Object,
onDropFiles: Function,
};

const ParameterTableFilters = (props: PropsType) => (
Expand Down Expand Up @@ -70,6 +71,7 @@ const ParameterTableFilters = (props: PropsType) => (
label={filter.label}
options={filter.options}
disabled={props.excludeTable}
onDropFiles={(files) => props.onDropFiles(filterIdx, filter.id, files)}
/>
);
case BIG_MULTI_SELECT:
Expand All @@ -95,6 +97,7 @@ const ParameterTableFilters = (props: PropsType) => (
}
startLoadingThreshold={filter.threshold || 1}
onLoad={(prefix) => props.onLoadFilterSuggestions(filterIdx, filter.id, prefix)}
onDropFiles={(files) => props.onDropFiles(filterIdx, filter.id, files)}
disabled={!!props.excludeTable}
/>
);
Expand Down
4 changes: 4 additions & 0 deletions frontend/lib/js/query-node-editor/QueryNodeEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type QueryNodeEditorState = {
onShowDescription: Function,
onToggleEditLabel: Function,
onReset: Function,
onDropFiles: Function,
}

export type PropsType = {
Expand Down Expand Up @@ -97,6 +98,7 @@ export const createConnectedQueryNodeEditor = (
setInputTableViewActive,
setFocusedInput,
reset,
onDropFiles
} = createQueryNodeEditorActions(ownProps.type);

return {
Expand All @@ -108,6 +110,7 @@ export const createConnectedQueryNodeEditor = (
onSelectInputTableView: (tableIdx) => dispatch(setInputTableViewActive(tableIdx)),
onShowDescription: (filterIdx) => dispatch(setFocusedInput(filterIdx)),
onReset: () => dispatch(reset()),
onDropFiles: (...params) => dispatch(onDropFiles(...params))
}
};
}
Expand All @@ -122,6 +125,7 @@ export const createConnectedQueryNodeEditor = (
editorState: {
...(stateProps.editorState || {}),
...(dispatchProps.editorState || {}),
onDropFiles: (...params) => dispatchProps.editorState.onDropFiles(...params)
}
};
};
Expand Down

0 comments on commit 48cb2a3

Please sign in to comment.