Add sorting of folders in the web ui

Fixes #140

Signed-off-by: Robin Appelman <robin@icewind.nl>
This commit is contained in:
Robin Appelman 2018-11-28 12:02:58 +01:00
parent 9abd51ce77
commit 98e57afbed
10 changed files with 176 additions and 111 deletions

View file

@ -105,8 +105,14 @@
#groupfolders-react-root th {
border-bottom: 1px #ddd solid;
cursor: pointer;
}
#groupfolders-react-root th .sort_arrow {
float: right;
color: #888;
}
#groupfolders-react-root td, #groupfolders-react-root th {
padding: 10px;
position: relative;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7,6 +7,7 @@ export interface Group {
}
export interface Folder {
id: number;
mount_point: string;
quota: number;
size: number;
@ -20,7 +21,7 @@ export class Api {
listFolders(): Thenable<Folder[]> {
return $.getJSON(this.getUrl('folders'))
.then((data: OCSResult<Folder[]>) => data.ocs.data);
.then((data: OCSResult<Folder[]>) => Object.keys(data.ocs.data).map(id => data.ocs.data[id]));
}
listGroups(): Thenable<Group[]> {

View file

@ -15,6 +15,12 @@
th {
border-bottom: 1px #ddd solid;
cursor: pointer;
.sort_arrow {
float: right;
color: #888;
}
}
td, th {

View file

@ -7,6 +7,8 @@ import {QuotaSelect} from './QuotaSelect';
import './App.css';
import {SubmitInput} from "./SubmitInput";
import {SortArrow} from "./SortArrow";
import FlipMove from "react-flip-move";
const defaultQuotaOptions = {
'1 GB': 1073741274,
@ -15,6 +17,8 @@ const defaultQuotaOptions = {
'Unlimited': -3
};
export type SortKey = 'mount_point' | 'quota' | 'groups';
export interface AppState {
folders: Folder[];
groups: Group[],
@ -23,6 +27,8 @@ export interface AppState {
editingMountPoint: number;
renameMountPoint: string;
filter: string;
sort: SortKey;
sortOrder: number;
}
export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search.Core> {
@ -35,19 +41,18 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search.
editingGroup: 0,
editingMountPoint: 0,
renameMountPoint: '',
filter: ''
filter: '',
sort: 'mount_point',
sortOrder: 1
};
componentDidMount() {
componentDidMount () {
this.api.listFolders().then((folders) => {
this.setState({folders});
});
this.api.listGroups().then((groups) => {
this.setState({groups});
});
// nc13
OC.Plugins.register('OCA.Search', this);
// nc14 and up
OC.Plugins.register('OCA.Search.Core', this);
}
@ -59,12 +64,13 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search.
this.setState({newMountPoint: ''});
this.api.createFolder(mountPoint).then((id) => {
const folders = this.state.folders;
folders[id] = {
folders.push({
mount_point: mountPoint,
groups: {},
quota: -3,
size: 0
};
size: 0,
id
});
this.setState({folders});
});
};
@ -75,82 +81,99 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search.
});
};
deleteFolder(id: number) {
const folderName = this.state.folders[id].mount_point;
deleteFolder (folder: Folder) {
OC.dialogs.confirm(
t('groupfolders', 'Are you sure you want to delete "{folderName}" and all files inside? This operation can not be undone', {folderName}),
t('groupfolders', 'Delete "{folderName}"?', {folderName}),
t('groupfolders', 'Are you sure you want to delete "{folderName}" and all files inside? This operation can not be undone', {folderName: folder.mount_point}),
t('groupfolders', 'Delete "{folderName}"?', {folderName: folder.mount_point}),
confirmed => {
if (confirmed) {
const folders = this.state.folders;
delete folders[id];
this.setState({folders});
this.api.deleteFolder(id);
this.setState({folders: this.state.folders.filter(item => item.id !== folder.id)});
this.api.deleteFolder(folder.id);
}
},
true
);
};
addGroup(folderId: number, group: string) {
addGroup (folder: Folder, group: string) {
const folders = this.state.folders;
folders[folderId].groups[group] = OC.PERMISSION_ALL;
folder.groups[group] = OC.PERMISSION_ALL;
this.setState({folders});
this.api.addGroup(folderId, group);
this.api.addGroup(folder.id, group);
}
removeGroup(folderId: number, group: string) {
removeGroup (folder: Folder, group: string) {
const folders = this.state.folders;
delete folders[folderId].groups[group];
delete folder.groups[group];
this.setState({folders});
this.api.removeGroup(folderId, group);
this.api.removeGroup(folder.id, group);
}
setPermissions(folderId: number, group: string, newPermissions: number) {
setPermissions (folder: Folder, group: string, newPermissions: number) {
const folders = this.state.folders;
folders[folderId].groups[group] = newPermissions;
folder.groups[group] = newPermissions;
this.setState({folders});
this.api.setPermissions(folderId, group, newPermissions);
this.api.setPermissions(folder.id, group, newPermissions);
}
setQuota(folderId: number, quota: number) {
setQuota (folder: Folder, quota: number) {
const folders = this.state.folders;
folders[folderId].quota = quota;
folder.quota = quota;
this.setState({folders});
this.api.setQuota(folderId, quota);
this.api.setQuota(folder.id, quota);
}
renameFolder(folderId: number, newName: string) {
renameFolder (folder: Folder, newName: string) {
const folders = this.state.folders;
folders[folderId].mount_point = newName;
// this.api.setQuota(folderId, quota);
folder.mount_point = newName;
this.setState({folders, editingMountPoint: 0});
this.api.renameFolder(folderId, newName);
this.api.renameFolder(folder.id, newName);
}
render() {
const rows = Object.keys(this.state.folders)
.filter(key => {
onSortClick = (sort: SortKey) => {
if (this.state.sort === sort) {
this.setState({sortOrder: -this.state.sortOrder});
} else {
this.setState({sortOrder: 1, sort});
}
};
render () {
const rows = this.state.folders
.filter(folder => {
if (this.state.filter === '') {
return true;
}
const id = parseInt(key, 10);
const row = this.state.folders[id];
return row.mount_point.toLowerCase().indexOf(this.state.filter.toLowerCase()) !== -1;
return folder.mount_point.toLowerCase().indexOf(this.state.filter.toLowerCase()) !== -1;
})
.map(key => {
const id = parseInt(key, 10);
const row = this.state.folders[id];
.sort((a, b) => {
switch (this.state.sort) {
case "mount_point":
return a.mount_point.localeCompare(b.mount_point) * this.state.sortOrder;
case "quota":
if (a.quota < 0 && b.quota >= 0) {
return this.state.sortOrder;
}
if (b.quota < 0 && a.quota >= 0) {
return -this.state.sortOrder;
}
return (a.quota - b.quota) * this.state.sortOrder;
case "groups":
return (Object.keys(a.groups).length - Object.keys(b.groups).length) * this.state.sortOrder;
}
})
.map(folder => {
const id = folder.id;
return <tr key={id}>
<td className="mountpoint">
{this.state.editingMountPoint === id ?
<SubmitInput
autoFocus={true}
onSubmitValue={this.renameFolder.bind(this, id)}
onSubmitValue={this.renameFolder.bind(this, folder)}
onClick={event => {
event.stopPropagation();
}}
initialValue={row.mount_point}
initialValue={folder.mount_point}
/> :
<a
className="action-rename"
@ -159,7 +182,7 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search.
this.setState({editingMountPoint: id})
}}
>
{row.mount_point}
{folder.mount_point}
</a>
}
</td>
@ -170,22 +193,22 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search.
event.stopPropagation();
this.setState({editingGroup: id})
}}
groups={row.groups}
groups={folder.groups}
allGroups={this.state.groups}
onAddGroup={this.addGroup.bind(this, id)}
removeGroup={this.removeGroup.bind(this, id)}
onSetPermissions={this.setPermissions.bind(this, id)}
onAddGroup={this.addGroup.bind(this, folder)}
removeGroup={this.removeGroup.bind(this, folder)}
onSetPermissions={this.setPermissions.bind(this, folder)}
/>
</td>
<td className="quota">
<QuotaSelect options={defaultQuotaOptions}
value={row.quota}
size={row.size}
onChange={this.setQuota.bind(this, id)}/>
value={folder.quota}
size={folder.size}
onChange={this.setQuota.bind(this, folder)}/>
</td>
<td className="remove">
<a className="icon icon-delete icon-visible"
onClick={this.deleteFolder.bind(this, id)}
onClick={this.deleteFolder.bind(this, folder)}
title={t('groupfolders', 'Delete')}/>
</td>
</tr>
@ -198,37 +221,43 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search.
<table>
<thead>
<tr>
<th>
<th onClick={() => this.onSortClick('mount_point')}>
{t('groupfolders', 'Folder name')}
<SortArrow name='mount_point' value={this.state.sort}
direction={this.state.sortOrder}/>
</th>
<th>
<th onClick={() => this.onSortClick('groups')}>
{t('groupfolders', 'Groups')}
<SortArrow name='groups' value={this.state.sort}
direction={this.state.sortOrder}/>
</th>
<th>
{t('groupfolders', 'Quota')}
<th onClick={() => this.onSortClick('quota')}>
{t('quota', 'Quota')}
<SortArrow name='quota' value={this.state.sort}
direction={this.state.sortOrder}/>
</th>
<th/>
</tr>
</thead>
<tbody>
{rows}
<tr>
<td>
<form action="#" onSubmit={this.createRow}>
<input
className="newgroup-name"
value={this.state.newMountPoint}
placeholder={t('groupfolders', 'Folder name')}
onChange={(event) => {
this.setState({newMountPoint: event.target.value})
}}/>
<input type="submit"
value={t('groupfolders', 'Create')}/>
</form>
</td>
<td colSpan={3}/>
</tr>
</tbody>
<FlipMove typeName='tbody'>
{rows}
<tr>
<td>
<form action="#" onSubmit={this.createRow}>
<input
className="newgroup-name"
value={this.state.newMountPoint}
placeholder={t('groupfolders', 'Folder name')}
onChange={(event) => {
this.setState({newMountPoint: event.target.value})
}}/>
<input type="submit"
value={t('groupfolders', 'Create')}/>
</form>
</td>
<td colSpan={3}/>
</tr>
</FlipMove>
</table>
</div>;
}

17
js/SortArrow.tsx Normal file
View file

@ -0,0 +1,17 @@
import * as React from 'react';
export interface SortArrowProps {
name: string;
value: string;
direction: number;
}
export function SortArrow({name, value, direction}: SortArrowProps) {
if (name === value) {
return (<span className='sort_arrow'>
{direction < 0 ? '▼' : '▲'}
</span>);
} else {
return <span/>;
}
}

61
package-lock.json generated
View file

@ -733,7 +733,7 @@
},
"util": {
"version": "0.10.3",
"resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
@ -755,7 +755,7 @@
},
"async": {
"version": "1.5.2",
"resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
"dev": true
},
@ -2489,7 +2489,7 @@
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"dev": true,
"requires": {
@ -2526,7 +2526,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"dev": true,
"requires": {
@ -2570,7 +2570,7 @@
},
"buffer": {
"version": "4.9.1",
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"dev": true,
"requires": {
@ -3043,7 +3043,7 @@
},
"create-hash": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"dev": true,
"requires": {
@ -3056,7 +3056,7 @@
},
"create-hmac": {
"version": "1.1.7",
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"dev": true,
"requires": {
@ -3353,7 +3353,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"dev": true,
"requires": {
@ -3622,7 +3622,7 @@
},
"events": {
"version": "1.1.1",
"resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
"dev": true
},
@ -3901,7 +3901,7 @@
},
"finalhandler": {
"version": "1.1.1",
"resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"dev": true,
"requires": {
@ -4591,7 +4591,7 @@
},
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
},
@ -4669,7 +4669,7 @@
},
"globby": {
"version": "6.1.0",
"resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
"dev": true,
"requires": {
@ -4682,7 +4682,7 @@
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
@ -4695,7 +4695,7 @@
},
"handle-thing": {
"version": "1.2.5",
"resolved": "http://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
"integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=",
"dev": true
},
@ -4844,7 +4844,7 @@
},
"http-errors": {
"version": "1.6.3",
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"dev": true,
"requires": {
@ -4873,7 +4873,7 @@
},
"http-proxy-middleware": {
"version": "0.18.0",
"resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
"integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
"dev": true,
"requires": {
@ -5263,7 +5263,7 @@
},
"is-obj": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
@ -5663,7 +5663,7 @@
},
"media-typer": {
"version": "0.3.0",
"resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true
},
@ -6241,7 +6241,7 @@
},
"parse-asn1": {
"version": "5.1.1",
"resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
"dev": true,
"requires": {
@ -7327,6 +7327,11 @@
"scheduler": "^0.11.2"
}
},
"react-flip-move": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/react-flip-move/-/react-flip-move-3.0.3.tgz",
"integrity": "sha512-gR2jvjUgIXI7ceFWJkr8owX4vKhV0IJoXIf/Dt7gESFe5OKiSz2H6d10mKTW8fN134NDI16J4HgEgq9pKqJd5A=="
},
"react-hot-loader": {
"version": "4.3.12",
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.12.tgz",
@ -7349,7 +7354,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
@ -7444,13 +7449,13 @@
},
"regjsgen": {
"version": "0.2.0",
"resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
"dev": true
},
"regjsparser": {
"version": "0.1.5",
"resolved": "http://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
"dev": true,
"requires": {
@ -7459,7 +7464,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
}
@ -7627,7 +7632,7 @@
},
"safe-regex": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
"dev": true,
"requires": {
@ -7811,7 +7816,7 @@
},
"sha.js": {
"version": "2.4.11",
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dev": true,
"requires": {
@ -8170,7 +8175,7 @@
},
"stream-browserify": {
"version": "2.0.1",
"resolved": "http://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
"integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
"dev": true,
"requires": {
@ -8254,7 +8259,7 @@
},
"strip-eof": {
"version": "1.0.0",
"resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
@ -9217,7 +9222,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {

View file

@ -38,6 +38,7 @@
"oc-react-components": "^0.2.0",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-flip-move": "^3.0.3",
"whatwg-fetch": "^3.0.0"
}
}