diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 32128744..9d7744ae 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -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 { diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go index 65029888..ceb378eb 100644 --- a/api/graphql/resolvers/user.go +++ b/api/graphql/resolvers/user.go @@ -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 +} diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index f4dcf526..c606cd64 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -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 diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRow.js b/ui/src/Pages/SettingsPage/Users/EditUserRow.js index ac21e939..7865f629 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRow.js @@ -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 }) => ( - - {filePath} - - -) - -EditRootPath.propTypes = { - filePath: PropTypes.string.isRequired, - removePath: PropTypes.func.isRequired, -} - -const NewRootPathInput = styled(Input)` - width: 100%; - margin-top: 24px; -` - -const EditNewRootPath = ({ state, updateInput }) => ( -
  • - updateInput(e, 'rootPath')} - action={{ - positive: true, - icon: 'add', - content: 'Add', - }} - /> -
  • -) - -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 => ( - {}} - /> - )) - - return ( - - {editRows} - - - ) -} - -EditRootPaths.propTypes = { - updateInput: PropTypes.func.isRequired, - user: PropTypes.object.isRequired, - state: PropTypes.object.isRequired, -} - const EditUserRow = ({ user, state, @@ -104,17 +28,17 @@ const EditUserRow = ({ /> - + { - setState({ + setState(state => ({ ...state, admin: data.checked, - }) + })) }} /> @@ -123,9 +47,9 @@ const EditUserRow = ({ + + ) +} + +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 ( +
  • + setValue(e.target.value)} + disabled={loading} + action={{ + positive: true, + icon: 'add', + content: 'Add', + onClick: () => { + addRootPath({ + variables: { + id: userID, + rootPath: value, + }, + }) + }, + }} + /> +
  • + ) +} + +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 => ( + + )) + + return ( + + {editRows} + + + ) +} + +EditRootPaths.propTypes = { + user: PropTypes.object.isRequired, +} diff --git a/ui/src/Pages/SettingsPage/Users/UserRow.js b/ui/src/Pages/SettingsPage/Users/UserRow.js index 7a19b14d..427defe4 100644 --- a/ui/src/Pages/SettingsPage/Users/UserRow.js +++ b/ui/src/Pages/SettingsPage/Users/UserRow.js @@ -46,6 +46,7 @@ const UserRow = ({ user, refetchUsers }) => { const [state, setState] = useState({ ...user, editing: false, + newRootPath: '', }) const [showConfirmDelete, setConfirmDelete] = useState(false) diff --git a/ui/src/Pages/SettingsPage/Users/UsersTable.js b/ui/src/Pages/SettingsPage/Users/UsersTable.js index fa628d99..279446b5 100644 --- a/ui/src/Pages/SettingsPage/Users/UsersTable.js +++ b/ui/src/Pages/SettingsPage/Users/UsersTable.js @@ -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 diff --git a/ui/src/Pages/SettingsPage/Users/ViewUserRow.js b/ui/src/Pages/SettingsPage/Users/ViewUserRow.js index d90432ac..17053fe7 100644 --- a/ui/src/Pages/SettingsPage/Users/ViewUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/ViewUserRow.js @@ -12,7 +12,7 @@ const PathList = styled.ul` const ViewUserRow = ({ user, - state, + // state, setState, scanUser, deleteUser, @@ -41,7 +41,7 @@ const ViewUserRow = ({