mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-28 11:20:59 -05: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
|
@ -455,6 +455,17 @@
|
||||||
"viewOnlyDescription": "This link points to a read-only version of this note. You can use this e.g. for feedback from friends and colleagues.",
|
"viewOnlyDescription": "This link points to a read-only version of this note. You can use this e.g. for feedback from friends and colleagues.",
|
||||||
"slidesDescription": "This link points to the presentation view of the slides."
|
"slidesDescription": "This link points to the presentation view of the slides."
|
||||||
},
|
},
|
||||||
|
"aliases": {
|
||||||
|
"title": "Aliases",
|
||||||
|
"explanation": "Aliases are alternative names for this note. You may access this note under all the listed names below.",
|
||||||
|
"addAlias": "Add alias",
|
||||||
|
"makePrimary": "Mark this alias as primary",
|
||||||
|
"isPrimary": "This is the primary alias",
|
||||||
|
"removeAlias": "Remove this alias",
|
||||||
|
"errorAddingAlias": "The chosen alias can not be added to this note",
|
||||||
|
"errorRemovingAlias": "There was an error removing the alias",
|
||||||
|
"errorMakingPrimary": "There was an error marking the alias as primary"
|
||||||
|
},
|
||||||
"preferences": {
|
"preferences": {
|
||||||
"title": "Preferences",
|
"title": "Preferences",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { Note, NoteDeletionOptions } from './types'
|
import type { Note, NoteDeletionOptions, NoteMetadata } from './types'
|
||||||
import type { MediaUpload } from '../media/types'
|
import type { MediaUpload } from '../media/types'
|
||||||
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
import { GetApiRequestBuilder } from '../common/api-request-builder/get-api-request-builder'
|
||||||
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
import { PostApiRequestBuilder } from '../common/api-request-builder/post-api-request-builder'
|
||||||
|
@ -23,6 +23,17 @@ export const getNote = async (noteIdOrAlias: string): Promise<Note> => {
|
||||||
return response.asParsedJsonObject()
|
return response.asParsedJsonObject()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the metadata of the specified note.
|
||||||
|
*
|
||||||
|
* @param noteIdOrAlias The id or alias of the note.
|
||||||
|
* @return Metadata of the specified note.
|
||||||
|
*/
|
||||||
|
export const getNoteMetadata = async (noteIdOrAlias: string): Promise<NoteMetadata> => {
|
||||||
|
const response = await new GetApiRequestBuilder<NoteMetadata>(`notes/${noteIdOrAlias}/metadata`).sendRequest()
|
||||||
|
return response.asParsedJsonObject()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of media objects associated with the specified note.
|
* Returns a list of media objects associated with the specified note.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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 styles from './style/sidebar.module.scss'
|
||||||
import { DocumentSidebarMenuSelection } from './types'
|
import { DocumentSidebarMenuSelection } from './types'
|
||||||
import { UsersOnlineSidebarMenu } from './users-online-sidebar-menu/users-online-sidebar-menu'
|
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.
|
* Renders the sidebar for the editor.
|
||||||
|
@ -50,6 +51,7 @@ export const Sidebar: React.FC = () => {
|
||||||
<NoteInfoSidebarEntry hide={selectionIsNotNone} />
|
<NoteInfoSidebarEntry hide={selectionIsNotNone} />
|
||||||
<RevisionSidebarEntry hide={selectionIsNotNone} />
|
<RevisionSidebarEntry hide={selectionIsNotNone} />
|
||||||
<PermissionsSidebarEntry hide={selectionIsNotNone} />
|
<PermissionsSidebarEntry hide={selectionIsNotNone} />
|
||||||
|
<AliasesSidebarEntry hide={selectionIsNotNone} />
|
||||||
<ImportMenuSidebarMenu
|
<ImportMenuSidebarMenu
|
||||||
menuId={DocumentSidebarMenuSelection.IMPORT}
|
menuId={DocumentSidebarMenuSelection.IMPORT}
|
||||||
selectedMenuId={selectedMenu}
|
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>
|
||||||
|
)
|
||||||
|
}
|
25
src/pages/api/mock-backend/private/alias.ts
Normal file
25
src/pages/api/mock-backend/private/alias.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { HttpMethod, respondToMatchingRequest } from '../../../../handler-utils/respond-to-matching-request'
|
||||||
|
import type { Alias, NewAliasDto } from '../../../../api/alias/types'
|
||||||
|
|
||||||
|
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
respondToMatchingRequest<Alias>(
|
||||||
|
HttpMethod.POST,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
{
|
||||||
|
name: (req.body as NewAliasDto).newAlias,
|
||||||
|
noteId: (req.body as NewAliasDto).noteIdOrAlias,
|
||||||
|
primaryAlias: false
|
||||||
|
},
|
||||||
|
201
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
|
@ -11,10 +11,12 @@ import type {
|
||||||
SetNoteDocumentContentAction,
|
SetNoteDocumentContentAction,
|
||||||
SetNotePermissionsFromServerAction,
|
SetNotePermissionsFromServerAction,
|
||||||
UpdateCursorPositionAction,
|
UpdateCursorPositionAction,
|
||||||
|
UpdateMetadataAction,
|
||||||
UpdateNoteTitleByFirstHeadingAction
|
UpdateNoteTitleByFirstHeadingAction
|
||||||
} from './types'
|
} from './types'
|
||||||
import { NoteDetailsActionType } from './types'
|
import { NoteDetailsActionType } from './types'
|
||||||
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
||||||
|
import { getNoteMetadata } from '../../api/notes'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the content of the current note, extracts and parses the frontmatter and extracts the markdown content part.
|
* Sets the content of the current note, extracts and parses the frontmatter and extracts the markdown content part.
|
||||||
|
@ -66,3 +68,14 @@ export const updateCursorPositions = (selection: CursorSelection): void => {
|
||||||
selection
|
selection
|
||||||
} as UpdateCursorPositionAction)
|
} as UpdateCursorPositionAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current note's metadata from the server.
|
||||||
|
*/
|
||||||
|
export const updateMetadata = async (): Promise<void> => {
|
||||||
|
const updatedMetadata = await getNoteMetadata(store.getState().noteDetails.id)
|
||||||
|
store.dispatch({
|
||||||
|
type: NoteDetailsActionType.UPDATE_METADATA,
|
||||||
|
updatedMetadata
|
||||||
|
} as UpdateMetadataAction)
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { buildStateFromUpdateCursorPosition } from './reducers/build-state-from-
|
||||||
import { buildStateFromFirstHeadingUpdate } from './reducers/build-state-from-first-heading-update'
|
import { buildStateFromFirstHeadingUpdate } from './reducers/build-state-from-first-heading-update'
|
||||||
import { buildStateFromServerDto } from './reducers/build-state-from-set-note-data-from-server'
|
import { buildStateFromServerDto } from './reducers/build-state-from-set-note-data-from-server'
|
||||||
import { buildStateFromServerPermissions } from './reducers/build-state-from-server-permissions'
|
import { buildStateFromServerPermissions } from './reducers/build-state-from-server-permissions'
|
||||||
|
import { buildStateFromMetadataUpdate } from './reducers/build-state-from-metadata-update'
|
||||||
|
|
||||||
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
||||||
state: NoteDetails = initialState,
|
state: NoteDetails = initialState,
|
||||||
|
@ -30,6 +31,8 @@ export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
||||||
return buildStateFromFirstHeadingUpdate(state, action.firstHeading)
|
return buildStateFromFirstHeadingUpdate(state, action.firstHeading)
|
||||||
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
|
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
|
||||||
return buildStateFromServerDto(action.noteFromServer)
|
return buildStateFromServerDto(action.noteFromServer)
|
||||||
|
case NoteDetailsActionType.UPDATE_METADATA:
|
||||||
|
return buildStateFromMetadataUpdate(state, action.updatedMetadata)
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { initialState } from '../initial-state'
|
||||||
|
import type { NoteMetadata } from '../../../api/notes/types'
|
||||||
|
import type { NoteDetails } from '../types/note-details'
|
||||||
|
import { buildStateFromMetadataUpdate } from './build-state-from-metadata-update'
|
||||||
|
|
||||||
|
describe('build state from server permissions', () => {
|
||||||
|
it('creates a new state with the given permissions', () => {
|
||||||
|
const state: NoteDetails = { ...initialState }
|
||||||
|
const metadata: NoteMetadata = {
|
||||||
|
updateUsername: 'test',
|
||||||
|
permissions: {
|
||||||
|
owner: null,
|
||||||
|
sharedToGroups: [],
|
||||||
|
sharedToUsers: []
|
||||||
|
},
|
||||||
|
editedBy: [],
|
||||||
|
primaryAddress: 'test-id',
|
||||||
|
tags: ['test'],
|
||||||
|
description: 'test',
|
||||||
|
id: 'test-id',
|
||||||
|
aliases: [],
|
||||||
|
title: 'test',
|
||||||
|
version: 2,
|
||||||
|
viewCount: 42,
|
||||||
|
createdAt: '2022-09-18T18:51:00.000+02:00',
|
||||||
|
updatedAt: '2022-09-18T18:52:00.000+02:00'
|
||||||
|
}
|
||||||
|
expect(buildStateFromMetadataUpdate(state, metadata)).toStrictEqual({
|
||||||
|
...state,
|
||||||
|
updateUsername: 'test',
|
||||||
|
permissions: {
|
||||||
|
owner: null,
|
||||||
|
sharedToGroups: [],
|
||||||
|
sharedToUsers: []
|
||||||
|
},
|
||||||
|
editedBy: [],
|
||||||
|
primaryAddress: 'test-id',
|
||||||
|
id: 'test-id',
|
||||||
|
aliases: [],
|
||||||
|
title: 'test',
|
||||||
|
version: 2,
|
||||||
|
viewCount: 42,
|
||||||
|
createdAt: 1663519860,
|
||||||
|
updatedAt: 1663519920
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { NoteMetadata } from '../../../api/notes/types'
|
||||||
|
import type { NoteDetails } from '../types/note-details'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link NoteDetails} redux state from a note metadata DTO received from the HTTP API.
|
||||||
|
* @param state The previous state to update.
|
||||||
|
* @param noteMetadata The updated metadata from the API.
|
||||||
|
* @return An updated {@link NoteDetails} redux state.
|
||||||
|
*/
|
||||||
|
export const buildStateFromMetadataUpdate = (state: NoteDetails, noteMetadata: NoteMetadata): NoteDetails => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
updateUsername: noteMetadata.updateUsername,
|
||||||
|
permissions: noteMetadata.permissions,
|
||||||
|
editedBy: noteMetadata.editedBy,
|
||||||
|
primaryAddress: noteMetadata.primaryAddress,
|
||||||
|
id: noteMetadata.id,
|
||||||
|
aliases: noteMetadata.aliases,
|
||||||
|
title: noteMetadata.title,
|
||||||
|
version: noteMetadata.version,
|
||||||
|
viewCount: noteMetadata.viewCount,
|
||||||
|
createdAt: DateTime.fromISO(noteMetadata.createdAt).toSeconds(),
|
||||||
|
updatedAt: DateTime.fromISO(noteMetadata.updatedAt).toSeconds()
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,9 +7,9 @@
|
||||||
import type { NoteDetails } from '../types/note-details'
|
import type { NoteDetails } from '../types/note-details'
|
||||||
import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
|
import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
|
||||||
import { initialState } from '../initial-state'
|
import { initialState } from '../initial-state'
|
||||||
import { DateTime } from 'luxon'
|
|
||||||
import { calculateLineStartIndexes } from '../calculate-line-start-indexes'
|
import { calculateLineStartIndexes } from '../calculate-line-start-indexes'
|
||||||
import type { Note } from '../../../api/notes/types'
|
import type { Note } from '../../../api/notes/types'
|
||||||
|
import { buildStateFromMetadataUpdate } from './build-state-from-metadata-update'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a {@link NoteDetails} redux state from a DTO received as an API response.
|
* Builds a {@link NoteDetails} redux state from a DTO received as an API response.
|
||||||
|
@ -28,25 +28,15 @@ export const buildStateFromServerDto = (dto: Note): NoteDetails => {
|
||||||
* @return The NoteDetails object corresponding to the DTO.
|
* @return The NoteDetails object corresponding to the DTO.
|
||||||
*/
|
*/
|
||||||
const convertNoteDtoToNoteDetails = (note: Note): NoteDetails => {
|
const convertNoteDtoToNoteDetails = (note: Note): NoteDetails => {
|
||||||
|
const stateWithMetadata = buildStateFromMetadataUpdate(initialState, note.metadata)
|
||||||
const newLines = note.content.split('\n')
|
const newLines = note.content.split('\n')
|
||||||
return {
|
return {
|
||||||
...initialState,
|
...stateWithMetadata,
|
||||||
updateUsername: note.metadata.updateUsername,
|
|
||||||
permissions: note.metadata.permissions,
|
|
||||||
editedBy: note.metadata.editedBy,
|
|
||||||
primaryAddress: note.metadata.primaryAddress,
|
|
||||||
id: note.metadata.id,
|
|
||||||
aliases: note.metadata.aliases,
|
|
||||||
title: note.metadata.title,
|
|
||||||
version: note.metadata.version,
|
|
||||||
viewCount: note.metadata.viewCount,
|
|
||||||
markdownContent: {
|
markdownContent: {
|
||||||
plain: note.content,
|
plain: note.content,
|
||||||
lines: newLines,
|
lines: newLines,
|
||||||
lineStartIndexes: calculateLineStartIndexes(newLines)
|
lineStartIndexes: calculateLineStartIndexes(newLines)
|
||||||
},
|
},
|
||||||
rawFrontmatter: '',
|
rawFrontmatter: ''
|
||||||
createdAt: DateTime.fromISO(note.metadata.createdAt).toSeconds(),
|
|
||||||
updatedAt: DateTime.fromISO(note.metadata.updatedAt).toSeconds()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Action } from 'redux'
|
import type { Action } from 'redux'
|
||||||
import type { Note, NotePermissions } from '../../api/notes/types'
|
import type { Note, NoteMetadata, NotePermissions } from '../../api/notes/types'
|
||||||
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
|
||||||
|
|
||||||
export enum NoteDetailsActionType {
|
export enum NoteDetailsActionType {
|
||||||
|
@ -13,7 +13,8 @@ export enum NoteDetailsActionType {
|
||||||
SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set',
|
SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set',
|
||||||
SET_NOTE_PERMISSIONS_FROM_SERVER = 'note-details/data/permissions/set',
|
SET_NOTE_PERMISSIONS_FROM_SERVER = 'note-details/data/permissions/set',
|
||||||
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
|
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
|
||||||
UPDATE_CURSOR_POSITION = 'note-details/updateCursorPosition'
|
UPDATE_CURSOR_POSITION = 'note-details/updateCursorPosition',
|
||||||
|
UPDATE_METADATA = 'note-details/update-metadata'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NoteDetailsActions =
|
export type NoteDetailsActions =
|
||||||
|
@ -22,6 +23,7 @@ export type NoteDetailsActions =
|
||||||
| SetNotePermissionsFromServerAction
|
| SetNotePermissionsFromServerAction
|
||||||
| UpdateNoteTitleByFirstHeadingAction
|
| UpdateNoteTitleByFirstHeadingAction
|
||||||
| UpdateCursorPositionAction
|
| UpdateCursorPositionAction
|
||||||
|
| UpdateMetadataAction
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action for updating the document content of the currently loaded note.
|
* Action for updating the document content of the currently loaded note.
|
||||||
|
@ -59,3 +61,11 @@ export interface UpdateCursorPositionAction extends Action<NoteDetailsActionType
|
||||||
type: NoteDetailsActionType.UPDATE_CURSOR_POSITION
|
type: NoteDetailsActionType.UPDATE_CURSOR_POSITION
|
||||||
selection: CursorSelection
|
selection: CursorSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for updating the metadata of the current note.
|
||||||
|
*/
|
||||||
|
export interface UpdateMetadataAction extends Action<NoteDetailsActionType> {
|
||||||
|
type: NoteDetailsActionType.UPDATE_METADATA
|
||||||
|
updatedMetadata: NoteMetadata
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue