const grey600 = grey["600"];
const grey700 = grey["700"];
const grey900 = grey["900"];
+const rocheBlue = '#06C';
const themeOptions: ArvadosThemeOptions = {
customs: {
},
palette: {
primary: {
- main: '#06C',
+ main: rocheBlue,
dark: blue.A100
}
}
import Typography from '@material-ui/core/Typography';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from '../../common/custom-theme';
+import * as classnames from "classnames";
type CssRules = 'attribute' | 'label' | 'value' | 'link';
interface DetailsAttributeDataProps {
label: string;
+ classLabel?: string;
value?: string | number;
+ classValue?: string;
link?: string;
children?: React.ReactNode;
}
type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
-export const DetailsAttribute = withStyles(styles)(({ label, link, value, children, classes }: DetailsAttributeProps) =>
+export const DetailsAttribute = withStyles(styles)(({ label, link, value, children, classes, classLabel, classValue }: DetailsAttributeProps) =>
<Typography component="div" className={classes.attribute}>
- <Typography component="span" className={classes.label}>{label}</Typography>
+ <Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
{ link
? <a href={link} className={classes.link} target='_blank'>{value}</a>
- : <Typography component="span" className={classes.value}>
+ : <Typography component="span" className={classnames([classes.value, classValue])}>
{value}
{children}
</Typography> }
}
export enum LinkClass {
- STAR = 'star'
+ STAR = 'star',
+ TAG = 'tag'
}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+const USER_UUID_REGEX = /.*tpzed.*/;
+const GROUP_UUID_REGEX = /.*-j7d0g-.*/;
+
+export enum ObjectTypes {
+ USER = "User",
+ GROUP = "Group",
+ UNKNOWN = "Unknown"
+}
+
+export const getUuidObjectType = (uuid: string) => {
+ switch (true) {
+ case USER_UUID_REGEX.test(uuid):
+ return ObjectTypes.USER;
+ case GROUP_UUID_REGEX.test(uuid):
+ return ObjectTypes.GROUP;
+ default:
+ return ObjectTypes.UNKNOWN;
+ }
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { LinkResource } from "./link";
+
+export interface TagResource extends LinkResource {
+ tailUuid: TagTailType;
+ properties: TagProperty;
+}
+
+export interface TagProperty {
+ key: string;
+ value: string;
+}
+
+export enum TagTailType {
+ COLLECTION = 'Collection',
+ JOB = 'Job'
+}
\ No newline at end of file
import { FavoriteService } from "./favorite-service/favorite-service";
import { AxiosInstance } from "axios";
import { CollectionService } from "./collection-service/collection-service";
+import { TagService } from "./tag-service/tag-service";
import Axios from "axios";
import { CollectionFilesService } from "./collection-files-service/collection-files-service";
import { KeepService } from "./keep-service/keep-service";
projectService: ProjectService;
linkService: LinkService;
favoriteService: FavoriteService;
+ tagService: TagService;
collectionService: CollectionService;
collectionFilesService: CollectionFilesService;
}
const linkService = new LinkService(apiClient);
const favoriteService = new FavoriteService(linkService, groupsService);
const collectionService = new CollectionService(apiClient, keepService);
+ const tagService = new TagService(linkService);
const collectionFilesService = new CollectionFilesService(collectionService);
return {
linkService,
favoriteService,
collectionService,
+ tagService,
collectionFilesService
};
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { LinkService } from "../link-service/link-service";
+import { LinkClass } from "../../models/link";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { TagTailType, TagResource } from "../../models/tag";
+import { OrderBuilder } from "../../common/api/order-builder";
+
+export class TagService {
+
+ constructor(private linkService: LinkService) { }
+
+ create(uuid: string, data: { key: string; value: string } ) {
+ return this.linkService
+ .create({
+ headUuid: uuid,
+ tailUuid: TagTailType.COLLECTION,
+ linkClass: LinkClass.TAG,
+ name: '',
+ properties: data
+ })
+ .then(tag => tag as TagResource );
+ }
+
+ list(uuid: string) {
+ const filters = FilterBuilder
+ .create()
+ .addEqual("headUuid", uuid)
+ .addEqual("tailUuid", TagTailType.COLLECTION)
+ .addEqual("linkClass", LinkClass.TAG);
+
+ const order = OrderBuilder
+ .create<TagResource>()
+ .addAsc('createdAt');
+
+ return this.linkService
+ .list({ filters, order })
+ .then(results => {
+ return results.items.map((tag => tag as TagResource ));
+ });
+ }
+
+}
import { createTree } from "../../models/tree";
import { RootState } from "../store";
import { ServiceRepository } from "../../services/services";
+import { TagResource, TagProperty } from "../../models/tag";
+import { snackbarActions } from "../snackbar/snackbar-actions";
export const collectionPanelActions = unionize({
LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
- LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>()
+ LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
+ LOAD_COLLECTION_TAGS: ofType<{ uuid: string }>(),
+ LOAD_COLLECTION_TAGS_SUCCESS: ofType<{ tags: TagResource[] }>(),
+ CREATE_COLLECTION_TAG: ofType<{ data: any }>(),
+ CREATE_COLLECTION_TAG_SUCCESS: ofType<{ tag: TagResource }>(),
+ DELETE_COLLECTION_TAG: ofType<{ uuid: string }>(),
+ DELETE_COLLECTION_TAG_SUCCESS: ofType<{ uuid: string }>()
}, { tag: 'type', value: 'payload' });
export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
+export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
+
export const loadCollection = (uuid: string, kind: ResourceKind) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
});
};
+export const loadCollectionTags = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION_TAGS({ uuid }));
+ return services.tagService
+ .list(uuid)
+ .then(tags => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION_TAGS_SUCCESS({ tags }));
+ });
+ };
+
+export const createCollectionTag = (data: TagProperty) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(collectionPanelActions.CREATE_COLLECTION_TAG({ data }));
+ const item = getState().collectionPanel.item;
+ const uuid = item ? item.uuid : '';
+ return services.tagService
+ .create(uuid, data)
+ .then(tag => {
+ dispatch(collectionPanelActions.CREATE_COLLECTION_TAG_SUCCESS({ tag }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Tag has been successfully added.",
+ hideDuration: 2000
+ }));
+ });
+ };
+export const deleteCollectionTag = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(collectionPanelActions.DELETE_COLLECTION_TAG({ uuid }));
+ return services.linkService
+ .delete(uuid)
+ .then(tag => {
+ dispatch(collectionPanelActions.DELETE_COLLECTION_TAG_SUCCESS({ uuid: tag.uuid }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Tag has been successfully deleted.",
+ hideDuration: 2000
+ }));
+ });
+ };
\ No newline at end of file
import { collectionPanelActions, CollectionPanelAction } from "./collection-panel-action";
import { CollectionResource } from "../../models/collection";
+import { TagResource } from "../../models/tag";
export interface CollectionPanelState {
item: CollectionResource | null;
+ tags: TagResource[];
}
const initialState = {
- item: null
+ item: null,
+ tags: []
};
export const collectionPanelReducer = (state: CollectionPanelState = initialState, action: CollectionPanelAction) =>
collectionPanelActions.match(action, {
default: () => state,
- LOAD_COLLECTION: () => state,
LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }),
+ LOAD_COLLECTION_TAGS_SUCCESS: ({ tags }) => ({...state, tags }),
+ CREATE_COLLECTION_TAG_SUCCESS: ({ tag }) => ({...state, tags: [...state.tags, tag] }),
+ DELETE_COLLECTION_TAG_SUCCESS: ({ uuid }) => ({...state, tags: state.tags.filter(tag => tag.uuid !== uuid) })
});
import { Resource, ResourceKind } from "../../models/resource";
import { projectPanelActions } from "../project-panel/project-panel-action";
import { getCollectionUrl } from "../../models/collection";
-import { getProjectUrl } from "../../models/project";
+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 { getUuidObjectType, ObjectTypes } from "../../models/object-types";
export const getResourceUrl = <T extends Resource>(resource: T): string => {
switch (resource.kind) {
}
};
+export const restoreBranch = (itemId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ 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));
+ uuids.forEach(uuid => {
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
+ });
+ };
+
+export const loadProjectAncestors = async (uuid: string, projectService: ProjectService): Promise<Array<ProjectResource>> => {
+ if (getUuidObjectType(uuid) === ObjectTypes.USER) {
+ return [];
+ } else {
+ const currentProject = await projectService.get(uuid);
+ const ancestors = await loadProjectAncestors(currentProject.ownerUuid, projectService);
+ return [...ancestors, currentProject];
+ }
+};
+
+const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise<any> => {
+ const [uuid, ...rest] = uuids;
+ if (uuid) {
+ await dispatch<any>(getProjectList(uuid));
+ return loadBranch(rest, dispatch);
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { require } from '../require';
+import { maxLength } from '../max-length';
+
+export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
+export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
\ No newline at end of file
export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
-export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { require } from './require';
+import { maxLength } from './max-length';
+
+export const TAG_KEY_VALIDATION = [require, maxLength(255)];
+export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
\ No newline at end of file
const { ownerUuid } = getState().projects.creator;
return dispatch<any>(createProject(data)).then(() => {
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Created a new project",
+ message: "Project has been successfully created.",
hideDuration: 2000
}));
dispatch(projectPanelActions.REQUEST_ITEMS());
import DialogTitle from '@material-ui/core/DialogTitle';
import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
import { FileUpload } from "../../components/file-upload/file-upload";
import { connect, DispatchProp } from "react-redux";
import { RootState } from "../../store/store";
import { compose } from 'redux';
import { ArvadosTheme } from '../../common/custom-theme';
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '../../../node_modules/@material-ui/core';
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
import { COLLECTION_FORM_NAME } from '../../store/collections/updater/collection-updater-action';
type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
StyleRulesCallback, WithStyles, withStyles, Card,
CardHeader, IconButton, CardContent, Grid, Chip
} from '@material-ui/core';
-import { connect } from 'react-redux';
+import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
import { ArvadosTheme } from '../../common/custom-theme';
import { RootState } from '../../store/store';
import { CollectionResource } from '../../models/collection';
import { CollectionPanelFiles } from '../../views-components/collection-panel-files/collection-panel-files';
import * as CopyToClipboard from 'react-copy-to-clipboard';
+import { TagResource } from '../../models/tag';
+import { CollectionTagForm } from './collection-tag-form';
+import { deleteCollectionTag } from '../../store/collection-panel/collection-panel-action';
-type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon';
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'value';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
- marginBottom: '20px'
+ marginBottom: theme.spacing.unit * 2
},
iconHeader: {
fontSize: '1.875rem',
color: theme.customs.colors.yellow700
},
tag: {
- marginRight: theme.spacing.unit
+ marginRight: theme.spacing.unit,
+ marginBottom: theme.spacing.unit
},
copyIcon: {
marginLeft: theme.spacing.unit,
fontSize: '1.125rem',
+ color: theme.palette.grey["500"],
cursor: 'pointer'
+ },
+ value: {
+ textTransform: 'none'
}
});
interface CollectionPanelDataProps {
item: CollectionResource;
+ tags: TagResource[];
}
interface CollectionPanelActionProps {
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: CollectionResource) => void;
}
-type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps
+type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps & DispatchProp
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const CollectionPanel = withStyles(styles)(
- connect((state: RootState) => ({ item: state.collectionPanel.item! }))(
- class extends React.Component<CollectionPanelProps> {
+ connect((state: RootState) => ({
+ item: state.collectionPanel.item,
+ tags: state.collectionPanel.tags
+ }))(
+ class extends React.Component<CollectionPanelProps> {
render() {
- const { classes, item, onContextMenu } = this.props;
+ const { classes, item, tags, onContextMenu } = this.props;
return <div>
<Card className={classes.card}>
<CardHeader
<CardContent>
<Grid container direction="column">
<Grid item xs={6}>
- <DetailsAttribute label='Collection UUID' value={item && item.uuid}>
+ <DetailsAttribute classValue={classes.value}
+ label='Collection UUID'
+ value={item && item.uuid}>
<CopyToClipboard text={item && item.uuid}>
<CopyIcon className={classes.copyIcon} />
</CopyToClipboard>
</DetailsAttribute>
+ <DetailsAttribute label='Number of files' value='14' />
<DetailsAttribute label='Content size' value='54 MB' />
- <DetailsAttribute label='Owner' value={item && item.ownerUuid} />
+ <DetailsAttribute classValue={classes.value} label='Owner' value={item && item.ownerUuid} />
</Grid>
</Grid>
</CardContent>
<CardHeader title="Tags" />
<CardContent>
<Grid container direction="column">
- <Grid item xs={4}>
- <Chip label="Tag 1" className={classes.tag}/>
- <Chip label="Tag 2" className={classes.tag}/>
- <Chip label="Tag 3" className={classes.tag}/>
+ <Grid item xs={12}><CollectionTagForm /></Grid>
+ <Grid item xs={12}>
+ {
+ tags.map(tag => {
+ return <Chip key={tag.etag} className={classes.tag}
+ onDelete={this.handleDelete(tag.uuid)}
+ label={renderTagLabel(tag)} />;
+ })
+ }
</Grid>
</Grid>
</CardContent>
</div>;
}
+ handleDelete = (uuid: string) => () => {
+ this.props.dispatch<any>(deleteCollectionTag(uuid));
+ }
+
componentWillReceiveProps({ match, item, onItemRouteChange }: CollectionPanelProps) {
if (!item || match.params.id !== item.uuid) {
onItemRouteChange(match.params.id);
}
)
);
+
+const renderTagLabel = (tag: TagResource) => {
+ const { properties } = tag;
+ return `${properties.key}: ${properties.value}`;
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field, reset } from 'redux-form';
+import { compose, Dispatch } from 'redux';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { StyleRulesCallback, withStyles, WithStyles, TextField, Button, CircularProgress } from '@material-ui/core';
+import { TagProperty } from '../../models/tag';
+import { createCollectionTag, COLLECTION_TAG_FORM_NAME } from '../../store/collection-panel/collection-panel-action';
+import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '../../validators/validators';
+
+type CssRules = 'form' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ form: {
+ marginBottom: theme.spacing.unit * 4
+ },
+ textField: {
+ marginRight: theme.spacing.unit
+ },
+ buttonWrapper: {
+ position: 'relative',
+ display: 'inline-block'
+ },
+ saveButton: {
+ boxShadow: 'none'
+ },
+ circularProgress: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ margin: 'auto'
+ }
+});
+
+interface CollectionTagFormDataProps {
+ submitting: boolean;
+ invalid: boolean;
+ pristine: boolean;
+}
+
+interface CollectionTagFormActionProps {
+ handleSubmit: any;
+}
+
+interface TextFieldProps {
+ label: string;
+ floatinglabeltext: string;
+ className?: string;
+ input?: string;
+ meta?: any;
+}
+
+type CollectionTagFormProps = CollectionTagFormDataProps & CollectionTagFormActionProps & WithStyles<CssRules>;
+
+export const CollectionTagForm = compose(
+ reduxForm({
+ form: COLLECTION_TAG_FORM_NAME,
+ onSubmit: (data: TagProperty, dispatch: Dispatch) => {
+ dispatch<any>(createCollectionTag(data));
+ dispatch(reset(COLLECTION_TAG_FORM_NAME));
+ }
+ }),
+ withStyles(styles))(
+
+ class CollectionTagForm extends React.Component<CollectionTagFormProps> {
+
+ render() {
+ const { classes, submitting, pristine, invalid, handleSubmit } = this.props;
+ return (
+ <form className={classes.form} onSubmit={handleSubmit}>
+ <Field name="key"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Key"
+ validate={TAG_KEY_VALIDATION}
+ className={classes.textField}
+ label="Key" />
+ <Field name="value"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Value"
+ validate={TAG_VALUE_VALIDATION}
+ className={classes.textField}
+ label="Value" />
+ <div className={classes.buttonWrapper}>
+ <Button type="submit" className={classes.saveButton}
+ color="primary"
+ size='small'
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ ADD
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+ </div>
+ </form>
+ );
+ }
+
+ renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
+ <TextField
+ helperText={touched && error}
+ label={label}
+ className={this.props.classes.textField}
+ error={touched && !!error}
+ autoComplete='off'
+ {...input}
+ {...custom}
+ />
+ )
+
+ }
+
+ );
\ No newline at end of file
import { resourceLabel } from '../../common/labels';
import { ArvadosTheme } from '../../common/custom-theme';
import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '../../views-components/data-explorer/renderers';
+import { restoreBranch } from '../../store/navigation/navigation-action';
type CssRules = "toolbar" | "button";
handleNewCollectionClick = () => {
this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
}
+
componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
if (match.params.id !== currentItemId) {
onItemRouteChange(match.params.id);
}
}
+
+ componentDidMount() {
+ if (this.props.match.params.id && this.props.currentItemId === '') {
+ this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
+ }
+ }
}
)
);
import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
import { FavoritePanel } from "../favorite-panel/favorite-panel";
import { CurrentTokenDialog } from '../../views-components/current-token-dialog/current-token-dialog';
-import { dataExplorerActions } from '../../store/data-explorer/data-explorer-action';
import { Snackbar } from '../../views-components/snackbar/snackbar';
import { favoritePanelActions } from '../../store/favorite-panel/favorite-panel-action';
import { CreateCollectionDialog } from '../../views-components/create-collection-dialog/create-collection-dialog';
import { CollectionPanel } from '../collection-panel/collection-panel';
-import { loadCollection } from '../../store/collection-panel/collection-panel-action';
+import { loadCollection, loadCollectionTags } from '../../store/collection-panel/collection-panel-action';
import { getCollectionUrl } from '../../models/collection';
import { RemoveDialog } from '../../views-components/remove-dialog/remove-dialog';
import { RenameDialog } from '../../views-components/rename-dialog/rename-dialog';
);
}
- renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
- onItemRouteChange={(collectionId) => this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION))}
+ renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
+ onItemRouteChange={(collectionId) => {
+ this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION));
+ this.props.dispatch<any>(loadCollectionTags(collectionId));
+ }}
onContextMenu={(event, item) => {
this.openContextMenu(event, {
uuid: item.uuid,