React & Antd UI: Create task (#840)
* Separated component user selector * Change job assignee * Basic create task window * Bug fixes and refactoring * Create task connected with a server * Loading status for a button * Reset loading on error response * UI improvements * Github/feedback/share windowmain
parent
690ecf9c66
commit
3b6961f4db
@ -0,0 +1,60 @@
|
|||||||
|
import { AnyAction, Dispatch, ActionCreator } from 'redux';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
|
import { ShareFileInfo } from '../reducers/interfaces';
|
||||||
|
import getCore from '../core';
|
||||||
|
|
||||||
|
const core = getCore();
|
||||||
|
|
||||||
|
export enum ShareActionTypes {
|
||||||
|
LOAD_SHARE_DATA = 'LOAD_SHARE_DATA',
|
||||||
|
LOAD_SHARE_DATA_SUCCESS = 'LOAD_SHARE_DATA_SUCCESS',
|
||||||
|
LOAD_SHARE_DATA_FAILED = 'LOAD_SHARE_DATA_FAILED',
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadShareData(): AnyAction {
|
||||||
|
const action = {
|
||||||
|
type: ShareActionTypes.LOAD_SHARE_DATA,
|
||||||
|
payload: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadShareDataSuccess(values: ShareFileInfo[], directory: string): AnyAction {
|
||||||
|
const action = {
|
||||||
|
type: ShareActionTypes.LOAD_SHARE_DATA_SUCCESS,
|
||||||
|
payload: {
|
||||||
|
values,
|
||||||
|
directory,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadShareDataFailed(error: any): AnyAction {
|
||||||
|
const action = {
|
||||||
|
type: ShareActionTypes.LOAD_SHARE_DATA_FAILED,
|
||||||
|
payload: {
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadShareDataAsync(directory: string, success: () => void, failure: () => void):
|
||||||
|
ThunkAction<Promise<void>, {}, {}, AnyAction> {
|
||||||
|
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||||
|
try {
|
||||||
|
dispatch(loadShareData());
|
||||||
|
const values = await core.server.share(directory);
|
||||||
|
success();
|
||||||
|
dispatch(loadShareDataSuccess(values as ShareFileInfo[], directory));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(loadShareDataFailed(error));
|
||||||
|
failure();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,295 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
Tooltip,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Form, { FormComponentProps } from 'antd/lib/form/Form';
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
import patterns from '../../utils/validation-patterns';
|
||||||
|
|
||||||
|
export interface AdvancedConfiguration {
|
||||||
|
bugTracker?: string;
|
||||||
|
zOrder: boolean;
|
||||||
|
imageQuality?: number;
|
||||||
|
overlapSize?: number;
|
||||||
|
segmentSize?: number;
|
||||||
|
startFrame?: number;
|
||||||
|
stopFrame?: number;
|
||||||
|
frameFilter?: string;
|
||||||
|
lfs: boolean;
|
||||||
|
repository?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = FormComponentProps & {
|
||||||
|
onSubmit(values: AdvancedConfiguration): void
|
||||||
|
installedGit: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AdvancedConfigurationForm extends React.PureComponent<Props> {
|
||||||
|
public async submit() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.props.form.validateFields((error, values) => {
|
||||||
|
if (!error) {
|
||||||
|
const filteredValues = { ...values };
|
||||||
|
delete filteredValues.frameStep;
|
||||||
|
|
||||||
|
this.props.onSubmit({
|
||||||
|
...values,
|
||||||
|
frameFilter: values.frameStep ? `step=${values.frameStep}` : undefined,
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetFields() {
|
||||||
|
this.props.form.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderZOrder() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Tooltip overlay='Enable order for shapes. Useful for segmentation tasks'>
|
||||||
|
{this.props.form.getFieldDecorator('zOrder', {
|
||||||
|
initialValue: false,
|
||||||
|
valuePropName: 'checked',
|
||||||
|
})(
|
||||||
|
<Checkbox>
|
||||||
|
<Text className='cvat-black-color'>
|
||||||
|
Z-order
|
||||||
|
</Text>
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderImageQuality() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Tooltip overlay='Defines image compression level'>
|
||||||
|
<Text className='cvat-black-color'> Image quality </Text>
|
||||||
|
{this.props.form.getFieldDecorator('imageQuality', {
|
||||||
|
initialValue: 70,
|
||||||
|
rules: [{
|
||||||
|
required: true,
|
||||||
|
message: 'This field is required'
|
||||||
|
}],
|
||||||
|
})(
|
||||||
|
<Input
|
||||||
|
size='large'
|
||||||
|
type='number'
|
||||||
|
min={5}
|
||||||
|
max={100}
|
||||||
|
suffix={<Icon type='percentage'/>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderOverlap() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Tooltip overlay='Defines a number of intersected frames between different segments'>
|
||||||
|
<Text className='cvat-black-color'> Overlap size </Text>
|
||||||
|
{this.props.form.getFieldDecorator('overlapSize')(
|
||||||
|
<Input size='large' type='number'/>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSegmentSize() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Tooltip overlay='Defines a number of frames in a segment'>
|
||||||
|
<Text className='cvat-black-color'> Segment size </Text>
|
||||||
|
{this.props.form.getFieldDecorator('segmentSize')(
|
||||||
|
<Input size='large' type='number'/>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStartFrame() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Text className='cvat-black-color'> Start frame </Text>
|
||||||
|
{this.props.form.getFieldDecorator('startFrame')(
|
||||||
|
<Input
|
||||||
|
size='large'
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStopFrame() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Text className='cvat-black-color'> Stop frame </Text>
|
||||||
|
{this.props.form.getFieldDecorator('stopFrame')(
|
||||||
|
<Input
|
||||||
|
size='large'
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFrameStep() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Text className='cvat-black-color'> Frame step </Text>
|
||||||
|
{this.props.form.getFieldDecorator('frameStep')(
|
||||||
|
<Input
|
||||||
|
size='large'
|
||||||
|
type='number'
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGitLFSBox() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Tooltip overlay='If annotation files are large, you can use git LFS feature'>
|
||||||
|
{this.props.form.getFieldDecorator('lfs', {
|
||||||
|
valuePropName: 'checked',
|
||||||
|
initialValue: false,
|
||||||
|
})(
|
||||||
|
<Checkbox>
|
||||||
|
<Text className='cvat-black-color'>
|
||||||
|
Use LFS (Large File Support)
|
||||||
|
</Text>
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGitRepositoryURL() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Tooltip overlay={`Attach a git repository to store annotations.
|
||||||
|
Path is specified in square brackets`}>
|
||||||
|
<Text className='cvat-black-color'> Dataset repository URL </Text>
|
||||||
|
{this.props.form.getFieldDecorator('repository', {
|
||||||
|
// TODO: Add pattern
|
||||||
|
})(
|
||||||
|
<Input
|
||||||
|
placeholder='e.g. https//github.com/user/repos [annotation/<anno_file_name>.zip]'
|
||||||
|
size='large'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGit() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
{this.renderGitRepositoryURL()}
|
||||||
|
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
{this.renderGitLFSBox()}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderBugTracker() {
|
||||||
|
return (
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
<Tooltip overlay='Attach issue tracker where the task is described'>
|
||||||
|
<Text className='cvat-black-color'> Issue tracker </Text>
|
||||||
|
{this.props.form.getFieldDecorator('bugTracker', {
|
||||||
|
rules: [{
|
||||||
|
...patterns.validateURL,
|
||||||
|
}]
|
||||||
|
})(
|
||||||
|
<Input
|
||||||
|
size='large'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<Row><Col>
|
||||||
|
{this.renderZOrder()}
|
||||||
|
</Col></Row>
|
||||||
|
|
||||||
|
<Row type='flex' justify='start'>
|
||||||
|
<Col span={7}>
|
||||||
|
{this.renderImageQuality()}
|
||||||
|
</Col>
|
||||||
|
<Col span={7} offset={1}>
|
||||||
|
{this.renderOverlap()}
|
||||||
|
</Col>
|
||||||
|
<Col span={7} offset={1}>
|
||||||
|
{this.renderSegmentSize()}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row type='flex' justify='start'>
|
||||||
|
<Col span={7}>
|
||||||
|
{this.renderStartFrame()}
|
||||||
|
</Col>
|
||||||
|
<Col span={7} offset={1}>
|
||||||
|
{this.renderStopFrame()}
|
||||||
|
</Col>
|
||||||
|
<Col span={7} offset={1}>
|
||||||
|
{this.renderFrameStep()}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{ this.props.installedGit ? this.renderGit() : null}
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
{this.renderBugTracker()}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form.create<Props>()(AdvancedConfigurationForm);
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
import Form, { FormComponentProps } from 'antd/lib/form/Form';
|
||||||
|
|
||||||
|
export interface BaseConfiguration {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = FormComponentProps & {
|
||||||
|
onSubmit(values: BaseConfiguration): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BasicConfigurationForm extends React.PureComponent<Props> {
|
||||||
|
public async submit() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.props.form.validateFields((error, values) => {
|
||||||
|
if (!error) {
|
||||||
|
this.props.onSubmit({
|
||||||
|
name: values.name,
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetFields() {
|
||||||
|
this.props.form.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { getFieldDecorator } = this.props.form;
|
||||||
|
return (
|
||||||
|
<Form onSubmit={(e: React.FormEvent) => e.preventDefault()}>
|
||||||
|
<Text type='secondary'> Name </Text>
|
||||||
|
<Form.Item style={{marginBottom: '0px'}}>
|
||||||
|
{ getFieldDecorator('name', {
|
||||||
|
rules: [{
|
||||||
|
required: true,
|
||||||
|
message: 'Please, specify a name',
|
||||||
|
}] // TODO: Add task name pattern
|
||||||
|
})(
|
||||||
|
<Input/>
|
||||||
|
) }
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form.create<Props>()(BasicConfigurationForm);
|
||||||
@ -0,0 +1,243 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Alert,
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
message,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form';
|
||||||
|
import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form';
|
||||||
|
import LabelsEditor from '../labels-editor/labels-editor';
|
||||||
|
import FileManagerContainer from '../../containers/file-manager/file-manager';
|
||||||
|
import { Files } from '../file-manager/file-manager';
|
||||||
|
|
||||||
|
export interface CreateTaskData {
|
||||||
|
basic: BaseConfiguration;
|
||||||
|
advanced: AdvancedConfiguration;
|
||||||
|
labels: any[];
|
||||||
|
files: Files,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onCreate: (data: CreateTaskData) => void;
|
||||||
|
status: string;
|
||||||
|
error: string;
|
||||||
|
installedGit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = CreateTaskData;
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
basic: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
zOrder: false,
|
||||||
|
lfs: false,
|
||||||
|
},
|
||||||
|
labels: [],
|
||||||
|
files: {
|
||||||
|
local: [],
|
||||||
|
share: [],
|
||||||
|
remote: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class CreateTaskContent extends React.PureComponent<Props, State> {
|
||||||
|
private basicConfigurationComponent: any;
|
||||||
|
private advancedConfigurationComponent: any;
|
||||||
|
private fileManagerContainer: any;
|
||||||
|
|
||||||
|
public constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { ...defaultState };
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateLabels = () => {
|
||||||
|
return !!this.state.labels.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateFiles = () => {
|
||||||
|
const files = this.fileManagerContainer.getFiles();
|
||||||
|
this.setState({
|
||||||
|
files,
|
||||||
|
});
|
||||||
|
const totalLen = Object.keys(files).reduce(
|
||||||
|
(acc, key) => acc + files[key].length, 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return !!totalLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSubmitBasicConfiguration = (values: BaseConfiguration) => {
|
||||||
|
this.setState({
|
||||||
|
basic: {...values},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleSubmitAdvancedConfiguration = (values: AdvancedConfiguration) => {
|
||||||
|
this.setState({
|
||||||
|
advanced: {...values},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleSubmitClick = () => {
|
||||||
|
if (!this.validateLabels()) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not create a task',
|
||||||
|
content: 'A task must contain at least one label',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateFiles()) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not create a task',
|
||||||
|
content: 'A task must contain at least one file',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.basicConfigurationComponent.submit()
|
||||||
|
.then(() => {
|
||||||
|
return this.advancedConfigurationComponent ?
|
||||||
|
this.advancedConfigurationComponent.submit() :
|
||||||
|
new Promise((resolve) => {
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.props.onCreate(this.state);
|
||||||
|
})
|
||||||
|
.catch((_: any) => {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not create a task',
|
||||||
|
content: 'Please, check configuration you specified',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderBasicBlock() {
|
||||||
|
return (
|
||||||
|
<Col span={24}>
|
||||||
|
<BasicConfigurationForm wrappedComponentRef={
|
||||||
|
(component: any) => { this.basicConfigurationComponent = component }
|
||||||
|
} onSubmit={this.handleSubmitBasicConfiguration}/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLabelsBlock() {
|
||||||
|
return (
|
||||||
|
<Col span={24}>
|
||||||
|
<Text type='secondary'> Labels </Text>
|
||||||
|
<LabelsEditor
|
||||||
|
labels={this.state.labels}
|
||||||
|
onSubmit={
|
||||||
|
(labels) => {
|
||||||
|
this.setState({
|
||||||
|
labels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFilesBlock() {
|
||||||
|
return (
|
||||||
|
<Col span={24}>
|
||||||
|
<FileManagerContainer ref={
|
||||||
|
(container: any) =>
|
||||||
|
this.fileManagerContainer = container
|
||||||
|
}/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAdvancedBlock() {
|
||||||
|
return (
|
||||||
|
<Col span={24}>
|
||||||
|
<Collapse>
|
||||||
|
<Collapse.Panel
|
||||||
|
header={
|
||||||
|
<Text className='cvat-title'> Advanced configuration </Text>
|
||||||
|
} key='1'>
|
||||||
|
<AdvancedConfigurationForm
|
||||||
|
installedGit={this.props.installedGit}
|
||||||
|
wrappedComponentRef={
|
||||||
|
(component: any) => {
|
||||||
|
this.advancedConfigurationComponent = component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSubmit={this.handleSubmitAdvancedConfiguration}
|
||||||
|
/>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Props) {
|
||||||
|
if (this.props.error && prevProps.error !== this.props.error) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not create task',
|
||||||
|
content: this.props.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.status === 'CREATED' && prevProps.status !== 'CREATED') {
|
||||||
|
message.success('The task has been created');
|
||||||
|
|
||||||
|
this.basicConfigurationComponent.resetFields();
|
||||||
|
if (this.advancedConfigurationComponent) {
|
||||||
|
this.advancedConfigurationComponent.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fileManagerContainer.reset();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...defaultState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const loading = !!this.props.status
|
||||||
|
&& this.props.status !== 'CREATED'
|
||||||
|
&& !this.props.error;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row type='flex' justify='start' align='middle' className='cvat-create-task-content'>
|
||||||
|
<Col span={24}>
|
||||||
|
<Text className='cvat-title'> Basic configuration </Text>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{ this.renderBasicBlock() }
|
||||||
|
{ this.renderLabelsBlock() }
|
||||||
|
{ this.renderFilesBlock() }
|
||||||
|
{ this.renderAdvancedBlock() }
|
||||||
|
|
||||||
|
<Col span={14}>
|
||||||
|
{loading ? <Alert message={this.props.status}/> : null}
|
||||||
|
</Col>
|
||||||
|
<Col span={10}>
|
||||||
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading}
|
||||||
|
type='danger'
|
||||||
|
onClick={this.handleSubmitClick}
|
||||||
|
> Submit </Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Modal,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
import CreateTaskContent, { CreateTaskData } from './create-task-content';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onCreate: (data: CreateTaskData) => void;
|
||||||
|
error: string;
|
||||||
|
status: string;
|
||||||
|
installedGit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateTaskPage(props: Props) {
|
||||||
|
return (
|
||||||
|
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
|
||||||
|
<Col md={20} lg={16} xl={14} xxl={9}>
|
||||||
|
<Text className='cvat-title'> Create a new task</Text>
|
||||||
|
<CreateTaskContent
|
||||||
|
status={props.status}
|
||||||
|
error={props.error}
|
||||||
|
onCreate={props.onCreate}
|
||||||
|
installedGit={props.installedGit}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
Popover,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FacebookShareButton,
|
||||||
|
LinkedinShareButton,
|
||||||
|
TwitterShareButton,
|
||||||
|
TelegramShareButton,
|
||||||
|
WhatsappShareButton,
|
||||||
|
VKShareButton,
|
||||||
|
RedditShareButton,
|
||||||
|
ViberShareButton,
|
||||||
|
FacebookIcon,
|
||||||
|
TwitterIcon,
|
||||||
|
TelegramIcon,
|
||||||
|
WhatsappIcon,
|
||||||
|
VKIcon,
|
||||||
|
RedditIcon,
|
||||||
|
ViberIcon,
|
||||||
|
LineIcon,
|
||||||
|
} from 'react-share';
|
||||||
|
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Feedback extends React.PureComponent<{}, State> {
|
||||||
|
public constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
active: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderContent() {
|
||||||
|
const githubURL = 'https://github.com/opencv/cvat';
|
||||||
|
const githubImage = 'https://raw.githubusercontent.com/opencv/'
|
||||||
|
+ 'cvat/develop/cvat/apps/documentation/static/documentation/images/cvat.jpg';
|
||||||
|
const questionsURL = 'https://gitter.im/opencv-cvat/public';
|
||||||
|
const feedbackURL = 'https://gitter.im/opencv-cvat/public';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Icon type='star'/>
|
||||||
|
<Text style={{marginLeft: '10px'}}>
|
||||||
|
Star us on <a target='_blank' href={githubURL}>GitHub</a>
|
||||||
|
</Text>
|
||||||
|
<br/>
|
||||||
|
<Icon type='like'/>
|
||||||
|
<Text style={{marginLeft: '10px'}}>
|
||||||
|
Left a <a target='_blank' href={feedbackURL}>feedback</a>
|
||||||
|
</Text>
|
||||||
|
<hr/>
|
||||||
|
<div style={{display: 'flex'}}>
|
||||||
|
<FacebookShareButton url={githubURL} quote='Computer Vision Annotation Tool'>
|
||||||
|
<FacebookIcon size={32} round={true} />
|
||||||
|
</FacebookShareButton>
|
||||||
|
<VKShareButton url={githubURL} title='Computer Vision Annotation Tool' image={githubImage} description='CVAT'>
|
||||||
|
<VKIcon size={32} round={true} />
|
||||||
|
</VKShareButton>
|
||||||
|
<TwitterShareButton url={githubURL} title='Computer Vision Annotation Tool' hashtags={['CVAT']}>
|
||||||
|
<TwitterIcon size={32} round={true} />
|
||||||
|
</TwitterShareButton>
|
||||||
|
<RedditShareButton url={githubURL} title='Computer Vision Annotation Tool'>
|
||||||
|
<RedditIcon size={32} round={true} />
|
||||||
|
</RedditShareButton>
|
||||||
|
<LinkedinShareButton url={githubURL}>
|
||||||
|
<LineIcon size={32} round={true} />
|
||||||
|
</LinkedinShareButton>
|
||||||
|
<TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'>
|
||||||
|
<TelegramIcon size={32} round={true} />
|
||||||
|
</TelegramShareButton>
|
||||||
|
<WhatsappShareButton url={githubURL} title='Computer Vision Annotation Tool'>
|
||||||
|
<WhatsappIcon size={32} round={true} />
|
||||||
|
</WhatsappShareButton>
|
||||||
|
<ViberShareButton url={githubURL} title='Computer Vision Annotation Tool'>
|
||||||
|
<ViberIcon size={32} round={true} />
|
||||||
|
</ViberShareButton>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<Text style={{marginTop: '50px'}}>
|
||||||
|
Do you need help? Contact us on <a href={questionsURL}>gitter</a>
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popover
|
||||||
|
placement='leftTop'
|
||||||
|
title={
|
||||||
|
<Text className='cvat-title'>Help to make CVAT better</Text>
|
||||||
|
}
|
||||||
|
content={this.renderContent()}
|
||||||
|
visible={this.state.active}
|
||||||
|
>
|
||||||
|
<Button style={{color: '#ff4d4f'}} className='cvat-feedback-button' type='link' onClick={() => {
|
||||||
|
this.setState({
|
||||||
|
active: !this.state.active,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
{ this.state.active ? <Icon type='close-circle' theme='filled'/> :
|
||||||
|
<Icon type='message' theme='twoTone'/> }
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Upload,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Tree, { AntTreeNode, TreeNodeNormal } from 'antd/lib/tree/Tree';
|
||||||
|
import { RcFile } from 'antd/lib/upload';
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
export interface Files {
|
||||||
|
local: File[];
|
||||||
|
share: string[];
|
||||||
|
remote: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
files: Files;
|
||||||
|
expandedKeys: string[];
|
||||||
|
active: 'local' | 'share' | 'remote';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
treeData: TreeNodeNormal[];
|
||||||
|
onLoadData: (key: string, success: () => void, failure: () => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FileManager extends React.PureComponent<Props, State> {
|
||||||
|
public constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
files: {
|
||||||
|
local: [],
|
||||||
|
share: [],
|
||||||
|
remote: [],
|
||||||
|
},
|
||||||
|
expandedKeys: [],
|
||||||
|
active: 'local',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loadData('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
private loadData = (key: string) => {
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
const success = () => resolve();
|
||||||
|
const failure = () => reject();
|
||||||
|
this.props.onLoadData(key, success, failure);
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLocalSelector() {
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane key='local' tab='My computer'>
|
||||||
|
<Upload.Dragger
|
||||||
|
multiple
|
||||||
|
fileList={this.state.files.local as any[]}
|
||||||
|
showUploadList={false}
|
||||||
|
beforeUpload={(_: RcFile, files: RcFile[]) => {
|
||||||
|
this.setState({
|
||||||
|
files: {
|
||||||
|
...this.state.files,
|
||||||
|
local: files
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<p className='ant-upload-drag-icon'>
|
||||||
|
<Icon type='inbox' />
|
||||||
|
</p>
|
||||||
|
<p className='ant-upload-text'>Click or drag files to this area</p>
|
||||||
|
<p className='ant-upload-hint'>
|
||||||
|
Support for a bulk images or a single video
|
||||||
|
</p>
|
||||||
|
</Upload.Dragger>
|
||||||
|
{ this.state.files.local.length ?
|
||||||
|
<>
|
||||||
|
<br/>
|
||||||
|
<Text className='cvat-black-color'>
|
||||||
|
{this.state.files.local.length} file(s) selected
|
||||||
|
</Text>
|
||||||
|
</> : null
|
||||||
|
}
|
||||||
|
</Tabs.TabPane>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderShareSelector() {
|
||||||
|
function renderTreeNodes(data: TreeNodeNormal[]) {
|
||||||
|
return data.map((item: TreeNodeNormal) => {
|
||||||
|
if (item.children) {
|
||||||
|
return (
|
||||||
|
<Tree.TreeNode title={item.title} key={item.key} dataRef={item} isLeaf={item.isLeaf}>
|
||||||
|
{renderTreeNodes(item.children)}
|
||||||
|
</Tree.TreeNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Tree.TreeNode key={item.key} {...item} dataRef={item} />;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane key='share' tab='Connected file share'>
|
||||||
|
{ this.props.treeData.length ?
|
||||||
|
<Tree
|
||||||
|
className='cvat-share-tree'
|
||||||
|
checkable
|
||||||
|
showLine
|
||||||
|
checkStrictly={false}
|
||||||
|
expandedKeys={this.state.expandedKeys}
|
||||||
|
checkedKeys={this.state.files.share}
|
||||||
|
loadData={(node: AntTreeNode) => {
|
||||||
|
return this.loadData(node.props.dataRef.key);
|
||||||
|
}}
|
||||||
|
onExpand={(expandedKeys: string[]) => {
|
||||||
|
this.setState({
|
||||||
|
expandedKeys,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCheck={(checkedKeys: string[] | {checked: string[], halfChecked: string[]}) => {
|
||||||
|
const keys = checkedKeys as string[];
|
||||||
|
this.setState({
|
||||||
|
files: {
|
||||||
|
...this.state.files,
|
||||||
|
share: keys,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
{ renderTreeNodes(this.props.treeData) }
|
||||||
|
</Tree> : <Text className='cvat-black-color'> No data found </Text>
|
||||||
|
}
|
||||||
|
</Tabs.TabPane>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderRemoteSelector() {
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane key='remote' tab='Remote sources'>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder='Enter one URL per line'
|
||||||
|
rows={6}
|
||||||
|
value={[...this.state.files.remote].join('\n')}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
this.setState({
|
||||||
|
files: {
|
||||||
|
...this.state.files,
|
||||||
|
remote: event.target.value.split('\n'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFiles(): Files {
|
||||||
|
return {
|
||||||
|
local: this.state.active === 'local' ? this.state.files.local : [],
|
||||||
|
share: this.state.active === 'share' ? this.state.files.share : [],
|
||||||
|
remote: this.state.active === 'remote' ? this.state.files.remote : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
this.setState({
|
||||||
|
expandedKeys: [],
|
||||||
|
active: 'local',
|
||||||
|
files: {
|
||||||
|
local: [],
|
||||||
|
share: [],
|
||||||
|
remote: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text type='secondary'> Select files </Text>
|
||||||
|
<Tabs type='card' tabBarGutter={5} onChange={(activeKey: string) => this.setState({
|
||||||
|
active: activeKey as any,
|
||||||
|
})}>
|
||||||
|
{ this.renderLocalSelector() }
|
||||||
|
{ this.renderShareSelector() }
|
||||||
|
{ this.renderRemoteSelector() }
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string | null;
|
||||||
|
users: any[];
|
||||||
|
onChange: (user: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserSelector(props: Props) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
defaultValue={props.value ? props.value : '\0'}
|
||||||
|
size='small'
|
||||||
|
showSearch
|
||||||
|
className='cvat-user-selector'
|
||||||
|
onChange={props.onChange}
|
||||||
|
>
|
||||||
|
<Select.Option key='-1' value='\0'>{'\0'}</Select.Option>
|
||||||
|
{ props.users.map((user) => {
|
||||||
|
return (
|
||||||
|
<Select.Option key={user.id} value={user.username}>
|
||||||
|
{user.username}
|
||||||
|
</Select.Option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,9 +1,48 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
export default function CreateTaskPageContainer() {
|
import { CombinedState } from '../../reducers/root-reducer';
|
||||||
|
import CreateTaskComponent from '../../components/create-task-page/create-task-page';
|
||||||
|
import { CreateTaskData } from '../../components/create-task-page/create-task-content';
|
||||||
|
import { createTaskAsync } from '../../actions/tasks-actions';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
creatingError: string;
|
||||||
|
status: string;
|
||||||
|
installedGit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
create: (data: CreateTaskData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
create: (data: CreateTaskData) => dispatch(createTaskAsync(data)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
const { creates } = state.tasks.activities;
|
||||||
|
return {
|
||||||
|
...creates,
|
||||||
|
installedGit: state.plugins.plugins.GIT_INTEGRATION,
|
||||||
|
creatingError: creates.creatingError ? creates.creatingError.toString() : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateTaskPageContainer(props: StateToProps & DispatchToProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<CreateTaskComponent
|
||||||
"Create Task Page"
|
error={props.creatingError}
|
||||||
</div>
|
status={props.status}
|
||||||
|
onCreate={props.create}
|
||||||
|
installedGit={props.installedGit}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(CreateTaskPageContainer);
|
||||||
|
|||||||
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { TreeNodeNormal } from 'antd/lib/tree/Tree'
|
||||||
|
import FileManagerComponent, { Files } from '../../components/file-manager/file-manager';
|
||||||
|
|
||||||
|
import { loadShareDataAsync } from '../../actions/share-actions';
|
||||||
|
import { ShareItem } from '../../reducers/interfaces';
|
||||||
|
import { CombinedState } from '../../reducers/root-reducer';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
treeData: TreeNodeNormal[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
getTreeData(key: string, success: () => void, failure: () => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
function convert(items: ShareItem[], path?: string): TreeNodeNormal[] {
|
||||||
|
return items.map((item): TreeNodeNormal => {
|
||||||
|
const key = `${path}/${item.name}`.replace(/\/+/g, '/'); // // => /
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
title: item.name,
|
||||||
|
isLeaf: item.type !== 'DIR',
|
||||||
|
children: convert(item.children, key),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { root } = state.share;
|
||||||
|
return {
|
||||||
|
treeData: convert(root.children, root.name),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
getTreeData: (key: string, success: () => void, failure: () => void) => {
|
||||||
|
dispatch(loadShareDataAsync(key, success, failure));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileManagerContainer extends React.PureComponent<StateToProps & DispatchToProps> {
|
||||||
|
private managerComponentRef: any;
|
||||||
|
|
||||||
|
public getFiles(): Files {
|
||||||
|
return this.managerComponentRef.getFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset(): Files {
|
||||||
|
return this.managerComponentRef.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<FileManagerComponent
|
||||||
|
treeData={this.props.treeData}
|
||||||
|
onLoadData={this.props.getTreeData}
|
||||||
|
ref={(component) => this.managerComponentRef = component}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
null,
|
||||||
|
{ forwardRef: true },
|
||||||
|
)(FileManagerContainer);
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { AnyAction } from 'redux';
|
||||||
|
|
||||||
|
import { ShareActionTypes } from '../actions/share-actions';
|
||||||
|
import { ShareState, ShareFileInfo, ShareItem } from './interfaces';
|
||||||
|
|
||||||
|
const defaultState: ShareState = {
|
||||||
|
root: {
|
||||||
|
name: '/',
|
||||||
|
type: 'DIR',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (state = defaultState, action: AnyAction): ShareState {
|
||||||
|
switch (action.type) {
|
||||||
|
case ShareActionTypes.LOAD_SHARE_DATA: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case ShareActionTypes.LOAD_SHARE_DATA_SUCCESS: {
|
||||||
|
const { values } = action.payload;
|
||||||
|
const { directory } = action.payload;
|
||||||
|
|
||||||
|
// Find directory item in storage
|
||||||
|
let dir = state.root;
|
||||||
|
for (const dirName of directory.split('/')) {
|
||||||
|
if (dirName) {
|
||||||
|
[dir] = dir.children.filter(
|
||||||
|
(child): boolean => child.name === dirName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update its children
|
||||||
|
dir.children = (values as ShareFileInfo[])
|
||||||
|
.map((value): ShareItem => ({
|
||||||
|
...value,
|
||||||
|
children: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case ShareActionTypes.LOAD_SHARE_DATA_FAILED: {
|
||||||
|
const { error } = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue