Hook up UI to add and remove multiple root paths for each user
This commit is contained in:
parent
6e2773cc65
commit
c198e68daf
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -46,6 +46,7 @@ const UserRow = ({ user, refetchUsers }) => {
|
|||
const [state, setState] = useState({
|
||||
...user,
|
||||
editing: false,
|
||||
newRootPath: '',
|
||||
})
|
||||
|
||||
const [showConfirmDelete, setConfirmDelete] = useState(false)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue