--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List } from '@material-ui/core';
+import { PopperProps } from '@material-ui/core/Popper';
+import { WithStyles } from '@material-ui/core/styles';
+import { noop } from 'lodash';
+
+export interface AutocompleteProps<Item, Suggestion> {
+ label?: string;
+ value: string;
+ items: Item[];
+ suggestions?: Suggestion[];
+ onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
+ onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
+ onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
+ onCreate?: () => void;
+ onDelete?: (item: Item, index: number) => void;
+ onSelect?: (suggestion: Suggestion) => void;
+ renderChipValue?: (item: Item) => string;
+ renderSuggestion?: (suggestion: Suggestion) => React.ReactNode;
+}
+
+export interface AutocompleteState {
+ suggestionsOpen: boolean;
+}
+export class Autocomplete<Value, Suggestion> extends React.Component<AutocompleteProps<Value, Suggestion>, AutocompleteState> {
+
+ state = {
+ suggestionsOpen: false,
+ };
+
+ containerRef = React.createRef<HTMLDivElement>();
+ inputRef = React.createRef<HTMLInputElement>();
+
+ render() {
+ return (
+ <RootRef rootRef={this.containerRef}>
+ <FormControl fullWidth>
+ {this.renderLabel()}
+ {this.renderInput()}
+ {this.renderSuggestions()}
+ </FormControl>
+ </RootRef>
+ );
+ }
+
+ renderLabel() {
+ const { label } = this.props;
+ return label && <InputLabel>{label}</InputLabel>;
+ }
+
+ renderInput() {
+ return <Input
+ inputRef={this.inputRef}
+ value={this.props.value}
+ startAdornment={this.renderChips()}
+ onFocus={this.handleFocus}
+ onBlur={this.handleBlur}
+ onChange={this.props.onChange}
+ onKeyPress={this.handleKeyPress}
+ />;
+ }
+
+ renderSuggestions() {
+ const { suggestions = [] } = this.props;
+ return (
+ <Popper
+ open={this.state.suggestionsOpen && suggestions.length > 0}
+ anchorEl={this.containerRef.current}>
+ <Paper onMouseDown={this.preventBlur}>
+ <List dense style={{ width: this.getSuggestionsWidth() }}>
+ {suggestions.map(
+ (suggestion, index) =>
+ <ListItem button key={index} onClick={this.handleSelect(suggestion)}>
+ {this.renderSuggestion(suggestion)}
+ </ListItem>
+ )}
+ </List>
+ </Paper>
+ </Popper>
+ );
+ }
+
+ handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
+ const { onFocus = noop } = this.props;
+ this.setState({ suggestionsOpen: true });
+ onFocus(event);
+ }
+
+ handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
+ setTimeout(() => {
+ const { onBlur = noop } = this.props;
+ this.setState({ suggestionsOpen: false });
+ onBlur(event);
+ });
+ }
+
+ handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
+ const { onCreate = noop } = this.props;
+ if (key === 'Enter' && this.props.value.length > 0) {
+ onCreate();
+ }
+ }
+
+ renderChips() {
+ const { items, onDelete } = this.props;
+ return items.map(
+ (item, index) =>
+ <Chip
+ label={this.renderChipValue(item)}
+ key={index}
+ onDelete={() => onDelete ? onDelete(item, index) : undefined} />
+ );
+ }
+
+ renderChipValue(value: Value) {
+ const { renderChipValue } = this.props;
+ return renderChipValue ? renderChipValue(value) : JSON.stringify(value);
+ }
+
+ preventBlur = (event: React.MouseEvent<HTMLElement>) => {
+ event.preventDefault();
+ }
+
+ handleClickAway = (event: React.MouseEvent<HTMLElement>) => {
+ if (event.target !== this.inputRef.current) {
+ this.setState({ suggestionsOpen: false });
+ }
+ }
+
+ handleSelect(suggestion: Suggestion) {
+ return () => {
+ const { onSelect = noop } = this.props;
+ const { current } = this.inputRef;
+ if (current) {
+ current.focus();
+ }
+ onSelect(suggestion);
+ };
+ }
+
+ renderSuggestion(suggestion: Suggestion) {
+ const { renderSuggestion } = this.props;
+ return renderSuggestion
+ ? renderSuggestion(suggestion)
+ : <ListItemText>{JSON.stringify(suggestion)}</ListItemText>;
+ }
+
+ getSuggestionsWidth() {
+ return this.containerRef.current ? this.containerRef.current.offsetWidth : 'auto';
+ }
+}
+
+type ChipClasses = 'root';
+
+const chipStyles: StyleRulesCallback<ChipClasses> = theme => ({
+ root: {
+ marginRight: theme.spacing.unit / 4,
+ height: theme.spacing.unit * 3,
+ }
+});
+
+const Chip = withStyles(chipStyles)(MuiChip);
+
+type PopperClasses = 'root';
+
+const popperStyles: StyleRulesCallback<ChipClasses> = theme => ({
+ root: {
+ zIndex: theme.zIndex.modal,
+ }
+});
+
+const Popper = withStyles(popperStyles)(
+ ({ classes, ...props }: PopperProps & WithStyles<PopperClasses>) =>
+ <MuiPopper {...props} className={classes.root} />
+);
+
+type InputClasses = 'root';
+
+const inputStyles: StyleRulesCallback<InputClasses> = () => ({
+ root: {
+ display: 'flex',
+ flexWrap: 'wrap',
+ },
+ input: {
+ minWidth: '20%',
+ flex: 1,
+ },
+});
+
+const Input = withStyles(inputStyles)(MuiInput);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DialogActions as MuiDialogActions } from '@material-ui/core/';
+import { StyleRulesCallback, withStyles } from '@material-ui/core';
+
+const styles: StyleRulesCallback<'root'> = theme => {
+ const margin = theme.spacing.unit * 3;
+ return {
+ root: {
+ marginRight: margin,
+ marginBottom: margin,
+ marginLeft: margin,
+ },
+ };
+};
+export const DialogActions = withStyles(styles)(MuiDialogActions);
import * as React from 'react';
import { WrappedFieldProps } from 'redux-form';
import { ArvadosTheme } from '~/common/custom-theme';
-import { StyleRulesCallback, WithStyles, withStyles, FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, withStyles, FormControl, InputLabel, Select, MenuItem, FormHelperText } from '@material-ui/core';
type CssRules = 'formControl' | 'selectWrapper' | 'select' | 'option';
))}
</Select>
</FormControl>
- );
\ No newline at end of file
+ );
+
+interface SelectFieldProps {
+ children: React.ReactNode;
+ label: string;
+}
+
+type SelectFieldCssRules = 'formControl';
+
+const selectFieldStyles: StyleRulesCallback<SelectFieldCssRules> = (theme: ArvadosTheme) => ({
+ formControl: {
+ marginBottom: theme.spacing.unit * 3
+ },
+});
+export const SelectField = withStyles(selectFieldStyles)(
+ (props: WrappedFieldProps & SelectFieldProps & WithStyles<SelectFieldCssRules>) =>
+ <FormControl error={props.meta.invalid} className={props.classes.formControl}>
+ <InputLabel>
+ {props.label}
+ </InputLabel>
+ <Select
+ {...props.input}>
+ {props.children}
+ </Select>
+ <FormHelperText>{props.meta.error}</FormHelperText>
+ </FormControl>
+);
\ No newline at end of file
type TextFieldProps = WrappedFieldProps & WithStyles<CssRules>;
-export const TextField = withStyles(styles)((props: TextFieldProps & { label?: string, autoFocus?: boolean, required?: boolean }) =>
+export const TextField = withStyles(styles)((props: TextFieldProps & {
+ label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, children: React.ReactNode
+}) =>
<MaterialTextField
helperText={props.meta.touched && props.meta.error}
className={props.classes.textField}
autoFocus={props.autoFocus}
fullWidth={true}
required={props.required}
+ select={props.select}
+ children={props.children}
{...props.input}
/>);
}
render() {
- return <RichTextEditor
+ return <RichTextEditor
value={this.state.value}
onChange={this.onChange}
placeholder={this.props.label} />;
);
export const DateTextField = withStyles(styles)
- ((props: TextFieldProps) =>
+ ((props: TextFieldProps) =>
<MaterialTextField
type="date"
disabled={props.meta.submitting}
name={props.input.name}
onChange={props.input.onChange}
value={props.input.value}
- />
+ />
);
\ No newline at end of file
export enum LinkClass {
STAR = 'star',
- TAG = 'tag'
+ TAG = 'tag',
+ PERMISSION = 'permission',
}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { LinkResource, LinkClass } from './link';
+
+export interface PermissionResource extends LinkResource {
+ linkClass: LinkClass.PERMISSION;
+}
+
+export enum PermissionLevel {
+ NONE = 'none',
+ CAN_READ = 'can_read',
+ CAN_WRITE = 'can_write',
+ CAN_MANAGE = 'can_manage',
+}
import { AxiosInstance } from "axios";
import { ApiActions } from "~/services/api/api-actions";
-export class LinkService extends CommonResourceService<LinkResource> {
+export class LinkService<Resource extends LinkResource = LinkResource> extends CommonResourceService<Resource> {
constructor(serverApi: AxiosInstance, actions: ApiActions) {
super(serverApi, "links", actions);
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { LinkService } from "~/services/link-service/link-service";
+import { PermissionResource } from "~/models/permission";
+import { ListArguments, ListResults, CommonResourceService } from '~/services/common-service/common-resource-service';
+import { LinkClass } from '../../models/link';
+
+export class PermissionService extends LinkService<PermissionResource> {
+
+ permissionListService = new CommonResourceService(this.serverApi, 'permissions', this.actions);
+ create(data?: Partial<PermissionResource>) {
+ return super.create({ ...data, linkClass: LinkClass.PERMISSION });
+ }
+
+ listResourcePermissions(uuid: string, args: ListArguments = {}): Promise<ListResults<PermissionResource>> {
+ const service = new CommonResourceService<PermissionResource>(this.serverApi, `permissions/${uuid}`, this.actions);
+ return service.list(args);
+ }
+
+}
import { ApiActions } from "~/services/api/api-actions";
import { WorkflowService } from "~/services/workflow-service/workflow-service";
import { SearchService } from '~/services/search-service/search-service';
+import { PermissionService } from "~/services/permission-service/permission-service";
export type ServiceRepository = ReturnType<typeof createServices>;
const keepService = new KeepService(apiClient, actions);
const linkService = new LinkService(apiClient, actions);
const logService = new LogService(apiClient, actions);
+ const permissionService = new PermissionService(apiClient, actions);
const projectService = new ProjectService(apiClient, actions);
const userService = new UserService(apiClient, actions);
const workflowService = new WorkflowService(apiClient, actions);
keepService,
linkService,
logService,
+ permissionService,
projectService,
searchService,
tagService,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { SHARING_DIALOG_NAME, SharingPublicAccessFormData, SHARING_PUBLIC_ACCESS_FORM_NAME, SHARING_INVITATION_FORM_NAME, SharingManagementFormData, SharingInvitationFormData, VisibilityLevel, getSharingMangementFormData, getSharingPublicAccessFormData } from './sharing-dialog-types';
+import { Dispatch } from 'redux';
+import { ServiceRepository } from "~/services/services";
+import { FilterBuilder } from '~/services/api/filter-builder';
+import { initialize, getFormValues, reset } from 'redux-form';
+import { SHARING_MANAGEMENT_FORM_NAME } from '~/store/sharing-dialog/sharing-dialog-types';
+import { RootState } from '~/store/store';
+import { getDialog } from '~/store/dialog/dialog-reducer';
+import { PermissionLevel } from '~/models/permission';
+import { getPublicGroupUuid } from "~/store/workflow-panel/workflow-panel-actions";
+import { PermissionResource } from '~/models/permission';
+import { differenceWith } from "lodash";
+import { withProgress } from "~/store/progress-indicator/with-progress";
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
+import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
+
+export const openSharingDialog = (resourceUuid: string) =>
+ (dispatch: Dispatch) => {
+ dispatch(dialogActions.OPEN_DIALOG({ id: SHARING_DIALOG_NAME, data: resourceUuid }));
+ dispatch<any>(loadSharingDialog);
+ };
+
+export const closeSharingDialog = () =>
+ dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME });
+
+export const connectSharingDialog = withDialog(SHARING_DIALOG_NAME);
+export const connectSharingDialogProgress = withProgress(SHARING_DIALOG_NAME);
+
+
+export const saveSharingDialogChanges = async (dispatch: Dispatch) => {
+ dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
+ await dispatch<any>(savePublicPermissionChanges);
+ await dispatch<any>(saveManagementChanges);
+ await dispatch<any>(sendInvitations);
+ dispatch(reset(SHARING_INVITATION_FORM_NAME));
+ await dispatch<any>(loadSharingDialog);
+};
+
+export const sendSharingInvitations = async (dispatch: Dispatch) => {
+ dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
+ await dispatch<any>(sendInvitations);
+ dispatch(closeSharingDialog());
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'New permissions created',
+ kind: SnackbarKind.SUCCESS,
+ }));
+};
+
+const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+
+ const dialog = getDialog<string>(getState().dialog, SHARING_DIALOG_NAME);
+
+ if (dialog) {
+ dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
+ const { items } = await permissionService.listResourcePermissions(dialog.data);
+ dispatch<any>(initializePublicAccessForm(items));
+ await dispatch<any>(initializeManagementForm(items));
+ dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+ }
+};
+
+const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
+ async (dispatch: Dispatch, getState: () => RootState, { userService }: ServiceRepository) => {
+
+ const filters = new FilterBuilder()
+ .addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
+ .getFilters();
+
+ const { items: users } = await userService.list({ filters });
+
+ const getEmail = (tailUuid: string) => {
+ const user = users.find(({ uuid }) => uuid === tailUuid);
+ return user
+ ? user.email
+ : tailUuid;
+ };
+
+ const managementPermissions = permissionLinks
+ .filter(item =>
+ item.tailUuid !== getPublicGroupUuid(getState()))
+ .map(({ tailUuid, name, uuid }) => ({
+ email: getEmail(tailUuid),
+ permissions: name as PermissionLevel,
+ permissionUuid: uuid,
+ }));
+
+ const managementFormData: SharingManagementFormData = {
+ permissions: managementPermissions,
+ initialPermissions: managementPermissions,
+ };
+
+ dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
+ };
+
+const initializePublicAccessForm = (permissionLinks: PermissionResource[]) =>
+ (dispatch: Dispatch, getState: () => RootState, ) => {
+
+ const [publicPermission] = permissionLinks
+ .filter(item => item.tailUuid === getPublicGroupUuid(getState()));
+
+ const publicAccessFormData: SharingPublicAccessFormData = publicPermission
+ ? {
+ visibility: VisibilityLevel.PUBLIC,
+ permissionUuid: publicPermission.uuid,
+ }
+ : {
+ visibility: permissionLinks.length > 0
+ ? VisibilityLevel.SHARED
+ : VisibilityLevel.PRIVATE,
+ permissionUuid: '',
+ };
+
+ dispatch(initialize(SHARING_PUBLIC_ACCESS_FORM_NAME, publicAccessFormData));
+ };
+
+const savePublicPermissionChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+ const state = getState();
+ const { user } = state.auth;
+ const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
+ if (dialog && user) {
+ const { permissionUuid, visibility } = getSharingPublicAccessFormData(state);
+
+ if (permissionUuid) {
+ if (visibility === VisibilityLevel.PUBLIC) {
+ await permissionService.update(permissionUuid, {
+ name: PermissionLevel.CAN_READ
+ });
+ } else {
+ await permissionService.delete(permissionUuid);
+ }
+
+ } else if (visibility === VisibilityLevel.PUBLIC) {
+
+ await permissionService.create({
+ ownerUuid: user.uuid,
+ headUuid: dialog.data,
+ tailUuid: getPublicGroupUuid(state),
+ name: PermissionLevel.CAN_READ,
+ });
+ }
+ }
+};
+
+const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+ const state = getState();
+ const { user } = state.auth;
+ const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
+ if (dialog && user) {
+
+ const { initialPermissions, permissions } = getSharingMangementFormData(state);
+ const { visibility } = getSharingPublicAccessFormData(state);
+
+
+ if (visibility === VisibilityLevel.PRIVATE) {
+
+ for (const permission of initialPermissions) {
+ await permissionService.delete(permission.permissionUuid);
+ }
+
+ } else {
+
+ const cancelledPermissions = differenceWith(
+ initialPermissions,
+ permissions,
+ (a, b) => a.permissionUuid === b.permissionUuid
+ );
+
+ for (const { permissionUuid } of cancelledPermissions) {
+ await permissionService.delete(permissionUuid);
+ }
+
+ for (const permission of permissions) {
+ await permissionService.update(permission.permissionUuid, { name: permission.permissions });
+ }
+
+ }
+ }
+};
+
+const sendInvitations = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+ const state = getState();
+ const { user } = state.auth;
+ const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
+ if (dialog && user) {
+
+ const invitations = getFormValues(SHARING_INVITATION_FORM_NAME)(state) as SharingInvitationFormData;
+
+ const invitationData = invitations.invitedPeople
+ .map(person => ({
+ ownerUuid: user.uuid,
+ headUuid: dialog.data,
+ tailUuid: person.uuid,
+ name: invitations.permissions
+ }));
+
+ for (const invitation of invitationData) {
+ await permissionService.create(invitation);
+ }
+
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PermissionLevel } from '~/models/permission';
+import { getFormValues, isDirty } from 'redux-form';
+import { RootState } from '~/store/store';
+
+export const SHARING_DIALOG_NAME = 'SHARING_DIALOG_NAME';
+export const SHARING_PUBLIC_ACCESS_FORM_NAME = 'SHARING_PUBLIC_ACCESS_FORM_NAME';
+export const SHARING_MANAGEMENT_FORM_NAME = 'SHARING_MANAGEMENT_FORM_NAME';
+export const SHARING_INVITATION_FORM_NAME = 'SHARING_INVITATION_FORM_NAME';
+
+export enum VisibilityLevel {
+ PRIVATE = 'Private',
+ SHARED = 'Shared',
+ PUBLIC = 'Public',
+}
+
+export interface SharingPublicAccessFormData {
+ visibility: VisibilityLevel;
+ permissionUuid: string;
+}
+
+export interface SharingManagementFormData {
+ permissions: SharingManagementFormDataRow[];
+ initialPermissions: SharingManagementFormDataRow[];
+}
+
+export interface SharingManagementFormDataRow {
+ email: string;
+ permissions: PermissionLevel;
+ permissionUuid: string;
+}
+
+export interface SharingInvitationFormData {
+ permissions: PermissionLevel;
+ invitedPeople: SharingInvitationFormPersonData[];
+}
+
+export interface SharingInvitationFormPersonData {
+ email: string;
+ name: string;
+ uuid: string;
+}
+
+export const getSharingMangementFormData = (state: any) =>
+ getFormValues(SHARING_MANAGEMENT_FORM_NAME)(state) as SharingManagementFormData;
+
+export const getSharingPublicAccessFormData = (state: any) =>
+ getFormValues(SHARING_PUBLIC_ACCESS_FORM_NAME)(state) as SharingPublicAccessFormData;
+
+export const hasChanges = (state: RootState) =>
+ isDirty(SHARING_PUBLIC_ACCESS_FORM_NAME)(state) ||
+ isDirty(SHARING_MANAGEMENT_FORM_NAME)(state) ||
+ isDirty(SHARING_INVITATION_FORM_NAME)(state);
return state.properties.uuidPrefix;
};
+export const getPublicUserUuid = (state: RootState) => {
+ const prefix = getProperty<string>(UUID_PREFIX_PROPERTY_NAME)(state.properties);
+ return `${prefix}-tpzed-anonymouspublic`;
+};
+export const getPublicGroupUuid = (state: RootState) => {
+ const prefix = getProperty<string>(UUID_PREFIX_PROPERTY_NAME)(state.properties);
+ return `${prefix}-j7d0g-anonymouspublic`;
+};
+
export const showWorkflowDetails = (uuid: string) =>
propertiesActions.SET_PROPERTY({ key: WORKFLOW_PANEL_DETAILS_UUID, value: uuid });
import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
+import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
export const collectionActionSet: ContextMenuActionSet = [[
{
icon: ShareIcon,
name: "Share",
- execute: (dispatch, resource) => {
- // add code
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSharingDialog(uuid));
}
},
{
import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
+import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
export const collectionResourceActionSet: ContextMenuActionSet = [[
{
{
icon: ShareIcon,
name: "Share",
- execute: (dispatch, resource) => {
- // add code
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSharingDialog(uuid));
}
},
{
import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
import { openProcessCommandDialog } from '~/store/processes/process-command-actions';
import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
+import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
export const processActionSet: ContextMenuActionSet = [[
{
icon: ShareIcon,
name: "Share",
- execute: (dispatch, resource) => {
- // add code
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSharingDialog(uuid));
}
},
{
import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
+import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
export const processResourceActionSet: ContextMenuActionSet = [[
{
{
icon: ShareIcon,
name: "Share",
- execute: (dispatch, resource) => {
- // add code
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSharingDialog(uuid));
}
},
{
import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
import { toggleProjectTrashed } from "~/store/trash/trash-actions";
import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
+import { ShareIcon } from '~/components/icon/icon';
+import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
export const projectActionSet: ContextMenuActionSet = [[
dispatch<any>(openProjectUpdateDialog(resource));
}
},
+ {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSharingDialog(uuid));
+ }
+ },
{
component: ToggleFavoriteAction,
execute: (dispatch, resource) => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+
+export interface AdvancedViewSwitchInjectedProps {
+ toggleAdvancedView: () => void;
+ advancedViewOpen: boolean;
+}
+
+export const connectAdvancedViewSwitch = (Component: React.ComponentType<AdvancedViewSwitchInjectedProps>) =>
+ class extends React.Component<{}, { advancedViewOpen: boolean }> {
+
+ state = { advancedViewOpen: false };
+
+ toggleAdvancedView = () => {
+ this.setState(({ advancedViewOpen }) => ({ advancedViewOpen: !advancedViewOpen }));
+ }
+
+ render() {
+ return <Component {...this.state} {...this} />;
+ }
+ };
+
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Autocomplete } from '~/components/autocomplete/autocomplete';
+import { UserResource } from '~/models/user';
+import { connect, DispatchProp } from 'react-redux';
+import { ServiceRepository } from '~/services/services';
+import { FilterBuilder } from '../../services/api/filter-builder';
+import { debounce } from 'debounce';
+import { ListItemText, Typography } from '@material-ui/core';
+import { noop } from 'lodash/fp';
+
+export interface Person {
+ name: string;
+ email: string;
+ uuid: string;
+}
+export interface PeopleSelectProps {
+
+ items: Person[];
+
+ onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
+ onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
+ onCreate?: (person: Person) => void;
+ onDelete?: (index: number) => void;
+ onSelect?: (person: Person) => void;
+
+}
+
+export interface PeopleSelectState {
+ value: string;
+ suggestions: UserResource[];
+}
+
+export const PeopleSelect = connect()(
+ class PeopleSelect extends React.Component<PeopleSelectProps & DispatchProp, PeopleSelectState> {
+
+ state: PeopleSelectState = {
+ value: '',
+ suggestions: []
+ };
+
+ render() {
+ return (
+ <Autocomplete
+ label='Invite people'
+ value={this.state.value}
+ items={this.props.items}
+ suggestions={this.state.suggestions}
+ onChange={this.handleChange}
+ onCreate={this.handleCreate}
+ onSelect={this.handleSelect}
+ onDelete={this.handleDelete}
+ onFocus={this.props.onFocus}
+ onBlur={this.props.onBlur}
+ renderChipValue={this.renderChipValue}
+ renderSuggestion={this.renderSuggestion} />
+ );
+ }
+
+ renderChipValue({ name, uuid }: Person) {
+ return name ? name : uuid;
+ }
+
+ renderSuggestion({ firstName, lastName, email }: UserResource) {
+ return (
+ <ListItemText>
+ <Typography noWrap>{`${firstName} ${lastName} <<${email}>>`}</Typography>
+ </ListItemText>
+ );
+ }
+
+ handleDelete = (_: Person, index: number) => {
+ const { onDelete = noop } = this.props;
+ onDelete(index);
+ }
+
+ handleCreate = () => {
+ const { onCreate } = this.props;
+ if (onCreate) {
+ this.setState({ value: '', suggestions: [] });
+ onCreate({
+ email: '',
+ name: '',
+ uuid: this.state.value,
+ });
+ }
+ }
+
+ handleSelect = ({ email, firstName, lastName, uuid }: UserResource) => {
+ const { onSelect = noop } = this.props;
+ this.setState({ value: '', suggestions: [] });
+ onSelect({
+ email,
+ name: `${firstName} ${lastName}`,
+ uuid,
+ });
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState({ value: event.target.value }, this.getSuggestions);
+ }
+
+ getSuggestions = debounce(() => this.props.dispatch<any>(this.requestSuggestions), 500);
+
+ requestSuggestions = async (_: void, __: void, { userService }: ServiceRepository) => {
+ const { value } = this.state;
+ const filters = new FilterBuilder()
+ .addILike('email', value)
+ .getFilters();
+ const { items } = await userService.list({ filters, limit: 5 });
+ this.setState({ suggestions: items });
+ }
+
+ });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { MenuItem, Select, withStyles, StyleRulesCallback } from '@material-ui/core';
+import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
+import Edit from '@material-ui/icons/Edit';
+import Computer from '@material-ui/icons/Computer';
+import { WithStyles } from '@material-ui/core/styles';
+import { SelectProps } from '@material-ui/core/Select';
+import { SelectItem } from './select-item';
+import { PermissionLevel } from '../../models/permission';
+
+export enum PermissionSelectValue {
+ READ = 'Read',
+ WRITE = 'Write',
+ MANAGE = 'Manage',
+}
+
+export const parsePermissionLevel = (value: PermissionSelectValue) => {
+ switch (value) {
+ case PermissionSelectValue.READ:
+ return PermissionLevel.CAN_READ;
+ case PermissionSelectValue.WRITE:
+ return PermissionLevel.CAN_WRITE;
+ case PermissionSelectValue.MANAGE:
+ return PermissionLevel.CAN_MANAGE;
+ default:
+ return PermissionLevel.NONE;
+ }
+};
+
+export const formatPermissionLevel = (value: PermissionLevel) => {
+ switch (value) {
+ case PermissionLevel.CAN_READ:
+ return PermissionSelectValue.READ;
+ case PermissionLevel.CAN_WRITE:
+ return PermissionSelectValue.WRITE;
+ case PermissionLevel.CAN_MANAGE:
+ return PermissionSelectValue.MANAGE;
+ default:
+ return PermissionSelectValue.READ;
+ }
+};
+
+
+export const PermissionSelect = (props: SelectProps) =>
+ <Select
+ {...props}
+ renderValue={renderPermissionItem}>
+ <MenuItem value={PermissionSelectValue.READ}>
+ {renderPermissionItem(PermissionSelectValue.READ)}
+ </MenuItem>
+ <MenuItem value={PermissionSelectValue.WRITE}>
+ {renderPermissionItem(PermissionSelectValue.WRITE)}
+ </MenuItem>
+ <MenuItem value={PermissionSelectValue.MANAGE}>
+ {renderPermissionItem(PermissionSelectValue.MANAGE)}
+ </MenuItem>
+ </Select>;
+
+const renderPermissionItem = (value: string) =>
+ <SelectItem {...{ value, icon: getIcon(value) }} />;
+
+const getIcon = (value: string) => {
+ switch (value) {
+ case PermissionSelectValue.READ:
+ return RemoveRedEye;
+ case PermissionSelectValue.WRITE:
+ return Edit;
+ case PermissionSelectValue.MANAGE:
+ return Computer;
+ default:
+ return Computer;
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Grid, withStyles, StyleRulesCallback } from '@material-ui/core';
+import { WithStyles } from '@material-ui/core/styles';
+import { SvgIconProps } from '@material-ui/core/SvgIcon';
+
+type SelectItemClasses = 'value' | 'icon';
+
+const permissionItemStyles: StyleRulesCallback<SelectItemClasses> = theme => ({
+ value: {
+ marginLeft: theme.spacing.unit,
+ },
+ icon: {
+ margin: `-${theme.spacing.unit / 2}px 0`
+ }
+});
+
+/**
+ * This component should be used as a child of MenuItem component.
+ */
+export const SelectItem = withStyles(permissionItemStyles)(
+ ({ value, icon: Icon, classes }: { value: string, icon: React.ComponentType<SvgIconProps> } & WithStyles<SelectItemClasses>) => {
+ return (
+ <Grid container alignItems='center'>
+ <Icon className={classes.icon} />
+ <span className={classes.value}>
+ {value}
+ </span>
+ </Grid>);
+ });
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Dialog, DialogTitle, Button, Grid, DialogContent, CircularProgress, Paper } from '@material-ui/core';
+import { DialogActions } from '~/components/dialog-actions/dialog-actions';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+
+
+export interface SharingDialogDataProps {
+ open: boolean;
+ loading: boolean;
+ saveEnabled: boolean;
+ advancedEnabled: boolean;
+ children: React.ReactNode;
+}
+export interface SharingDialogActionProps {
+ onClose: () => void;
+ onExited: () => void;
+ onSave: () => void;
+ onAdvanced: () => void;
+}
+export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
+ const { children, open, loading, advancedEnabled, saveEnabled, onAdvanced, onClose, onExited, onSave } = props;
+ return <Dialog
+ {...{ open, onClose, onExited }}
+ fullWidth
+ maxWidth='sm'
+ disableBackdropClick
+ disableEscapeKeyDown>
+ <DialogTitle>
+ Sharing settings
+ </DialogTitle>
+ <DialogContent>
+ {children}
+ </DialogContent>
+ <DialogActions>
+ <Grid container spacing={8}>
+ {advancedEnabled &&
+ <Grid item>
+ <Button
+ color='primary'
+ onClick={onAdvanced}>
+ Advanced
+ </Button>
+ </Grid>
+ }
+ <Grid item xs />
+ <Grid item>
+ <Button onClick={onClose}>
+ Close
+ </Button>
+ </Grid>
+ <Grid item>
+ <Button
+ variant='contained'
+ color='primary'
+ onClick={onSave}
+ disabled={!saveEnabled}>
+ Save
+ </Button>
+ </Grid>
+ </Grid>
+ </DialogActions>
+ {
+ loading && <LoadingIndicator />
+ }
+ </Dialog>;
+};
+
+const loadingIndicatorStyles: StyleRulesCallback<'root'> = theme => ({
+ root: {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
+ },
+});
+
+const LoadingIndicator = withStyles(loadingIndicatorStyles)(
+ (props: WithStyles<'root'>) =>
+ <Paper classes={props.classes}>
+ <CircularProgress />
+ </Paper>
+);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Grid, Typography } from '@material-ui/core';
+
+import { SharingInvitationForm } from './sharing-invitation-form';
+import { SharingManagementForm } from './sharing-management-form';
+import { SharingPublicAccessForm } from './sharing-public-access-form';
+
+export const SharingDialogContent = (props: { advancedViewOpen: boolean }) =>
+ <Grid container direction='column' spacing={24}>
+ {props.advancedViewOpen &&
+ <>
+ <Grid item>
+ <Typography variant='subheading'>
+ Who can access
+ </Typography>
+ <SharingPublicAccessForm />
+ <SharingManagementForm />
+ </Grid>
+ </>
+ }
+ <Grid item>
+ <SharingInvitationForm />
+ </Grid>
+ </Grid>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose, Dispatch } from 'redux';
+import { connect } from 'react-redux';
+
+import * as React from 'react';
+import { connectSharingDialog, saveSharingDialogChanges, connectSharingDialogProgress, sendSharingInvitations } from '~/store/sharing-dialog/sharing-dialog-actions';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { RootState } from '~/store/store';
+
+import SharingDialogComponent, { SharingDialogDataProps, SharingDialogActionProps } from './sharing-dialog-component';
+import { SharingDialogContent } from './sharing-dialog-content';
+import { connectAdvancedViewSwitch, AdvancedViewSwitchInjectedProps } from './advanced-view-switch';
+import { hasChanges } from '~/store/sharing-dialog/sharing-dialog-types';
+import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+
+type Props = WithDialogProps<string> & AdvancedViewSwitchInjectedProps & WithProgressStateProps;
+
+const mapStateToProps = (state: RootState, { advancedViewOpen, working, ...props }: Props): SharingDialogDataProps => ({
+ ...props,
+ saveEnabled: hasChanges(state),
+ loading: working,
+ advancedEnabled: !advancedViewOpen,
+ children: <SharingDialogContent {...{ advancedViewOpen }} />,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch, { toggleAdvancedView, advancedViewOpen, ...props }: Props): SharingDialogActionProps => ({
+ ...props,
+ onClose: props.closeDialog,
+ onExited: () => {
+ if (advancedViewOpen) {
+ toggleAdvancedView();
+ }
+ },
+ onSave: () => {
+ if (advancedViewOpen) {
+ dispatch<any>(saveSharingDialogChanges);
+ } else {
+ dispatch<any>(sendSharingInvitations);
+ }
+ },
+ onAdvanced: toggleAdvancedView,
+});
+
+export const SharingDialog = compose(
+ connectAdvancedViewSwitch,
+ connectSharingDialog,
+ connectSharingDialogProgress,
+ connect(mapStateToProps, mapDispatchToProps)
+)(SharingDialogComponent);
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Field, WrappedFieldProps, FieldArray, WrappedFieldArrayProps } from 'redux-form';
+import { Grid, FormControl, InputLabel } from '@material-ui/core';
+import { PermissionSelect, parsePermissionLevel, formatPermissionLevel } from './permission-select';
+import { PeopleSelect, Person } from './people-select';
+
+export default () =>
+ <Grid container spacing={8}>
+ <Grid item xs={8}>
+ <InvitedPeopleField />
+ </Grid>
+ <Grid item xs={4}>
+ <PermissionSelectField />
+ </Grid>
+ </Grid>;
+
+const InvitedPeopleField = () =>
+ <FieldArray
+ name='invitedPeople'
+ component={InvitedPeopleFieldComponent} />;
+
+
+const InvitedPeopleFieldComponent = ({ fields }: WrappedFieldArrayProps<Person>) =>
+ <PeopleSelect
+ items={fields.getAll() || []}
+ onSelect={fields.push}
+ onDelete={fields.remove} />;
+
+const PermissionSelectField = () =>
+ <Field
+ name='permissions'
+ component={PermissionSelectComponent}
+ format={formatPermissionLevel}
+ parse={parsePermissionLevel} />;
+
+const PermissionSelectComponent = ({ input }: WrappedFieldProps) =>
+ <FormControl fullWidth>
+ <InputLabel>Authorization</InputLabel>
+ <PermissionSelect {...input} />
+ </FormControl>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { reduxForm } from 'redux-form';
+import { connect } from 'react-redux';
+import { compose } from 'redux';
+import SharingInvitationFormComponent from './sharing-invitation-form-component';
+import { SHARING_INVITATION_FORM_NAME } from '~/store/sharing-dialog/sharing-dialog-types';
+import { PermissionLevel } from '~/models/permission';
+
+export const SharingInvitationForm = compose(
+ connect(() => ({
+ initialValues: {
+ permissions: PermissionLevel.CAN_READ,
+ invitedPeople: [],
+ }
+ })),
+ reduxForm({ form: SHARING_INVITATION_FORM_NAME })
+)(SharingInvitationFormComponent);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Grid, StyleRulesCallback, Divider, IconButton, Typography } from '@material-ui/core';
+import { Field, WrappedFieldProps, WrappedFieldArrayProps, FieldArray, FieldsProps } from 'redux-form';
+import { PermissionSelect, formatPermissionLevel, parsePermissionLevel } from './permission-select';
+import { WithStyles } from '@material-ui/core/styles';
+import withStyles from '@material-ui/core/styles/withStyles';
+import { CloseIcon } from '~/components/icon/icon';
+
+
+export default () =>
+ <FieldArray name='permissions' component={SharingManagementFieldArray} />;
+
+const SharingManagementFieldArray = ({ fields }: WrappedFieldArrayProps<{ email: string }>) =>
+ <div>
+ {
+ fields.map((field, index, fields) =>
+ <PermissionManagementRow key={field} {...{ field, index, fields }} />)
+ }
+ <Divider />
+ </div>;
+
+const permissionManagementRowStyles: StyleRulesCallback<'root'> = theme => ({
+ root: {
+ padding: `${theme.spacing.unit}px 0`,
+ }
+});
+const PermissionManagementRow = withStyles(permissionManagementRowStyles)(
+ ({ field, index, fields, classes }: { field: string, index: number, fields: FieldsProps<{ email: string }> } & WithStyles<'root'>) =>
+ <>
+ <Divider />
+ <Grid container alignItems='center' spacing={8} wrap='nowrap' className={classes.root}>
+ <Grid item xs={8}>
+ <Typography noWrap variant='subheading'>{fields.get(index).email}</Typography>
+ </Grid>
+ <Grid item xs={4} container wrap='nowrap'>
+ <Field
+ name={`${field}.permissions`}
+ component={PermissionSelectComponent}
+ format={formatPermissionLevel}
+ parse={parsePermissionLevel} />
+ <IconButton onClick={() => fields.remove(index)}>
+ <CloseIcon />
+ </IconButton>
+ </Grid>
+ </Grid>
+ </>
+);
+
+const PermissionSelectComponent = ({ input }: WrappedFieldProps) =>
+ <PermissionSelect fullWidth disableUnderline {...input} />;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { reduxForm } from 'redux-form';
+import SharingManagementFormComponent from './sharing-management-form-component';
+import { SHARING_MANAGEMENT_FORM_NAME } from '~/store/sharing-dialog/sharing-dialog-types';
+
+export const SharingManagementForm = reduxForm(
+ { form: SHARING_MANAGEMENT_FORM_NAME }
+)(SharingManagementFormComponent);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Grid, StyleRulesCallback, Divider, Typography } from '@material-ui/core';
+import { Field, WrappedFieldProps } from 'redux-form';
+import { WithStyles } from '@material-ui/core/styles';
+import withStyles from '@material-ui/core/styles/withStyles';
+import { VisibilityLevelSelect } from './visibility-level-select';
+import { VisibilityLevel } from '~/store/sharing-dialog/sharing-dialog-types';
+
+const sharingPublicAccessStyles: StyleRulesCallback<'root'> = theme => ({
+ root: {
+ padding: `${theme.spacing.unit * 2}px 0`,
+ }
+});
+
+const SharingPublicAccessForm = withStyles(sharingPublicAccessStyles)(
+ ({ classes, visibility }: WithStyles<'root'> & { visibility: VisibilityLevel }) =>
+ <>
+ <Divider />
+ <Grid container alignItems='center' spacing={8} className={classes.root}>
+ <Grid item xs={8}>
+ <Typography variant='subheading'>
+ {renderVisibilityInfo(visibility)}
+ </Typography>
+ </Grid>
+ <Grid item xs={4} container wrap='nowrap'>
+ <Field name='visibility' component={VisibilityLevelSelectComponent} />
+ </Grid>
+ </Grid>
+ </>
+);
+
+const renderVisibilityInfo = (visibility: VisibilityLevel) => {
+ switch (visibility) {
+ case VisibilityLevel.PUBLIC:
+ return 'Anyone can access';
+ case VisibilityLevel.SHARED:
+ return 'Specific people can access';
+ case VisibilityLevel.PRIVATE:
+ return 'Only you can access';
+ default:
+ return '';
+ }
+};
+
+export default ({ visibility }: { visibility: VisibilityLevel }) =>
+ <SharingPublicAccessForm {...{ visibility }} />;
+
+const VisibilityLevelSelectComponent = ({ input }: WrappedFieldProps) =>
+ <VisibilityLevelSelect fullWidth disableUnderline {...input} />;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { reduxForm } from 'redux-form';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import SharingPublicAccessFormComponent from './sharing-public-access-form-component';
+import { SHARING_PUBLIC_ACCESS_FORM_NAME } from '~/store/sharing-dialog/sharing-dialog-types';
+import { RootState } from '~/store/store';
+import { getSharingPublicAccessFormData } from '../../store/sharing-dialog/sharing-dialog-types';
+
+export const SharingPublicAccessForm = compose(
+ reduxForm(
+ { form: SHARING_PUBLIC_ACCESS_FORM_NAME }
+ ),
+ connect(
+ (state: RootState) => {
+ const { visibility } = getSharingPublicAccessFormData(state);
+ return { visibility };
+ }
+ )
+)(SharingPublicAccessFormComponent);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { MenuItem, Select, withStyles, StyleRulesCallback } from '@material-ui/core';
+import Lock from '@material-ui/icons/Lock';
+import People from '@material-ui/icons/People';
+import Public from '@material-ui/icons/Public';
+import { WithStyles } from '@material-ui/core/styles';
+import { SelectProps } from '@material-ui/core/Select';
+import { SelectItem } from './select-item';
+import { VisibilityLevel } from '~/store/sharing-dialog/sharing-dialog-types';
+
+
+type VisibilityLevelSelectClasses = 'value';
+
+const VisibilityLevelSelectStyles: StyleRulesCallback<VisibilityLevelSelectClasses> = theme => ({
+ value: {
+ marginLeft: theme.spacing.unit,
+ }
+});
+export const VisibilityLevelSelect = withStyles(VisibilityLevelSelectStyles)(
+ ({ classes, ...props }: SelectProps & WithStyles<VisibilityLevelSelectClasses>) =>
+ <Select
+ {...props}
+ renderValue={renderPermissionItem}
+ inputProps={{ classes }}>
+ <MenuItem value={VisibilityLevel.PUBLIC}>
+ {renderPermissionItem(VisibilityLevel.PUBLIC)}
+ </MenuItem>
+ <MenuItem value={VisibilityLevel.SHARED}>
+ {renderPermissionItem(VisibilityLevel.SHARED)}
+ </MenuItem>
+ <MenuItem value={VisibilityLevel.PRIVATE}>
+ {renderPermissionItem(VisibilityLevel.PRIVATE)}
+ </MenuItem>
+ </Select>);
+
+const renderPermissionItem = (value: string) =>
+ <SelectItem {...{ value, icon: getIcon(value) }} />;
+
+const getIcon = (value: string) => {
+ switch (value) {
+ case VisibilityLevel.PUBLIC:
+ return Public;
+ case VisibilityLevel.SHARED:
+ return People;
+ case VisibilityLevel.PRIVATE:
+ return Lock;
+ default:
+ return Lock;
+ }
+};
import SplitterLayout from 'react-splitter-layout';
import { WorkflowPanel } from '~/views/workflow-panel/workflow-panel';
import { SearchResultsPanel } from '~/views/search-results-panel/search-results-panel';
+import { SharingDialog } from '~/views-components/sharing-dialog/sharing-dialog';
import { AdvancedTabDialog } from '~/views-components/advanced-tab-dialog/advanced-tab-dialog';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
<ProcessCommandDialog />
<RenameFileDialog />
<RichTextEditorDialog />
+ <SharingDialog />
<Snackbar />
<UpdateCollectionDialog />
<UpdateProcessDialog />