✨(frontend) edit title inline
We can now edit the title of the document inline. This is a feature that is very useful for users who want to change the title of the document without having to go to the document management page.
This commit is contained in:
@@ -14,6 +14,7 @@ and this project adheres to
|
|||||||
- ✨(ci) add security scan #291
|
- ✨(ci) add security scan #291
|
||||||
- ✨(frontend) Activate versions feature #240
|
- ✨(frontend) Activate versions feature #240
|
||||||
- ✨(frontend) one-click document creation #275
|
- ✨(frontend) one-click document creation #275
|
||||||
|
- ✨(frontend) edit title inline #275
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
|||||||
@@ -29,32 +29,21 @@ export const createDoc = async (
|
|||||||
length: number,
|
length: number,
|
||||||
isPublic: boolean = false,
|
isPublic: boolean = false,
|
||||||
) => {
|
) => {
|
||||||
const buttonCreate = page.getByRole('button', {
|
|
||||||
name: 'Create the document',
|
|
||||||
});
|
|
||||||
|
|
||||||
const randomDocs = randomName(docName, browserName, length);
|
const randomDocs = randomName(docName, browserName, length);
|
||||||
|
|
||||||
for (let i = 0; i < randomDocs.length; i++) {
|
for (let i = 0; i < randomDocs.length; i++) {
|
||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
await header.locator('h2').getByText('Docs').click();
|
await header.locator('h2').getByText('Docs').click();
|
||||||
|
|
||||||
const buttonCreateHomepage = page.getByRole('button', {
|
|
||||||
name: 'Create a new document',
|
|
||||||
});
|
|
||||||
await buttonCreateHomepage.click();
|
|
||||||
|
|
||||||
// Fill input
|
|
||||||
await page
|
await page
|
||||||
.getByRole('textbox', {
|
.getByRole('button', {
|
||||||
name: 'Document name',
|
name: 'Create a new document',
|
||||||
})
|
})
|
||||||
.fill(randomDocs[i]);
|
.click();
|
||||||
|
|
||||||
await expect(buttonCreate).toBeEnabled();
|
await page.getByRole('heading', { name: 'Untitled document' }).click();
|
||||||
await buttonCreate.click();
|
await page.keyboard.type(randomDocs[i]);
|
||||||
|
await page.getByText('Created at ').click();
|
||||||
await expect(page.locator('h2').getByText(randomDocs[i])).toBeVisible();
|
|
||||||
|
|
||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
Doc,
|
Doc,
|
||||||
Role,
|
Role,
|
||||||
currentDocRole,
|
currentDocRole,
|
||||||
useTransRole,
|
useTrans,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
import { ModalVersion, Versions } from '@/features/docs/doc-versioning';
|
import { ModalVersion, Versions } from '@/features/docs/doc-versioning';
|
||||||
import { useDate } from '@/hook';
|
import { useDate } from '@/hook';
|
||||||
|
|
||||||
import { DocTagPublic } from './DocTagPublic';
|
import { DocTagPublic } from './DocTagPublic';
|
||||||
|
import { DocTitle } from './DocTitle';
|
||||||
import { DocToolBox } from './DocToolBox';
|
import { DocToolBox } from './DocToolBox';
|
||||||
|
|
||||||
interface DocHeaderProps {
|
interface DocHeaderProps {
|
||||||
@@ -25,7 +26,7 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
|
|||||||
const { colorsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { formatDate } = useDate();
|
const { formatDate } = useDate();
|
||||||
const transRole = useTransRole();
|
const { transRole } = useTrans();
|
||||||
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,14 +55,8 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
|
|||||||
$background={colorsTokens()['greyscale-100']}
|
$background={colorsTokens()['greyscale-100']}
|
||||||
$margin={{ horizontal: 'small' }}
|
$margin={{ horizontal: 'small' }}
|
||||||
/>
|
/>
|
||||||
<Box $gap="1rem" $direction="row">
|
<Box $gap="1rem" $direction="row" $align="center">
|
||||||
<Text
|
<DocTitle doc={doc} />
|
||||||
as="h2"
|
|
||||||
$align="center"
|
|
||||||
$margin={{ all: 'none', left: 'tiny' }}
|
|
||||||
>
|
|
||||||
{doc.title}
|
|
||||||
</Text>
|
|
||||||
{versionId && (
|
{versionId && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
VariantType,
|
||||||
|
useToastProvider,
|
||||||
|
} from '@openfun/cunningham-react';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { createGlobalStyle } from 'styled-components';
|
||||||
|
|
||||||
|
import { Box, Text } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import {
|
||||||
|
Doc,
|
||||||
|
KEY_DOC,
|
||||||
|
KEY_LIST_DOC,
|
||||||
|
useTrans,
|
||||||
|
useUpdateDoc,
|
||||||
|
} from '@/features/docs/doc-management';
|
||||||
|
import { isFirefox } from '@/utils/userAgent';
|
||||||
|
|
||||||
|
const DocTitleStyle = createGlobalStyle`
|
||||||
|
.c__tooltip {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface DocTitleProps {
|
||||||
|
doc: Doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocTitle = ({ doc }: DocTitleProps) => {
|
||||||
|
if (!doc.abilities.partial_update) {
|
||||||
|
return (
|
||||||
|
<Text as="h2" $align="center" $margin={{ all: 'none', left: 'tiny' }}>
|
||||||
|
{doc.title}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DocTitleInput doc={doc} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
const [titleDisplay, setTitleDisplay] = useState(doc.title);
|
||||||
|
const { toast } = useToastProvider();
|
||||||
|
const { untitledDocument } = useTrans();
|
||||||
|
const isUntitled = titleDisplay === untitledDocument;
|
||||||
|
|
||||||
|
const { mutate: updateDoc } = useUpdateDoc({
|
||||||
|
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTitleSubmit = (inputText: string) => {
|
||||||
|
let sanitizedTitle = inputText.trim();
|
||||||
|
sanitizedTitle = sanitizedTitle.replace(/(\r\n|\n|\r)/gm, '');
|
||||||
|
|
||||||
|
// When blank we set to untitled
|
||||||
|
if (!sanitizedTitle) {
|
||||||
|
sanitizedTitle = untitledDocument;
|
||||||
|
setTitleDisplay(sanitizedTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If mutation we update
|
||||||
|
if (sanitizedTitle !== doc.title) {
|
||||||
|
updateDoc(
|
||||||
|
{ id: doc.id, title: sanitizedTitle },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
if (sanitizedTitle !== untitledDocument) {
|
||||||
|
toast(
|
||||||
|
t('Document title updated successfully'),
|
||||||
|
VariantType.SUCCESS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleTitleSubmit(e.currentTarget.textContent || '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnClick = () => {
|
||||||
|
if (isUntitled) {
|
||||||
|
setTitleDisplay('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DocTitleStyle />
|
||||||
|
<Tooltip content={t('Rename')} placement="top">
|
||||||
|
<Box
|
||||||
|
as="h2"
|
||||||
|
$radius="4px"
|
||||||
|
$padding={{ horizontal: 'tiny', vertical: '4px' }}
|
||||||
|
$align="center"
|
||||||
|
$margin="none"
|
||||||
|
contentEditable={isFirefox() ? 'true' : 'plaintext-only'}
|
||||||
|
onClick={handleOnClick}
|
||||||
|
onBlurCapture={(e) =>
|
||||||
|
handleTitleSubmit(e.currentTarget.textContent || '')
|
||||||
|
}
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
suppressContentEditableWarning={true}
|
||||||
|
$color={
|
||||||
|
isUntitled
|
||||||
|
? colorsTokens()['greyscale-200']
|
||||||
|
: colorsTokens()['greyscale-text']
|
||||||
|
}
|
||||||
|
$css={`
|
||||||
|
${isUntitled && 'font-style: italic;'}
|
||||||
|
cursor: text;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
transition: box-shadow 0.5s, border-color 0.5s;
|
||||||
|
border: 1px dashed transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(0, 123, 255, 0.25);
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{titleDisplay}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user