Skip to content

Commit 155e7df

Browse files
committed
✨(frontend) interlinking custom inline content
We want to be able to interlink documents in the editor. We created a custom inline content that allows users to interlink documents.
1 parent afa48b6 commit 155e7df

File tree

21 files changed

+672
-140
lines changed

21 files changed

+672
-140
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to
1414
- ✨(backend) allow masking documents from the list view #1171
1515
- ✨(frontend) subdocs can manage link reach #1190
1616
- ✨(frontend) add duplicate action to doc tree #1175
17+
- ✨(frontend) Interlinking doc #904
1718
- ✨(frontend) add multi columns support for editor #1219
1819

1920
### Changed

src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,4 +706,59 @@ test.describe('Doc Editor', () => {
706706
'pink',
707707
);
708708
});
709+
710+
test('it checks interlink feature', async ({ page, browserName }) => {
711+
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
712+
713+
await verifyDocName(page, randomDoc);
714+
715+
const { name: docChild1 } = await createRootSubPage(
716+
page,
717+
browserName,
718+
'doc-interlink-child-1',
719+
);
720+
721+
await verifyDocName(page, docChild1);
722+
723+
const { name: docChild2 } = await createRootSubPage(
724+
page,
725+
browserName,
726+
'doc-interlink-child-2',
727+
);
728+
729+
await verifyDocName(page, docChild2);
730+
731+
await page.locator('.bn-block-outer').last().fill('/');
732+
await page.getByText('Link a doc').first().click();
733+
734+
const input = page.locator(
735+
"span[data-inline-content-type='interlinkingSearchInline'] input",
736+
);
737+
const searchContainer = page.locator('.quick-search-container');
738+
739+
await input.fill('doc-interlink');
740+
741+
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
742+
await expect(searchContainer.getByText(docChild1)).toBeVisible();
743+
await expect(searchContainer.getByText(docChild2)).toBeVisible();
744+
745+
await input.pressSequentially('-child');
746+
747+
await expect(searchContainer.getByText(docChild1)).toBeVisible();
748+
await expect(searchContainer.getByText(docChild2)).toBeVisible();
749+
await expect(searchContainer.getByText(randomDoc)).toBeHidden();
750+
751+
// use keydown to select the second result
752+
await page.keyboard.press('ArrowDown');
753+
await page.keyboard.press('Enter');
754+
755+
const interlink = page.getByRole('link', {
756+
name: 'child-2',
757+
});
758+
759+
await expect(interlink).toBeVisible();
760+
await interlink.click();
761+
762+
await verifyDocName(page, docChild2);
763+
});
709764
});

src/frontend/apps/e2e/__tests__/app-impress/doc-grid-dnd.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ test.describe('Doc grid dnd mobile', () => {
179179
await expect(docsGrid.getByRole('row').first()).toBeVisible();
180180
await expect(docsGrid.locator('.--docs--grid-droppable')).toHaveCount(0);
181181

182-
await createDoc(page, 'Draggable doc mobile', browserName, 1, false, true);
182+
await createDoc(page, 'Draggable doc mobile', browserName, 1, true);
183183

184184
await createRootSubPage(
185185
page,

src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,11 @@ export const createDoc = async (
7878
docName: string,
7979
browserName: string,
8080
length: number = 1,
81-
isChild: boolean = false,
8281
isMobile: boolean = false,
8382
) => {
8483
const randomDocs = randomName(docName, browserName, length);
8584

8685
for (let i = 0; i < randomDocs.length; i++) {
87-
if (!isChild && !isMobile) {
88-
const header = page.locator('header').first();
89-
await header.locator('h2').getByText('Docs').click();
90-
}
91-
9286
if (isMobile) {
9387
await page
9488
.getByRole('button', { name: 'Open the header menu' })

src/frontend/apps/impress/src/components/Box.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface BoxProps {
1616
$background?: CSSProperties['background'];
1717
$color?: CSSProperties['color'];
1818
$css?: string | RuleSet<object>;
19+
$cursor?: CSSProperties['cursor'];
1920
$direction?: CSSProperties['flexDirection'];
2021
$display?: CSSProperties['display'];
2122
$effect?: 'show' | 'hide';
@@ -44,13 +45,13 @@ export interface BoxProps {
4445
export type BoxType = ComponentPropsWithRef<typeof Box>;
4546

4647
export const Box = styled('div')<BoxProps>`
47-
display: flex;
48-
flex-direction: column;
4948
${({ $align }) => $align && `align-items: ${$align};`}
5049
${({ $background }) => $background && `background: ${$background};`}
5150
${({ $color }) => $color && `color: ${$color};`}
52-
${({ $direction }) => $direction && `flex-direction: ${$direction};`}
53-
${({ $display }) => $display && `display: ${$display};`}
51+
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
52+
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
53+
${({ $display, as }) =>
54+
`display: ${$display || as?.match('span|input') ? 'inline-flex' : 'flex'};`}
5455
${({ $flex }) => $flex && `flex: ${$flex};`}
5556
${({ $gap }) => $gap && `gap: ${$gap};`}
5657
${({ $height }) => $height && `height: ${$height};`}

src/frontend/apps/impress/src/components/DropdownMenu.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type DropdownMenuProps = {
2525
arrowCss?: BoxProps['$css'];
2626
buttonCss?: BoxProps['$css'];
2727
disabled?: boolean;
28+
opened?: boolean;
2829
topMessage?: string;
2930
selectedValues?: string[];
3031
afterOpenChange?: (isOpen: boolean) => void;
@@ -38,12 +39,13 @@ export const DropdownMenu = ({
3839
arrowCss,
3940
buttonCss,
4041
label,
42+
opened,
4143
topMessage,
4244
afterOpenChange,
4345
selectedValues,
4446
}: PropsWithChildren<DropdownMenuProps>) => {
4547
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
46-
const [isOpen, setIsOpen] = useState(false);
48+
const [isOpen, setIsOpen] = useState(opened ?? false);
4749
const blockButtonRef = useRef<HTMLDivElement>(null);
4850

4951
const onOpenChange = (isOpen: boolean) => {

src/frontend/apps/impress/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './BoxButton';
44
export * from './Card';
55
export * from './DropButton';
66
export * from './DropdownMenu';
7+
export * from './quick-search';
78
export * from './Icon';
89
export * from './InfiniteScroll';
910
export * from './Link';

src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { Command } from 'cmdk';
2-
import { ReactNode, useRef } from 'react';
2+
import {
3+
PropsWithChildren,
4+
ReactNode,
5+
useEffect,
6+
useRef,
7+
useState,
8+
} from 'react';
39

410
import { hasChildrens } from '@/utils/children';
511

@@ -30,7 +36,6 @@ export type QuickSearchProps = {
3036
loading?: boolean;
3137
label?: string;
3238
placeholder?: string;
33-
children?: ReactNode;
3439
};
3540

3641
export const QuickSearch = ({
@@ -42,14 +47,47 @@ export const QuickSearch = ({
4247
label,
4348
placeholder,
4449
children,
45-
}: QuickSearchProps) => {
50+
}: PropsWithChildren<QuickSearchProps>) => {
4651
const ref = useRef<HTMLDivElement | null>(null);
52+
const [selectedValue, setSelectedValue] = useState<string>('');
53+
54+
// Auto-select first item when children change
55+
useEffect(() => {
56+
if (!children) {
57+
setSelectedValue('');
58+
return;
59+
}
60+
61+
// Small delay for DOM to update
62+
const timeoutId = setTimeout(() => {
63+
const firstItem = ref.current?.querySelector('[cmdk-item]');
64+
if (firstItem) {
65+
const value =
66+
firstItem.getAttribute('data-value') ||
67+
firstItem.getAttribute('value') ||
68+
firstItem.textContent?.trim() ||
69+
'';
70+
if (value) {
71+
setSelectedValue(value);
72+
}
73+
}
74+
}, 50);
75+
76+
return () => clearTimeout(timeoutId);
77+
}, [children]);
4778

4879
return (
4980
<>
5081
<QuickSearchStyle />
5182
<div className="quick-search-container">
52-
<Command label={label} shouldFilter={false} ref={ref}>
83+
<Command
84+
label={label}
85+
shouldFilter={false}
86+
ref={ref}
87+
value={selectedValue}
88+
onValueChange={setSelectedValue}
89+
tabIndex={0}
90+
>
5391
{showInput && (
5492
<QuickSearchInput
5593
loading={loading}

src/frontend/apps/impress/src/components/quick-search/QuickSearchGroup.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command } from 'cmdk';
22
import { ReactNode } from 'react';
33

4-
import { Box } from '../Box';
4+
import { Box, Text } from '@/components';
55

66
import { QuickSearchData } from './QuickSearch';
77
import { QuickSearchItem } from './QuickSearchItem';
@@ -23,6 +23,7 @@ export const QuickSearchGroup = <T,>({
2323
key={group.groupName}
2424
heading={group.groupName}
2525
forceMount={false}
26+
contentEditable={false}
2627
>
2728
{group.startActions?.map((action, index) => {
2829
return (
@@ -58,7 +59,13 @@ export const QuickSearchGroup = <T,>({
5859
);
5960
})}
6061
{group.emptyString && group.elements.length === 0 && (
61-
<span className="ml-b clr-greyscale-500">{group.emptyString}</span>
62+
<Text
63+
$variation="500"
64+
$margin={{ left: '2xs', bottom: '3xs' }}
65+
$size="sm"
66+
>
67+
{group.emptyString}
68+
</Text>
6269
)}
6370
</Command.Group>
6471
</Box>

0 commit comments

Comments
 (0)