Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cccc1ae6f | |||
| de9259f636 | |||
| 0f49230231 | |||
| cbe6a67704 | |||
| f91223fe4a | |||
| 330096eb47 | |||
| ff995c6cd9 | |||
| 2e4a1b8ff9 | |||
| 004d637c8b | |||
| 8a0330a30f | |||
| 677392b89b | |||
| b8e1d12aea |
@@ -15,10 +15,14 @@ and this project adheres to
|
||||
- 💄(frontend) improve comments highlights #1961
|
||||
- ♿️(frontend) improve BoxButton a11y and native button semantics #2103
|
||||
- ♿️(frontend) improve language picker accessibility #2069
|
||||
- ♿️(frontend) add aria-hidden to decorative icons in dropdown menu #2093
|
||||
- 🐛(backend) move lock table closer to the insert operation targeted
|
||||
- ♿️(frontend) replace ARIA grid pattern with list in docs grid #2131
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
|
||||
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
|
||||
|
||||
## [v4.8.3] - 2026-03-23
|
||||
|
||||
|
||||
@@ -674,17 +674,8 @@ class DocumentViewSet(
|
||||
|
||||
return drf.response.Response(serializer.data)
|
||||
|
||||
@transaction.atomic
|
||||
def perform_create(self, serializer):
|
||||
"""Set the current user as creator and owner of the newly created object."""
|
||||
|
||||
# locks the table to ensure safe concurrent access
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
# Remove file from validated_data as it's not a model field
|
||||
# Process it if present
|
||||
uploaded_file = serializer.validated_data.pop("file", None)
|
||||
@@ -702,15 +693,25 @@ class DocumentViewSet(
|
||||
)
|
||||
serializer.validated_data["content"] = converted_content
|
||||
serializer.validated_data["title"] = uploaded_file.name
|
||||
logger.info("conversion ended successfully")
|
||||
except ConversionError as err:
|
||||
logger.error("could not convert file content with error: %s", err)
|
||||
raise drf.exceptions.ValidationError(
|
||||
{"file": ["Could not convert file content"]}
|
||||
) from err
|
||||
|
||||
obj = models.Document.add_root(
|
||||
creator=self.request.user,
|
||||
**serializer.validated_data,
|
||||
)
|
||||
with transaction.atomic():
|
||||
# locks the table to ensure safe concurrent access
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
obj = models.Document.add_root(
|
||||
creator=self.request.user,
|
||||
**serializer.validated_data,
|
||||
)
|
||||
serializer.instance = obj
|
||||
models.DocumentAccess.objects.create(
|
||||
document=obj,
|
||||
@@ -827,6 +828,7 @@ class DocumentViewSet(
|
||||
|
||||
queryset = self.queryset.filter(path_list)
|
||||
queryset = queryset.filter(id__in=favorite_documents_ids)
|
||||
queryset = queryset.filter(ancestors_deleted_at__isnull=True)
|
||||
queryset = queryset.annotate_user_roles(user)
|
||||
queryset = queryset.annotate(
|
||||
is_favorite=db.Value(True, output_field=db.BooleanField())
|
||||
|
||||
+10
-11
@@ -267,6 +267,16 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
|
||||
# transaction.atomic is used in a context manager to avoid a transaction if
|
||||
# the settings USER_ONBOARDING_SANDBOX_DOCUMENT is unused
|
||||
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
|
||||
try:
|
||||
template_document = Document.objects.get(id=sandbox_id)
|
||||
except Document.DoesNotExist:
|
||||
logger.warning(
|
||||
"Onboarding sandbox document with id %s does not exist. Skipping.",
|
||||
sandbox_id,
|
||||
)
|
||||
return
|
||||
|
||||
with transaction.atomic():
|
||||
# locks the table to ensure safe concurrent access
|
||||
with connection.cursor() as cursor:
|
||||
@@ -274,17 +284,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
f'LOCK TABLE "{Document._meta.db_table}" ' # noqa: SLF001
|
||||
"IN SHARE ROW EXCLUSIVE MODE;"
|
||||
)
|
||||
|
||||
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
|
||||
try:
|
||||
template_document = Document.objects.get(id=sandbox_id)
|
||||
except Document.DoesNotExist:
|
||||
logger.warning(
|
||||
"Onboarding sandbox document with id %s does not exist. Skipping.",
|
||||
sandbox_id,
|
||||
)
|
||||
return
|
||||
|
||||
sandbox_document = Document.add_root(
|
||||
title=template_document.title,
|
||||
content=template_document.content,
|
||||
|
||||
@@ -45,6 +45,8 @@ class Converter:
|
||||
def convert(self, data, content_type, accept):
|
||||
"""Convert input into other formats using external microservices."""
|
||||
|
||||
logger.info("converting content from %s to %s", content_type, accept)
|
||||
|
||||
if content_type == mime_types.DOCX and accept == mime_types.YJS:
|
||||
blocknote_data = self.docspec.convert(
|
||||
data, mime_types.DOCX, mime_types.BLOCKNOTE
|
||||
|
||||
@@ -114,3 +114,29 @@ def test_api_document_favorite_list_with_favorite_children():
|
||||
assert content[0]["id"] == str(children[0].id)
|
||||
assert content[1]["id"] == str(children[1].id)
|
||||
assert content[2]["id"] == str(access.document.id)
|
||||
|
||||
|
||||
def test_api_document_favorite_list_with_deleted_child():
|
||||
"""
|
||||
Authenticated users should not see deleted documents in their favorite list.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
root = factories.DocumentFactory(creator=user, users=[user], favorited_by=[user])
|
||||
child1, child2 = factories.DocumentFactory.create_batch(
|
||||
2, parent=root, favorited_by=[user]
|
||||
)
|
||||
|
||||
child1.delete()
|
||||
|
||||
response = client.get("/api/v1.0/documents/favorite_list/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["count"] == 2
|
||||
|
||||
content = response.json()["results"]
|
||||
|
||||
assert content[0]["id"] == str(root.id)
|
||||
assert content[1]["id"] == str(child2.id)
|
||||
|
||||
@@ -400,7 +400,7 @@ test.describe('Doc grid dnd mobile', () => {
|
||||
await expect(page.getByTestId('docs-grid')).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
await expect(docsGrid.getByRole('row').first()).toBeVisible();
|
||||
await expect(docsGrid.getByRole('listitem').first()).toBeVisible();
|
||||
await expect(docsGrid.locator('.--docs--grid-droppable')).toHaveCount(0);
|
||||
|
||||
await createDoc(page, 'Draggable doc mobile', browserName, 1, true);
|
||||
|
||||
@@ -76,7 +76,7 @@ test.describe('Documents Grid mobile', () => {
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const rows = docsGrid.getByRole('row');
|
||||
const rows = docsGrid.getByRole('listitem');
|
||||
const row = rows.filter({
|
||||
hasText: 'My mocked document',
|
||||
});
|
||||
@@ -289,6 +289,29 @@ test.describe('Documents Grid', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('opens a document with keyboard (Tab + Enter)', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
|
||||
const [docTitle] = await createDoc(page, 'keyboard-nav-test', browserName);
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const row = await getGridRow(page, docTitle);
|
||||
const link = row.getByRole('link').first();
|
||||
|
||||
await link.focus();
|
||||
await expect(link).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(page).toHaveURL(/\/docs\//);
|
||||
await verifyDocName(page, docTitle);
|
||||
});
|
||||
|
||||
test('checks the infinite scroll', async ({ page }) => {
|
||||
let docs: SmallDoc[];
|
||||
const responsePromisePage1 = page.waitForResponse((response) => {
|
||||
|
||||
@@ -183,11 +183,11 @@ export const verifyDocName = async (page: Page, docName: string) => {
|
||||
};
|
||||
|
||||
export const getGridRow = async (page: Page, title: string) => {
|
||||
const docsGrid = page.getByRole('grid');
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const rows = docsGrid.getByRole('row');
|
||||
const rows = docsGrid.getByRole('listitem');
|
||||
|
||||
const row = rows
|
||||
.filter({
|
||||
@@ -215,7 +215,7 @@ export const goToGridDoc = async (
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const rows = docsGrid.getByRole('row');
|
||||
const rows = docsGrid.getByRole('listitem');
|
||||
|
||||
const row = title
|
||||
? rows.filter({
|
||||
|
||||
@@ -275,7 +275,6 @@ export const DropdownMenu = ({
|
||||
<Box
|
||||
$theme="neutral"
|
||||
$variation={isDisabled ? 'tertiary' : 'primary'}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{option.icon}
|
||||
</Box>
|
||||
|
||||
@@ -118,13 +118,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const options: DropdownMenuOption[] = [
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: <GroupSVG width={24} height={24} />,
|
||||
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: modalShare.open,
|
||||
show: isSmallMobile,
|
||||
},
|
||||
{
|
||||
label: t('Export'),
|
||||
icon: <DownloadSVG width={24} height={24} />,
|
||||
icon: <DownloadSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
setIsModalExportOpen(true);
|
||||
},
|
||||
@@ -133,9 +133,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: doc.is_favorite ? (
|
||||
<KeepOffSVG width={24} height={24} />
|
||||
<KeepOffSVG width={24} height={24} aria-hidden="true" />
|
||||
) : (
|
||||
<KeepSVG width={24} height={24} />
|
||||
<KeepSVG width={24} height={24} aria-hidden="true" />
|
||||
),
|
||||
callback: () => {
|
||||
if (doc.is_favorite) {
|
||||
@@ -148,7 +148,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: <HistorySVG width={24} height={24} />,
|
||||
icon: <HistorySVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.versions_list,
|
||||
callback: () => {
|
||||
selectHistoryModal.open();
|
||||
@@ -158,7 +158,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Remove emoji'),
|
||||
icon: <RemoveEmojiSVG width={24} height={24} />,
|
||||
icon: <RemoveEmojiSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
updateDocEmoji(doc.id, doc.title ?? '', '');
|
||||
},
|
||||
@@ -167,7 +167,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Add emoji'),
|
||||
icon: <AddEmojiSVG width={24} height={24} />,
|
||||
icon: <AddEmojiSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
updateDocEmoji(doc.id, doc.title ?? '', '📄');
|
||||
},
|
||||
@@ -176,12 +176,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Copy link'),
|
||||
icon: <AddLinkSVG width={24} height={24} />,
|
||||
icon: <AddLinkSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: copyDocLink,
|
||||
},
|
||||
{
|
||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
||||
icon: <MarkdownCopySVG width={24} height={24} />,
|
||||
icon: <MarkdownCopySVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
void copyCurrentEditorToClipboard('markdown');
|
||||
},
|
||||
@@ -189,7 +189,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Duplicate'),
|
||||
icon: <ContentCopySVG width={24} height={24} />,
|
||||
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.duplicate,
|
||||
callback: () => {
|
||||
duplicateDoc({
|
||||
@@ -202,7 +202,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
{
|
||||
label: isChild ? t('Delete sub-document') : t('Delete document'),
|
||||
icon: <DeleteSVG width={24} height={24} />,
|
||||
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.destroy,
|
||||
callback: () => {
|
||||
setIsModalRemoveOpen(true);
|
||||
|
||||
+60
-1
@@ -1,4 +1,9 @@
|
||||
import { DndContext, DragOverlay, Modifier } from '@dnd-kit/core';
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
Modifier,
|
||||
UniqueIdentifier,
|
||||
} from '@dnd-kit/core';
|
||||
import { getEventCoordinates } from '@dnd-kit/utilities';
|
||||
import { useModal } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
|
||||
@@ -107,6 +112,59 @@ export const DraggableDocGridContentList = ({
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dndAccessibility = useMemo(
|
||||
() => ({
|
||||
screenReaderInstructions: {
|
||||
draggable: t(
|
||||
'To pick up a draggable item, press space or enter. While dragging, use the arrow keys to move the item. Press space or enter again to drop the item in its new position, or press escape to cancel.',
|
||||
),
|
||||
},
|
||||
announcements: {
|
||||
onDragStart({ active }: { active: { id: UniqueIdentifier } }) {
|
||||
return t('Picked up document {{id}}.', { id: active.id });
|
||||
},
|
||||
onDragOver({
|
||||
active,
|
||||
over,
|
||||
}: {
|
||||
active: { id: UniqueIdentifier };
|
||||
over: { id: UniqueIdentifier } | null;
|
||||
}) {
|
||||
if (over) {
|
||||
return t('Document {{activeId}} is over document {{overId}}.', {
|
||||
activeId: active.id,
|
||||
overId: over.id,
|
||||
});
|
||||
}
|
||||
return t('Document {{id}} is no longer over a droppable area.', {
|
||||
id: active.id,
|
||||
});
|
||||
},
|
||||
onDragEnd({
|
||||
active,
|
||||
over,
|
||||
}: {
|
||||
active: { id: UniqueIdentifier };
|
||||
over: { id: UniqueIdentifier } | null;
|
||||
}) {
|
||||
if (over) {
|
||||
return t(
|
||||
'Document {{activeId}} was dropped over document {{overId}}.',
|
||||
{ activeId: active.id, overId: over.id },
|
||||
);
|
||||
}
|
||||
return t('Document {{id}} was dropped.', { id: active.id });
|
||||
},
|
||||
onDragCancel({ active }: { active: { id: UniqueIdentifier } }) {
|
||||
return t('Dragging was cancelled. Document {{id}} was dropped.', {
|
||||
id: active.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const overlayText = useMemo(() => {
|
||||
if (!canDrag) {
|
||||
return t('You must be the owner to move the document');
|
||||
@@ -147,6 +205,7 @@ export const DraggableDocGridContentList = ({
|
||||
modifiers={[snapToTopLeft]}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
accessibility={dndAccessibility}
|
||||
>
|
||||
{docs.map((doc) => (
|
||||
<DraggableDocGridItem
|
||||
|
||||
@@ -140,36 +140,30 @@ export const DocsGrid = ({
|
||||
$overflow="auto"
|
||||
$padding={{ vertical: 'sm', horizontal: isDesktop ? 'md' : 'xs' }}
|
||||
>
|
||||
<Box role="grid" aria-label={t('Documents grid')}>
|
||||
<Box role="rowgroup">
|
||||
<Box
|
||||
$direction="row"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap="10px"
|
||||
data-testid="docs-grid-header"
|
||||
role="row"
|
||||
>
|
||||
<Box $flex={flexLeft} $padding="3xs" role="columnheader">
|
||||
<Text $size="xs" $variation="secondary" $weight="500">
|
||||
{t('Name')}
|
||||
<Box aria-label={t('Documents grid')}>
|
||||
<Box
|
||||
$direction="row"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap="10px"
|
||||
data-testid="docs-grid-header"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Box $flex={flexLeft} $padding="3xs">
|
||||
<Text $size="xs" $variation="secondary" $weight="500">
|
||||
{t('Name')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box $flex={flexRight} $padding={{ vertical: '3xs' }}>
|
||||
<Text $size="xs" $weight="500" $variation="secondary">
|
||||
{DocDefaultFilter.TRASHBIN === target
|
||||
? t('Days remaining')
|
||||
: t('Updated at')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box
|
||||
$flex={flexRight}
|
||||
$padding={{ vertical: '3xs' }}
|
||||
role="columnheader"
|
||||
>
|
||||
<Text $size="xs" $weight="500" $variation="secondary">
|
||||
{DocDefaultFilter.TRASHBIN === target
|
||||
? t('Days remaining')
|
||||
: t('Updated at')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box role="rowgroup">
|
||||
<Box role="list">
|
||||
<DocGridContentList docs={docs} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+6
-6
@@ -72,9 +72,9 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
{
|
||||
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||
icon: doc.is_favorite ? (
|
||||
<KeepOffSVG width={24} height={24} />
|
||||
<KeepOffSVG width={24} height={24} aria-hidden="true" />
|
||||
) : (
|
||||
<KeepSVG width={24} height={24} />
|
||||
<KeepSVG width={24} height={24} aria-hidden="true" />
|
||||
),
|
||||
callback: () => {
|
||||
if (doc.is_favorite) {
|
||||
@@ -88,7 +88,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Share'),
|
||||
icon: <GroupSVG width={24} height={24} />,
|
||||
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
shareModal.open();
|
||||
},
|
||||
@@ -97,7 +97,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Move into a doc'),
|
||||
icon: <DocMoveInSVG width={24} height={24} />,
|
||||
icon: <DocMoveInSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
importModal.open();
|
||||
},
|
||||
@@ -106,7 +106,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Duplicate'),
|
||||
icon: <ContentCopySVG width={24} height={24} />,
|
||||
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
|
||||
disabled: !doc.abilities.duplicate,
|
||||
callback: () => {
|
||||
duplicateDoc({
|
||||
@@ -119,7 +119,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
},
|
||||
{
|
||||
label: t('Delete'),
|
||||
icon: <DeleteSVG width={24} height={24} />,
|
||||
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
|
||||
callback: () => deleteModal.open(),
|
||||
disabled: !doc.abilities.destroy,
|
||||
testId: `docs-grid-actions-remove-${doc.id}`,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { flexLeft, flexRight } = useResponsiveDocGrid();
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const dateToDisplay = useDateToDisplay(doc, isInTrashbin);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -46,7 +47,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
$direction="row"
|
||||
$width="100%"
|
||||
$align="center"
|
||||
role="row"
|
||||
role="listitem"
|
||||
$gap="20px"
|
||||
$padding={{ vertical: '4xs', horizontal: isDesktop ? 'base' : 'xs' }}
|
||||
$css={css`
|
||||
@@ -65,7 +66,6 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
>
|
||||
<Box
|
||||
$flex={flexLeft}
|
||||
role="gridcell"
|
||||
$css={css`
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
@@ -90,9 +90,15 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
||||
$align="center"
|
||||
$justify={isDesktop ? 'space-between' : 'flex-end'}
|
||||
$gap="32px"
|
||||
role="gridcell"
|
||||
>
|
||||
<StyledLink href={`/docs/${doc.id}`} tabIndex={-1}>
|
||||
<StyledLink
|
||||
href={`/docs/${doc.id}`}
|
||||
tabIndex={-1}
|
||||
aria-label={t('{{title}}, updated {{date}}', {
|
||||
title: doc.title || untitledDocument,
|
||||
date: dateToDisplay,
|
||||
})}
|
||||
>
|
||||
<DocsGridItemDate
|
||||
doc={doc}
|
||||
isDesktop={isDesktop}
|
||||
@@ -196,23 +202,11 @@ const IconPublic = ({ isPublic }: { isPublic: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DocsGridItemDate = ({
|
||||
doc,
|
||||
isDesktop,
|
||||
isInTrashbin,
|
||||
}: {
|
||||
doc: Doc;
|
||||
isDesktop: boolean;
|
||||
isInTrashbin: boolean;
|
||||
}) => {
|
||||
const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => {
|
||||
const { data: config } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
const { relativeDate, calculateDaysLeft } = useDate();
|
||||
|
||||
if (!isDesktop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dateToDisplay = relativeDate(doc.updated_at);
|
||||
|
||||
if (isInTrashbin && config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) {
|
||||
@@ -224,6 +218,24 @@ export const DocsGridItemDate = ({
|
||||
dateToDisplay = `${daysLeft} ${t('days', { count: daysLeft })}`;
|
||||
}
|
||||
|
||||
return dateToDisplay;
|
||||
};
|
||||
|
||||
export const DocsGridItemDate = ({
|
||||
doc,
|
||||
isDesktop,
|
||||
isInTrashbin,
|
||||
}: {
|
||||
doc: Doc;
|
||||
isDesktop: boolean;
|
||||
isInTrashbin: boolean;
|
||||
}) => {
|
||||
const dateToDisplay = useDateToDisplay(doc, isInTrashbin);
|
||||
|
||||
if (!isDesktop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
$size="xs"
|
||||
|
||||
Reference in New Issue
Block a user