feat(frontend): deactivate alias buttons if user is not owner

These buttons and their functionality only work if the user is the owner, so it doesn't make sense to make it possible to press them otherwise…

Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2023-03-24 16:10:34 +01:00 committed by Tilman Vatteroth
parent 107ec7a522
commit 09e56a418e
5 changed files with 122 additions and 5 deletions

View file

@ -1,5 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AliasesListEntry disables button in AliasesListEntry if it's not primary 1`] = `
<div>
<li
class="list-group-item d-flex flex-row justify-content-between align-items-center"
>
test-primary
<div>
<button
class="me-2 btn btn-light"
data-testid="aliasButtonMakePrimary"
disabled=""
title="editor.modal.aliases.makePrimary"
type="button"
>
BootstrapIconMock_StarFill
</button>
<button
class="text-danger btn btn-light"
data-testid="aliasButtonRemove"
disabled=""
title="editor.modal.aliases.removeAlias"
type="button"
>
BootstrapIconMock_X
</button>
</div>
</li>
</div>
`;
exports[`AliasesListEntry disables button in AliasesListEntry if it's primary 1`] = `
<div>
<li
class="list-group-item d-flex flex-row justify-content-between align-items-center"
>
test-primary
<div>
<button
class="me-2 text-warning btn btn-light"
data-testid="aliasIsPrimary"
disabled=""
title="editor.modal.aliases.isPrimary"
type="button"
>
BootstrapIconMock_Star
</button>
<button
class="text-danger btn btn-light"
data-testid="aliasButtonRemove"
disabled=""
title="editor.modal.aliases.removeAlias"
type="button"
>
BootstrapIconMock_X
</button>
</div>
</li>
</div>
`;
exports[`AliasesListEntry renders an AliasesListEntry that is not primary 1`] = ` exports[`AliasesListEntry renders an AliasesListEntry that is not primary 1`] = `
<div> <div>
<li <li

View file

@ -1,10 +1,11 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { addAlias } from '../../../../api/alias' import { addAlias } from '../../../../api/alias'
import { useApplicationState } from '../../../../hooks/common/use-application-state' import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { useIsOwner } from '../../../../hooks/common/use-is-owner'
import { useOnInputChange } from '../../../../hooks/common/use-on-input-change' import { useOnInputChange } from '../../../../hooks/common/use-on-input-change'
import { updateMetadata } from '../../../../redux/note-details/methods' import { updateMetadata } from '../../../../redux/note-details/methods'
import { testId } from '../../../../utils/test-id' import { testId } from '../../../../utils/test-id'
@ -25,6 +26,7 @@ export const AliasesAddForm: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { showErrorNotification } = useUiNotifications() const { showErrorNotification } = useUiNotifications()
const noteId = useApplicationState((state) => state.noteDetails.id) const noteId = useApplicationState((state) => state.noteDetails.id)
const isOwner = useIsOwner()
const [newAlias, setNewAlias] = useState('') const [newAlias, setNewAlias] = useState('')
const onAddAlias = useCallback( const onAddAlias = useCallback(
@ -54,6 +56,7 @@ export const AliasesAddForm: React.FC = () => {
placeholder={t('editor.modal.aliases.addAlias') ?? undefined} placeholder={t('editor.modal.aliases.addAlias') ?? undefined}
onChange={onNewAliasInputChange} onChange={onNewAliasInputChange}
isInvalid={!newAliasValid} isInvalid={!newAliasValid}
disabled={!isOwner}
required={true} required={true}
{...testId('addAliasInput')} {...testId('addAliasInput')}
/> />
@ -61,7 +64,7 @@ export const AliasesAddForm: React.FC = () => {
type={'submit'} type={'submit'}
variant='light' variant='light'
className={'text-secondary ms-2'} className={'text-secondary ms-2'}
disabled={!newAliasValid || newAlias === ''} disabled={!isOwner || !newAliasValid || newAlias === ''}
title={t('editor.modal.aliases.addAlias') ?? undefined} title={t('editor.modal.aliases.addAlias') ?? undefined}
{...testId('addAliasButton')}> {...testId('addAliasButton')}>
<UiIcon icon={IconPlus} /> <UiIcon icon={IconPlus} />

View file

@ -1,11 +1,12 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import * as AliasModule from '../../../../api/alias' import * as AliasModule from '../../../../api/alias'
import type { Alias } from '../../../../api/alias/types' import type { Alias } from '../../../../api/alias/types'
import * as NoteDetailsReduxModule from '../../../../redux/note-details/methods' import * as NoteDetailsReduxModule from '../../../../redux/note-details/methods'
import { mockNoteOwnership } from '../../../../test-utils/note-ownership'
import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n' import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n'
import * as useUiNotificationsModule from '../../../notifications/ui-notification-boundary' import * as useUiNotificationsModule from '../../../notifications/ui-notification-boundary'
import { AliasesListEntry } from './aliases-list-entry' import { AliasesListEntry } from './aliases-list-entry'
@ -15,6 +16,8 @@ import React from 'react'
jest.mock('../../../../api/alias') jest.mock('../../../../api/alias')
jest.mock('../../../../redux/note-details/methods') jest.mock('../../../../redux/note-details/methods')
jest.mock('../../../notifications/ui-notification-boundary') jest.mock('../../../notifications/ui-notification-boundary')
// This needs to be mocked here in addition to note-ownership.ts, because jest doesn't work otherwise
jest.mock('../../../../hooks/common/use-application-state')
const deletePromise = Promise.resolve() const deletePromise = Promise.resolve()
const markAsPrimaryPromise = Promise.resolve({ name: 'mock', primaryAlias: true, noteId: 'mock' }) const markAsPrimaryPromise = Promise.resolve({ name: 'mock', primaryAlias: true, noteId: 'mock' })
@ -32,12 +35,13 @@ describe('AliasesListEntry', () => {
}) })
}) })
afterAll(() => { afterEach(() => {
jest.resetAllMocks() jest.resetAllMocks()
jest.resetModules() jest.resetModules()
}) })
it('renders an AliasesListEntry that is primary', async () => { it('renders an AliasesListEntry that is primary', async () => {
mockNoteOwnership('test', 'test')
const testAlias: Alias = { const testAlias: Alias = {
name: 'test-primary', name: 'test-primary',
primaryAlias: true, primaryAlias: true,
@ -54,7 +58,19 @@ describe('AliasesListEntry', () => {
expect(NoteDetailsReduxModule.updateMetadata).toBeCalled() expect(NoteDetailsReduxModule.updateMetadata).toBeCalled()
}) })
it("disables button in AliasesListEntry if it's primary", () => {
mockNoteOwnership('test2', 'test')
const testAlias: Alias = {
name: 'test-primary',
primaryAlias: true,
noteId: 'test-note-id'
}
const view = render(<AliasesListEntry alias={testAlias} />)
expect(view.container).toMatchSnapshot()
})
it('renders an AliasesListEntry that is not primary', async () => { it('renders an AliasesListEntry that is not primary', async () => {
mockNoteOwnership('test', 'test')
const testAlias: Alias = { const testAlias: Alias = {
name: 'test-non-primary', name: 'test-non-primary',
primaryAlias: false, primaryAlias: false,
@ -77,4 +93,15 @@ describe('AliasesListEntry', () => {
await markAsPrimaryPromise await markAsPrimaryPromise
expect(NoteDetailsReduxModule.updateMetadata).toBeCalled() expect(NoteDetailsReduxModule.updateMetadata).toBeCalled()
}) })
it("disables button in AliasesListEntry if it's not primary", () => {
mockNoteOwnership('test2', 'test')
const testAlias: Alias = {
name: 'test-primary',
primaryAlias: false,
noteId: 'test-note-id'
}
const view = render(<AliasesListEntry alias={testAlias} />)
expect(view.container).toMatchSnapshot()
})
}) })

View file

@ -1,10 +1,11 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { deleteAlias, markAliasAsPrimary } from '../../../../api/alias' import { deleteAlias, markAliasAsPrimary } from '../../../../api/alias'
import type { Alias } from '../../../../api/alias/types' import type { Alias } from '../../../../api/alias/types'
import { useIsOwner } from '../../../../hooks/common/use-is-owner'
import { updateMetadata } from '../../../../redux/note-details/methods' import { updateMetadata } from '../../../../redux/note-details/methods'
import { testId } from '../../../../utils/test-id' import { testId } from '../../../../utils/test-id'
import { UiIcon } from '../../../common/icons/ui-icon' import { UiIcon } from '../../../common/icons/ui-icon'
@ -29,6 +30,7 @@ export interface AliasesListEntryProps {
export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) => { export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { showErrorNotification } = useUiNotifications() const { showErrorNotification } = useUiNotifications()
const isOwner = useIsOwner()
const onRemoveClick = useCallback(() => { const onRemoveClick = useCallback(() => {
deleteAlias(alias.name) deleteAlias(alias.name)
@ -60,6 +62,7 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
<Button <Button
className={'me-2'} className={'me-2'}
variant='light' variant='light'
disabled={!isOwner}
title={t('editor.modal.aliases.makePrimary') ?? undefined} title={t('editor.modal.aliases.makePrimary') ?? undefined}
onClick={onMakePrimaryClick} onClick={onMakePrimaryClick}
{...testId('aliasButtonMakePrimary')}> {...testId('aliasButtonMakePrimary')}>
@ -69,6 +72,7 @@ export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) =>
<Button <Button
variant='light' variant='light'
className={'text-danger'} className={'text-danger'}
disabled={!isOwner}
title={t('editor.modal.aliases.removeAlias') ?? undefined} title={t('editor.modal.aliases.removeAlias') ?? undefined}
onClick={onRemoveClick} onClick={onRemoveClick}
{...testId('aliasButtonRemove')}> {...testId('aliasButtonRemove')}>

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as useApplicationStateModule from '../hooks/common/use-application-state'
import type { ApplicationState } from '../redux/application-state'
jest.mock('../hooks/common/use-application-state')
export const mockNoteOwnership = (ownUsername: string, noteOwner: string) => {
jest.spyOn(useApplicationStateModule, 'useApplicationState').mockImplementation((fn) => {
return fn({
noteDetails: {
permissions: {
owner: noteOwner
}
},
user: {
username: ownUsername
}
} as ApplicationState)
})
}