REACT_APP_ARVADOS_CONFIG_URL=/config.json
REACT_APP_ARVADOS_API_HOST=qr1hi.arvadosapi.com
-REACT_APP_ARVADOS_KEEP_WEB_HOST=download.qr1hi.arvadosapi.com
+REACT_APP_ARVADOS_KEEP_WEB_HOST=collections.qr1hi.arvadosapi.com
HTTPS=true
\ No newline at end of file
"version": "0.1.0",
"private": true,
"dependencies": {
- "@material-ui/core": "1.4.2",
- "@material-ui/icons": "2.0.0",
+ "@material-ui/core": "1.5.0",
+ "@material-ui/icons": "2.0.2",
"@types/lodash": "4.14.116",
- "@types/react-copy-to-clipboard": "4.2.5",
- "@types/react-dropzone": "4.2.1",
- "@types/redux-form": "7.4.4",
+ "@types/react-copy-to-clipboard": "4.2.6",
+ "@types/react-dropzone": "4.2.2",
+ "@types/redux-form": "7.4.5",
"axios": "0.18.0",
"classnames": "2.2.6",
"lodash": "4.17.10",
"react": "16.4.2",
"react-copy-to-clipboard": "5.0.1",
"react-dom": "16.4.2",
- "react-dropzone": "4.2.13",
+ "react-dropzone": "5.0.1",
"react-redux": "5.0.7",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
},
"devDependencies": {
"@types/classnames": "^2.2.4",
- "@types/enzyme": "3.1.12",
- "@types/enzyme-adapter-react-16": "1.0.2",
+ "@types/enzyme": "3.1.13",
+ "@types/enzyme-adapter-react-16": "1.0.3",
"@types/jest": "23.3.1",
- "@types/node": "10.5.5",
+ "@types/node": "10.7.1",
"@types/react": "16.4",
- "@types/react-dom": "16.0.6",
+ "@types/react-dom": "16.0.7",
"@types/react-redux": "6.0.6",
"@types/react-router": "4.0.29",
"@types/react-router-dom": "4.3.0",
"@types/react-router-redux": "5.0.15",
"@types/redux-devtools": "3.0.44",
- "@types/redux-form": "7.4.4",
+ "@types/redux-form": "7.4.5",
"axios-mock-adapter": "1.15.0",
- "enzyme": "3.3.0",
- "enzyme-adapter-react-16": "1.1.1",
+ "enzyme": "3.4.4",
+ "enzyme-adapter-react-16": "1.2.0",
"jest-localstorage-mock": "2.2.0",
"redux-devtools": "3.4.1",
"redux-form": "7.4.2",
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { FilterBuilder } from "./filter-builder";
-import { OrderBuilder } from "./order-builder";
import { AxiosInstance, AxiosPromise } from "axios";
import { Resource } from "~/models/resource";
export interface ListArguments {
limit?: number;
offset?: number;
- filters?: FilterBuilder;
- order?: OrderBuilder;
+ filters?: string;
+ order?: string;
select?: string[];
distinct?: boolean;
count?: string;
const { filters, order, ...other } = args;
const params = {
...other,
- filters: filters ? filters.serialize() : undefined,
- order: order ? order.getOrder() : undefined
+ filters: filters ? `[${filters}]` : undefined,
+ order: order ? order : undefined
};
return CommonResourceService.defaultResponse(
this.serverApi
let filters: FilterBuilder;
beforeEach(() => {
- filters = FilterBuilder.create();
+ filters = new FilterBuilder();
});
it("should add 'equal' rule", () => {
expect(
- filters.addEqual("etag", "etagValue").serialize()
- ).toEqual(`[["etag","=","etagValue"]]`);
+ filters.addEqual("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","=","etagValue"]`);
});
it("should add 'like' rule", () => {
expect(
- filters.addLike("etag", "etagValue").serialize()
- ).toEqual(`[["etag","like","%etagValue%"]]`);
+ filters.addLike("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","like","%etagValue%"]`);
});
it("should add 'ilike' rule", () => {
expect(
- filters.addILike("etag", "etagValue").serialize()
- ).toEqual(`[["etag","ilike","%etagValue%"]]`);
+ filters.addILike("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","ilike","%etagValue%"]`);
});
it("should add 'is_a' rule", () => {
expect(
- filters.addIsA("etag", "etagValue").serialize()
- ).toEqual(`[["etag","is_a","etagValue"]]`);
+ filters.addIsA("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","is_a","etagValue"]`);
});
it("should add 'is_a' rule for set", () => {
expect(
- filters.addIsA("etag", ["etagValue1", "etagValue2"]).serialize()
- ).toEqual(`[["etag","is_a",["etagValue1","etagValue2"]]]`);
+ filters.addIsA("etag", ["etagValue1", "etagValue2"]).getFilters()
+ ).toEqual(`["etag","is_a",["etagValue1","etagValue2"]]`);
});
it("should add 'in' rule", () => {
expect(
- filters.addIn("etag", "etagValue").serialize()
- ).toEqual(`[["etag","in","etagValue"]]`);
+ filters.addIn("etag", "etagValue").getFilters()
+ ).toEqual(`["etag","in","etagValue"]`);
});
it("should add 'in' rule for set", () => {
expect(
- filters.addIn("etag", ["etagValue1", "etagValue2"]).serialize()
- ).toEqual(`[["etag","in",["etagValue1","etagValue2"]]]`);
+ filters.addIn("etag", ["etagValue1", "etagValue2"]).getFilters()
+ ).toEqual(`["etag","in",["etagValue1","etagValue2"]]`);
});
it("should add multiple rules", () => {
filters
.addIn("etag", ["etagValue1", "etagValue2"])
.addEqual("href", "hrefValue")
- .serialize()
- ).toEqual(`[["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]]`);
- });
-
- it("should concatenate multiple builders", () => {
- expect(
- filters
- .concat(FilterBuilder.create().addIn("etag", ["etagValue1", "etagValue2"]))
- .concat(FilterBuilder.create().addEqual("href", "hrefValue"))
- .serialize()
- ).toEqual(`[["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]]`);
+ .getFilters()
+ ).toEqual(`["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]`);
});
it("should add attribute prefix", () => {
- expect(FilterBuilder
- .create("myPrefix")
- .addIn("etag", ["etagValue1", "etagValue2"])
- .serialize())
- .toEqual(`[["my_prefix.etag","in",["etagValue1","etagValue2"]]]`);
+ expect(new FilterBuilder()
+ .addIn("etag", ["etagValue1", "etagValue2"], "myPrefix")
+ .getFilters())
+ .toEqual(`["my_prefix.etag","in",["etagValue1","etagValue2"]]`);
});
-
-
-
-
});
import * as _ from "lodash";
-export class FilterBuilder {
- static create(resourcePrefix = "") {
- return new FilterBuilder(resourcePrefix);
- }
-
- constructor(
- private resourcePrefix = "",
- private filters = "") { }
+export function joinFilters(filters0?: string, filters1?: string) {
+ return [filters0, filters1].filter(s => s).join(",");
+}
- public addEqual(field: string, value?: string) {
- return this.addCondition(field, "=", value);
- }
+export class FilterBuilder {
+ constructor(private filters = "") { }
- public addLike(field: string, value?: string) {
- return this.addCondition(field, "like", value, "%", "%");
+ public addEqual(field: string, value?: string, resourcePrefix?: string) {
+ return this.addCondition(field, "=", value, "", "", resourcePrefix );
}
- public addILike(field: string, value?: string) {
- return this.addCondition(field, "ilike", value, "%", "%");
+ public addLike(field: string, value?: string, resourcePrefix?: string) {
+ return this.addCondition(field, "like", value, "%", "%", resourcePrefix);
}
- public addIsA(field: string, value?: string | string[]) {
- return this.addCondition(field, "is_a", value);
+ public addILike(field: string, value?: string, resourcePrefix?: string) {
+ return this.addCondition(field, "ilike", value, "%", "%", resourcePrefix);
}
- public addIn(field: string, value?: string | string[]) {
- return this.addCondition(field, "in", value);
+ public addIsA(field: string, value?: string | string[], resourcePrefix?: string) {
+ return this.addCondition(field, "is_a", value, "", "", resourcePrefix);
}
- public concat(filterBuilder: FilterBuilder) {
- return new FilterBuilder(this.resourcePrefix, this.filters + (this.filters && filterBuilder.filters ? "," : "") + filterBuilder.getFilters());
+ public addIn(field: string, value?: string | string[], resourcePrefix?: string) {
+ return this.addCondition(field, "in", value, "", "", resourcePrefix);
}
public getFilters() {
return this.filters;
}
- public serialize() {
- return "[" + this.filters + "]";
- }
-
- private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "") {
+ private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "", resourcePrefix?: string) {
if (value) {
value = typeof value === "string"
? `"${prefix}${value}${postfix}"`
: `["${value.join(`","`)}"]`;
- const resourcePrefix = this.resourcePrefix
- ? _.snakeCase(this.resourcePrefix) + "."
+ const resPrefix = resourcePrefix
+ ? _.snakeCase(resourcePrefix) + "."
: "";
- this.filters += `${this.filters ? "," : ""}["${resourcePrefix}${_.snakeCase(field.toString())}","${cond}",${value}]`;
+ this.filters += `${this.filters ? "," : ""}["${resPrefix}${_.snakeCase(field)}","${cond}",${value}]`;
}
return this;
}
describe("OrderBuilder", () => {
it("should build correct order query", () => {
- const order = OrderBuilder
- .create()
+ const order = new OrderBuilder()
.addAsc("kind")
.addDesc("modifiedAt")
.getOrder();
- expect(order).toEqual(["kind asc", "modified_at desc"]);
- });
-
- it("should combine results with other builder", () => {
- const order = OrderBuilder
- .create()
- .addAsc("kind")
- .concat(OrderBuilder
- .create("properties")
- .addDesc("modifiedAt"))
- .getOrder();
- expect(order).toEqual(["kind asc", "properties.modified_at desc"]);
+ expect(order).toEqual("kind asc,modified_at desc");
});
});
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { Resource } from "../../models/resource";
+import { Resource } from "~/models/resource";
-export class OrderBuilder<T extends Resource = Resource> {
-
- static create<T extends Resource = Resource>(prefix?: string){
- return new OrderBuilder<T>([], prefix);
- }
+export enum OrderDirection { ASC, DESC }
- private constructor(
- private order: string[] = [],
- private prefix = ""){}
+export class OrderBuilder<T extends Resource = Resource> {
- private addRule (direction: string, attribute: keyof T) {
- const prefix = this.prefix ? this.prefix + "." : "";
- const order = [...this.order, `${prefix}${_.snakeCase(attribute.toString())} ${direction}`];
- return new OrderBuilder<T>(order, prefix);
- }
+ constructor(private order: string[] = []) {}
- addAsc(attribute: keyof T) {
- return this.addRule("asc", attribute);
+ addOrder(direction: OrderDirection, attribute: keyof T, prefix?: string) {
+ this.order.push(`${prefix ? prefix + "." : ""}${_.snakeCase(attribute.toString())} ${direction === OrderDirection.ASC ? "asc" : "desc"}`);
+ return this;
}
- addDesc(attribute: keyof T) {
- return this.addRule("desc", attribute);
+ addAsc(attribute: keyof T, prefix?: string) {
+ return this.addOrder(OrderDirection.ASC, attribute, prefix);
}
- concat(orderBuilder: OrderBuilder){
- return new OrderBuilder<T>(
- this.order.concat(orderBuilder.getOrder()),
- this.prefix
- );
+ addDesc(attribute: keyof T, prefix?: string) {
+ return this.addOrder(OrderDirection.DESC, attribute, prefix);
}
getOrder() {
- return this.order.slice();
+ return this.order.join(",");
}
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const getTagValue = (document: Document | Element, tagName: string, defaultValue: string) => {
+ const [el] = Array.from(document.getElementsByTagName(tagName));
+ return el ? el.innerHTML : defaultValue;
+};
{items.map((group, groupIndex) =>
<React.Fragment key={groupIndex}>
{group.map((item, actionIndex) =>
- <ListItem
- button
- key={actionIndex}
- onClick={() => onItemClick(item)}>
- {item.icon &&
- <ListItemIcon>
- <item.icon />
- </ListItemIcon>}
- {item.name &&
- <ListItemText>
- {item.name}
- </ListItemText>}
- {item.component &&
- <item.component />}
- </ListItem>)}
+ item.component
+ ? <item.component
+ key={actionIndex}
+ onClick={() => onItemClick(item)} />
+ : <ListItem
+ button
+ key={actionIndex}
+ onClick={() => onItemClick(item)}>
+ {item.icon &&
+ <ListItemIcon>
+ <item.icon />
+ </ListItemIcon>}
+ {item.name &&
+ <ListItemText>
+ {item.name}
+ </ListItemText>}
+ </ListItem>)}
{groupIndex < items.length - 1 && <Divider />}
</React.Fragment>)}
</List>
import MoreVertIcon from "@material-ui/icons/MoreVert";
import { ColumnSelector } from "../column-selector/column-selector";
import { DataTable, DataColumns } from "../data-table/data-table";
-import { DataColumn } from "../data-table/data-column";
+import { DataColumn, SortDirection } from "../data-table/data-column";
import { DataTableFilterItem } from '../data-table-filters/data-table-filters';
import { SearchInput } from '../search-input/search-input';
import { ArvadosTheme } from "~/common/custom-theme";
}
interface DataExplorerActionProps<T> {
+ onSetColumns: (columns: DataColumns<T>) => void;
onSearch: (value: string) => void;
onRowClick: (item: T) => void;
onRowDoubleClick: (item: T) => void;
export const DataExplorer = withStyles(styles)(
class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+ componentDidMount() {
+ if (this.props.onSetColumns) {
+ this.props.onSetColumns(this.props.columns);
+ }
+ }
render() {
- const {
- columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
- rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
- items, itemsAvailable, onRowClick, onRowDoubleClick, defaultIcon, defaultMessages, classes
+ const {
+ columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
+ rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
+ items, itemsAvailable, onRowClick, onRowDoubleClick, defaultIcon, defaultMessages, classes
} = this.props;
return <div>
{ items.length > 0 ? (
</Toolbar>
</Paper>
) : (
- <DefaultView
+ <DefaultView
classRoot={classes.defaultRoot}
icon={defaultIcon}
classIcon={classes.defaultIcon}
name: "Actions",
selected: true,
configurable: false,
+ sortDirection: SortDirection.NONE,
+ filters: [],
key: "context-actions",
render: this.renderContextMenuTrigger,
width: "auto"
import * as React from "react";
export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
+ key?: React.Key;
name: string;
selected: boolean;
configurable: boolean;
- key?: React.Key;
- sortDirection?: SortDirection;
- filters?: F[];
+ sortDirection: SortDirection;
+ filters: F[];
render: (item: T) => React.ReactElement<any>;
renderHeader?: () => React.ReactElement<any>;
width?: string;
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { ReactElement } from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from '~/common/custom-theme';
import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
import * as classnames from "classnames";
import { ListItemTextIcon } from '../list-item-text-icon/list-item-text-icon';
import { Dispatch } from "redux";
+import { RouteComponentProps, withRouter } from "react-router";
type CssRules = 'active' | 'row' | 'root' | 'list' | 'iconClose' | 'iconOpen' | 'toggableIconContainer' | 'toggableIcon';
export interface SidePanelItem {
id: string;
name: string;
+ url: string;
icon: IconType;
- active?: boolean;
open?: boolean;
margin?: boolean;
openAble?: boolean;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
}
-type SidePanelProps = SidePanelDataProps & WithStyles<CssRules>;
+type SidePanelProps = RouteComponentProps<{}> & SidePanelDataProps & WithStyles<CssRules>;
-export const SidePanel = withStyles(styles)(
+export const SidePanel = withStyles(styles)(withRouter(
class extends React.Component<SidePanelProps> {
- render(): ReactElement<any> {
+ render() {
const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
const { root, row, list, toggableIconContainer } = classes;
+
+ const path = this.props.location.pathname.split('/');
+ const activeUrl = path.length > 1 ? "/" + path[1] : "/";
return (
<div className={root}>
<List>
- {sidePanelItems.map(it => (
- <span key={it.name}>
+ {sidePanelItems.map(it => {
+ const active = it.url === activeUrl;
+ return <span key={it.name}>
<ListItem button className={list} onClick={() => toggleActive(it.id)}
onContextMenu={this.handleRowContextMenu(it)}>
<span className={row}>
{it.openAble ? (
<i onClick={() => toggleOpen(it.id)} className={toggableIconContainer}>
<ListItemIcon
- className={this.getToggableIconClassNames(it.open, it.active)}>
+ className={this.getToggableIconClassNames(it.open, active)}>
< SidePanelRightArrowIcon/>
</ListItemIcon>
</i>
) : null}
- <ListItemTextIcon icon={it.icon} name={it.name} isActive={it.active}
+ <ListItemTextIcon icon={it.icon} name={it.name} isActive={active}
hasMargin={it.margin}/>
</span>
</ListItem>
{children}
</Collapse>
) : null}
- </span>
- ))}
+ </span>;
+ })}
</List>
</div>
);
(event: React.MouseEvent<HTMLElement>) =>
item.openAble ? this.props.onContextMenu(event, item) : null
}
-);
+));
import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
import { collectionResourceActionSet } from './views-components/context-menu/action-sets/collection-resource-action-set';
+const getBuildNumber = () => "BN-" + (process.env.BUILD_NUMBER || "dev");
+const getGitCommit = () => "GIT-" + (process.env.GIT_COMMIT || "latest").substr(0, 7);
+const getBuildInfo = () => getBuildNumber() + " / " + getGitCommit();
+
+const buildInfo = getBuildInfo();
+
+console.log(`Starting arvados [${buildInfo}]`);
+
addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
store.dispatch(initAuth());
store.dispatch(getProjectList(services.authService.getUuid()));
-
+
const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props}/>;
- const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} {...props}/>;
+ const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props}/>;
const App = () =>
<MuiThemeProvider theme={CustomTheme}>
//
// SPDX-License-Identifier: AGPL-3.0
-import { Tree } from './tree';
+import { Tree, createTree, setNode } from './tree';
export type CollectionFilesTree = Tree<CollectionDirectory | CollectionFile>;
export interface CollectionDirectory {
path: string;
+ url: string;
id: string;
name: string;
type: CollectionFileType.DIRECTORY;
export interface CollectionFile {
path: string;
+ url: string;
id: string;
name: string;
size: number;
id: '',
name: '',
path: '',
+ url: '',
type: CollectionFileType.DIRECTORY,
...data
});
id: '',
name: '',
path: '',
+ url: '',
size: 0,
type: CollectionFileType.FILE,
...data
});
+
+export const createCollectionFilesTree = (data: Array<CollectionDirectory | CollectionFile>) => {
+ const directories = data.filter(item => item.type === CollectionFileType.DIRECTORY);
+ directories.sort((a, b) => a.path.localeCompare(b.path));
+ const files = data.filter(item => item.type === CollectionFileType.FILE);
+ return [...directories, ...files]
+ .reduce((tree, item) => setNode({
+ children: [],
+ id: item.id,
+ parent: item.path,
+ value: item
+ })(tree), createTree<CollectionDirectory | CollectionFile>());
+};
\ No newline at end of file
id: '/a',
name: 'a',
size: 0,
- type: 'file'
+ type: 'file',
+ url: ''
}, {
path: '',
id: '/b',
name: 'b',
size: 0,
- type: 'file'
+ type: 'file',
+ url: ''
}, {
path: '',
id: '/output.txt',
name: 'output.txt',
size: 33,
- type: 'file'
+ type: 'file',
+ url: ''
}, {
path: '/c',
id: '/c/d',
name: 'd',
size: 0,
- type: 'file'
+ type: 'file',
+ url: ''
},]);
});
path: "",
id: '/c',
name: 'c',
- type: 'directory'
+ type: 'directory',
+ url: ''
}, {
path: '/c',
id: '/c/user',
name: 'user',
- type: 'directory'
+ type: 'directory',
+ url: ''
}, {
path: '/c/user',
id: '/c/user/results',
name: 'results',
- type: 'directory'
+ type: 'directory',
+ url: ''
},]);
});
//
// SPDX-License-Identifier: AGPL-3.0
+import * as _ from "lodash";
import { CommonResourceService } from "~/common/api/common-resource-service";
import { CollectionResource } from "~/models/collection";
import axios, { AxiosInstance } from "axios";
import { KeepService } from "../keep-service/keep-service";
+import { WebDAV } from "~/common/webdav";
+import { AuthService } from "../auth-service/auth-service";
+import { mapTree, getNodeChildren, getNode, TreeNode } from "../../models/tree";
+import { getTagValue } from "~/common/xml";
import { FilterBuilder } from "~/common/api/filter-builder";
-import { CollectionFile, createCollectionFile } from "~/models/collection-file";
+import { CollectionFile, createCollectionFile, CollectionFileType, CollectionDirectory, createCollectionDirectory } from '~/models/collection-file';
import { parseKeepManifestText, stringifyKeepManifest } from "../collection-files-service/collection-manifest-parser";
-import * as _ from "lodash";
import { KeepManifestStream } from "~/models/keep-manifest";
+import { createCollectionFilesTree } from '~/models/collection-file';
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
export class CollectionService extends CommonResourceService<CollectionResource> {
- constructor(serverApi: AxiosInstance, private keepService: KeepService) {
+ constructor(serverApi: AxiosInstance, private keepService: KeepService, private webdavClient: WebDAV, private authService: AuthService) {
super(serverApi, "collections");
}
+ async files(uuid: string) {
+ const request = await this.webdavClient.propfind(`/c=${uuid}`);
+ if (request.responseXML != null) {
+ const files = this.extractFilesData(request.responseXML);
+ const tree = createCollectionFilesTree(files);
+ const sortedTree = mapTree(node => {
+ const children = getNodeChildren(node.id)(tree).map(id => getNode(id)(tree)) as TreeNode<CollectionDirectory | CollectionFile>[];
+ children.sort((a, b) =>
+ a.value.type !== b.value.type
+ ? a.value.type === CollectionFileType.DIRECTORY ? -1 : 1
+ : a.value.name.localeCompare(b.value.name)
+ );
+ return { ...node, children: children.map(child => child.id) };
+ })(tree);
+ return sortedTree;
+ }
+ return Promise.reject();
+ }
+
+ async deleteFile(collectionUuid: string, filePath: string) {
+ return this.webdavClient.delete(`/c=${collectionUuid}${filePath}`);
+ }
+
+ extractFilesData(document: Document) {
+ const collectionUrlPrefix = /\/c=[0-9a-zA-Z\-]*/;
+ return Array
+ .from(document.getElementsByTagName('D:response'))
+ .slice(1) // omit first element which is collection itself
+ .map(element => {
+ const name = getTagValue(element, 'D:displayname', '');
+ const size = parseInt(getTagValue(element, 'D:getcontentlength', '0'), 10);
+ const pathname = getTagValue(element, 'D:href', '');
+ const nameSuffix = `/${name || ''}`;
+ const directory = pathname
+ .replace(collectionUrlPrefix, '')
+ .replace(nameSuffix, '');
+ const href = this.webdavClient.defaults.baseURL + pathname + '?api_token=' + this.authService.getApiToken();
+
+ const data = {
+ url: href,
+ id: `${directory}/${name}`,
+ name,
+ path: directory,
+ };
+
+ return getTagValue(element, 'D:resourcetype', '')
+ ? createCollectionDirectory(data)
+ : createCollectionFile({ ...data, size });
+
+ });
+ }
+
+
private readFile(file: File): Promise<ArrayBuffer> {
return new Promise<ArrayBuffer>(resolve => {
const reader = new FileReader();
}
uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress): Promise<CollectionResource | never> {
- const filters = FilterBuilder.create()
+ const filters = new FilterBuilder()
.addEqual("service_type", "proxy");
- return this.keepService.list({ filters }).then(data => {
+ return this.keepService.list({ filters: filters.getFilters() }).then(data => {
if (data.items && data.items.length > 0) {
const serviceHost =
(data.items[0].serviceSslFlag ? "https://" : "http://") +
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { LinkResource } from "~/models/link";
-import { GroupContentsResource, GroupContentsResourcePrefix } from "../groups-service/groups-service";
-import { OrderBuilder } from "~/common/api/order-builder";
-
-export class FavoriteOrderBuilder {
-
- static create(
- linkOrder = OrderBuilder.create<LinkResource>(),
- contentOrder = OrderBuilder.create<GroupContentsResource>()) {
- return new FavoriteOrderBuilder(linkOrder, contentOrder);
- }
-
- private constructor(
- private linkOrder: OrderBuilder<LinkResource>,
- private contentOrder: OrderBuilder<GroupContentsResource>
- ) { }
-
- addAsc(attribute: "name") {
- const linkOrder = this.linkOrder.addAsc(attribute);
- const contentOrder = this.contentOrder
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION).addAsc(attribute))
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS).addAsc(attribute))
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT).addAsc(attribute));
- return FavoriteOrderBuilder.create(linkOrder, contentOrder);
- }
-
- addDesc(attribute: "name") {
- const linkOrder = this.linkOrder.addDesc(attribute);
- const contentOrder = this.contentOrder
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION).addDesc(attribute))
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS).addDesc(attribute))
- .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT).addDesc(attribute));
- return FavoriteOrderBuilder.create(linkOrder, contentOrder);
- }
-
- getLinkOrder() {
- return this.linkOrder;
- }
-
- getContentOrder() {
- return this.contentOrder;
- }
-
-}
it("unmarks resource as favorite", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "linkUuid" }] }));
- const filters = FilterBuilder
- .create()
+ const filters = new FilterBuilder()
.addEqual('tailUuid', "userUuid")
.addEqual('headUuid', "resourceUuid")
.addEqual('linkClass', LinkClass.STAR);
const newFavorite = await favoriteService.delete({ userUuid: "userUuid", resourceUuid: "resourceUuid" });
- expect(list.mock.calls[0][0].filters.getFilters()).toEqual(filters.getFilters());
+ expect(list.mock.calls[0][0].filters).toEqual(filters.getFilters());
expect(linkService.delete).toHaveBeenCalledWith("linkUuid");
expect(newFavorite[0].uuid).toEqual("linkUuid");
});
it("lists favorite resources", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "headUuid" }] }));
- const listFilters = FilterBuilder
- .create()
+ const listFilters = new FilterBuilder()
.addEqual('tailUuid', "userUuid")
.addEqual('linkClass', LinkClass.STAR);
const contents = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "resourceUuid" }] }));
- const contentFilters = FilterBuilder.create().addIn('uuid', ["headUuid"]);
+ const contentFilters = new FilterBuilder().addIn('uuid', ["headUuid"]);
linkService.list = list;
groupService.contents = contents;
const favoriteService = new FavoriteService(linkService, groupService);
const favorites = await favoriteService.list("userUuid");
- expect(list.mock.calls[0][0].filters.getFilters()).toEqual(listFilters.getFilters());
+ expect(list.mock.calls[0][0].filters).toEqual(listFilters.getFilters());
expect(contents.mock.calls[0][0]).toEqual("userUuid");
- expect(contents.mock.calls[0][1].filters.getFilters()).toEqual(contentFilters.getFilters());
+ expect(contents.mock.calls[0][1].filters).toEqual(contentFilters.getFilters());
expect(favorites).toEqual({ items: [{ uuid: "resourceUuid" }] });
});
it("checks if resources are present in favorites", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "foo" }] }));
- const listFilters = FilterBuilder
- .create()
+ const listFilters = new FilterBuilder()
.addIn("headUuid", ["foo", "oof"])
.addEqual("tailUuid", "userUuid")
.addEqual("linkClass", LinkClass.STAR);
const favorites = await favoriteService.checkPresenceInFavorites("userUuid", ["foo", "oof"]);
- expect(list.mock.calls[0][0].filters.getFilters()).toEqual(listFilters.getFilters());
+ expect(list.mock.calls[0][0].filters).toEqual(listFilters.getFilters());
expect(favorites).toEqual({ foo: true, oof: false });
});
import { LinkService } from "../link-service/link-service";
import { GroupsService, GroupContentsResource } from "../groups-service/groups-service";
-import { LinkResource, LinkClass } from "~/models/link";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { LinkClass } from "~/models/link";
+import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
import { ListResults } from "~/common/api/common-resource-service";
-import { FavoriteOrderBuilder } from "./favorite-order-builder";
-import { OrderBuilder } from "~/common/api/order-builder";
export interface FavoriteListArguments {
limit?: number;
offset?: number;
- filters?: FilterBuilder;
- order?: FavoriteOrderBuilder;
+ filters?: string;
+ linkOrder?: string;
+ contentOrder?: string;
}
export class FavoriteService {
constructor(
private linkService: LinkService,
private groupsService: GroupsService
- ) { }
+ ) {}
create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
return this.linkService.create({
delete(data: { userUuid: string; resourceUuid: string; }) {
return this.linkService
.list({
- filters: FilterBuilder
- .create()
+ filters: new FilterBuilder()
.addEqual('tailUuid', data.userUuid)
.addEqual('headUuid', data.resourceUuid)
.addEqual('linkClass', LinkClass.STAR)
+ .getFilters()
})
.then(results => Promise.all(
results.items.map(item => this.linkService.delete(item.uuid))));
}
- list(userUuid: string, { filters, limit, offset, order }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
- const listFilter = FilterBuilder
- .create()
+ list(userUuid: string, { filters, limit, offset, linkOrder, contentOrder }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
+ const listFilters = new FilterBuilder()
.addEqual('tailUuid', userUuid)
- .addEqual('linkClass', LinkClass.STAR);
+ .addEqual('linkClass', LinkClass.STAR)
+ .getFilters();
return this.linkService
.list({
- filters: filters ? filters.concat(listFilter) : listFilter,
+ filters: joinFilters(filters, listFilters),
limit,
offset,
- order: order ? order.getLinkOrder() : OrderBuilder.create<LinkResource>()
+ order: linkOrder
})
.then(results => {
const uuids = results.items.map(item => item.headUuid);
return this.groupsService.contents(userUuid, {
limit,
offset,
- order: order ? order.getContentOrder() : OrderBuilder.create<GroupContentsResource>(),
- filters: FilterBuilder.create().addIn('uuid', uuids),
+ order: contentOrder,
+ filters: new FilterBuilder().addIn('uuid', uuids).getFilters(),
recursive: true
});
});
checkPresenceInFavorites(userUuid: string, resourceUuids: string[]): Promise<Record<string, boolean>> {
return this.linkService
.list({
- filters: FilterBuilder
- .create()
+ filters: new FilterBuilder()
.addIn("headUuid", resourceUuids)
.addEqual("tailUuid", userUuid)
.addEqual("linkClass", LinkClass.STAR)
+ .getFilters()
})
.then(({ items }) => resourceUuids.reduce((results, uuid) => {
const isFavorite = items.some(item => item.headUuid === uuid);
import * as _ from "lodash";
import { CommonResourceService, ListResults } from "~/common/api/common-resource-service";
-import { FilterBuilder } from "~/common/api/filter-builder";
-import { OrderBuilder } from "~/common/api/order-builder";
import { AxiosInstance } from "axios";
import { GroupResource } from "~/models/group";
import { CollectionResource } from "~/models/collection";
export interface ContentsArguments {
limit?: number;
offset?: number;
- order?: OrderBuilder;
- filters?: FilterBuilder;
+ order?: string;
+ filters?: string;
recursive?: boolean;
}
const { filters, order, ...other } = args;
const params = {
...other,
- filters: filters ? filters.serialize() : undefined,
- order: order ? order.getOrder() : undefined
+ filters: filters ? `[${filters}]` : undefined,
+ order: order ? order : undefined
};
return this.serverApi
.get(this.resourceType + `${uuid}/contents/`, {
const resource = await projectService.list();
expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
params: {
- filters: FilterBuilder
- .create()
+ filters: "[" + new FilterBuilder()
.addEqual("groupClass", "project")
- .serialize()
+ .getFilters() + "]",
+ order: undefined
}
});
});
import { ProjectResource } from "~/models/project";
import { GroupClass } from "~/models/group";
import { ListArguments } from "~/common/api/common-resource-service";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
export class ProjectService extends GroupsService<ProjectResource> {
list(args: ListArguments = {}) {
return super.list({
...args,
- filters: this.addProjectFilter(args.filters)
+ filters: joinFilters(
+ args.filters,
+ new FilterBuilder()
+ .addEqual("groupClass", GroupClass.PROJECT)
+ .getFilters()
+ )
});
}
-
- private addProjectFilter(filters?: FilterBuilder) {
- return FilterBuilder
- .create()
- .concat(filters
- ? filters
- : FilterBuilder.create())
- .concat(FilterBuilder
- .create()
- .addEqual("groupClass", GroupClass.PROJECT));
- }
}
const projectService = new ProjectService(apiClient);
const linkService = new LinkService(apiClient);
const favoriteService = new FavoriteService(linkService, groupsService);
- const collectionService = new CollectionService(apiClient, keepService);
+ const collectionService = new CollectionService(apiClient, keepService, webdavClient, authService);
const tagService = new TagService(linkService);
const collectionFilesService = new CollectionFilesService(collectionService);
}
list(uuid: string) {
- const filters = FilterBuilder
- .create()
+ const filters = new FilterBuilder()
.addEqual("headUuid", uuid)
.addEqual("tailUuid", TagTailType.COLLECTION)
- .addEqual("linkClass", LinkClass.TAG);
+ .addEqual("linkClass", LinkClass.TAG)
+ .getFilters();
- const order = OrderBuilder
- .create<TagResource>()
- .addAsc('createdAt');
+ const order = new OrderBuilder<TagResource>()
+ .addAsc('createdAt')
+ .getOrder();
return this.linkService
.list({ filters, order })
USER_DETAILS_REQUEST: {},
USER_DETAILS_SUCCESS: ofType<User>()
}, {
- tag: 'type',
- value: 'payload'
- });
+ tag: 'type',
+ value: 'payload'
+});
function setAuthorizationHeader(services: ServiceRepository, token: string) {
services.apiClient.defaults.headers.common = {
import { unionize, ofType, UnionOf } from "unionize";
import { Dispatch } from "redux";
-import { ResourceKind } from "~/models/resource";
+import { loadCollectionFiles } from "./collection-panel-files/collection-panel-files-actions";
import { CollectionResource } from "~/models/collection";
import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
import { createTree } from "~/models/tree";
import { snackbarActions } from "../snackbar/snackbar-actions";
export const collectionPanelActions = unionize({
- LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
+ LOAD_COLLECTION: ofType<{ uuid: string }>(),
LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
LOAD_COLLECTION_TAGS: ofType<{ uuid: string }>(),
LOAD_COLLECTION_TAGS_SUCCESS: ofType<{ tags: TagResource[] }>(),
export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
-export const loadCollection = (uuid: string, kind: ResourceKind) =>
+export const loadCollection = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
+ dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
return services.collectionService
.get(uuid)
.then(item => {
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
- return services.collectionFilesService.getFiles(item.uuid);
- })
- .then(files => {
- dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
+ dispatch<any>(loadCollectionFiles(uuid));
});
};
// SPDX-License-Identifier: AGPL-3.0
import { default as unionize, ofType, UnionOf } from "unionize";
-import { CollectionFilesTree } from "~/models/collection-file";
+import { Dispatch } from "redux";
+import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
+import { ServiceRepository } from "~/services/services";
+import { RootState } from "../../store";
+import { snackbarActions } from "../../snackbar/snackbar-actions";
+import { dialogActions } from "../../dialog/dialog-actions";
+import { getNodeValue, getNodeDescendants } from "~/models/tree";
+import { CollectionPanelDirectory, CollectionPanelFile } from "./collection-panel-files-state";
export const collectionPanelFilesAction = unionize({
SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
}, { tag: 'type', value: 'payload' });
export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
+
+export const loadCollectionFiles = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const files = await services.collectionService.files(uuid);
+ dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
+ };
+
+export const removeCollectionFiles = (filePaths: string[]) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { item } = getState().collectionPanel;
+ if (item) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+ const promises = filePaths.map(filePath => services.collectionService.deleteFile(item.uuid, filePath));
+ await Promise.all(promises);
+ dispatch<any>(loadCollectionFiles(item.uuid));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 }));
+ }
+ };
+
+export const removeCollectionsSelectedFiles = () =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const tree = getState().collectionPanelFiles;
+ const allFiles = getNodeDescendants('')(tree)
+ .map(id => getNodeValue(id)(tree))
+ .filter(file => file !== undefined) as Array<CollectionPanelDirectory | CollectionPanelFile>;
+
+ const selectedDirectories = allFiles.filter(file => file.selected && file.type === CollectionFileType.DIRECTORY);
+ const selectedFiles = allFiles.filter(file => file.selected && !selectedDirectories.some(dir => dir.id === file.path));
+ const paths = [...selectedDirectories, ...selectedFiles].map(file => file.id);
+ dispatch<any>(removeCollectionFiles(paths));
+ };
+
+export const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+export const openFileRemoveDialog = (filePath: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const file = getNodeValue(filePath)(getState().collectionPanelFiles);
+ if (file) {
+ const title = file.type === CollectionFileType.DIRECTORY
+ ? 'Removing directory'
+ : 'Removing file';
+ const text = file.type === CollectionFileType.DIRECTORY
+ ? 'Are you sure you want to remove this directory?'
+ : 'Are you sure you want to remove this file?';
+
+ dispatch(dialogActions.OPEN_DIALOG({
+ id: FILE_REMOVE_DIALOG,
+ data: {
+ title,
+ text,
+ confirmButtonLabel: 'Remove',
+ filePath
+ }
+ }));
+ }
+ };
+
+export const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
+export const openMultipleFilesRemoveDialog = () =>
+ dialogActions.OPEN_DIALOG({
+ id: MULTIPLE_FILES_REMOVE_DIALOG,
+ data: {
+ title: 'Removing files',
+ text: 'Are you sure you want to remove selected files?',
+ confirmButtonLabel: 'Remove'
+ }
+ });
//
// SPDX-License-Identifier: AGPL-3.0
-import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile } from "./collection-panel-files-state";
+import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile, mergeCollectionPanelFilesStates } from "./collection-panel-files-state";
import { CollectionPanelFilesAction, collectionPanelFilesAction } from "./collection-panel-files-actions";
import { createTree, mapTreeValues, getNode, setNode, getNodeAncestors, getNodeDescendants, setNodeValueWith, mapTree } from "~/models/tree";
import { CollectionFileType } from "~/models/collection-file";
export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
return collectionPanelFilesAction.match(action, {
SET_COLLECTION_FILES: files =>
- mapTree(mapCollectionFileToCollectionPanelFile)(files),
+ mergeCollectionPanelFilesStates(state, mapTree(mapCollectionFileToCollectionPanelFile)(files)),
TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
toggleCollapse(data.id)(state),
//
// SPDX-License-Identifier: AGPL-3.0
+import { Tree, TreeNode, mapTreeValues, getNodeValue } from '~/models/tree';
import { CollectionFile, CollectionDirectory, CollectionFileType } from '~/models/collection-file';
-import { Tree, TreeNode } from '~/models/tree';
export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
: { ...node.value, selected: false }
};
};
+
+export const mergeCollectionPanelFilesStates = (oldState: CollectionPanelFilesState, newState: CollectionPanelFilesState) => {
+ return mapTreeValues((value: CollectionPanelDirectory | CollectionPanelFile) => {
+ const oldValue = getNodeValue(value.id)(oldState);
+ return oldValue
+ ? oldValue.type === CollectionFileType.DIRECTORY
+ ? { ...value, collapsed: oldValue.collapsed, selected: oldValue.selected }
+ : { ...value, selected: oldValue.selected }
+ : value;
+ })(newState);
+};
import { Dispatch, MiddlewareAPI } from "redux";
import { RootState } from "../store";
+import { DataColumns } from "~/components/data-table/data-table";
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
export abstract class DataExplorerMiddlewareService {
protected readonly id: string;
return this.id;
}
+ public getColumnFilters<T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] {
+ const column = columns.find(c => c.name === columnName);
+ return column ? column.filters.filter(f => f.selected) : [];
+ }
+
abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
}
import { MiddlewareAPI } from "redux";
import { DataColumns } from "~/components/data-table/data-table";
import { dataExplorerActions } from "./data-explorer-action";
+import { SortDirection } from "~/components/data-table/data-column";
describe("DataExplorerMiddleware", () => {
name: "Column",
selected: true,
configurable: false,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: jest.fn()
}],
requestItems: jest.fn(),
name: "Column",
selected: true,
configurable: false,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: jest.fn()
}],
requestItems: jest.fn(),
it('should set columns', () => {
const columns: DataColumns<any> = [{
name: "Column 1",
+ filters: [],
render: jest.fn(),
selected: true,
- configurable: true
+ configurable: true,
+ sortDirection: SortDirection.NONE
}];
const state = dataExplorerReducer(undefined,
dataExplorerActions.SET_COLUMNS({ id: "Data explorer", columns }));
it('should toggle sorting', () => {
const columns: DataColumns<any> = [{
name: "Column 1",
+ filters: [],
render: jest.fn(),
selected: true,
configurable: true,
sortDirection: SortDirection.ASC
}, {
name: "Column 2",
+ filters: [],
render: jest.fn(),
selected: true,
configurable: true,
it('should set filters', () => {
const columns: DataColumns<any> = [{
name: "Column 1",
+ filters: [],
render: jest.fn(),
selected: true,
- configurable: true
+ configurable: true,
+ sortDirection: SortDirection.NONE
}];
const filters: DataTableFilterItem[] = [{
const update = (state: DataExplorerState, id: string, updateFn: (dataExplorer: DataExplorer) => DataExplorer) =>
({ ...state, [id]: updateFn(getDataExplorer(state, id)) });
+const canUpdateColumns = (prevColumns: DataColumns<any>, nextColumns: DataColumns<any>) => {
+ if (prevColumns.length !== nextColumns.length) {
+ return true;
+ }
+ for (let i = 0; i < nextColumns.length; i++) {
+ const pc = prevColumns[i];
+ const nc = nextColumns[i];
+ if (pc.key !== nc.key || pc.name !== nc.name) {
+ return true;
+ }
+ }
+ return false;
+};
+
const setColumns = (columns: DataColumns<any>) =>
(dataExplorer: DataExplorer) =>
- ({ ...dataExplorer, columns });
+ ({ ...dataExplorer, columns: canUpdateColumns(dataExplorer.columns, columns) ? columns : dataExplorer.columns });
const mapColumns = (mapFn: (column: DataColumn<any>) => DataColumn<any>) =>
(dataExplorer: DataExplorer) =>
// SPDX-License-Identifier: AGPL-3.0
import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
-import { FavoritePanelFilter, FavoritePanelColumnNames } from "~/views/favorite-panel/favorite-panel";
+import { FavoritePanelColumnNames, FavoritePanelFilter } from "~/views/favorite-panel/favorite-panel";
import { RootState } from "../store";
import { DataColumns } from "~/components/data-table/data-table";
import { FavoritePanelItem, resourceToDataItem } from "~/views/favorite-panel/favorite-panel-item";
-import { FavoriteOrderBuilder } from "~/services/favorite-service/favorite-order-builder";
import { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
import { FilterBuilder } from "~/common/api/filter-builder";
import { checkPresenceInFavorites } from "../favorites/favorites-actions";
import { favoritePanelActions } from "./favorite-panel-action";
import { Dispatch, MiddlewareAPI } from "redux";
+import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
+import { LinkResource } from "~/models/link";
+import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
const dataExplorer = api.getState().dataExplorer[this.getId()];
const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
- const sortColumn = dataExplorer.columns.find(
- ({ sortDirection }) => sortDirection !== undefined && sortDirection !== "none"
- );
- const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
- const order = FavoriteOrderBuilder.create();
- if (typeFilters.length > 0) {
- this.services.favoriteService
- .list(this.services.authService.getUuid()!, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- order: sortColumn!.name === FavoritePanelColumnNames.NAME
- ? sortColumn!.sortDirection === SortDirection.ASC
- ? order.addDesc("name")
- : order.addAsc("name")
- : order,
- filters: FilterBuilder
- .create()
- .addIsA("headUuid", typeFilters.map(filter => filter.type))
- .addILike("name", dataExplorer.searchValue)
- })
- .then(response => {
- api.dispatch(favoritePanelActions.SET_ITEMS({
- items: response.items.map(resourceToDataItem),
- itemsAvailable: response.itemsAvailable,
- page: Math.floor(response.offset / response.limit),
- rowsPerPage: response.limit
- }));
- api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
- });
- } else {
- api.dispatch(favoritePanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+ const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+
+ const linkOrder = new OrderBuilder<LinkResource>();
+ const contentOrder = new OrderBuilder<GroupContentsResource>();
+
+ if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+ const direction = sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ linkOrder.addOrder(direction, "name");
+ contentOrder
+ .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
+ .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
}
+
+ this.services.favoriteService
+ .list(this.services.authService.getUuid()!, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ linkOrder: linkOrder.getOrder(),
+ contentOrder: contentOrder.getOrder(),
+ filters: new FilterBuilder()
+ .addIsA("headUuid", typeFilters.map(filter => filter.type))
+ .addILike("name", dataExplorer.searchValue)
+ .getFilters()
+ })
+ .then(response => {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: response.items.map(resourceToDataItem),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+ })
+ .catch(() => {
+ api.dispatch(favoritePanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ });
}
}
-
-const getColumnFilters = (columns: DataColumns<FavoritePanelItem, FavoritePanelFilter>, columnName: string) => {
- const column = columns.find(c => c.name === columnName);
- return column && column.filters ? column.filters.filter(f => f.selected) : [];
-};
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import { projectActions, getProjectList } from "../project/project-action";
+import { getProjectList, projectActions } from "../project/project-action";
import { push } from "react-router-redux";
import { TreeItemStatus } from "~/components/tree/tree";
import { findTreeItem } from "../project/project-reducer";
import { RootState } from "../store";
-import { Resource, ResourceKind } from "~/models/resource";
+import { ResourceKind } from "~/models/resource";
import { projectPanelActions } from "../project-panel/project-panel-action";
import { getCollectionUrl } from "~/models/collection";
import { getProjectUrl, ProjectResource } from "~/models/project";
import { ProjectService } from "~/services/project-service/project-service";
import { ServiceRepository } from "~/services/services";
import { sidePanelActions } from "../side-panel/side-panel-action";
-import { SidePanelIdentifiers } from "../side-panel/side-panel-reducer";
+import { SidePanelId } from "../side-panel/side-panel-reducer";
import { getUuidObjectType, ObjectTypes } from "~/models/object-types";
-export const getResourceUrl = <T extends Resource>(resource: T): string => {
- switch (resource.kind) {
- case ResourceKind.PROJECT: return getProjectUrl(resource.uuid);
- case ResourceKind.COLLECTION: return getCollectionUrl(resource.uuid);
- default: return resource.href;
+export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
+ switch (resourceKind) {
+ case ResourceKind.PROJECT: return getProjectUrl(resourceUuid);
+ case ResourceKind.COLLECTION: return getCollectionUrl(resourceUuid);
+ default:
+ return '';
}
};
const treeItem = findTreeItem(projects.items, itemId);
if (treeItem) {
- const resourceUrl = getResourceUrl(treeItem.data);
+ const resourceUrl = getResourceUrl(treeItem.data.kind, treeItem.data.uuid);
if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
if (router.location && !router.location.pathname.includes(resourceUrl)) {
const ancestors = await loadProjectAncestors(itemId, services.projectService);
const uuids = ancestors.map(ancestor => ancestor.uuid);
await loadBranch(uuids, dispatch);
- dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelIdentifiers.PROJECTS));
- dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
uuids.forEach(uuid => {
dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
});
import { ServiceRepository } from "~/services/services";
import { ProjectPanelItem, resourceToDataItem } from "~/views/project-panel/project-panel-item";
import { SortDirection } from "~/components/data-table/data-column";
-import { OrderBuilder } from "~/common/api/order-builder";
+import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
import { FilterBuilder } from "~/common/api/filter-builder";
-import { GroupContentsResourcePrefix, GroupContentsResource } from "~/services/groups-service/groups-service";
+import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
import { checkPresenceInFavorites } from "../favorites/favorites-actions";
import { projectPanelActions } from "./project-panel-action";
import { Dispatch, MiddlewareAPI } from "redux";
+import { ProjectResource } from "~/models/project";
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
const state = api.getState();
const dataExplorer = state.dataExplorer[this.getId()];
const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
- const typeFilters = getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
- const statusFilters = getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
- const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
- const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC;
- if (typeFilters.length > 0) {
- this.services.groupsService
- .contents(state.projects.currentItemId, {
- limit: dataExplorer.rowsPerPage,
- offset: dataExplorer.page * dataExplorer.rowsPerPage,
- order: sortColumn
- ? sortColumn.name === ProjectPanelColumnNames.NAME
- ? getOrder("name", sortDirection)
- : getOrder("createdAt", sortDirection)
- : OrderBuilder.create(),
- filters: FilterBuilder
- .create()
- .concat(FilterBuilder
- .create()
- .addIsA("uuid", typeFilters.map(f => f.type)))
- .concat(FilterBuilder
- .create(GroupContentsResourcePrefix.PROCESS)
- .addIn("state", statusFilters.map(f => f.type)))
- .concat(getSearchFilter(dataExplorer.searchValue))
- })
- .then(response => {
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: response.items.map(resourceToDataItem),
- itemsAvailable: response.itemsAvailable,
- page: Math.floor(response.offset / response.limit),
- rowsPerPage: response.limit
- }));
- api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
- });
- } else {
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- }
- }
-}
+ const typeFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
+ const statusFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
+ const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
-const getColumnFilters = (columns: DataColumns<ProjectPanelItem, ProjectPanelFilter>, columnName: string) => {
- const column = columns.find(c => c.name === columnName);
- return column && column.filters ? column.filters.filter(f => f.selected) : [];
-};
+ const order = new OrderBuilder<ProjectResource>();
-const getOrder = (attribute: "name" | "createdAt", direction: SortDirection) =>
- [
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
- OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)
- ].reduce((acc, b) =>
- acc.concat(direction === SortDirection.ASC
- ? b.addAsc(attribute)
- : b.addDesc(attribute)), OrderBuilder.create());
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
-const getSearchFilter = (searchValue: string) =>
- searchValue
- ? [
- FilterBuilder.create(GroupContentsResourcePrefix.COLLECTION),
- FilterBuilder.create(GroupContentsResourcePrefix.PROCESS),
- FilterBuilder.create(GroupContentsResourcePrefix.PROJECT)]
- .reduce((acc, b) =>
- acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
- : FilterBuilder.create();
+ const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+ order
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
+ .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
+ }
+
+ this.services.groupsService
+ .contents(state.projects.currentItemId, {
+ limit: dataExplorer.rowsPerPage,
+ offset: dataExplorer.page * dataExplorer.rowsPerPage,
+ order: order.getOrder(),
+ filters: new FilterBuilder()
+ .addIsA("uuid", typeFilters.map(f => f.type))
+ .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+ .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+ .getFilters()
+ })
+ .then(response => {
+ api.dispatch(projectPanelActions.SET_ITEMS({
+ items: response.items.map(resourceToDataItem),
+ itemsAvailable: response.itemsAvailable,
+ page: Math.floor(response.offset / response.limit),
+ rowsPerPage: response.limit
+ }));
+ api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+ })
+ .catch(() => {
+ api.dispatch(projectPanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage
+ }));
+ });
+ }
+}
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
return services.projectService.list({
- filters: FilterBuilder
- .create()
+ filters: new FilterBuilder()
.addEqual("ownerUuid", parentUuid)
+ .getFilters()
}).then(({ items: projects }) => {
dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
dispatch<any>(checkPresenceInFavorites(projects.map(project => project.uuid)));
// SPDX-License-Identifier: AGPL-3.0
import { default as unionize, ofType, UnionOf } from "unionize";
+import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
export const sidePanelActions = unionize({
- TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<string>(),
- TOGGLE_SIDE_PANEL_ITEM_ACTIVE: ofType<string>(),
- RESET_SIDE_PANEL_ACTIVITY: ofType<{}>(),
+ TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<SidePanelId>()
}, {
tag: 'type',
value: 'payload'
import { ProjectsIcon } from "~/components/icon/icon";
describe('side-panel-reducer', () => {
-
- it('should toggle activity on side-panel', () => {
- const initialState = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: false,
- active: false,
- }
- ];
- const project = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: false,
- active: true,
- }
- ];
-
- const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(initialState[0].id));
- expect(state).toEqual(project);
- });
-
it('should open side-panel item', () => {
const initialState = [
{
id: "1",
name: "Projects",
+ url: "/projects",
icon: ProjectsIcon,
- open: false,
- active: false,
+ open: false
}
];
const project = [
name: "Projects",
icon: ProjectsIcon,
open: true,
- active: false,
+ url: "/projects"
}
];
const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
expect(state).toEqual(project);
});
-
- it('should remove activity on side-panel item', () => {
- const initialState = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: false,
- active: true,
- }
- ];
- const project = [
- {
- id: "1",
- name: "Projects",
- icon: ProjectsIcon,
- open: false,
- active: false,
- }
- ];
-
- const state = sidePanelReducer(initialState, sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(initialState[0].id));
- expect(state).toEqual(project);
- });
});
//
// SPDX-License-Identifier: AGPL-3.0
-import * as _ from "lodash";
import { sidePanelActions, SidePanelAction } from './side-panel-action';
import { SidePanelItem } from '~/components/side-panel/side-panel';
import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "~/components/icon/icon";
import { projectPanelActions } from "../project-panel/project-panel-action";
import { projectActions } from "../project/project-action";
import { getProjectUrl } from "../../models/project";
+import { columns as projectPanelColumns } from "../../views/project-panel/project-panel";
+import { columns as favoritePanelColumns } from "../../views/favorite-panel/favorite-panel";
export type SidePanelState = SidePanelItem[];
-export const sidePanelReducer = (state: SidePanelState = sidePanelData, action: SidePanelAction) => {
- if (state.length === 0) {
- return sidePanelData;
- } else {
- return sidePanelActions.match(action, {
- TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
- state.map(it => ({...it, open: itemId === it.id && it.open === false})),
- TOGGLE_SIDE_PANEL_ITEM_ACTIVE: itemId => {
- const sidePanel = _.cloneDeep(state);
- resetSidePanelActivity(sidePanel);
- sidePanel.forEach(it => {
- if (it.id === itemId) {
- it.active = true;
- }
- });
- return sidePanel;
- },
- RESET_SIDE_PANEL_ACTIVITY: () => {
- const sidePanel = _.cloneDeep(state);
- resetSidePanelActivity(sidePanel);
- return sidePanel;
- },
- default: () => state
- });
- }
+export const sidePanelReducer = (state: SidePanelState = sidePanelItems, action: SidePanelAction) => {
+ return sidePanelActions.match(action, {
+ TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
+ state.map(it => ({...it, open: itemId === it.id && it.open === false})),
+ default: () => state
+ });
};
-export enum SidePanelIdentifiers {
+export enum SidePanelId {
PROJECTS = "Projects",
SHARED_WITH_ME = "SharedWithMe",
WORKFLOWS = "Workflows",
TRASH = "Trash"
}
-export const sidePanelData = [
+export const sidePanelItems = [
{
- id: SidePanelIdentifiers.PROJECTS,
+ id: SidePanelId.PROJECTS,
name: "Projects",
+ url: "/projects",
icon: ProjectsIcon,
open: false,
active: false,
margin: true,
openAble: true,
activeAction: (dispatch: Dispatch, uuid: string) => {
- dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
dispatch(push(getProjectUrl(uuid)));
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
+ dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
dispatch(projectPanelActions.RESET_PAGINATION());
- dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(projectPanelActions.REQUEST_ITEMS());
}
},
{
- id: SidePanelIdentifiers.SHARED_WITH_ME,
+ id: SidePanelId.SHARED_WITH_ME,
name: "Shared with me",
+ url: "/shared",
icon: ShareMeIcon,
active: false,
+ activeAction: (dispatch: Dispatch) => {
+ dispatch(push("/shared"));
+ }
},
{
- id: SidePanelIdentifiers.WORKFLOWS,
+ id: SidePanelId.WORKFLOWS,
name: "Workflows",
+ url: "/workflows",
icon: WorkflowIcon,
active: false,
+ activeAction: (dispatch: Dispatch) => {
+ dispatch(push("/workflows"));
+ }
},
{
- id: SidePanelIdentifiers.RECENT_OPEN,
+ id: SidePanelId.RECENT_OPEN,
name: "Recent open",
+ url: "/recent",
icon: RecentIcon,
active: false,
+ activeAction: (dispatch: Dispatch) => {
+ dispatch(push("/recent"));
+ }
},
{
- id: SidePanelIdentifiers.FAVORITES,
+ id: SidePanelId.FAVORITES,
name: "Favorites",
+ url: "/favorites",
icon: FavoriteIcon,
active: false,
activeAction: (dispatch: Dispatch) => {
dispatch(push("/favorites"));
+ dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
dispatch(favoritePanelActions.RESET_PAGINATION());
dispatch(favoritePanelActions.REQUEST_ITEMS());
}
},
{
- id: SidePanelIdentifiers.TRASH,
+ id: SidePanelId.TRASH,
name: "Trash",
+ url: "/trash",
icon: TrashIcon,
active: false,
+ activeAction: (dispatch: Dispatch) => {
+ dispatch(push("/trash"));
+ }
}
];
-
-function resetSidePanelActivity(sidePanel: SidePanelItem[]) {
- for (const t of sidePanel) {
- t.active = false;
- }
-}
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
-import { openMultipleFilesRemoveDialog } from "~/views-components/file-remove-dialog/multiple-files-remove-dialog";
+import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
import { createCollectionWithSelected } from "~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected";
}
}, {
name: "Remove selected",
- execute: (dispatch, resource) => {
+ execute: (dispatch) => {
dispatch(openMultipleFilesRemoveDialog());
}
}, {
import { ContextMenuActionSet } from "../context-menu-action-set";
import { RenameIcon, DownloadIcon, RemoveIcon } from "~/components/icon/icon";
import { openRenameFileDialog } from "../../rename-file-dialog/rename-file-dialog";
-import { openFileRemoveDialog } from "../../file-remove-dialog/file-remove-dialog";
+import { DownloadCollectionFileAction } from "../actions/download-collection-file-action";
+import { openFileRemoveDialog } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
dispatch<any>(openRenameFileDialog(resource.name));
}
}, {
- name: "Download",
- icon: DownloadIcon,
- execute: (dispatch, resource) => {
- return;
- }
+ component: DownloadCollectionFileAction,
+ execute: () => { return; }
}, {
name: "Remove",
icon: RemoveIcon,
execute: (dispatch, resource) => {
- dispatch(openFileRemoveDialog(resource.uuid));
+ dispatch<any>(openFileRemoveDialog(resource.uuid));
}
}]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ListItemIcon, ListItemText, Button, ListItem } from "@material-ui/core";
+import { DownloadIcon } from "../../../components/icon/icon";
+
+export const DownloadAction = (props: { href?: string, download?: string, onClick?: () => void }) => {
+ const targetProps = props.download ? {} : { target: '_blank' };
+ const downloadProps = props.download ? { download: props.download } : {};
+ return props.href
+ ? <a
+ style={{ textDecoration: 'none' }}
+ href={props.href}
+ onClick={props.onClick}
+ {...targetProps}
+ {...downloadProps}>
+ <ListItem button>
+ <ListItemIcon>
+ <DownloadIcon />
+ </ListItemIcon>
+ <ListItemText>
+ Download
+ </ListItemText>
+ </ListItem>
+ </a >
+ : null;
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { RootState } from "../../../store/store";
+import { DownloadAction } from "./download-action";
+import { getNodeValue } from "../../../models/tree";
+import { CollectionFileType } from "../../../models/collection-file";
+
+const mapStateToProps = (state: RootState) => {
+ const { resource } = state.contextMenu;
+ if (resource) {
+ const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
+ if (file) {
+ return {
+ href: file.url,
+ download: file.type === CollectionFileType.DIRECTORY ? undefined : file.name
+ };
+ }
+ }
+ return {};
+};
+
+export const DownloadCollectionFileAction = connect(mapStateToProps)(DownloadAction);
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { ListItemIcon, ListItemText } from "@material-ui/core";
+import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
import { AddFavoriteIcon, RemoveFavoriteIcon } from "~/components/icon/icon";
import { connect } from "react-redux";
import { RootState } from "~/store/store";
-const mapStateToProps = (state: RootState) => ({
- isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true
+const mapStateToProps = (state: RootState, props: { onClick: () => {} }) => ({
+ isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true,
+ onClick: props.onClick
});
-export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean }) =>
- <>
+export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean, onClick: () => void }) =>
+ <ListItem
+ button
+ onClick={props.onClick}>
<ListItemIcon>
{props.isFavorite
? <RemoveFavoriteIcon />
: <AddFavoriteIcon />}
</ListItemIcon>
- <ListItemText>
+ <ListItemText style={{ textDecoration: 'none' }}>
{props.isFavorite
? <>Remove from favorites</>
: <>Add to favorites</>}
</ListItemText>
- </>);
+ </ListItem >);
extractKey?: (item: any) => React.Key;
}
-const mapStateToProps = (state: RootState, { id }: Props) =>
- getDataExplorer(state.dataExplorer, id);
+const mapStateToProps = (state: RootState, { id, columns }: Props) => {
+ const s = getDataExplorer(state.dataExplorer, id);
+ if (s.columns.length === 0) {
+ s.columns = columns;
+ }
+ return s;
+};
const mapDispatchToProps = () => {
- let prevColumns: DataColumns<any>;
- return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
- if (columns !== prevColumns) {
- prevColumns = columns;
+ return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
+ onSetColumns: (columns: DataColumns<any>) => {
dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
- }
- return {
- onSearch: (searchValue: string) => {
- dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
- },
+ },
+
+ onSearch: (searchValue: string) => {
+ dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
+ },
- onColumnToggle: (column: DataColumn<any>) => {
- dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
- },
+ onColumnToggle: (column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
+ },
- onSortToggle: (column: DataColumn<any>) => {
- dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
- },
+ onSortToggle: (column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
+ },
- onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
- dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
- },
+ onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
+ },
- onChangePage: (page: number) => {
- dispatch(dataExplorerActions.SET_PAGE({ id, page }));
- },
+ onChangePage: (page: number) => {
+ dispatch(dataExplorerActions.SET_PAGE({ id, page }));
+ },
- onChangeRowsPerPage: (rowsPerPage: number) => {
- dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
- },
+ onChangeRowsPerPage: (rowsPerPage: number) => {
+ dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+ },
- onRowClick,
+ onRowClick,
- onRowDoubleClick,
+ onRowDoubleClick,
- onContextMenu,
- };
- };
+ onContextMenu,
+ });
};
export const DataExplorer = connect(mapStateToProps, mapDispatchToProps())(DataExplorerComponent);
import { Dispatch } from "redux";
import { connect } from "react-redux";
import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
-import { withDialog } from "~/store/dialog/with-dialog";
-import { dialogActions } from "~/store/dialog/dialog-actions";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { withDialog, WithDialogProps } from '~/store/dialog/with-dialog';
+import { RootState } from '~/store/store';
+import { removeCollectionFiles, FILE_REMOVE_DIALOG } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
-const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+const mapStateToProps = (state: RootState, props: WithDialogProps<{ filePath: string }>) => ({
+ filePath: props.data.filePath
+});
-const mapDispatchToProps = (dispatch: Dispatch) => ({
- onConfirm: () => {
- // TODO: dispatch action that removes single file
- dispatch(dialogActions.CLOSE_DIALOG({ id: FILE_REMOVE_DIALOG }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing file...', hideDuration: 2000 }));
- setTimeout(() => {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'File removed.', hideDuration: 2000 }));
- }, 1000);
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<{ filePath: string }>) => ({
+ onConfirm: (filePath: string) => {
+ props.closeDialog();
+ dispatch<any>(removeCollectionFiles([filePath]));
}
});
-export const openFileRemoveDialog = (fileId: string) =>
- dialogActions.OPEN_DIALOG({
- id: FILE_REMOVE_DIALOG,
- data: {
- title: 'Removing file',
- text: 'Are you sure you want to remove this file?',
- confirmButtonLabel: 'Remove',
- fileId
- }
+const mergeProps = (
+ stateProps: { filePath: string },
+ dispatchProps: { onConfirm: (filePath: string) => void },
+ props: WithDialogProps<{ filePath: string }>) => ({
+ onConfirm: () => dispatchProps.onConfirm(stateProps.filePath),
+ ...props
});
export const [FileRemoveDialog] = [ConfirmationDialog]
- .map(withDialog(FILE_REMOVE_DIALOG))
- .map(connect(undefined, mapDispatchToProps));
+ .map(connect(mapStateToProps, mapDispatchToProps, mergeProps))
+ .map(withDialog(FILE_REMOVE_DIALOG));
import { Dispatch } from "redux";
import { connect } from "react-redux";
+import { MULTIPLE_FILES_REMOVE_DIALOG, removeCollectionsSelectedFiles } from "../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
-import { withDialog } from "~/store/dialog/with-dialog";
-import { dialogActions } from "~/store/dialog/dialog-actions";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
-const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
onConfirm: () => {
- // TODO: dispatch action that removes multiple files
- dispatch(dialogActions.CLOSE_DIALOG({ id: MULTIPLE_FILES_REMOVE_DIALOG }));
- dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Removing files...', hideDuration: 2000}));
- setTimeout(() => {
- dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Files removed.', hideDuration: 2000}));
- }, 1000);
+ props.closeDialog();
+ dispatch<any>(removeCollectionsSelectedFiles());
}
});
-export const openMultipleFilesRemoveDialog = () =>
- dialogActions.OPEN_DIALOG({
- id: MULTIPLE_FILES_REMOVE_DIALOG,
- data: {
- title: 'Removing files',
- text: 'Are you sure you want to remove selected files?',
- confirmButtonLabel: 'Remove'
- }
- });
-
export const [MultipleFilesRemoveDialog] = [ConfirmationDialog]
- .map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG))
- .map(connect(undefined, mapDispatchToProps));
+ .map(connect(undefined, mapDispatchToProps))
+ .map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG));
breadcrumbs: Breadcrumb[];
user?: User;
menuItems: MainAppBarMenuItems;
+ buildInfo: string;
}
export interface MainAppBarActionProps {
<Grid container justify="space-between">
<Grid item xs={3}>
<Typography variant="headline" color="inherit" noWrap>
- Arvados
+ Arvados 2
</Typography>
<Typography variant="body1" color="inherit" noWrap >
- Workbench 2
+ {props.buildInfo}
</Typography>
</Grid>
<Grid item xs={6} container alignItems="center">
const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
- const filters = FilterBuilder
- .create()
- .addEqual('ownerUuid', ownerUuid);
+ const filters = new FilterBuilder()
+ .addEqual('ownerUuid', ownerUuid)
+ .getFilters();
const { items } = await services.projectService.list({ filters });
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
+ filters: [],
render: renderName,
width: "450px"
},
name: "Status",
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
filters: [
{
name: ContainerRequestState.COMMITTED,
name: FavoritePanelColumnNames.TYPE,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
filters: [
{
name: resourceLabel(ResourceKind.COLLECTION),
name: FavoritePanelColumnNames.OWNER,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderOwner(item.owner),
width: "200px"
},
name: FavoritePanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderFileSize(item.fileSize),
width: "50px"
},
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderDate(item.lastModified),
width: "150px"
}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
- extractKey={(item: FavoritePanelItem) => item.uuid}
+ extractKey={(item: FavoritePanelItem) => item.uuid}
defaultIcon={FavoriteIcon}
defaultMessages={['Your favorites list is empty.']}/>
;
selected: true,
configurable: true,
sortDirection: SortDirection.ASC,
+ filters: [],
render: renderName,
width: "450px"
},
name: "Status",
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
filters: [
{
name: ContainerRequestState.COMMITTED,
name: ProjectPanelColumnNames.TYPE,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
filters: [
{
name: resourceLabel(ResourceKind.COLLECTION),
name: ProjectPanelColumnNames.OWNER,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderOwner(item.owner),
width: "200px"
},
name: ProjectPanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderFileSize(item.fileSize),
width: "50px"
},
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
+ filters: [],
render: item => renderDate(item.lastModified),
width: "150px"
}
import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
-import { SidePanelIdentifiers } from '~/store/side-panel/side-panel-reducer';
import { ProjectResource } from '~/models/project';
import { ResourceKind } from '~/models/resource';
import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
sidePanelItems: SidePanelItem[];
}
-interface WorkbenchServiceProps {
+interface WorkbenchGeneralProps {
authService: AuthService;
+ buildInfo: string;
}
interface WorkbenchActionProps {
}
-type WorkbenchProps = WorkbenchDataProps & WorkbenchServiceProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
+type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
interface NavBreadcrumb extends Breadcrumb {
itemId: string;
searchText={this.state.searchText}
user={this.props.user}
menuItems={this.state.menuItems}
+ buildInfo={this.props.buildInfo}
{...this.mainAppBarActions} />
</div>
{user &&
toggleActive={itemId => {
this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
}} />
</SidePanel>
</Drawer>}
renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
onItemRouteChange={(collectionId) => {
- this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION));
+ this.props.dispatch<any>(loadCollection(collectionId));
this.props.dispatch<any>(loadCollectionTags(collectionId));
}}
onContextMenu={(event, item) => {
onItemDoubleClick={item => {
switch (item.kind) {
case ResourceKind.COLLECTION:
- this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(loadCollection(item.uuid));
this.props.dispatch(push(getCollectionUrl(item.uuid)));
default:
this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
onItemDoubleClick={item => {
switch (item.kind) {
case ResourceKind.COLLECTION:
- this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+ this.props.dispatch(loadCollection(item.uuid));
this.props.dispatch(push(getCollectionUrl(item.uuid)));
default:
this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
}
}}
}
toggleSidePanelActive = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
esutils "^2.0.2"
js-tokens "^3.0.0"
-"@babel/runtime@^7.0.0-beta.42":
- version "7.0.0-beta.54"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf"
+"@babel/runtime@7.0.0-beta.42":
+ version "7.0.0-beta.42"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.42.tgz#352e40c92e0460d3e82f49bd7e79f6cda76f919f"
+ dependencies:
+ core-js "^2.5.3"
+ regenerator-runtime "^0.11.1"
+
+"@babel/runtime@7.0.0-beta.56":
+ version "7.0.0-beta.56"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.56.tgz#cda612dffd5b1719a7b8e91e3040bd6ae64de8b0"
dependencies:
- core-js "^2.5.7"
regenerator-runtime "^0.12.0"
-"@material-ui/core@1.4.2":
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.2.tgz#8a1282e985d4922a4d2b4f7e287d8a716a2fc108"
+"@material-ui/core@1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.5.0.tgz#00884bb4139d98786d05a97803d19426d4afa55d"
dependencies:
- "@babel/runtime" "^7.0.0-beta.42"
+ "@babel/runtime" "7.0.0-beta.42"
"@types/jss" "^9.5.3"
"@types/react-transition-group" "^2.0.8"
brcast "^3.0.1"
normalize-scroll-left "^0.1.2"
popper.js "^1.14.1"
prop-types "^15.6.0"
- react-event-listener "^0.6.0"
+ react-event-listener "^0.6.2"
react-jss "^8.1.0"
react-transition-group "^2.2.1"
- recompose "^0.27.0"
+ recompose "^0.28.0"
warning "^4.0.1"
-"@material-ui/icons@2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.0.tgz#f2c4e80d0cb4bbbd433127781da67d93393535f8"
+"@material-ui/icons@2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.2.tgz#0150c38cda089ef284e9b4a730dfe6e88a0b5de6"
dependencies:
- "@babel/runtime" "^7.0.0-beta.42"
- recompose "^0.27.0"
+ "@babel/runtime" "7.0.0-beta.42"
+ recompose "^0.28.0"
"@types/cheerio@*":
version "0.22.8"
version "2.2.6"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.6.tgz#dbe8a666156d556ed018e15a4c65f08937c3f628"
-"@types/enzyme-adapter-react-16@1.0.2":
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz#15ae37c64d6221a6f4b3a4aacc357cf773859de4"
+"@types/enzyme-adapter-react-16@1.0.3":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.3.tgz#0cf7025b036694ca8d596fe38f24162e7117acf1"
dependencies:
"@types/enzyme" "*"
-"@types/enzyme@*", "@types/enzyme@3.1.12":
+"@types/enzyme@*":
version "3.1.12"
resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c"
dependencies:
"@types/cheerio" "*"
"@types/react" "*"
+"@types/enzyme@3.1.13":
+ version "3.1.13"
+ resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.13.tgz#4bbc5c81fa40c9fc7efee25c4a23cb37119a33ea"
+ dependencies:
+ "@types/cheerio" "*"
+ "@types/react" "*"
+
"@types/history@*":
version "4.6.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
version "10.5.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
-"@types/node@10.5.5":
- version "10.5.5"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.5.tgz#8e84d24e896cd77b0d4f73df274027e3149ec2ba"
+"@types/node@10.7.1":
+ version "10.7.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
-"@types/react-copy-to-clipboard@4.2.5":
- version "4.2.5"
- resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.5.tgz#bda288b4256288676019b75ca86f1714bbd206d4"
+"@types/react-copy-to-clipboard@4.2.6":
+ version "4.2.6"
+ resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.6.tgz#d1374550dec803f17f26ec71b62783c5737bfc02"
dependencies:
"@types/react" "*"
-"@types/react-dom@16.0.6":
- version "16.0.6"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
+"@types/react-dom@16.0.7":
+ version "16.0.7"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.7.tgz#54d0f867a76b90597e8432030d297982f25c20ba"
dependencies:
"@types/node" "*"
"@types/react" "*"
-"@types/react-dropzone@4.2.1":
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/@types/react-dropzone/-/react-dropzone-4.2.1.tgz#4a973b63a8a227e263ff4eece053f643220f28fc"
+"@types/react-dropzone@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@types/react-dropzone/-/react-dropzone-4.2.2.tgz#af0a2595169700c8ab1114e9096285499beaff40"
dependencies:
"@types/react" "*"
"@types/react" "*"
redux "^3.6.0"
-"@types/redux-form@7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.4.tgz#2cf62b8eb1dc1b1df95b6b25c2763db196e5c190"
+"@types/redux-form@7.4.5":
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.5.tgz#fae0fa6cfbc613867093d1e0f6a84db17177305e"
dependencies:
"@types/react" "*"
redux "^3.6.0 || ^4.0.0"
version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+array.prototype.flat@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.10.0"
+ function-bind "^1.1.1"
+
arrify@^1.0.0, arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
version "2.1.1"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
-attr-accept@^1.0.3:
+attr-accept@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
dependencies:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
-core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7:
+core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
-enzyme-adapter-react-16@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4"
+enzyme-adapter-react-16@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.2.0.tgz#c6e80f334e0a817873262d7d01ee9e4747e3c97e"
dependencies:
- enzyme-adapter-utils "^1.3.0"
- lodash "^4.17.4"
- object.assign "^4.0.4"
+ enzyme-adapter-utils "^1.5.0"
+ function.prototype.name "^1.1.0"
+ object.assign "^4.1.0"
object.values "^1.0.4"
- prop-types "^15.6.0"
+ prop-types "^15.6.2"
+ react-is "^16.4.2"
react-reconciler "^0.7.0"
react-test-renderer "^16.0.0-0"
-enzyme-adapter-utils@^1.3.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.4.0.tgz#c403b81e8eb9953658569e539780964bdc98de62"
+enzyme-adapter-utils@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.5.0.tgz#a020ab3ae79bb1c85e1d51f48f35e995e0eed810"
dependencies:
+ function.prototype.name "^1.1.0"
object.assign "^4.1.0"
- prop-types "^15.6.0"
+ prop-types "^15.6.2"
-enzyme@3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
+enzyme@3.4.4:
+ version "3.4.4"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.4.4.tgz#92c7c6b9e59d4ef0c3d36a75dccc0e41a5c14d21"
dependencies:
+ array.prototype.flat "^1.2.1"
cheerio "^1.0.0-rc.2"
- function.prototype.name "^1.0.3"
- has "^1.0.1"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
is-boolean-object "^1.0.0"
- is-callable "^1.1.3"
+ is-callable "^1.1.4"
is-number-object "^1.0.3"
is-string "^1.0.4"
is-subset "^0.1.1"
lodash "^4.17.4"
- object-inspect "^1.5.0"
+ object-inspect "^1.6.0"
object-is "^1.0.1"
object.assign "^4.1.0"
object.entries "^1.0.4"
dependencies:
is-arrayish "^0.2.1"
-es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
+es-abstract@^1.10.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
dependencies:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
-function.prototype.name@^1.0.3:
+function.prototype.name@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
dependencies:
is-number "^3.0.0"
kind-of "^4.0.0"
-has@^1.0.1:
+has@^1.0.1, has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
dependencies:
dependencies:
builtin-modules "^1.0.0"
-is-callable@^1.1.1, is-callable@^1.1.3:
+is-callable@^1.1.1, is-callable@^1.1.3, is-callable@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
define-property "^0.2.5"
kind-of "^3.0.3"
-object-inspect@^1.5.0:
+object-inspect@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
dependencies:
isobject "^3.0.0"
-object.assign@^4.0.4, object.assign@^4.1.0:
+object.assign@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.0"
-react-dropzone@4.2.13:
- version "4.2.13"
- resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-4.2.13.tgz#31393c079b4e5ddcc176c095cebc3545d1248b9d"
+react-dropzone@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-5.0.1.tgz#3ed201215794c0f650c6f25a8311a9d96d35ebb6"
dependencies:
- attr-accept "^1.0.3"
+ attr-accept "^1.1.3"
prop-types "^15.5.7"
react-error-overlay@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
-react-event-listener@^0.6.0:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.1.tgz#41c7a80a66b398c27dd511e22712b02f3d4eccca"
+react-event-listener@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.2.tgz#df405e9578be052b77a76e4c3914686637caecff"
dependencies:
- "@babel/runtime" "^7.0.0-beta.42"
+ "@babel/runtime" "7.0.0-beta.42"
prop-types "^15.6.0"
warning "^4.0.1"
version "16.4.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
+react-is@^16.4.2:
+ version "16.4.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88"
+
react-jss@^8.1.0:
version "8.6.1"
resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.6.1.tgz#a06e2e1d2c4d91b4d11befda865e6c07fbd75252"
dependencies:
util.promisify "^1.0.0"
-recompose@^0.27.0:
- version "0.27.1"
- resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
+recompose@^0.28.0:
+ version "0.28.2"
+ resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.28.2.tgz#19e679227bdf979e0d31b73ffe7ae38c9194f4a7"
dependencies:
- babel-runtime "^6.26.0"
+ "@babel/runtime" "7.0.0-beta.56"
change-emitter "^0.1.2"
fbjs "^0.8.1"
hoist-non-react-statics "^2.3.1"
version "1.4.0"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
-regenerator-runtime@^0.11.0:
+regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"