Commit 80df2e79 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '81-admin-file-repository' into 'master'

Resolve "Admin: File repository"

Closes #81

See merge request genesys-pgr/genesys-ui!87
parents f847c346 9b6a5871
......@@ -93,6 +93,35 @@
"roles": "User roles"
}
}
},
"repository": {
"file": {
"formTitle": "Update file metadata",
"form": {
"title": "Title",
"description": "Description",
"subject": "Subject",
"rightsHolder": "Rights Holder",
"originalUrl": "Original URL",
"license": "License",
"extent": "Extent",
"creator": "Creator",
"contentType": "Content Type",
"bibliographicCitation": "Bibliographic Citation",
"accessRights": "Access Rights"
}
},
"folder": {
"createDialogTitle": "Create new folder",
"createBtn": "Create new folder",
"updateDialogTitle": "Update folder metadata",
"updateBtn": "Edit folder",
"form": {
"title": "Title",
"description": "Description",
"name": "Folder name"
}
}
}
}
},
......
......@@ -6,7 +6,7 @@ import { Permissions, IUserPermissions } from 'model/acl/ACL';
*/
class RepositoryFile implements IUserPermissions {
public _permissions: Permissions;
public clazz: string = 'org.genesys.filerepository.model.RepositoryFile';
public static clazz: string = 'org.genesys.filerepository.model.RepositoryFile';
public accessRights: string;
public active: boolean;
......
import { normalize } from 'path';
// Constants
import { RECEIVE_FOLDER_DETAILS, RECEIVE_IMAGE_GALLERY } from 'repository/constants';
import {RECEIVE_FOLDER_DETAILS, RECEIVE_IMAGE_GALLERY, UPDATE_SUBFOLDER_LIST, UPDATE_FILE_LIST, RECEIVE_FILE} from 'repository/constants';
// Model
import FolderDetails from 'model/repository/FolderDetails';
......@@ -14,6 +14,8 @@ import { log } from 'utilities/debug';
import RepositoryFile from 'model/repository/RepositoryFile';
import RepositoryFolder from 'model/repository/RepositoryFolder';
import ImageGallery from 'model/repository/ImageGallery';
import navigateTo from 'actions/navigation';
import * as _ from 'lodash';
const receiveFolder = (folder: FolderDetails, error = null) => ({
......@@ -26,6 +28,20 @@ const receiveGallery = (gallery: ImageGallery, error = null) => ({
payload: { gallery, error },
});
const receiveFile = (file: RepositoryFile, error = null) => ({
type: RECEIVE_FILE,
payload: { file, error },
});
const updateSubfolders = (subFolder: RepositoryFolder) => ({
type: UPDATE_SUBFOLDER_LIST,
payload: { subFolder },
});
const updateFiles = (deletedFile: RepositoryFile) => ({
type: UPDATE_FILE_LIST,
payload: { deletedFile },
});
// Get folder details
export const getFolder = (path: string = '/') => (dispatch, getState) => {
......@@ -52,6 +68,57 @@ export const deleteFolder = (path: string) => (dispatch, getState) => {
});
};
export const removeFile = (uuid: string) => (dispatch) => {
return RepositoryService.removeFile(uuid)
.then((file: RepositoryFile) => {
console.log(`File deleted`, file);
dispatch(updateFiles(file));
return true;
})
.catch((error) => {
log('Error', error);
return false;
});
};
// Create or load folder at specified path
export const ensureFolder = (path: string) => (dispatch, getState) => {
return RepositoryService.ensureFolder(normalize(path))
.then((folder: RepositoryFolder) => {
dispatch(updateSubfolders(folder));
})
.catch((error) => {
log('Error', error);
});
};
export const updateFolder = (folder: RepositoryFolder) => (dispatch, getState) => {
return RepositoryService.updateFolder(folder)
.then((updated: FolderDetails) => {
dispatch(receiveFolder(updated));
})
.catch((error) => {
log('Error', error);
});
};
export const updateFile = (metadata: RepositoryFile) => (dispatch, getState) => {
if (_.isEqual({...metadata}, {...getState().repository.admin.file})) {
dispatch(goToFolder(metadata.folder));
}
return RepositoryService.updateFile(metadata)
.then((updated: RepositoryFile) => {
dispatch(receiveFile(updated));
dispatch(goToFolder(updated.folder));
})
.catch((error) => {
log('Error', error);
});
};
const goToFolder = (folder: RepositoryFolder | string) => (dispatch) => {
dispatch(navigateTo(`/admin/repository/f${typeof folder === 'object' ? folder.path : folder}/`));
};
// Upload file
export const uploadFile = (path: string, file: File) => (dispatch, getState) => {
......@@ -76,6 +143,17 @@ export const getGallery = (path: string) => (dispatch, getState) => {
});
};
// Get file
export const getFile = (uuid: string) => (dispatch, getState) => {
return RepositoryService.getFile(uuid)
.then((file: RepositoryFile) => {
dispatch(receiveFile(file));
})
.catch((error) => {
log('Error', error);
});
};
// Get folder details
export const createGallery = (path: string, title: string, description?: string) => (dispatch, getState) => {
const gallery: ImageGallery = new ImageGallery();
......
export const RECEIVE_FOLDER_DETAILS = 'App/Repository/RECEIVE_FOLDER_DETAILS';
export const RECEIVE_IMAGE_GALLERY = 'App/Repository/RECEIVE_IMAGE_GALLERY';
export const RECEIVE_FILE = 'App/Repository/RECEIVE_FILE';
export const UPDATE_SUBFOLDER_LIST = 'App/Repository/UPDATE_SUBFOLDER_LIST';
export const UPDATE_FILE_LIST = 'App/Repository/UPDATE_FILE_LIST';
export const EDIT_FOLDER_FORM = 'Form/Repository/EDIT_FOLDER_FORM';
export const EDIT_FILE_FORM = 'Form/Repository/EDIT_FILE_FORM';
import update from 'immutability-helper';
import { IReducerAction } from 'model/common.model';
import { RECEIVE_FOLDER_DETAILS, RECEIVE_IMAGE_GALLERY } from 'repository/constants';
import {RECEIVE_FOLDER_DETAILS, RECEIVE_IMAGE_GALLERY, UPDATE_SUBFOLDER_LIST, UPDATE_FILE_LIST, RECEIVE_FILE} from 'repository/constants';
import RepositoryFile from 'model/repository/RepositoryFile';
import FolderDetails from 'model/repository/FolderDetails';
import ImageGallery from 'model/repository/ImageGallery';
......@@ -30,6 +30,49 @@ export default function reducer(state = INITIAL_STATE, action: IReducerAction =
break;
}
case UPDATE_SUBFOLDER_LIST: {
const { subFolder } = action.payload;
const receivedIndex = state.folder ? state.folder.subFolders.findIndex((item) => item.uuid === subFolder.uuid) : -1;
if (receivedIndex === -1) {
const newSubFolders = [...state.folder.subFolders, subFolder];
return update(state, {
folder: {
subFolders: {
$set: newSubFolders,
},
},
});
}
return state;
}
case UPDATE_FILE_LIST: {
const { deletedFile } = action.payload;
const files: RepositoryFile[] = [];
if (state.folder && state.folder.files) {
state.folder.files.forEach((item) => {
if (item.uuid !== deletedFile.uuid) {
files.push(item);
}
});
}
return update(state, {
folder: {
files: {
$set: files,
},
},
});
}
case RECEIVE_FILE: {
return update(state, {
loading: { $set: false},
file: { $set: action.payload.file },
error: { $set: action.payload.error },
});
}
case RECEIVE_IMAGE_GALLERY: {
return update(state, {
loading: { $set: false},
......
// Admin
import RepositoryBrowser from 'repository/ui/RepositoryBrowser';
import ImageGalleryPage from 'repository/ui/ImageGalleryPage';
import EditFilePage from 'repository/ui/EditFilePage';
// Public
......@@ -32,4 +33,13 @@ export const adminRoutes = [
title: 'Image gallery',
},
},
{
path: '/repository/edit/:uuid',
component: EditFilePage,
exact: true,
root: '',
extraProps: {
title: 'Edit File',
},
},
];
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {translate} from 'react-i18next';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import {ensureFolder} from 'repository/actions/admin';
import RepositoryFolder from 'model/repository/RepositoryFolder';
import FolderDetails from 'model/repository/FolderDetails';
import FolderForm from './c/FolderForm';
import {withStyles} from '@material-ui/core/styles';
/*tslint:disable*/
const styles = (theme) => ({
createButton: {
[theme.breakpoints.down('sm')]: {
width: '100%',
margin: '8px 0',
},
},
});
/*tslint:enable*/
interface ICreateFolderDialogProps extends React.ClassAttributes<any> {
ensureFolder: (path: string) => Promise<RepositoryFolder>;
folder: FolderDetails;
root?: string;
path?: string;
classes: any;
variant?: 'text' | 'flat' | 'outlined' | 'contained' | 'raised' | 'fab' | 'extendedFab';
t: any;
}
class CreateFolderDialog extends React.Component<ICreateFolderDialogProps, any> {
public state = {
open: false,
};
private show = () => {
this.setState(
(state) => ({...state, open: true}));
}
private hide = () => {
this.setState({open: false});
}
private handleSubmit = (newFolder: RepositoryFolder) => {
const { folder, ensureFolder, path } = this.props;
const absolutePath: string = folder.folder && folder.folder.path ? folder.folder.path : path;
ensureFolder(`/${absolutePath}/${newFolder.name}`).then(() => {
this.setState({open: false});
});
}
public render() {
const { classes, variant = 'raised', t} = this.props;
return (
<span>
<Button className={ classes.createButton } onClick={ this.show } variant={ variant }>{ t(`p.admin.repository.folder.createBtn`) }</Button>
{ this.state.open &&
<Dialog open={ this.state.open } onClose={ this.hide } maxWidth="xs" fullWidth>
<DialogTitle>{ t(`p.admin.repository.folder.createDialogTitle`) }</DialogTitle>
<DialogContent>
<FolderForm t={ t } onCancel={ this.hide } onSubmit={ this.handleSubmit }/>
</DialogContent>
</Dialog>
}
</span>
);
}
}
const mapStateToProps = (state) => ({
folder: state.repository.admin.folder,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
ensureFolder,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)<any>(translate()(CreateFolderDialog)));
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {translate} from 'react-i18next';
// Actions
import navigateTo from 'actions/navigation';
// UI
import Loading from 'ui/common/Loading';
import Card, { CardContent} from 'ui/common/Card';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';
import {PageContents} from 'ui/layout/PageLayout';
import FileForm from './c/FileForm';
import {getFile, updateFile} from 'repository/actions/admin';
import RepositoryFile from 'model/repository/RepositoryFile';
interface IEditPageProps extends React.ClassAttributes<any> {
t: any;
file: RepositoryFile;
fileUuid: string;
enableAccount: (uuid: string, enable?: boolean) => void;
lockAccount: (uuid: string, lock?: boolean) => void;
deleteAccount: (uuid: string) => void;
getFile: (uuid: string) => void;
navigateTo: (path: string) => void;
updateFile: (file: RepositoryFile) => void;
}
class EditFilePage extends React.Component<IEditPageProps, any> {
protected static needs = [
({ params: { uuid } }) => getFile(uuid),
];
constructor(props: IEditPageProps, context: any) {
super(props, context);
}
public componentWillMount() {
const {fileUuid, file, getFile} = this.props;
if (fileUuid && (!file || fileUuid !== file.uuid)) {
getFile(fileUuid);
}
}
private handleCancel = () => {
const { file, navigateTo } = this.props;
navigateTo(`/admin/repository/f${typeof file.folder === 'object' ? file.folder.path : file.folder}/`);
}
private handleSubmit = (updated: RepositoryFile) => {
this.props.updateFile(updated);
}
public render() {
const {file, t} = this.props;
return file === null ? (<Loading/>) : (
<div>
<ContentHeaderWithButton title={ t(`p.admin.repository.file.formTitle`) } />
<PageContents>
<Card>
<CardContent>
<FileForm t={ t } initialValues={ file } onCancel={ this.handleCancel } onSubmit={ this.handleSubmit }/>
</CardContent>
</Card>
</PageContents>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => ({
file: state.repository.admin.file,
fileUuid: ownProps.match.params.uuid,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
getFile,
updateFile,
navigateTo,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)((translate()(EditFilePage)));
......@@ -4,7 +4,7 @@ import { bindActionCreators } from 'redux';
import { withStyles } from '@material-ui/core/styles';
import { normalize } from 'path';
import { getFolder, uploadFile, deleteFolder, createGallery } from 'repository/actions/admin';
import { getFolder, uploadFile, deleteFolder, createGallery, removeFile } from 'repository/actions/admin';
import FolderDetails from 'model/repository/FolderDetails';
// import RepositoryFile from 'model/repository/RepositoryFile';
......@@ -23,6 +23,9 @@ import confirmAlert from 'utilities/confirmAlert';
import { FolderCrumbs } from './c/FolderCrumbs';
import Grid from '@material-ui/core/Grid';
import FileUploader from 'ui/common/file-uploader';
import CreateFolderDialog from './CreateFolderDialog';
import UpdateFolderDialog from './UpdateFolderDialog';
interface IRepositoryBrowserProps extends React.ClassAttributes<any> {
classes: any;
......@@ -35,6 +38,7 @@ interface IRepositoryBrowserProps extends React.ClassAttributes<any> {
getFolder: (path: string) => any;
uploadFile: (path: string, file: File) => any;
deleteFolder: (path: string) => any;
removeFile: (uuid: string) => any;
createGallery: (path: string, title: string, description?: string) => any;
navigateTo: (path: string, qs?: any) => any;
}
......@@ -71,13 +75,11 @@ class RepositoryBrowser extends React.Component<IRepositoryBrowserProps, any> {
}
}
protected upload = (e) => {
protected handleUploading = (files: File[]) => {
const { uploadFile, folderPath } = this.props;
const file = e.target.files[0];
uploadFile(folderPath, file);
e.target.files = null;
e.target.value = null;
for (const item of files) {
uploadFile(folderPath, item);
}
}
protected deleteFolder = (e) => {
......@@ -92,6 +94,14 @@ class RepositoryBrowser extends React.Component<IRepositoryBrowserProps, any> {
});
}
protected deleteFile = (uuid: string) => {
this.props.removeFile(uuid);
}
protected handleEditButton = (uuid: string) => {
this.props.navigateTo(`/admin/repository/edit/${uuid}`);
}
protected createGallery = (e) => {
const { createGallery, navigateTo, folder, folderPath } = this.props;
......@@ -127,8 +137,10 @@ class RepositoryBrowser extends React.Component<IRepositoryBrowserProps, any> {
<ContentHeaderWithButton title={ <FolderCrumbs root={ root } path={ path } /> } buttons={
<span>
{ ! folder.folder ? null :
folder.gallery ? <Button key="viewg" variant="raised" onClick={ this.goToGallery }>View gallery</Button> : <Button onClick={ this.createGallery } key="createg">Create gallery</Button> }
{ folder.folder && folder.folder._permissions.delete && <Button onClick={ this.deleteFolder } key="deletef">Delete folder</Button> }
folder.gallery ? <Button key="viewg" variant="raised" onClick={ this.goToGallery }>View gallery</Button> : <Button onClick={ this.createGallery } key="createg" variant="raised">Create gallery</Button> }
{ folder.folder && folder.folder._permissions.delete && <Button onClick={ this.deleteFolder } key="deletef" variant="raised">Delete folder</Button> }
{ (! folder.folder || (folder.folder && folder.folder._permissions.create)) && <CreateFolderDialog root={ root } path={ path } /> }
{ folder.folder && folder.folder._permissions.write && <UpdateFolderDialog/> }
{ folder.folder && folder.folder._permissions.manage && <Permissions clazz={ RepositoryFolder.clazz } id={ folder.folder.id } /> }
</span>
} />
......@@ -139,11 +151,11 @@ class RepositoryBrowser extends React.Component<IRepositoryBrowserProps, any> {
</div>
<Grid item>
<Grid container spacing={ 16 }>
{ folder.files.map((file) => <Grid key={ file.uuid } item xs={ 12 }><FileCard file={ file } /></Grid>) }
{ folder.files.map((file) => <Grid key={ file.uuid } item xs={ 12 }><FileCard file={ file } deleteFile={ this.deleteFile } editFile={ this. handleEditButton }/></Grid>) }
</Grid>
</Grid>
<div>
<input type="file" onChange={ this.upload } />
<FileUploader handleUploading={ this.handleUploading }/>
</div>
</PageContents>
</div>
......@@ -164,6 +176,7 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({
getFolder,
uploadFile,
deleteFolder,
removeFile,
createGallery,
navigateTo,
}, dispatch);
......
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {translate} from 'react-i18next';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import {updateFolder} from 'repository/actions/admin';
import RepositoryFolder from 'model/repository/RepositoryFolder';
import FolderDetails from 'model/repository/FolderDetails';
import FolderForm from './c/FolderForm';
import {withStyles} from '@material-ui/core/styles';
/*tslint:disable*/
const styles = (theme) => ({
createButton: {
[theme.breakpoints.down('sm')]: {
width: '100%',
margin: '8px 0',
},
},
});
/*tslint:enable*/
interface IUpdateFolderDialogProps extends React.ClassAttributes<any> {
updateFolder: (folder: RepositoryFolder) => Promise<FolderDetails>;
folder: FolderDetails;
classes: any;
variant?: 'text' | 'flat' | 'outlined' | 'contained' | 'raised' | 'fab' | 'extendedFab';
t: any;
}
class UpdateFolderDialog extends React.Component<IUpdateFolderDialogProps, any> {
public state = {
open: false,
};
private show = () => {
this.setState(
(state) => ({...state, open: true}));
}
private hide = () => {
this.setState({open: false});
}
private handleSubmit = (folder: RepositoryFolder) => {
const { updateFolder } = this.props;
updateFolder(folder).then(() => {
this.setState({open: false});
});
}
public render() {
const { t, classes, variant = 'raised', folder} = this.props;
return (
<span>
<Button className={ classes.createButton } onClick={ this.show } variant={ variant }>{ t(`p.admin.repository.folder.updateBtn`) }</Button>
{ this.state.open &&
<Dialog open={ this.state.open } onClose={ this.hide } maxWidth="md" fullWidth>
<DialogTitle>{ t(`p.admin.repository.folder.updateDialogTitle`) }</DialogTitle>
<DialogContent>
<FolderForm t={ t } initialValues={ folder.folder } edit onCancel={ this.hide } onSubmit={ this.handleSubmit }/>
</DialogContent>
</Dialog>