1- import React , { forwardRef , memo , useCallback , useEffect , useRef } from 'react' ;
1+ import React , { useContext , useEffect , useRef } from 'react' ;
22import { Subuser } from '@/state/server/subusers' ;
3- import { Form , Formik , FormikHelpers , useFormikContext } from 'formik' ;
3+ import { Form , Formik } from 'formik' ;
44import { array , object , string } from 'yup' ;
5- import Modal , { RequiredModalProps } from '@/components/elements/Modal' ;
65import Field from '@/components/elements/Field' ;
76import { Actions , useStoreActions , useStoreState } from 'easy-peasy' ;
87import { ApplicationStore } from '@/state' ;
9- import TitledGreyBox from '@/components/elements/TitledGreyBox' ;
10- import Checkbox from '@/components/elements/Checkbox' ;
11- import styled from 'styled-components/macro' ;
128import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser' ;
139import { ServerContext } from '@/state/server' ;
1410import FlashMessageRender from '@/components/FlashMessageRender' ;
@@ -17,104 +13,33 @@ import { usePermissions } from '@/plugins/usePermissions';
1713import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo' ;
1814import tw from 'twin.macro' ;
1915import Button from '@/components/elements/Button' ;
20- import Label from '@/components/elements/Label' ;
21- import Input from '@/components/elements/Input' ;
22- import isEqual from 'react-fast-compare' ;
16+ import PermissionTitleBox from '@/components/server/users/PermissionTitleBox' ;
17+ import asModal from '@/hoc/asModal' ;
18+ import PermissionRow from '@/components/server/users/PermissionRow' ;
19+ import ModalContext from '@/context/ModalContext' ;
2320
2421type Props = {
2522 subuser ?: Subuser ;
26- } & RequiredModalProps ;
23+ } ;
2724
2825interface Values {
2926 email : string ;
3027 permissions : string [ ] ;
3128}
3229
33- const PermissionLabel = styled . label `
34- ${ tw `flex items-center border border-transparent rounded md:p-2 transition-colors duration-75` } ;
35- text-transform: none;
36-
37- &:not(.disabled) {
38- ${ tw `cursor-pointer` } ;
39-
40- &:hover {
41- ${ tw `border-neutral-500 bg-neutral-800` } ;
42- }
43- }
44-
45- &:not(:first-of-type) {
46- ${ tw `mt-4 sm:mt-2` } ;
47- }
48-
49- &.disabled {
50- ${ tw `opacity-50` } ;
51-
52- & input[type="checkbox"]:not(:checked) {
53- ${ tw `border-0` } ;
54- }
55- }
56- ` ;
57-
58- interface TitleProps {
59- isEditable : boolean ;
60- permission : string ;
61- permissions : string [ ] ;
62- children : React . ReactNode ;
63- className ?: string ;
64- }
65-
66- const PermissionTitledBox = memo ( ( { isEditable, permission, permissions, className, children } : TitleProps ) => {
67- const { values, setFieldValue } = useFormikContext < Values > ( ) ;
68-
69- const onCheckboxClicked = useCallback ( ( e : React . ChangeEvent < HTMLInputElement > ) => {
70- console . log ( e . currentTarget . checked , [
71- ...values . permissions ,
72- ...permissions . filter ( p => ! values . permissions . includes ( p ) ) ,
73- ] ) ;
74-
75- if ( e . currentTarget . checked ) {
76- setFieldValue ( 'permissions' , [
77- ...values . permissions ,
78- ...permissions . filter ( p => ! values . permissions . includes ( p ) ) ,
79- ] ) ;
80- } else {
81- setFieldValue ( 'permissions' , [
82- ...values . permissions . filter ( p => ! permissions . includes ( p ) ) ,
83- ] ) ;
84- }
85- } , [ permissions , values . permissions ] ) ;
86-
87- return (
88- < TitledGreyBox
89- title = {
90- < div css = { tw `flex items-center` } >
91- < p css = { tw `text-sm uppercase flex-1` } > { permission } </ p >
92- { isEditable &&
93- < Input
94- type = { 'checkbox' }
95- checked = { permissions . every ( p => values . permissions . includes ( p ) ) }
96- onChange = { onCheckboxClicked }
97- />
98- }
99- </ div >
100- }
101- className = { className }
102- >
103- { children }
104- </ TitledGreyBox >
105- ) ;
106- } , isEqual ) ;
30+ const EditSubuserModal = ( { subuser } : Props ) => {
31+ const ref = useRef < HTMLHeadingElement > ( null ) ;
32+ const uuid = ServerContext . useStoreState ( state => state . server . data ! . uuid ) ;
33+ const appendSubuser = ServerContext . useStoreActions ( actions => actions . subusers . appendSubuser ) ;
34+ const { clearFlashes, clearAndAddHttpError } = useStoreActions ( ( actions : Actions < ApplicationStore > ) => actions . flashes ) ;
35+ const { dismiss, toggleSpinner } = useContext ( ModalContext ) ;
10736
108- const EditSubuserModal = forwardRef < HTMLHeadingElement , Props > ( ( { subuser, ...props } , ref ) => {
109- const { isSubmitting } = useFormikContext < Values > ( ) ;
110- const [ canEditUser ] = usePermissions ( subuser ? [ 'user.update' ] : [ 'user.create' ] ) ;
37+ const isRootAdmin = useStoreState ( state => state . user . data ! . rootAdmin ) ;
11138 const permissions = useStoreState ( state => state . permissions . data ) ;
112-
113- const user = useStoreState ( state => state . user . data ! ) ;
114-
11539 // The currently logged in user's permissions. We're going to filter out any permissions
11640 // that they should not need.
11741 const loggedInPermissions = ServerContext . useStoreState ( state => state . server . permissions ) ;
42+ const [ canEditUser ] = usePermissions ( subuser ? [ 'user.update' ] : [ 'user.create' ] ) ;
11843
11944 // The permissions that can be modified by this user.
12045 const editablePermissions = useDeepCompareMemo ( ( ) => {
@@ -123,120 +48,25 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
12348
12449 const list : string [ ] = ( [ ] as string [ ] ) . concat . apply ( [ ] , Object . values ( cleaned ) ) ;
12550
126- if ( user . rootAdmin || ( loggedInPermissions . length === 1 && loggedInPermissions [ 0 ] === '*' ) ) {
51+ if ( isRootAdmin || ( loggedInPermissions . length === 1 && loggedInPermissions [ 0 ] === '*' ) ) {
12752 return list ;
12853 }
12954
13055 return list . filter ( key => loggedInPermissions . indexOf ( key ) >= 0 ) ;
131- } , [ permissions , loggedInPermissions ] ) ;
56+ } , [ isRootAdmin , permissions , loggedInPermissions ] ) ;
13257
133- return (
134- < Modal { ...props } top = { false } showSpinnerOverlay = { isSubmitting } >
135- < div css = { tw `flex justify-between` } >
136- < div >
137- < h2 css = { tw `text-2xl mr-4` } ref = { ref } >
138- { subuser ?
139- `${ canEditUser ? 'Modify' : 'View' } permissions for ${ subuser . email } `
140- :
141- 'Create new subuser '
142- }
143- </ h2 >
144- </ div >
145- < div >
146- < Button type = { 'submit' } css = { tw `w-full sm:w-auto` } >
147- { subuser ? 'Save' : 'Invite User' }
148- </ Button >
149- </ div >
150- </ div >
151- < FlashMessageRender byKey = { 'user:edit' } css = { tw `mt-4` } />
152- { ( ! user . rootAdmin && loggedInPermissions [ 0 ] !== '*' ) &&
153- < div css = { tw `mt-4 pl-4 py-2 border-l-4 border-cyan-400` } >
154- < p css = { tw `text-sm text-neutral-300` } >
155- Only permissions which your account is currently assigned may be selected when creating or
156- modifying other users.
157- </ p >
158- </ div >
159- }
160- { ! subuser &&
161- < div css = { tw `mt-4` } >
162- < Field
163- name = { 'email' }
164- label = { 'User Email' }
165- description = { 'Enter the email address of the user you wish to invite as a subuser for this server.' }
166- />
167- </ div >
168- }
169- < div css = { tw `my-6` } >
170- { Object . keys ( permissions ) . filter ( key => key !== 'websocket' ) . map ( ( key , index ) => {
171- const group = Object . keys ( permissions [ key ] . keys ) . map ( pkey => `${ key } .${ pkey } ` ) ;
172-
173- return (
174- < PermissionTitledBox
175- key = { `permission_${ key } ` }
176- isEditable = { canEditUser }
177- permission = { key }
178- permissions = { group }
179- css = { index > 0 ? tw `mt-4` : undefined }
180- >
181- < p css = { tw `text-sm text-neutral-400 mb-4` } >
182- { permissions [ key ] . description }
183- </ p >
184- { Object . keys ( permissions [ key ] . keys ) . map ( pkey => (
185- < PermissionLabel
186- key = { `permission_${ key } _${ pkey } ` }
187- htmlFor = { `permission_${ key } _${ pkey } ` }
188- className = { ( ! canEditUser || editablePermissions . indexOf ( `${ key } .${ pkey } ` ) < 0 ) ? 'disabled' : undefined }
189- >
190- < div css = { tw `p-2` } >
191- < Checkbox
192- id = { `permission_${ key } _${ pkey } ` }
193- name = { 'permissions' }
194- value = { `${ key } .${ pkey } ` }
195- css = { tw `w-5 h-5 mr-2` }
196- disabled = { ! canEditUser || editablePermissions . indexOf ( `${ key } .${ pkey } ` ) < 0 }
197- />
198- </ div >
199- < div css = { tw `flex-1` } >
200- < Label as = { 'p' } css = { tw `font-medium` } > { pkey } </ Label >
201- { permissions [ key ] . keys [ pkey ] . length > 0 &&
202- < p css = { tw `text-xs text-neutral-400 mt-1` } >
203- { permissions [ key ] . keys [ pkey ] }
204- </ p >
205- }
206- </ div >
207- </ PermissionLabel >
208- ) ) }
209- </ PermissionTitledBox >
210- ) ;
211- } ) }
212- </ div >
213- < Can action = { subuser ? 'user.update' : 'user.create' } >
214- < div css = { tw `pb-6 flex justify-end` } >
215- < Button type = { 'submit' } css = { tw `w-full sm:w-auto` } >
216- { subuser ? 'Save' : 'Invite User' }
217- </ Button >
218- </ div >
219- </ Can >
220- </ Modal >
221- ) ;
222- } ) ;
223-
224- export default ( { subuser, ...props } : Props ) => {
225- const ref = useRef < HTMLHeadingElement > ( null ) ;
226- const uuid = ServerContext . useStoreState ( state => state . server . data ! . uuid ) ;
227- const appendSubuser = ServerContext . useStoreActions ( actions => actions . subusers . appendSubuser ) ;
228- const { clearFlashes, clearAndAddHttpError } = useStoreActions ( ( actions : Actions < ApplicationStore > ) => actions . flashes ) ;
229-
230- const submit = ( values : Values , { setSubmitting } : FormikHelpers < Values > ) => {
58+ const submit = ( values : Values ) => {
59+ toggleSpinner ( true ) ;
23160 clearFlashes ( 'user:edit' ) ;
61+
23262 createOrUpdateSubuser ( uuid , values , subuser )
23363 . then ( subuser => {
23464 appendSubuser ( subuser ) ;
235- props . onDismissed ( ) ;
65+ dismiss ( ) ;
23666 } )
23767 . catch ( error => {
23868 console . error ( error ) ;
239- setSubmitting ( false ) ;
69+ toggleSpinner ( false ) ;
24070 clearAndAddHttpError ( { key : 'user:edit' , error } ) ;
24171
24272 if ( ref . current ) {
@@ -245,10 +75,8 @@ export default ({ subuser, ...props }: Props) => {
24575 } ) ;
24676 } ;
24777
248- useEffect ( ( ) => {
249- return ( ) => {
250- clearFlashes ( 'user:edit' ) ;
251- } ;
78+ useEffect ( ( ) => ( ) => {
79+ clearFlashes ( 'user:edit' ) ;
25280 } , [ ] ) ;
25381
25482 return (
@@ -267,8 +95,68 @@ export default ({ subuser, ...props }: Props) => {
26795 } ) }
26896 >
26997 < Form >
270- < EditSubuserModal ref = { ref } subuser = { subuser } { ...props } />
98+ < div css = { tw `flex justify-between` } >
99+ < h2 css = { tw `text-2xl` } ref = { ref } >
100+ { subuser ? `${ canEditUser ? 'Modify' : 'View' } permissions for ${ subuser . email } ` : 'Create new subuser' }
101+ </ h2 >
102+ < div >
103+ < Button type = { 'submit' } css = { tw `w-full sm:w-auto` } >
104+ { subuser ? 'Save' : 'Invite User' }
105+ </ Button >
106+ </ div >
107+ </ div >
108+ < FlashMessageRender byKey = { 'user:edit' } css = { tw `mt-4` } />
109+ { ( ! isRootAdmin && loggedInPermissions [ 0 ] !== '*' ) &&
110+ < div css = { tw `mt-4 pl-4 py-2 border-l-4 border-cyan-400` } >
111+ < p css = { tw `text-sm text-neutral-300` } >
112+ Only permissions which your account is currently assigned may be selected when creating or
113+ modifying other users.
114+ </ p >
115+ </ div >
116+ }
117+ { ! subuser &&
118+ < div css = { tw `mt-6` } >
119+ < Field
120+ name = { 'email' }
121+ label = { 'User Email' }
122+ description = { 'Enter the email address of the user you wish to invite as a subuser for this server.' }
123+ />
124+ </ div >
125+ }
126+ < div css = { tw `my-6` } >
127+ { Object . keys ( permissions ) . filter ( key => key !== 'websocket' ) . map ( ( key , index ) => (
128+ < PermissionTitleBox
129+ key = { `permission_${ key } ` }
130+ title = { key }
131+ isEditable = { canEditUser }
132+ permissions = { Object . keys ( permissions [ key ] . keys ) . map ( pkey => `${ key } .${ pkey } ` ) }
133+ css = { index > 0 ? tw `mt-4` : undefined }
134+ >
135+ < p css = { tw `text-sm text-neutral-400 mb-4` } >
136+ { permissions [ key ] . description }
137+ </ p >
138+ { Object . keys ( permissions [ key ] . keys ) . map ( pkey => (
139+ < PermissionRow
140+ key = { `permission_${ key } .${ pkey } ` }
141+ permission = { `${ key } .${ pkey } ` }
142+ disabled = { ! canEditUser || editablePermissions . indexOf ( `${ key } .${ pkey } ` ) < 0 }
143+ />
144+ ) ) }
145+ </ PermissionTitleBox >
146+ ) ) }
147+ </ div >
148+ < Can action = { subuser ? 'user.update' : 'user.create' } >
149+ < div css = { tw `pb-6 flex justify-end` } >
150+ < Button type = { 'submit' } css = { tw `w-full sm:w-auto` } >
151+ { subuser ? 'Save' : 'Invite User' }
152+ </ Button >
153+ </ div >
154+ </ Can >
271155 </ Form >
272156 </ Formik >
273157 ) ;
274158} ;
159+
160+ export default asModal < Props > ( {
161+ top : false ,
162+ } ) ( EditSubuserModal ) ;
0 commit comments