mirror of
https://github.com/netzbegruenung/groupfolders.git
synced 2024-05-03 09:53:41 +02:00
allow changing mountpoint of existing group folders
Signed-off-by: Robin Appelman <robin@icewind.nl>
This commit is contained in:
parent
958d647459
commit
6c78ef13d5
|
@ -44,6 +44,11 @@ $application->registerRoutes(
|
|||
'name' => 'Folder#setQuota',
|
||||
'url' => '/folders/{id}/quota',
|
||||
'verb' => 'POST'
|
||||
],
|
||||
[
|
||||
'name' => 'Folder#renameFolder',
|
||||
'url' => '/folders/{id}/mountpoint',
|
||||
'verb' => 'POST'
|
||||
]
|
||||
],
|
||||
]
|
||||
|
|
|
@ -63,4 +63,10 @@ export class Api {
|
|||
quota
|
||||
});
|
||||
}
|
||||
|
||||
renameFolder(folderId: number, mountpoint: string): Thenable<void> {
|
||||
return $.post(this.getUrl(`folders/${folderId}/mountpoint`), {
|
||||
mountpoint
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
57
js/App.css
57
js/App.css
|
@ -21,32 +21,73 @@
|
|||
position: relative;
|
||||
display: table-cell;
|
||||
|
||||
&.mountpoint {
|
||||
& > form {
|
||||
margin: -10px;
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.action-rename {
|
||||
&::after {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url('../../../core/img/actions/rename.svg') no-repeat center;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.groups {
|
||||
width: 400px;
|
||||
|
||||
a.icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.remove {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
a.icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
.icon {
|
||||
opacity: 1;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.action-rename {
|
||||
&::after {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.5s;
|
||||
opacity: 0;
|
||||
//transition: opacity 0.5s;
|
||||
|
||||
&.icon-visible {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.newgroup-name {
|
||||
|
|
59
js/App.tsx
59
js/App.tsx
|
@ -1,11 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import {Component} from 'react';
|
||||
import {ChangeEvent, Component} from 'react';
|
||||
|
||||
import {Api, Folder} from './Api';
|
||||
import {FolderGroups} from './FolderGroups';
|
||||
import {QuotaSelect} from './QuotaSelect';
|
||||
|
||||
import './App.css';
|
||||
import {SubmitInput} from "./SubmitInput";
|
||||
|
||||
const defaultQuotaOptions = {
|
||||
'1 GB': 1073741274,
|
||||
|
@ -18,7 +19,9 @@ export interface AppState {
|
|||
folders: Folder[];
|
||||
groups: string[],
|
||||
newMountPoint: string;
|
||||
editing: number;
|
||||
editingGroup: number;
|
||||
editingMountPoint: number;
|
||||
renameMountPoint: string;
|
||||
}
|
||||
|
||||
export class App extends Component<{}, AppState> {
|
||||
|
@ -28,7 +31,9 @@ export class App extends Component<{}, AppState> {
|
|||
folders: [],
|
||||
groups: [],
|
||||
newMountPoint: '',
|
||||
editing: 0
|
||||
editingGroup: 0,
|
||||
editingMountPoint: 0,
|
||||
renameMountPoint: ''
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -58,7 +63,7 @@ export class App extends Component<{}, AppState> {
|
|||
});
|
||||
};
|
||||
|
||||
deleteFolder(id) {
|
||||
deleteFolder(id: number) {
|
||||
const folderName = this.state.folders[id].mount_point;
|
||||
OC.dialogs.confirm(
|
||||
t('groupfolders', 'Are you sure you want to delete "{folderName}" and all files inside. This operation can not be undone', {folderName}),
|
||||
|
@ -75,46 +80,74 @@ export class App extends Component<{}, AppState> {
|
|||
);
|
||||
};
|
||||
|
||||
addGroup(folderId, group) {
|
||||
addGroup(folderId: number, group: string) {
|
||||
const folders = this.state.folders;
|
||||
folders[folderId].groups[group] = OC.PERMISSION_ALL;
|
||||
this.setState({folders});
|
||||
this.api.addGroup(folderId, group);
|
||||
}
|
||||
|
||||
removeGroup(folderId, group) {
|
||||
removeGroup(folderId: number, group: string) {
|
||||
const folders = this.state.folders;
|
||||
delete folders[folderId].groups[group];
|
||||
this.setState({folders});
|
||||
this.api.removeGroup(folderId, group);
|
||||
}
|
||||
|
||||
setPermissions(folderId, group, newPermissions) {
|
||||
setPermissions(folderId: number, group: string, newPermissions: number) {
|
||||
const folders = this.state.folders;
|
||||
folders[folderId].groups[group] = newPermissions;
|
||||
this.setState({folders});
|
||||
this.api.setPermissions(folderId, group, newPermissions);
|
||||
}
|
||||
|
||||
setQuota(folderId, quota) {
|
||||
setQuota(folderId: number, quota: number) {
|
||||
const folders = this.state.folders;
|
||||
folders[folderId].quota = quota;
|
||||
this.setState({folders});
|
||||
this.api.setQuota(folderId, quota);
|
||||
}
|
||||
|
||||
renameFolder(folderId: number, newName: string) {
|
||||
const folders = this.state.folders;
|
||||
folders[folderId].mount_point = newName;
|
||||
// this.api.setQuota(folderId, quota);
|
||||
this.setState({folders, editingMountPoint: 0});
|
||||
this.api.renameFolder(folderId, newName);
|
||||
}
|
||||
|
||||
render() {
|
||||
const rows = Object.keys(this.state.folders).map((key) => {
|
||||
const id = parseInt(key, 10);
|
||||
const row = this.state.folders[id];
|
||||
return <tr key={id}>
|
||||
<td className="mountpoint">{row.mount_point}</td>
|
||||
<td className="mountpoint">
|
||||
{this.state.editingMountPoint === id ?
|
||||
<SubmitInput
|
||||
autoFocus={true}
|
||||
onSubmitValue={this.renameFolder.bind(this, id)}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
initialValue={row.mount_point}
|
||||
/> :
|
||||
<a
|
||||
className="action-rename"
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
this.setState({editingMountPoint: id})
|
||||
}}
|
||||
>
|
||||
{row.mount_point}
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
<td className="groups">
|
||||
<FolderGroups
|
||||
edit={this.state.editing === id}
|
||||
edit={this.state.editingGroup === id}
|
||||
showEdit={event => {
|
||||
event.stopPropagation();
|
||||
this.setState({editing: id})
|
||||
this.setState({editingGroup: id})
|
||||
}}
|
||||
groups={row.groups}
|
||||
allGroups={this.state.groups}
|
||||
|
@ -130,7 +163,7 @@ export class App extends Component<{}, AppState> {
|
|||
onChange={this.setQuota.bind(this, id)}/>
|
||||
</td>
|
||||
<td className="remove">
|
||||
<a className="icon icon-delete"
|
||||
<a className="icon icon-delete icon-visible"
|
||||
onClick={this.deleteFolder.bind(this, id)}
|
||||
title={t('groupfolders', 'Delete')}/>
|
||||
</td>
|
||||
|
@ -139,7 +172,7 @@ export class App extends Component<{}, AppState> {
|
|||
|
||||
return <div id="groupfolders-react-root"
|
||||
onClick={() => {
|
||||
this.setState({editing: 0})
|
||||
this.setState({editingGroup: 0, editingMountPoint: 0})
|
||||
}}>
|
||||
<table>
|
||||
<thead>
|
||||
|
|
40
js/SubmitInput.tsx
Normal file
40
js/SubmitInput.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Component, InputHTMLAttributes,
|
||||
SyntheticEvent
|
||||
} from 'react';
|
||||
|
||||
export interface SubmitInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
initialValue?: string;
|
||||
onSubmitValue: (value: string) => void;
|
||||
}
|
||||
|
||||
export interface SubmitInputState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class SubmitInput extends Component<SubmitInputProps, SubmitInputState> {
|
||||
state: SubmitInputState = {
|
||||
value: ''
|
||||
};
|
||||
|
||||
constructor(props: SubmitInputProps) {
|
||||
super(props);
|
||||
this.state.value = props.initialValue || '';
|
||||
}
|
||||
|
||||
onSubmit = (event: SyntheticEvent<any>) => {
|
||||
event.preventDefault();
|
||||
this.props.onSubmitValue(this.state.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {initialValue, onSubmitValue, ...props} = this.props;
|
||||
|
||||
return <form onSubmit={this.onSubmit}>
|
||||
<input type="text" value={this.state.value}
|
||||
{...props}
|
||||
onChange={event => this.setState({value: event.currentTarget.value})}/>
|
||||
</form>;
|
||||
}
|
||||
}
|
|
@ -113,4 +113,12 @@ class FolderController extends Controller {
|
|||
public function setQuota($id, $quota) {
|
||||
$this->manager->setFolderQuota($id, $quota);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param string $mountpoint
|
||||
*/
|
||||
public function renameFolder($id, $mountpoint) {
|
||||
$this->manager->renameFolder($id, $mountpoint);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,4 +172,13 @@ class FolderManager {
|
|||
->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)));
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
public function renameFolder($folderId, $newMountPoint) {
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
$query->update('group_folders')
|
||||
->set('mount_point', $query->createNamedParameter($newMountPoint))
|
||||
->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)));
|
||||
$query->execute();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"style-loader": "^0.16.1",
|
||||
"ts-loader": "^2.3.2",
|
||||
"typescript": "^2.4.2",
|
||||
"url-loader": "^0.5.9",
|
||||
"webpack": "^2.3.3",
|
||||
"webpack-dev-server": "^2.4.2"
|
||||
},
|
||||
|
|
|
@ -135,4 +135,16 @@ class FolderManagerTest extends TestCase {
|
|||
['mount_point' => 'bar', 'groups' => []]
|
||||
]);
|
||||
}
|
||||
|
||||
public function testRenameFolder() {
|
||||
$folderId1 = $this->manager->createFolder('foo');
|
||||
$this->manager->createFolder('other');
|
||||
|
||||
$this->manager->renameFolder($folderId1, 'bar');
|
||||
|
||||
$this->assertHasFolders([
|
||||
['mount_point' => 'bar', 'groups' => []],
|
||||
['mount_point' => 'other', 'groups' => []],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,12 @@ module.exports = {
|
|||
test: /\.tsx?$/,
|
||||
use: ['react-hot-loader/webpack', 'ts-loader']
|
||||
},
|
||||
{
|
||||
test: /.*\.(gif|png|jpe?g|svg|webp)(\?.+)?$/i,
|
||||
use: [
|
||||
'url-loader?limit=5000&hash=sha512&digest=hex&name=[hash].[ext]'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ['react-hot-loader/webpack', 'babel-loader']
|
||||
|
|
|
@ -44,6 +44,12 @@ module.exports = {
|
|||
test: /\.tsx?$/,
|
||||
use: ['ts-loader']
|
||||
},
|
||||
{
|
||||
test: /.*\.(gif|png|jpe?g|svg|webp)(\?.+)?$/i,
|
||||
use: [
|
||||
'url-loader?limit=5000&hash=sha512&digest=hex&name=[hash].[ext]'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ['babel-loader']
|
||||
|
|
Loading…
Reference in a new issue