Add UI for multi root users

This commit is contained in:
viktorstrate
2020-12-27 22:32:46 +01:00
parent 96546f6556
commit 6e2773cc65
8 changed files with 457 additions and 301 deletions

View File

@@ -4,7 +4,7 @@ import styled from 'styled-components'
import Layout from '../../Layout' import Layout from '../../Layout'
import ScannerSection from './ScannerSection' import ScannerSection from './ScannerSection'
import UsersTable from './UsersTable' import UsersTable from './Users/UsersTable'
export const SectionTitle = styled.h2` export const SectionTitle = styled.h2`
margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important; margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important;

View File

@@ -1,298 +0,0 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import {
Button,
Checkbox,
Form,
Icon,
Input,
Modal,
Table,
} from 'semantic-ui-react'
const updateUserMutation = gql`
mutation updateUser(
$id: ID!
$username: String
$rootPath: String
$admin: Boolean
) {
updateUser(
id: $id
username: $username
rootPath: $rootPath
admin: $admin
) {
id
username
rootPath
admin
}
}
`
const deleteUserMutation = gql`
mutation deleteUser($id: ID!) {
deleteUser(id: $id) {
id
username
}
}
`
const changeUserPasswordMutation = gql`
mutation changeUserPassword($userId: ID!, $password: String!) {
updateUser(id: $userId, password: $password) {
id
}
}
`
const scanUserMutation = gql`
mutation scanUser($userId: ID!) {
scanUser(userId: $userId) {
success
}
}
`
const ChangePasswordModal = ({ onClose, user, ...props }) => {
const [passwordInput, setPasswordInput] = useState('')
const [changePassword] = useMutation(changeUserPasswordMutation, {
onCompleted: () => {
onClose && onClose()
},
})
return (
<Modal {...props}>
<Modal.Header>Change password</Modal.Header>
<Modal.Content>
<p>
Change password for <b>{user.username}</b>
</p>
<Form>
<Form.Field>
<label>New password</label>
<Input
placeholder="password"
onChange={e => setPasswordInput(e.target.value)}
type="password"
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => onClose && onClose()}>Cancel</Button>
<Button
positive
onClick={() => {
changePassword({
variables: {
userId: user.id,
password: passwordInput,
},
})
}}
>
Change password
</Button>
</Modal.Actions>
</Modal>
)
}
ChangePasswordModal.propTypes = {
onClose: PropTypes.func,
user: PropTypes.object.isRequired,
}
const UserRow = ({ user, refetchUsers }) => {
const [state, setState] = useState({
...user,
editing: false,
})
const [showConfirmDelete, setConfirmDelete] = useState(false)
const [showChangePassword, setChangePassword] = useState(false)
function updateInput(event, key) {
setState({
...state,
[key]: event.target.value,
})
}
const [updateUser, { loading: updateUserLoading }] = useMutation(
updateUserMutation,
{
onCompleted: data => {
setState({
...data.updateUser,
editing: false,
})
refetchUsers()
},
}
)
const [deleteUser] = useMutation(deleteUserMutation, {
onCompleted: () => {
refetchUsers()
},
})
const [scanUser, { called: scanUserCalled }] = useMutation(scanUserMutation, {
onCompleted: () => {
refetchUsers()
},
})
if (state.editing) {
return (
<Table.Row>
<Table.Cell>
<Input
style={{ width: '100%' }}
placeholder={user.username}
value={state.username}
onChange={e => updateInput(e, 'username')}
/>
</Table.Cell>
<Table.Cell>
<Input
style={{ width: '100%' }}
placeholder={user.rootPath}
value={state.rootPath}
onChange={e => updateInput(e, 'rootPath')}
/>
</Table.Cell>
<Table.Cell>
<Checkbox
toggle
checked={state.admin}
onChange={(_, data) => {
setState({
...state,
admin: data.checked,
})
}}
/>
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button
negative
onClick={() =>
setState({
...state.oldState,
})
}
>
Cancel
</Button>
<Button
loading={updateUserLoading}
disabled={updateUserLoading}
positive
onClick={() =>
updateUser({
variables: {
id: user.id,
username: state.username,
rootPath: state.rootPath,
admin: state.admin,
},
})
}
>
Save
</Button>
</Button.Group>
</Table.Cell>
</Table.Row>
)
}
return (
<Table.Row>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{user.rootPath}</Table.Cell>
<Table.Cell>
{user.admin ? <Icon name="checkmark" size="large" /> : null}
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button
onClick={() => {
setState({ ...state, editing: true, oldState: state })
}}
>
<Icon name="edit" />
Edit
</Button>
<Button
disabled={scanUserCalled}
onClick={() => scanUser({ variables: { userId: user.id } })}
>
<Icon name="sync" />
Scan
</Button>
<Button onClick={() => setChangePassword(true)}>
<Icon name="key" />
Change password
</Button>
<ChangePasswordModal
user={user}
open={showChangePassword}
onClose={() => setChangePassword(false)}
/>
<Button
negative
onClick={() => {
setConfirmDelete(true)
}}
>
<Icon name="delete" />
Delete
</Button>
<Modal open={showConfirmDelete}>
<Modal.Header>Delete user</Modal.Header>
<Modal.Content>
<p>
{`Are you sure, you want to delete `}
<b>{user.username}</b>?
</p>
<p>{`This action cannot be undone`}</p>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setConfirmDelete(false)}>Cancel</Button>
<Button
negative
onClick={() => {
setConfirmDelete(false)
deleteUser({
variables: {
id: user.id,
},
})
}}
>
Delete {user.username}
</Button>
</Modal.Actions>
</Modal>
</Button.Group>
</Table.Cell>
</Table.Row>
)
}
UserRow.propTypes = {
user: PropTypes.object.isRequired,
refetchUsers: PropTypes.func.isRequired,
}
export default UserRow

View File

@@ -0,0 +1,158 @@
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { Button, Checkbox, Icon, Input, Table } from 'semantic-ui-react'
import { UserRowProps } from './UserRow'
const RootPathListItem = styled.li`
display: flex;
justify-content: space-between;
align-items: center;
`
const EditRootPath = ({ filePath, removePath }) => (
<RootPathListItem>
<span>{filePath}</span>
<Button negative onClick={() => removePath()}>
<Icon name="remove" />
Remove
</Button>
</RootPathListItem>
)
EditRootPath.propTypes = {
filePath: PropTypes.string.isRequired,
removePath: PropTypes.func.isRequired,
}
const NewRootPathInput = styled(Input)`
width: 100%;
margin-top: 24px;
`
const EditNewRootPath = ({ state, updateInput }) => (
<li>
<NewRootPathInput
style={{ width: '100%' }}
value={state.rootPath}
onChange={e => updateInput(e, 'rootPath')}
action={{
positive: true,
icon: 'add',
content: 'Add',
}}
/>
</li>
)
EditNewRootPath.propTypes = {
state: PropTypes.object.isRequired,
updateInput: PropTypes.func.isRequired,
}
const RootPathList = styled.ul`
margin: 0;
padding: 0;
list-style: none;
`
const EditRootPaths = ({ user, state, updateInput }) => {
const editRows = user.rootAlbums.map(album => (
<EditRootPath
key={album.id}
filePath={album.filePath}
removePath={() => {}}
/>
))
return (
<RootPathList>
{editRows}
<EditNewRootPath state={state} updateInput={updateInput} />
</RootPathList>
)
}
EditRootPaths.propTypes = {
updateInput: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
state: PropTypes.object.isRequired,
}
const EditUserRow = ({
user,
state,
setState,
updateUser,
updateUserLoading,
}) => {
function updateInput(event, key) {
setState(state => ({
...state,
[key]: event.target.value,
}))
}
return (
<Table.Row>
<Table.Cell>
<Input
style={{ width: '100%' }}
placeholder={user.username}
value={state.username}
onChange={e => updateInput(e, 'username')}
/>
</Table.Cell>
<Table.Cell>
<EditRootPaths user={user} state={state} updateInput={updateInput} />
</Table.Cell>
<Table.Cell>
<Checkbox
toggle
checked={state.admin}
onChange={(_, data) => {
setState({
...state,
admin: data.checked,
})
}}
/>
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button
negative
onClick={() =>
setState({
...state.oldState,
})
}
>
Cancel
</Button>
<Button
loading={updateUserLoading}
disabled={updateUserLoading}
positive
onClick={() =>
updateUser({
variables: {
id: user.id,
username: state.username,
rootPath: state.rootPath,
admin: state.admin,
},
})
}
>
Save
</Button>
</Button.Group>
</Table.Cell>
</Table.Row>
)
}
EditUserRow.propTypes = UserRowProps
export default EditUserRow

View File

@@ -0,0 +1,66 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { gql, useMutation } from '@apollo/client'
import { Button, Form, Input, Modal } from 'semantic-ui-react'
const changeUserPasswordMutation = gql`
mutation changeUserPassword($userId: ID!, $password: String!) {
updateUser(id: $userId, password: $password) {
id
}
}
`
const ChangePasswordModal = ({ onClose, user, ...props }) => {
const [passwordInput, setPasswordInput] = useState('')
const [changePassword] = useMutation(changeUserPasswordMutation, {
onCompleted: () => {
onClose && onClose()
},
})
return (
<Modal {...props}>
<Modal.Header>Change password</Modal.Header>
<Modal.Content>
<p>
Change password for <b>{user.username}</b>
</p>
<Form>
<Form.Field>
<label>New password</label>
<Input
placeholder="password"
onChange={e => setPasswordInput(e.target.value)}
type="password"
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => onClose && onClose()}>Cancel</Button>
<Button
positive
onClick={() => {
changePassword({
variables: {
userId: user.id,
password: passwordInput,
},
})
}}
>
Change password
</Button>
</Modal.Actions>
</Modal>
)
}
ChangePasswordModal.propTypes = {
onClose: PropTypes.func,
user: PropTypes.object.isRequired,
}
export default ChangePasswordModal

View File

@@ -0,0 +1,121 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import EditUserRow from './EditUserRow'
import ViewUserRow from './ViewUserRow'
const updateUserMutation = gql`
mutation updateUser(
$id: ID!
$username: String
$rootPath: String
$admin: Boolean
) {
updateUser(
id: $id
username: $username
rootPath: $rootPath
admin: $admin
) {
id
username
rootPath
admin
}
}
`
const deleteUserMutation = gql`
mutation deleteUser($id: ID!) {
deleteUser(id: $id) {
id
username
}
}
`
const scanUserMutation = gql`
mutation scanUser($userId: ID!) {
scanUser(userId: $userId) {
success
}
}
`
const UserRow = ({ user, refetchUsers }) => {
const [state, setState] = useState({
...user,
editing: false,
})
const [showConfirmDelete, setConfirmDelete] = useState(false)
const [showChangePassword, setChangePassword] = useState(false)
const [updateUser, { loading: updateUserLoading }] = useMutation(
updateUserMutation,
{
onCompleted: data => {
setState({
...data.updateUser,
editing: false,
})
refetchUsers()
},
}
)
const [deleteUser] = useMutation(deleteUserMutation, {
onCompleted: () => {
refetchUsers()
},
})
const [scanUser, { called: scanUserCalled }] = useMutation(scanUserMutation, {
onCompleted: () => {
refetchUsers()
},
})
const props = {
user,
state,
setState,
scanUser,
updateUser,
updateUserLoading,
deleteUser,
setChangePassword,
setConfirmDelete,
scanUserCalled,
showChangePassword,
showConfirmDelete,
}
if (state.editing) {
return <EditUserRow {...props} />
}
return <ViewUserRow {...props} />
}
UserRow.propTypes = {
user: PropTypes.object.isRequired,
refetchUsers: PropTypes.func.isRequired,
}
export const UserRowProps = {
user: PropTypes.object.isRequired,
state: PropTypes.object.isRequired,
setState: PropTypes.func.isRequired,
scanUser: PropTypes.func.isRequired,
updateUser: PropTypes.func.isRequired,
updateUserLoading: PropTypes.bool.isRequired,
deleteUser: PropTypes.func.isRequired,
setChangePassword: PropTypes.func.isRequired,
setConfirmDelete: PropTypes.func.isRequired,
scanUserCalled: PropTypes.func.isRequired,
showChangePassword: PropTypes.func.isRequired,
showConfirmDelete: PropTypes.func.isRequired,
}
export default UserRow

View File

@@ -4,7 +4,7 @@ import { Table, Loader, Button, Icon } from 'semantic-ui-react'
import { useQuery, gql } from '@apollo/client' import { useQuery, gql } from '@apollo/client'
import UserRow from './UserRow' import UserRow from './UserRow'
import AddUserRow from './AddUserRow' import AddUserRow from './AddUserRow'
import { SectionTitle } from './SettingsPage' import { SectionTitle } from '../SettingsPage'
const USERS_QUERY = gql` const USERS_QUERY = gql`
query settingsUsersQuery { query settingsUsersQuery {
@@ -13,7 +13,7 @@ const USERS_QUERY = gql`
username username
# rootPath # rootPath
admin admin
albums { rootAlbums {
id id
filePath filePath
} }

View File

@@ -0,0 +1,109 @@
import React from 'react'
import { Button, Icon, Table, Modal } from 'semantic-ui-react'
import styled from 'styled-components'
import ChangePasswordModal from './UserChangePassword'
import { UserRowProps } from './UserRow'
const PathList = styled.ul`
margin: 0;
padding: 0 0 0 12px;
list-style: none;
`
const ViewUserRow = ({
user,
state,
setState,
scanUser,
deleteUser,
setChangePassword,
setConfirmDelete,
scanUserCalled,
showChangePassword,
showConfirmDelete,
}) => {
const paths = (
<PathList>
{user.rootAlbums.map(album => (
<li key={album.id}>{album.filePath}</li>
))}
</PathList>
)
return (
<Table.Row>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{paths}</Table.Cell>
<Table.Cell>
{user.admin ? <Icon name="checkmark" size="large" /> : null}
</Table.Cell>
<Table.Cell>
<Button.Group>
<Button
onClick={() => {
setState({ ...state, editing: true, oldState: state })
}}
>
<Icon name="edit" />
Edit
</Button>
<Button
disabled={scanUserCalled}
onClick={() => scanUser({ variables: { userId: user.id } })}
>
<Icon name="sync" />
Scan
</Button>
<Button onClick={() => setChangePassword(true)}>
<Icon name="key" />
Change password
</Button>
<ChangePasswordModal
user={user}
open={showChangePassword}
onClose={() => setChangePassword(false)}
/>
<Button
negative
onClick={() => {
setConfirmDelete(true)
}}
>
<Icon name="delete" />
Delete
</Button>
<Modal open={showConfirmDelete}>
<Modal.Header>Delete user</Modal.Header>
<Modal.Content>
<p>
{`Are you sure, you want to delete `}
<b>{user.username}</b>?
</p>
<p>{`This action cannot be undone`}</p>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setConfirmDelete(false)}>Cancel</Button>
<Button
negative
onClick={() => {
setConfirmDelete(false)
deleteUser({
variables: {
id: user.id,
},
})
}}
>
Delete {user.username}
</Button>
</Modal.Actions>
</Modal>
</Button.Group>
</Table.Cell>
</Table.Row>
)
}
ViewUserRow.propTypes = UserRowProps
export default ViewUserRow