mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-17 00:24:43 -04:00
build new permissions modal (#532)
build new permissions modal Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Co-authored-by: Erik Michelson <github@erik.michelson.eu> Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
d500ebcd19
commit
ac00bc98c0
7 changed files with 350 additions and 27 deletions
|
@ -329,7 +329,21 @@
|
||||||
"button": "Delete note"
|
"button": "Delete note"
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"title": "Permissions"
|
"title": "Permissions",
|
||||||
|
"owner": "Owner",
|
||||||
|
"sharedWithUsers": "Shared with users",
|
||||||
|
"sharedWithGroups": "Also share with…",
|
||||||
|
"editUser": "Change {{name}}'s permissions to view and edit",
|
||||||
|
"viewOnlyUser": "Change {{name}}'s permissions to view only",
|
||||||
|
"removeUser": "Remove {{name}}'s permissions",
|
||||||
|
"addUser": "Add user",
|
||||||
|
"editGroup": "Change permissions of group \"{{name}}\" to view & edit",
|
||||||
|
"viewOnlyGroup": "Change permissions of group \"{{name}}\" to view only",
|
||||||
|
"denyGroup": "Deny access to group \"{{name}}\"",
|
||||||
|
"addGroup": "Add group",
|
||||||
|
"allUser": "Everyone",
|
||||||
|
"allLoggedInUser": "All logged-in users",
|
||||||
|
"error": "An error occurred while fetching the user information of this note."
|
||||||
},
|
},
|
||||||
"shareLink": {
|
"shareLink": {
|
||||||
"title": "Share link",
|
"title": "Share link",
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import React, { Fragment, useState } from 'react'
|
|
||||||
import { Modal } from 'react-bootstrap'
|
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
|
||||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
|
||||||
|
|
||||||
export const PermissionButton: React.FC = () => {
|
|
||||||
const [showReadOnly, setShowReadOnly] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<TranslatedIconButton size={'sm'} className={'mx-1'} icon={'lock'} variant={'light'} onClick={() => setShowReadOnly(true)} i18nKey={'editor.documentBar.permissions'}/>
|
|
||||||
<CommonModal
|
|
||||||
show={showReadOnly}
|
|
||||||
onHide={() => setShowReadOnly(false)}
|
|
||||||
closeButton={true}
|
|
||||||
titleI18nKey={'editor.modal.permissions.title'}>
|
|
||||||
<Modal.Body>
|
|
||||||
<img className={'w-100'} src={'https://thumbs.gfycat.com/ImpassionedDeliriousIndianpalmsquirrel-size_restricted.gif'} alt={'Placeholder'}/>
|
|
||||||
</Modal.Body>
|
|
||||||
</CommonModal>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { PinToHistoryButton } from './buttons/pin-to-history-button'
|
||||||
|
import { ShareLinkButton } from './buttons/share-link-button'
|
||||||
import { ConnectionIndicator } from './connection-indicator/connection-indicator'
|
import { ConnectionIndicator } from './connection-indicator/connection-indicator'
|
||||||
import { DocumentInfoButton } from './document-info/document-info-button'
|
import { DocumentInfoButton } from './document-info/document-info-button'
|
||||||
import { EditorMenu } from './menus/editor-menu'
|
import { EditorMenu } from './menus/editor-menu'
|
||||||
import { ExportMenu } from './menus/export-menu'
|
import { ExportMenu } from './menus/export-menu'
|
||||||
import { ImportMenu } from './menus/import-menu'
|
import { ImportMenu } from './menus/import-menu'
|
||||||
import { PermissionButton } from './buttons/permission-button'
|
import { PermissionButton } from './permissions/permission-button'
|
||||||
import { PinToHistoryButton } from './buttons/pin-to-history-button'
|
|
||||||
import { ShareLinkButton } from './buttons/share-link-button'
|
|
||||||
import { RevisionButton } from './revisions/revision-button'
|
import { RevisionButton } from './revisions/revision-button'
|
||||||
|
|
||||||
export interface DocumentBarProps {
|
export interface DocumentBarProps {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React, { Fragment, useState } from 'react'
|
||||||
|
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||||
|
import { PermissionModal } from './permission-modal'
|
||||||
|
|
||||||
|
export const PermissionButton: React.FC = () => {
|
||||||
|
const [showPermissionModal, setShowPermissionModal] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<TranslatedIconButton size={'sm'} className={'mx-1'} icon={'lock'} variant={'light'} onClick={() => setShowPermissionModal(true)} i18nKey={'editor.documentBar.permissions'}/>
|
||||||
|
<PermissionModal show={showPermissionModal} onChangeShow={setShowPermissionModal}/>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||||
|
|
||||||
|
export interface PermissionGroupEntryProps {
|
||||||
|
title: string
|
||||||
|
editMode: GroupMode
|
||||||
|
onChangeEditMode: (newMode: GroupMode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GroupMode {
|
||||||
|
NONE,
|
||||||
|
VIEW,
|
||||||
|
EDIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionGroupEntry: React.FC<PermissionGroupEntryProps> = ({ title, editMode, onChangeEditMode }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
||||||
|
<Trans i18nKey={title}/>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
type='radio'
|
||||||
|
name='edit-mode'
|
||||||
|
value={editMode}
|
||||||
|
onChange={onChangeEditMode}
|
||||||
|
>
|
||||||
|
<ToggleButton
|
||||||
|
title={ t('editor.modal.permissions.denyGroup', { name: t(title) })}
|
||||||
|
variant={'light'}
|
||||||
|
className={'text-secondary'}
|
||||||
|
value={GroupMode.NONE}
|
||||||
|
>
|
||||||
|
<ForkAwesomeIcon icon='ban'/>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
title={ t('editor.modal.permissions.viewOnlyGroup', { name: t(title) })}
|
||||||
|
variant={'light'}
|
||||||
|
className={'text-secondary'}
|
||||||
|
value={GroupMode.VIEW}
|
||||||
|
>
|
||||||
|
<ForkAwesomeIcon icon='eye'/>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
title={t('editor.modal.permissions.editGroup', { name: t(title) })}
|
||||||
|
variant={'light'}
|
||||||
|
className={'text-secondary'}
|
||||||
|
value={GroupMode.EDIT}
|
||||||
|
>
|
||||||
|
<ForkAwesomeIcon icon='pencil'/>
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React, { ReactElement, useState } from 'react'
|
||||||
|
import { Button, FormControl, InputGroup, ToggleButton, ToggleButtonGroup } from 'react-bootstrap'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||||
|
import { Principal } from './permission-modal'
|
||||||
|
|
||||||
|
export interface PermissionListProps {
|
||||||
|
list: Principal[]
|
||||||
|
identifier: (entry: Principal) => ReactElement
|
||||||
|
changeEditMode: (id: Principal['id'], canEdit: Principal['canEdit']) => void
|
||||||
|
removeEntry: (id: Principal['id']) => void
|
||||||
|
createEntry: (name: Principal['name']) => void
|
||||||
|
editI18nKey: string
|
||||||
|
viewI18nKey: string
|
||||||
|
removeI18nKey: string
|
||||||
|
addI18nKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EditMode {
|
||||||
|
VIEW,
|
||||||
|
EDIT
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionList: React.FC<PermissionListProps> = ({ list, identifier, changeEditMode, removeEntry, createEntry, editI18nKey, viewI18nKey, removeI18nKey, addI18nKey }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [newEntry, setNewEntry] = useState('')
|
||||||
|
|
||||||
|
const addEntry = () => {
|
||||||
|
createEntry(newEntry)
|
||||||
|
setNewEntry('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={'list-group'}>
|
||||||
|
{list.map(entry => (
|
||||||
|
<li key={entry.id} className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
||||||
|
{identifier(entry)}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant='light'
|
||||||
|
className={'text-danger mr-2'}
|
||||||
|
title={t(removeI18nKey, { name: entry.name })}
|
||||||
|
onClick={() => removeEntry(entry.id)}
|
||||||
|
>
|
||||||
|
<ForkAwesomeIcon icon={'times'}/>
|
||||||
|
</Button>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
type='radio'
|
||||||
|
name='edit-mode'
|
||||||
|
value={entry.canEdit ? EditMode.EDIT : EditMode.VIEW}
|
||||||
|
onChange={(value: EditMode) => changeEditMode(entry.id, value === EditMode.EDIT)}
|
||||||
|
>
|
||||||
|
<ToggleButton
|
||||||
|
title={ t(viewI18nKey, { name: entry.name })}
|
||||||
|
variant={'light'}
|
||||||
|
className={'text-secondary'}
|
||||||
|
value={EditMode.VIEW}
|
||||||
|
>
|
||||||
|
<ForkAwesomeIcon icon='eye'/>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
title={t(editI18nKey, { name: entry.name })}
|
||||||
|
variant={'light'}
|
||||||
|
className={'text-secondary'}
|
||||||
|
value={EditMode.EDIT}
|
||||||
|
>
|
||||||
|
<ForkAwesomeIcon icon='pencil'/>
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li className={'list-group-item'}>
|
||||||
|
<form onSubmit={event => {
|
||||||
|
event.preventDefault()
|
||||||
|
addEntry()
|
||||||
|
}}>
|
||||||
|
<InputGroup className={'mr-1 mb-1'}>
|
||||||
|
<FormControl
|
||||||
|
value={newEntry}
|
||||||
|
placeholder={t(addI18nKey)}
|
||||||
|
aria-label={t(addI18nKey)}
|
||||||
|
onChange={event => setNewEntry(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant='light'
|
||||||
|
className={'text-secondary ml-2'}
|
||||||
|
title={t(addI18nKey)}
|
||||||
|
onClick={addEntry}
|
||||||
|
>
|
||||||
|
<ForkAwesomeIcon icon={'plus'}/>
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Alert, Modal } from 'react-bootstrap'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { getUserById } from '../../../../api/users'
|
||||||
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
|
import { UserAvatar, UserAvatarProps } from '../../../common/user-avatar/user-avatar'
|
||||||
|
import { GroupMode, PermissionGroupEntry } from './permission-group-entry'
|
||||||
|
import { PermissionList } from './permission-list'
|
||||||
|
|
||||||
|
export interface PermissionsModalProps {
|
||||||
|
show: boolean,
|
||||||
|
onChangeShow: (newShow: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Principal {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
photo: string
|
||||||
|
canEdit: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotePermissions {
|
||||||
|
owner: string
|
||||||
|
sharedTo: {
|
||||||
|
username: string
|
||||||
|
canEdit: boolean
|
||||||
|
}[],
|
||||||
|
sharedToGroup: {
|
||||||
|
id: string
|
||||||
|
canEdit: boolean
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EVERYONE_GROUP_ID = '1'
|
||||||
|
export const EVERYONE_LOGGED_IN_GROUP_ID = '2'
|
||||||
|
|
||||||
|
const permissionsApiResponse: NotePermissions = {
|
||||||
|
owner: 'dermolly',
|
||||||
|
sharedTo: [{
|
||||||
|
username: 'emcrx',
|
||||||
|
canEdit: true
|
||||||
|
}, {
|
||||||
|
username: 'mrdrogdrog',
|
||||||
|
canEdit: false
|
||||||
|
}],
|
||||||
|
sharedToGroup: [{
|
||||||
|
id: EVERYONE_GROUP_ID,
|
||||||
|
canEdit: true
|
||||||
|
}, {
|
||||||
|
id: EVERYONE_LOGGED_IN_GROUP_ID,
|
||||||
|
canEdit: false
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onChangeShow }) => {
|
||||||
|
useTranslation()
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
const [userList, setUserList] = useState<Principal[]>([])
|
||||||
|
const [owner, setOwner] = useState<UserAvatarProps>()
|
||||||
|
const [allUserPermissions, setAllUserPermissions] = useState(GroupMode.NONE)
|
||||||
|
const [allLoggedInUserPermissions, setAllLoggedInUserPermissions] = useState(GroupMode.NONE)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// set owner
|
||||||
|
getUserById(permissionsApiResponse.owner).then(response => {
|
||||||
|
setOwner({
|
||||||
|
name: response.name,
|
||||||
|
photo: response.photo
|
||||||
|
})
|
||||||
|
}).catch(() => setError(true))
|
||||||
|
// set user List
|
||||||
|
permissionsApiResponse.sharedTo.forEach(shareUser => {
|
||||||
|
getUserById(shareUser.username).then(response => {
|
||||||
|
setUserList(list => list.concat([{
|
||||||
|
id: response.id,
|
||||||
|
name: response.name,
|
||||||
|
photo: response.photo,
|
||||||
|
canEdit: shareUser.canEdit
|
||||||
|
}]))
|
||||||
|
}).catch(() => setError(true))
|
||||||
|
})
|
||||||
|
// set group List
|
||||||
|
permissionsApiResponse.sharedToGroup.forEach(sharedGroup => {
|
||||||
|
if (sharedGroup.id === EVERYONE_GROUP_ID) {
|
||||||
|
setAllUserPermissions(sharedGroup.canEdit ? GroupMode.EDIT : GroupMode.VIEW)
|
||||||
|
} else if (sharedGroup.id === EVERYONE_LOGGED_IN_GROUP_ID) {
|
||||||
|
setAllLoggedInUserPermissions(sharedGroup.canEdit ? GroupMode.EDIT : GroupMode.VIEW)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const changeUserMode = (userId: Principal['id'], canEdit: Principal['canEdit']) => {
|
||||||
|
setUserList(list =>
|
||||||
|
list
|
||||||
|
.map(user => {
|
||||||
|
if (user.id === userId) {
|
||||||
|
user.canEdit = canEdit
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeUser = (userId: Principal['id']) => {
|
||||||
|
setUserList(list => list.filter(user => user.id !== userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addUser = (name: Principal['name']) => {
|
||||||
|
setUserList(list => list.concat({
|
||||||
|
id: name,
|
||||||
|
photo: '/avatar.png',
|
||||||
|
name: name,
|
||||||
|
canEdit: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonModal
|
||||||
|
show={show}
|
||||||
|
onHide={() => onChangeShow(false)}
|
||||||
|
closeButton={true}
|
||||||
|
titleI18nKey={'editor.modal.permissions.title'}>
|
||||||
|
<Modal.Body>
|
||||||
|
<h5 className={'mb-3'}><Trans i18nKey={'editor.modal.permissions.owner'}/></h5>
|
||||||
|
<ShowIf condition={error}>
|
||||||
|
<Alert variant='danger'>
|
||||||
|
<Trans i18nKey='editor.modal.permissions.error'/>
|
||||||
|
</Alert>
|
||||||
|
</ShowIf>
|
||||||
|
<ul className={'list-group'}>
|
||||||
|
<li className={'list-group-item d-flex flex-row align-items-center'}>
|
||||||
|
<UserAvatar name={owner?.name ?? ''} photo={owner?.photo ?? ''}/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h5 className={'my-3'}><Trans i18nKey={'editor.modal.permissions.sharedWithUsers'}/></h5>
|
||||||
|
<PermissionList
|
||||||
|
list={userList}
|
||||||
|
identifier={entry => (<UserAvatar name={entry.name} photo={entry.photo}/>)}
|
||||||
|
changeEditMode={changeUserMode}
|
||||||
|
removeEntry={removeUser}
|
||||||
|
createEntry={addUser}
|
||||||
|
editI18nKey={'editor.modal.permissions.editUser'}
|
||||||
|
viewI18nKey={'editor.modal.permissions.viewOnlyUser'}
|
||||||
|
removeI18nKey={'editor.modal.permissions.removeUser'}
|
||||||
|
addI18nKey={'editor.modal.permissions.addUser'}
|
||||||
|
/>
|
||||||
|
<h5 className={'my-3'}><Trans i18nKey={'editor.modal.permissions.sharedWithGroups'}/></h5>
|
||||||
|
<ul className={'list-group'}>
|
||||||
|
<PermissionGroupEntry
|
||||||
|
title={'editor.modal.permissions.allUser'}
|
||||||
|
editMode={allUserPermissions}
|
||||||
|
onChangeEditMode={setAllUserPermissions}
|
||||||
|
/>
|
||||||
|
<PermissionGroupEntry
|
||||||
|
title={'editor.modal.permissions.allLoggedInUser'}
|
||||||
|
editMode={allLoggedInUserPermissions}
|
||||||
|
onChangeEditMode={setAllLoggedInUserPermissions}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</Modal.Body>
|
||||||
|
</CommonModal>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue