Skip to content

Commit e64b135

Browse files
committed
feat(commands): scope search to current level and simplify menu UI
1 parent 6a5201d commit e64b135

File tree

6 files changed

+106
-90
lines changed

6 files changed

+106
-90
lines changed

packages/editor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@use-kona/editor",
3-
"version": "0.1.17",
3+
"version": "0.1.18",
44
"type": "module",
55
"exports": {
66
".": "./src/index.ts"

packages/editor/src/plugins/CommandsPlugin/Menu.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export const Menu = (props: Props) => {
4444
});
4545

4646
const isBrowseMode = typeof store.filter === 'string' && store.filter === '';
47-
const isSearchMode = typeof store.filter === 'string' && store.filter !== '';
4847

4948
const { commands, isLoading, isError } = useResolvedCommands({
5049
rootCommands,
@@ -87,10 +86,13 @@ export const Menu = (props: Props) => {
8786
}, [store.isOpen, store.openId]);
8887

8988
useEffect(() => {
90-
if (store.filter === false || isSearchMode) {
89+
if (
90+
store.filter === false ||
91+
(typeof store.filter === 'string' && store.filter !== '')
92+
) {
9193
setActive(0);
9294
}
93-
}, [isSearchMode, store.filter]);
95+
}, [store.filter]);
9496

9597
useEffect(() => {
9698
if (!entries.length) {
@@ -268,8 +270,6 @@ export const Menu = (props: Props) => {
268270
return null;
269271
}
270272

271-
const pathLabel = path.map((item) => item.title).join(' / ');
272-
273273
return createPortal(
274274
renderMenu(
275275
<>
@@ -289,9 +289,6 @@ export const Menu = (props: Props) => {
289289
event.preventDefault();
290290
}}
291291
>
292-
{isBrowseMode && pathLabel && (
293-
<div className={styles.path}>{pathLabel}</div>
294-
)}
295292
{entries.map((entry, index) => {
296293
if (entry.type === 'back') {
297294
return (
@@ -348,11 +345,6 @@ export const Menu = (props: Props) => {
348345
</span>
349346
<span className={styles.content}>
350347
<span>{entry.command.command.title}</span>
351-
{isSearchMode && entry.command.breadcrumb && (
352-
<span className={styles.breadcrumb}>
353-
{entry.command.breadcrumb}
354-
</span>
355-
)}
356348
</span>
357349
{entry.command.isSubmenu && isBrowseMode && (
358350
<span className={styles.submenu} aria-hidden="true">

packages/editor/src/plugins/CommandsPlugin/resolveCommands.spec.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,23 +102,17 @@ describe('CommandsResolver', () => {
102102
expect(result.commands[0]?.command.name).toBe('code');
103103
});
104104

105-
it('returns global leaf query matches with breadcrumbs', async () => {
105+
it('searches only root level when path is empty', async () => {
106106
const resolver = new CommandsResolver();
107107
const rootCommands: Command[] = [
108108
{
109109
name: 'insert',
110110
title: 'Insert',
111111
commandName: 'insert',
112112
icon: null,
113-
getCommands: () => [
114-
leaf({
115-
name: 'heading-1',
116-
title: 'Heading 1',
117-
commandName: 'heading1',
118-
}),
119-
leaf({ name: 'code', title: 'Code', commandName: 'code' }),
120-
],
113+
getCommands: () => [leaf({ name: 'code', title: 'Code' })],
121114
},
115+
leaf({ name: 'paragraph', title: 'Paragraph' }),
122116
];
123117

124118
const request = resolver.resolve({
@@ -130,9 +124,32 @@ describe('CommandsResolver', () => {
130124

131125
const result = await request.promise;
132126

127+
expect(result.commands).toHaveLength(0);
128+
});
129+
130+
it('searches only current nested level', async () => {
131+
const resolver = new CommandsResolver();
132+
const rootCommands: Command[] = [
133+
{
134+
name: 'insert',
135+
title: 'Insert',
136+
commandName: 'insert',
137+
icon: null,
138+
getCommands: () => [leaf({ name: 'code', title: 'Code' })],
139+
},
140+
];
141+
142+
const request = resolver.resolve({
143+
rootCommands,
144+
filter: 'code',
145+
path: [{ name: 'insert', title: 'Insert', commandName: 'insert' }],
146+
editor,
147+
});
148+
149+
const result = await request.promise;
150+
133151
expect(result.commands).toHaveLength(1);
134152
expect(result.commands[0]?.command.name).toBe('code');
135-
expect(result.commands[0]?.breadcrumb).toBe('Insert');
136153
});
137154

138155
it('returns matching submenu commands in query mode', async () => {
@@ -185,13 +202,13 @@ describe('CommandsResolver', () => {
185202
const firstRequest = resolver.resolve({
186203
rootCommands,
187204
filter: 'a',
188-
path: [],
205+
path: [{ name: 'remote', title: 'Remote', commandName: 'remote' }],
189206
editor,
190207
});
191208
const secondRequest = resolver.resolve({
192209
rootCommands,
193210
filter: 'b',
194-
path: [],
211+
path: [{ name: 'remote', title: 'Remote', commandName: 'remote' }],
195212
editor,
196213
});
197214

@@ -243,7 +260,7 @@ describe('CommandsResolver', () => {
243260
const loadingRequest = resolver.resolve({
244261
rootCommands,
245262
filter: 'code',
246-
path: [],
263+
path: [{ name: 'remote', title: 'Remote', commandName: 'remote' }],
247264
editor,
248265
});
249266

packages/editor/src/plugins/CommandsPlugin/resolveCommands.ts

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export type ResolvedCommand = {
1616
command: Command;
1717
key: string;
1818
path: CommandPathEntry[];
19-
breadcrumb: string;
2019
isSubmenu: boolean;
2120
};
2221

@@ -84,75 +83,78 @@ const toResolvedCommand = (
8483
command,
8584
path,
8685
key: pathToKey(path),
87-
breadcrumb: parentPath.map((item) => item.title).join(' / '),
8886
isSubmenu: Boolean(command.getCommands),
8987
};
9088
};
9189

92-
const resolveBrowseCommands = async (
90+
const resolveCurrentLevelCommands = async (
9391
params: ResolveCommandsParams,
9492
resolveChildCommands: ResolveChildCommands,
93+
queryForCurrentLevel: string,
9594
) => {
9695
const { rootCommands, path, editor } = params;
9796
let currentCommands = rootCommands;
9897
let currentPath: CommandPathEntry[] = [];
9998

100-
for (const item of path) {
99+
for (let index = 0; index < path.length; index++) {
100+
const item = path[index];
101+
const isLastPathItem = index === path.length - 1;
101102
const command = currentCommands.find((entry) => entry.name === item.name);
102103
if (!command?.getCommands) {
103-
return [];
104+
return {
105+
commands: [],
106+
path: currentPath,
107+
};
104108
}
105109

106110
currentPath = [...currentPath, toPathEntry(command)];
107111
currentCommands = await resolveChildCommands({
108112
command,
109113
path: currentPath,
110-
query: '',
114+
query: isLastPathItem ? queryForCurrentLevel : '',
111115
editor,
112116
});
113117
}
114118

115-
return currentCommands.map((command) =>
116-
toResolvedCommand(command, currentPath),
119+
return {
120+
commands: currentCommands,
121+
path: currentPath,
122+
};
123+
};
124+
125+
const resolveBrowseCommands = async (
126+
params: ResolveCommandsParams,
127+
resolveChildCommands: ResolveChildCommands,
128+
) => {
129+
const currentLevel = await resolveCurrentLevelCommands(
130+
params,
131+
resolveChildCommands,
132+
'',
133+
);
134+
135+
return currentLevel.commands.map((command) =>
136+
toResolvedCommand(command, currentLevel.path),
117137
);
118138
};
119139

120140
const resolveSearchCommands = async (
121141
params: ResolveCommandsParams,
122142
resolveChildCommands: ResolveChildCommands,
123143
) => {
124-
const { rootCommands, filter, editor } = params;
125-
const commands: ResolvedCommand[] = [];
126-
127-
const walk = async (
128-
levelCommands: Command[],
129-
parentPath: CommandPathEntry[],
130-
): Promise<void> => {
131-
for (const command of levelCommands) {
132-
if (command.getCommands) {
133-
if (isCommandMatchesQuery(command, filter)) {
134-
commands.push(toResolvedCommand(command, parentPath));
135-
}
144+
const { filter } = params;
145+
const currentLevel = await resolveCurrentLevelCommands(
146+
params,
147+
resolveChildCommands,
148+
filter,
149+
);
136150

137-
const currentPath = [...parentPath, toPathEntry(command)];
138-
const children = await resolveChildCommands({
139-
command,
140-
path: currentPath,
141-
query: filter,
142-
editor,
143-
});
144-
await walk(children, currentPath);
145-
continue;
146-
}
147-
148-
if (command.action && isCommandMatchesQuery(command, filter)) {
149-
commands.push(toResolvedCommand(command, parentPath));
150-
}
151-
}
152-
};
151+
const matchedCommands = currentLevel.commands.filter((command) =>
152+
isCommandMatchesQuery(command, filter),
153+
);
153154

154-
await walk(rootCommands, []);
155-
return commands;
155+
return matchedCommands.map((command) =>
156+
toResolvedCommand(command, currentLevel.path),
157+
);
156158
};
157159

158160
export class CommandsResolver {

packages/editor/src/plugins/CommandsPlugin/styles.module.css

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
background-color: var(--kona-editor-background-color, #fff);
1212
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.025);
1313
border: 1px solid var(--kona-editor-border-color, #ddd);
14-
border-radius: 4px;
14+
border-radius: 8px;
1515
transition:
1616
transform 0.12s ease,
1717
opacity 0.12s ease;
@@ -79,29 +79,13 @@
7979
display: flex;
8080
flex: 1;
8181
min-width: 0;
82-
flex-direction: column;
83-
row-gap: 2px;
84-
}
85-
86-
.breadcrumb {
87-
font-size: 11px;
88-
color: var(--kona-editor-secondary-text-color, #777);
89-
white-space: nowrap;
90-
overflow: hidden;
91-
text-overflow: ellipsis;
82+
flex-direction: row;
9283
}
9384

9485
.submenu {
9586
color: var(--kona-editor-secondary-text-color, #777);
9687
}
9788

98-
.path {
99-
padding: 6px 8px;
100-
font-size: 11px;
101-
color: var(--kona-editor-secondary-text-color, #777);
102-
border-bottom: 1px solid var(--kona-editor-border-color, #ddd);
103-
}
104-
10589
.systemRow {
10690
min-height: 32px;
10791
padding: 8px;

packages/editor/src/plugins/CommandsPlugin/useResolvedCommands.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,42 @@ export const useResolvedCommands = (params: Params) => {
2626
const { rootCommands, filter, path, editor, isOpen } = params;
2727
const resolverRef = useRef(new CommandsResolver());
2828
const [state, setState] = useState<ResolvedCommandsState>(EMPTY_STATE);
29+
const prevIsOpenRef = useRef(false);
30+
const prevPathKeyRef = useRef('');
31+
const prevQueryRef = useRef('');
2932
const query = typeof filter === 'string' ? filter : '';
3033

3134
useEffect(() => {
3235
if (!isOpen || filter === false) {
3336
setState(EMPTY_STATE);
37+
prevIsOpenRef.current = false;
38+
prevPathKeyRef.current = '';
39+
prevQueryRef.current = '';
3440
return;
3541
}
3642

37-
setState({
38-
commands: [],
39-
isLoading: true,
40-
isError: false,
41-
});
43+
const pathKey = path.map((item) => item.name).join('/');
44+
const isNewSession = !prevIsOpenRef.current;
45+
const isPathChanged = !isNewSession && prevPathKeyRef.current !== pathKey;
46+
const isQueryChanged = !isNewSession && prevQueryRef.current !== query;
47+
48+
if (isNewSession || isPathChanged) {
49+
setState({
50+
commands: [],
51+
isLoading: true,
52+
isError: false,
53+
});
54+
} else if (isQueryChanged) {
55+
setState((state) => ({
56+
...state,
57+
isLoading: true,
58+
isError: false,
59+
}));
60+
}
61+
62+
prevIsOpenRef.current = true;
63+
prevPathKeyRef.current = pathKey;
64+
prevQueryRef.current = query;
4265

4366
const timeout = window.setTimeout(
4467
() => {
@@ -49,8 +72,6 @@ export const useResolvedCommands = (params: Params) => {
4972
editor,
5073
});
5174

52-
setState(request.state);
53-
5475
request.promise.then((resolved) => {
5576
setState(resolved);
5677
});

0 commit comments

Comments
 (0)