1
Fork 0

Hook up UI to add and remove multiple root paths for each user

This commit is contained in:
viktorstrate 2020-12-30 18:36:26 +01:00
parent 6e2773cc65
commit c198e68daf
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
8 changed files with 387 additions and 86 deletions

View File

@ -129,6 +129,8 @@ type ComplexityRoot struct {
ShareAlbum func(childComplexity int, albumID int, expire *time.Time, password *string) int
ShareMedia func(childComplexity int, mediaID int, expire *time.Time, password *string) int
UpdateUser func(childComplexity int, id int, username *string, password *string, admin *bool) int
UserAddRootPath func(childComplexity int, id int, rootPath string) int
UserRemoveRootAlbum func(childComplexity int, userID int, albumID int) int
}
Notification struct {
@ -246,6 +248,8 @@ type MutationResolver interface {
UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error)
CreateUser(ctx context.Context, username string, password *string, admin bool) (*models.User, error)
DeleteUser(ctx context.Context, id int) (*models.User, error)
UserAddRootPath(ctx context.Context, id int, rootPath string) (*models.Album, error)
UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error)
SetPeriodicScanInterval(ctx context.Context, interval int) (int, error)
SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error)
}
@ -771,6 +775,30 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.UpdateUser(childComplexity, args["id"].(int), args["username"].(*string), args["password"].(*string), args["admin"].(*bool)), true
case "Mutation.userAddRootPath":
if e.complexity.Mutation.UserAddRootPath == nil {
break
}
args, err := ec.field_Mutation_userAddRootPath_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UserAddRootPath(childComplexity, args["id"].(int), args["rootPath"].(string)), true
case "Mutation.userRemoveRootAlbum":
if e.complexity.Mutation.UserRemoveRootAlbum == nil {
break
}
args, err := ec.field_Mutation_userRemoveRootAlbum_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UserRemoveRootAlbum(childComplexity, args["userId"].(int), args["albumId"].(int)), true
case "Notification.content":
if e.complexity.Notification.Content == nil {
break
@ -1372,6 +1400,10 @@ type Mutation {
): User @isAdmin
deleteUser(id: ID!): User @isAdmin
"Add a root path from where to look for media for the given user"
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
userRemoveRootAlbum(userId: ID!, albumId: ID!): Album @isAdmin
"""
Set how often, in seconds, the server should automatically scan for new media,
a value of 0 will disable periodic scans
@ -1935,6 +1967,54 @@ func (ec *executionContext) field_Mutation_updateUser_args(ctx context.Context,
return args, nil
}
func (ec *executionContext) field_Mutation_userAddRootPath_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["id"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
arg0, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["id"] = arg0
var arg1 string
if tmp, ok := rawArgs["rootPath"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rootPath"))
arg1, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["rootPath"] = arg1
return args, nil
}
func (ec *executionContext) field_Mutation_userRemoveRootAlbum_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["userId"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("userId"))
arg0, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["userId"] = arg0
var arg1 int
if tmp, ok := rawArgs["albumId"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("albumId"))
arg1, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["albumId"] = arg1
return args, nil
}
func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -4236,6 +4316,124 @@ func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field grap
return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_userAddRootPath(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_userAddRootPath_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UserAddRootPath(rctx, args["id"].(int), args["rootPath"].(string))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
return nil, errors.New("directive isAdmin is not implemented")
}
return ec.directives.IsAdmin(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*models.Album); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.Album`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*models.Album)
fc.Result = res
return ec.marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_userRemoveRootAlbum(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_userRemoveRootAlbum_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UserRemoveRootAlbum(rctx, args["userId"].(int), args["albumId"].(int))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
return nil, errors.New("directive isAdmin is not implemented")
}
return ec.directives.IsAdmin(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*models.Album); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.Album`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*models.Album)
fc.Result = res
return ec.marshalOAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_setPeriodicScanInterval(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -8008,6 +8206,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
out.Values[i] = ec._Mutation_createUser(ctx, field)
case "deleteUser":
out.Values[i] = ec._Mutation_deleteUser(ctx, field)
case "userAddRootPath":
out.Values[i] = ec._Mutation_userAddRootPath(ctx, field)
case "userRemoveRootAlbum":
out.Values[i] = ec._Mutation_userRemoveRootAlbum(ctx, field)
case "setPeriodicScanInterval":
out.Values[i] = ec._Mutation_setPeriodicScanInterval(ctx, field)
if out.Values[i] == graphql.Null {

View File

@ -217,3 +217,39 @@ func (r *mutationResolver) DeleteUser(ctx context.Context, id int) (*models.User
return &user, nil
}
func (r *mutationResolver) UserAddRootPath(ctx context.Context, id int, rootPath string) (*models.Album, error) {
var user models.User
if err := r.Database.First(&user, id).Error; err != nil {
return nil, err
}
// TODO: Check if path exists and that user does not already own rootPath, directly or indirectly
newAlbum, err := scanner.NewRootAlbum(r.Database, rootPath, &user)
if err != nil {
return nil, err
}
return newAlbum, nil
}
func (r *mutationResolver) UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error) {
var album models.Album
if err := r.Database.First(&album, albumID).Error; err != nil {
return nil, err
}
result := r.Database.Exec("DELETE FROM user_albums WHERE album_id = ? AND user_id = ?", albumID, userID)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, errors.New("No relation deleted")
}
return &album, nil
}

View File

@ -95,6 +95,10 @@ type Mutation {
): User @isAdmin
deleteUser(id: ID!): User @isAdmin
"Add a root path from where to look for media for the given user"
userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin
userRemoveRootAlbum(userId: ID!, albumId: ID!): Album @isAdmin
"""
Set how often, in seconds, the server should automatically scan for new media,
a value of 0 will disable periodic scans

View File

@ -1,84 +1,8 @@
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 { Button, Checkbox, Input, Table } from 'semantic-ui-react'
import { EditRootPaths } from './EditUserRowRootPaths'
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,
@ -104,17 +28,17 @@ const EditUserRow = ({
/>
</Table.Cell>
<Table.Cell>
<EditRootPaths user={user} state={state} updateInput={updateInput} />
<EditRootPaths user={user} />
</Table.Cell>
<Table.Cell>
<Checkbox
toggle
checked={state.admin}
onChange={(_, data) => {
setState({
setState(state => ({
...state,
admin: data.checked,
})
}))
}}
/>
</Table.Cell>
@ -123,9 +47,9 @@ const EditUserRow = ({
<Button
negative
onClick={() =>
setState({
setState(state => ({
...state.oldState,
})
}))
}
>
Cancel

View File

@ -0,0 +1,134 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { gql, useMutation } from '@apollo/client'
import { Button, Icon, Input } from 'semantic-ui-react'
import styled from 'styled-components'
import { USERS_QUERY } from './UsersTable'
const userAddRootPathMutation = gql`
mutation userAddRootPath($id: ID!, $rootPath: String!) {
userAddRootPath(id: $id, rootPath: $rootPath) {
id
}
}
`
const userRemoveAlbumPathMutation = gql`
mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) {
userRemoveRootAlbum(userId: $userId, albumId: $albumId) {
id
}
}
`
const RootPathListItem = styled.li`
display: flex;
justify-content: space-between;
align-items: center;
`
const EditRootPath = ({ album, user }) => {
const [removeAlbumPath, { loading }] = useMutation(
userRemoveAlbumPathMutation,
{
refetchQueries: [
{
query: USERS_QUERY,
},
],
}
)
return (
<RootPathListItem>
<span>{album.filePath}</span>
<Button
negative
disabled={loading}
onClick={() =>
removeAlbumPath({
variables: {
userId: user.id,
albumId: album.id,
},
})
}
>
<Icon name="remove" />
Remove
</Button>
</RootPathListItem>
)
}
EditRootPath.propTypes = {
album: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
}
const NewRootPathInput = styled(Input)`
width: 100%;
margin-top: 24px;
`
const EditNewRootPath = ({ userID }) => {
const [value, setValue] = useState('')
const [addRootPath, { loading }] = useMutation(userAddRootPathMutation, {
refetchQueries: [
{
query: USERS_QUERY,
},
],
})
return (
<li>
<NewRootPathInput
style={{ width: '100%' }}
value={value}
onChange={e => setValue(e.target.value)}
disabled={loading}
action={{
positive: true,
icon: 'add',
content: 'Add',
onClick: () => {
addRootPath({
variables: {
id: userID,
rootPath: value,
},
})
},
}}
/>
</li>
)
}
EditNewRootPath.propTypes = {
userID: PropTypes.string.isRequired,
}
const RootPathList = styled.ul`
margin: 0;
padding: 0;
list-style: none;
`
export const EditRootPaths = ({ user }) => {
const editRows = user.rootAlbums.map(album => (
<EditRootPath key={album.id} album={album} user={user} />
))
return (
<RootPathList>
{editRows}
<EditNewRootPath userID={user.id} />
</RootPathList>
)
}
EditRootPaths.propTypes = {
user: PropTypes.object.isRequired,
}

View File

@ -46,6 +46,7 @@ const UserRow = ({ user, refetchUsers }) => {
const [state, setState] = useState({
...user,
editing: false,
newRootPath: '',
})
const [showConfirmDelete, setConfirmDelete] = useState(false)

View File

@ -6,7 +6,7 @@ import UserRow from './UserRow'
import AddUserRow from './AddUserRow'
import { SectionTitle } from '../SettingsPage'
const USERS_QUERY = gql`
export const USERS_QUERY = gql`
query settingsUsersQuery {
user {
id

View File

@ -12,7 +12,7 @@ const PathList = styled.ul`
const ViewUserRow = ({
user,
state,
// state,
setState,
scanUser,
deleteUser,
@ -41,7 +41,7 @@ const ViewUserRow = ({
<Button.Group>
<Button
onClick={() => {
setState({ ...state, editing: true, oldState: state })
setState(state => ({ ...state, editing: true, oldState: state }))
}}
>
<Icon name="edit" />