mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-24 20:14:35 -04:00
Add interface for managing aliases (#1347)
* Add alias management Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Use React components instead of css classes Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Add tests Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Use notifications hook instead of redux methods Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Use test ids Signed-off-by: Erik Michelson <github@erik.michelson.eu> * Use test ids in other place as well Signed-off-by: Erik Michelson <github@erik.michelson.eu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
7d2c71b392
commit
488876e949
23 changed files with 812 additions and 17 deletions
|
@ -0,0 +1,30 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AliasesAddForm renders the input form 1`] = `
|
||||
<div>
|
||||
<form>
|
||||
<div
|
||||
class="mr-1 mb-1 input-group has-validation"
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
data-testid="addAliasInput"
|
||||
placeholder="editor.modal.aliases.addAlias"
|
||||
required=""
|
||||
value=""
|
||||
/>
|
||||
<button
|
||||
class="text-secondary ml-2 btn btn-light"
|
||||
data-testid="addAliasButton"
|
||||
disabled=""
|
||||
title="editor.modal.aliases.addAlias"
|
||||
type="submit"
|
||||
>
|
||||
<i
|
||||
class="fa fa-plus "
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,66 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AliasesListEntry renders an AliasesListEntry that is not primary 1`] = `
|
||||
<div>
|
||||
<li
|
||||
class="list-group-item d-flex flex-row justify-content-between align-items-center"
|
||||
>
|
||||
test-non-primary
|
||||
<div>
|
||||
<button
|
||||
class="mr-2 btn btn-light"
|
||||
data-testid="aliasButtonMakePrimary"
|
||||
title="editor.modal.aliases.makePrimary"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="fa fa-star-o "
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="text-danger btn btn-light"
|
||||
data-testid="aliasButtonRemove"
|
||||
title="editor.modal.aliases.removeAlias"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="fa fa-times "
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AliasesListEntry renders an AliasesListEntry that is primary 1`] = `
|
||||
<div>
|
||||
<li
|
||||
class="list-group-item d-flex flex-row justify-content-between align-items-center"
|
||||
>
|
||||
test-primary
|
||||
<div>
|
||||
<button
|
||||
class="mr-2 text-warning btn btn-light"
|
||||
data-testid="aliasIsPrimary"
|
||||
disabled=""
|
||||
title="editor.modal.aliases.isPrimary"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="fa fa-star "
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="text-danger btn btn-light"
|
||||
data-testid="aliasButtonRemove"
|
||||
title="editor.modal.aliases.removeAlias"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
class="fa fa-times "
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AliasesList renders the AliasList sorted 1`] = `
|
||||
<div>
|
||||
<span>
|
||||
Alias:
|
||||
a-test
|
||||
(
|
||||
non-primary
|
||||
)
|
||||
</span>
|
||||
<span>
|
||||
Alias:
|
||||
b-test
|
||||
(
|
||||
primary
|
||||
)
|
||||
</span>
|
||||
<span>
|
||||
Alias:
|
||||
z-test
|
||||
(
|
||||
non-primary
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,32 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AliasesModal renders the modal 1`] = `
|
||||
<div>
|
||||
<span>
|
||||
This is a mock implementation of a Modal:
|
||||
<dialog>
|
||||
<div
|
||||
class="modal-body"
|
||||
>
|
||||
<p>
|
||||
editor.modal.aliases.explanation
|
||||
</p>
|
||||
<div
|
||||
class="list-group"
|
||||
>
|
||||
<span>
|
||||
This is a mock for the AliasesList that is tested separately.
|
||||
</span>
|
||||
<div
|
||||
class="list-group-item"
|
||||
>
|
||||
<span>
|
||||
This is a mock for the AliasesAddForm that is tested separately.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render, act, screen } from '@testing-library/react'
|
||||
import testEvent from '@testing-library/user-event'
|
||||
import React from 'react'
|
||||
import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n'
|
||||
import * as AliasModule from '../../../../api/alias'
|
||||
import * as NoteDetailsReduxModule from '../../../../redux/note-details/methods'
|
||||
import * as useApplicationStateModule from '../../../../hooks/common/use-application-state'
|
||||
import { AliasesAddForm } from './aliases-add-form'
|
||||
import * as useUiNotificationsModule from '../../../notifications/ui-notification-boundary'
|
||||
|
||||
jest.mock('../../../../api/alias')
|
||||
jest.mock('../../../../redux/note-details/methods')
|
||||
jest.mock('../../../../hooks/common/use-application-state')
|
||||
jest.mock('../../../notifications/ui-notification-boundary')
|
||||
|
||||
const addPromise = Promise.resolve({ name: 'mock', primaryAlias: true, noteId: 'mock' })
|
||||
|
||||
describe('AliasesAddForm', () => {
|
||||
beforeEach(async () => {
|
||||
await mockI18n()
|
||||
jest.spyOn(AliasModule, 'addAlias').mockImplementation(() => addPromise)
|
||||
jest.spyOn(NoteDetailsReduxModule, 'updateMetadata').mockImplementation(() => Promise.resolve())
|
||||
jest.spyOn(useApplicationStateModule, 'useApplicationState').mockReturnValue('mock-note')
|
||||
jest.spyOn(useUiNotificationsModule, 'useUiNotifications').mockReturnValue({
|
||||
showErrorNotification: jest.fn(),
|
||||
dismissNotification: jest.fn(),
|
||||
dispatchUiNotification: jest.fn()
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
it('renders the input form', async () => {
|
||||
const view = render(<AliasesAddForm />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
const button = await screen.findByTestId('addAliasButton')
|
||||
expect(button).toBeDisabled()
|
||||
const input = await screen.findByTestId('addAliasInput')
|
||||
await testEvent.type(input, 'abc')
|
||||
expect(button).toBeEnabled()
|
||||
act(() => {
|
||||
button.click()
|
||||
})
|
||||
expect(AliasModule.addAlias).toBeCalledWith('mock-note', 'abc')
|
||||
await addPromise
|
||||
expect(NoteDetailsReduxModule.updateMetadata).toBeCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
import { Button, Form, InputGroup } from 'react-bootstrap'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import { addAlias } from '../../../../api/alias'
|
||||
import { updateMetadata } from '../../../../redux/note-details/methods'
|
||||
import { useOnInputChange } from '../../../../hooks/common/use-on-input-change'
|
||||
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||
import { testId } from '../../../../utils/test-id'
|
||||
|
||||
const validAliasRegex = /^[a-z0-9_-]*$/
|
||||
|
||||
/**
|
||||
* Form for adding a new alias to a note.
|
||||
*/
|
||||
export const AliasesAddForm: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||
const [newAlias, setNewAlias] = useState('')
|
||||
|
||||
const onAddAlias = useCallback(
|
||||
(event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
addAlias(noteId, newAlias)
|
||||
.then(updateMetadata)
|
||||
.catch(showErrorNotification('editor.modal.aliases.errorAddingAlias'))
|
||||
.finally(() => {
|
||||
setNewAlias('')
|
||||
})
|
||||
},
|
||||
[noteId, newAlias, setNewAlias, showErrorNotification]
|
||||
)
|
||||
|
||||
const onNewAliasInputChange = useOnInputChange(setNewAlias)
|
||||
|
||||
const newAliasValid = useMemo(() => {
|
||||
return validAliasRegex.test(newAlias)
|
||||
}, [newAlias])
|
||||
|
||||
return (
|
||||
<form onSubmit={onAddAlias}>
|
||||
<InputGroup className={'mr-1 mb-1'} hasValidation={true}>
|
||||
<Form.Control
|
||||
value={newAlias}
|
||||
placeholder={t('editor.modal.aliases.addAlias')}
|
||||
onChange={onNewAliasInputChange}
|
||||
isInvalid={!newAliasValid}
|
||||
required={true}
|
||||
{...testId('addAliasInput')}
|
||||
/>
|
||||
<Button
|
||||
type={'submit'}
|
||||
variant='light'
|
||||
className={'text-secondary ml-2'}
|
||||
disabled={!newAliasValid || newAlias === ''}
|
||||
title={t('editor.modal.aliases.addAlias')}
|
||||
{...testId('addAliasButton')}>
|
||||
<ForkAwesomeIcon icon={'plus'} />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render, act, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n'
|
||||
import type { Alias } from '../../../../api/alias/types'
|
||||
import { AliasesListEntry } from './aliases-list-entry'
|
||||
import * as AliasModule from '../../../../api/alias'
|
||||
import * as NoteDetailsReduxModule from '../../../../redux/note-details/methods'
|
||||
import * as useUiNotificationsModule from '../../../notifications/ui-notification-boundary'
|
||||
|
||||
jest.mock('../../../../api/alias')
|
||||
jest.mock('../../../../redux/note-details/methods')
|
||||
jest.mock('../../../notifications/ui-notification-boundary')
|
||||
|
||||
const deletePromise = Promise.resolve()
|
||||
const markAsPrimaryPromise = Promise.resolve({ name: 'mock', primaryAlias: true, noteId: 'mock' })
|
||||
|
||||
describe('AliasesListEntry', () => {
|
||||
beforeEach(async () => {
|
||||
await mockI18n()
|
||||
jest.spyOn(AliasModule, 'deleteAlias').mockImplementation(() => deletePromise)
|
||||
jest.spyOn(AliasModule, 'markAliasAsPrimary').mockImplementation(() => markAsPrimaryPromise)
|
||||
jest.spyOn(NoteDetailsReduxModule, 'updateMetadata').mockImplementation(() => Promise.resolve())
|
||||
jest.spyOn(useUiNotificationsModule, 'useUiNotifications').mockReturnValue({
|
||||
showErrorNotification: jest.fn(),
|
||||
dismissNotification: jest.fn(),
|
||||
dispatchUiNotification: jest.fn()
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
it('renders an AliasesListEntry that is primary', async () => {
|
||||
const testAlias: Alias = {
|
||||
name: 'test-primary',
|
||||
primaryAlias: true,
|
||||
noteId: 'test-note-id'
|
||||
}
|
||||
const view = render(<AliasesListEntry alias={testAlias} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
const button = await screen.findByTestId('aliasButtonRemove')
|
||||
act(() => {
|
||||
button.click()
|
||||
})
|
||||
expect(AliasModule.deleteAlias).toBeCalledWith(testAlias.name)
|
||||
await deletePromise
|
||||
expect(NoteDetailsReduxModule.updateMetadata).toBeCalled()
|
||||
})
|
||||
|
||||
it('renders an AliasesListEntry that is not primary', async () => {
|
||||
const testAlias: Alias = {
|
||||
name: 'test-non-primary',
|
||||
primaryAlias: false,
|
||||
noteId: 'test-note-id'
|
||||
}
|
||||
const view = render(<AliasesListEntry alias={testAlias} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
const buttonRemove = await screen.findByTestId('aliasButtonRemove')
|
||||
act(() => {
|
||||
buttonRemove.click()
|
||||
})
|
||||
expect(AliasModule.deleteAlias).toBeCalledWith(testAlias.name)
|
||||
await deletePromise
|
||||
expect(NoteDetailsReduxModule.updateMetadata).toBeCalled()
|
||||
const buttonMakePrimary = await screen.findByTestId('aliasButtonMakePrimary')
|
||||
act(() => {
|
||||
buttonMakePrimary.click()
|
||||
})
|
||||
expect(AliasModule.markAliasAsPrimary).toBeCalledWith(testAlias.name)
|
||||
await markAsPrimaryPromise
|
||||
expect(NoteDetailsReduxModule.updateMetadata).toBeCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React, { useCallback } from 'react'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import type { Alias } from '../../../../api/alias/types'
|
||||
import { deleteAlias, markAliasAsPrimary } from '../../../../api/alias'
|
||||
import { updateMetadata } from '../../../../redux/note-details/methods'
|
||||
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
|
||||
import { testId } from '../../../../utils/test-id'
|
||||
|
||||
export interface AliasesListEntryProps {
|
||||
alias: Alias
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that shows an entry in the aliases list with buttons to remove it or mark it as primary.
|
||||
*
|
||||
* @param alias The alias.
|
||||
*/
|
||||
export const AliasesListEntry: React.FC<AliasesListEntryProps> = ({ alias }) => {
|
||||
const { t } = useTranslation()
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const onRemoveClick = useCallback(() => {
|
||||
deleteAlias(alias.name)
|
||||
.then(updateMetadata)
|
||||
.catch(showErrorNotification(t('editor.modal.aliases.errorRemovingAlias')))
|
||||
}, [alias, t, showErrorNotification])
|
||||
|
||||
const onMakePrimaryClick = useCallback(() => {
|
||||
markAliasAsPrimary(alias.name)
|
||||
.then(updateMetadata)
|
||||
.catch(showErrorNotification(t('editor.modal.aliases.errorMakingPrimary')))
|
||||
}, [alias, t, showErrorNotification])
|
||||
|
||||
return (
|
||||
<li className={'list-group-item d-flex flex-row justify-content-between align-items-center'}>
|
||||
{alias.name}
|
||||
<div>
|
||||
<ShowIf condition={alias.primaryAlias}>
|
||||
<Button
|
||||
className={'mr-2 text-warning'}
|
||||
variant='light'
|
||||
disabled={true}
|
||||
title={t('editor.modal.aliases.isPrimary')}
|
||||
{...testId('aliasIsPrimary')}>
|
||||
<ForkAwesomeIcon icon={'star'} />
|
||||
</Button>
|
||||
</ShowIf>
|
||||
<ShowIf condition={!alias.primaryAlias}>
|
||||
<Button
|
||||
className={'mr-2'}
|
||||
variant='light'
|
||||
title={t('editor.modal.aliases.makePrimary')}
|
||||
onClick={onMakePrimaryClick}
|
||||
{...testId('aliasButtonMakePrimary')}>
|
||||
<ForkAwesomeIcon icon={'star-o'} />
|
||||
</Button>
|
||||
</ShowIf>
|
||||
<Button
|
||||
variant='light'
|
||||
className={'text-danger'}
|
||||
title={t('editor.modal.aliases.removeAlias')}
|
||||
onClick={onRemoveClick}
|
||||
{...testId('aliasButtonRemove')}>
|
||||
<ForkAwesomeIcon icon={'times'} />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n'
|
||||
import type { Alias } from '../../../../api/alias/types'
|
||||
import * as useApplicationStateModule from '../../../../hooks/common/use-application-state'
|
||||
import * as AliasesListEntryModule from './aliases-list-entry'
|
||||
import type { AliasesListEntryProps } from './aliases-list-entry'
|
||||
import { AliasesList } from './aliases-list'
|
||||
|
||||
jest.mock('../../../../hooks/common/use-application-state')
|
||||
jest.mock('./aliases-list-entry')
|
||||
|
||||
describe('AliasesList', () => {
|
||||
beforeEach(async () => {
|
||||
await mockI18n()
|
||||
jest.spyOn(useApplicationStateModule, 'useApplicationState').mockReturnValue([
|
||||
{
|
||||
name: 'a-test',
|
||||
noteId: 'note-id',
|
||||
primaryAlias: false
|
||||
},
|
||||
{
|
||||
name: 'z-test',
|
||||
noteId: 'note-id',
|
||||
primaryAlias: false
|
||||
},
|
||||
{
|
||||
name: 'b-test',
|
||||
noteId: 'note-id',
|
||||
primaryAlias: true
|
||||
}
|
||||
] as Alias[])
|
||||
jest.spyOn(AliasesListEntryModule, 'AliasesListEntry').mockImplementation((({ alias }) => {
|
||||
return (
|
||||
<span>
|
||||
Alias: {alias.name} ({alias.primaryAlias ? 'primary' : 'non-primary'})
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<AliasesListEntryProps>)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
it('renders the AliasList sorted', () => {
|
||||
const view = render(<AliasesList />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import type { ApplicationState } from '../../../../redux/application-state'
|
||||
import { AliasesListEntry } from './aliases-list-entry'
|
||||
|
||||
/**
|
||||
* Renders the list of aliases.
|
||||
*/
|
||||
export const AliasesList: React.FC = () => {
|
||||
const aliases = useApplicationState((state: ApplicationState) => state.noteDetails.aliases)
|
||||
|
||||
const aliasesDom = useMemo(() => {
|
||||
return aliases
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((alias) => <AliasesListEntry alias={alias} key={alias.name} />)
|
||||
}, [aliases])
|
||||
|
||||
return <Fragment>{aliasesDom}</Fragment>
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { render } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import type { CommonModalProps } from '../../../common/modals/common-modal'
|
||||
import * as CommonModalModule from '../../../common/modals/common-modal'
|
||||
import * as AliasesListModule from './aliases-list'
|
||||
import * as AliasesAddFormModule from './aliases-add-form'
|
||||
import * as useUiNotificationsModule from '../../../notifications/ui-notification-boundary'
|
||||
import { AliasesModal } from './aliases-modal'
|
||||
import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n'
|
||||
|
||||
jest.mock('./aliases-list')
|
||||
jest.mock('./aliases-add-form')
|
||||
jest.mock('../../../common/modals/common-modal')
|
||||
jest.mock('../../../notifications/ui-notification-boundary')
|
||||
|
||||
describe('AliasesModal', () => {
|
||||
beforeEach(async () => {
|
||||
await mockI18n()
|
||||
jest.spyOn(CommonModalModule, 'CommonModal').mockImplementation((({ children }) => {
|
||||
return (
|
||||
<span>
|
||||
This is a mock implementation of a Modal: <dialog>{children}</dialog>
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<PropsWithChildren<CommonModalProps>>)
|
||||
jest.spyOn(AliasesListModule, 'AliasesList').mockImplementation((() => {
|
||||
return <span>This is a mock for the AliasesList that is tested separately.</span>
|
||||
}) as React.FC)
|
||||
jest.spyOn(AliasesAddFormModule, 'AliasesAddForm').mockImplementation((() => {
|
||||
return <span>This is a mock for the AliasesAddForm that is tested separately.</span>
|
||||
}) as React.FC)
|
||||
jest.spyOn(useUiNotificationsModule, 'useUiNotifications').mockReturnValue({
|
||||
showErrorNotification: jest.fn(),
|
||||
dismissNotification: jest.fn(),
|
||||
dispatchUiNotification: jest.fn()
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
it('renders the modal', () => {
|
||||
const view = render(<AliasesModal show={true} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React from 'react'
|
||||
import { ListGroup, ListGroupItem, Modal } from 'react-bootstrap'
|
||||
import type { CommonModalProps } from '../../../common/modals/common-modal'
|
||||
import { CommonModal } from '../../../common/modals/common-modal'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { AliasesList } from './aliases-list'
|
||||
import { AliasesAddForm } from './aliases-add-form'
|
||||
|
||||
/**
|
||||
* Component that holds a modal containing a list of aliases associated with the current note.
|
||||
*
|
||||
* @param show True when the modal should be visible, false otherwise.
|
||||
* @param onHide Callback that is executed when the modal is dismissed.
|
||||
*/
|
||||
export const AliasesModal: React.FC<CommonModalProps> = ({ show, onHide }) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<CommonModal show={show} onHide={onHide} title={'editor.modal.aliases.title'} showCloseButton={true}>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
<Trans i18nKey={'editor.modal.aliases.explanation'} />
|
||||
</p>
|
||||
<ListGroup>
|
||||
<AliasesList />
|
||||
<ListGroupItem>
|
||||
<AliasesAddForm />
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
</Modal.Body>
|
||||
</CommonModal>
|
||||
)
|
||||
}
|
|
@ -17,6 +17,7 @@ import { ShareSidebarEntry } from './specific-sidebar-entries/share-sidebar-entr
|
|||
import styles from './style/sidebar.module.scss'
|
||||
import { DocumentSidebarMenuSelection } from './types'
|
||||
import { UsersOnlineSidebarMenu } from './users-online-sidebar-menu/users-online-sidebar-menu'
|
||||
import { AliasesSidebarEntry } from './specific-sidebar-entries/aliases-sidebar-entry'
|
||||
|
||||
/**
|
||||
* Renders the sidebar for the editor.
|
||||
|
@ -50,6 +51,7 @@ export const Sidebar: React.FC = () => {
|
|||
<NoteInfoSidebarEntry hide={selectionIsNotNone} />
|
||||
<RevisionSidebarEntry hide={selectionIsNotNone} />
|
||||
<PermissionsSidebarEntry hide={selectionIsNotNone} />
|
||||
<AliasesSidebarEntry hide={selectionIsNotNone} />
|
||||
<ImportMenuSidebarMenu
|
||||
menuId={DocumentSidebarMenuSelection.IMPORT}
|
||||
selectedMenuId={selectedMenu}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import type { SpecificSidebarEntryProps } from '../types'
|
||||
import { SidebarButton } from '../sidebar-button/sidebar-button'
|
||||
import { AliasesModal } from '../../document-bar/aliases/aliases-modal'
|
||||
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
||||
|
||||
/**
|
||||
* Component that shows a button in the editor sidebar for opening the aliases modal.
|
||||
*
|
||||
* @param className Additional CSS classes that should be added to the sidebar button.
|
||||
* @param hide True when the sidebar button should be hidden, False otherwise.
|
||||
*/
|
||||
export const AliasesSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
||||
useTranslation()
|
||||
const [showModal, setShowModal, setHideModal] = useBooleanState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton hide={hide} className={className} icon={'tags'} onClick={setShowModal}>
|
||||
<Trans i18nKey={'editor.modal.aliases.title'} />
|
||||
</SidebarButton>
|
||||
<AliasesModal show={showModal} onHide={setHideModal} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue