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 }) => (
-