@autoafleveren/ui 1.5.1 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/icons.cjs +85 -85
- package/dist/icons.js +4215 -4031
- package/dist/types/composables/useContextMenu/index.d.ts +2 -2
- package/dist/ui-storybook.css +1 -1
- package/dist/ui.cjs +41 -41
- package/dist/ui.js +9164 -8953
- package/package.json +2 -2
- package/src/modules/components/AppContextMenu/AppContextMenu.vue +39 -3
- package/src/modules/components/AppContextMenu/__tests__/app-context-menu.spec.ts +150 -7
- package/src/modules/components/AppContextMenu/index.d.ts +4 -0
- package/src/modules/components/AppDataTable/AppDataTable.vue +1 -1
- package/src/modules/composables/useContextMenu/index.ts +5 -5
- package/src/modules/composables/useEcho/index.ts +5 -1
- package/src/modules/layouts/Platform/Platform.vue +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autoafleveren/ui",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/*",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"packageManager": "yarn@4.6.0",
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@awesome.me/kit-ed6848cc31": "^1.0.
|
|
73
|
+
"@awesome.me/kit-ed6848cc31": "^1.0.10",
|
|
74
74
|
"@floating-ui/vue": "^1.1.7",
|
|
75
75
|
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
|
76
76
|
"@fortawesome/vue-fontawesome": "^3.1.1",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { computed, nextTick, onUnmounted, ref } from 'vue';
|
|
3
|
-
import { onClickOutside } from '@vueuse/core';
|
|
3
|
+
import { onClickOutside, useDebounceFn } from '@vueuse/core';
|
|
4
4
|
import { useActionBar, useClientPoint, useContextMenu, useFocusTrap } from '~composables';
|
|
5
5
|
import AppActionBarItem from '~components/AppActionBar/AppActionBarItem.vue';
|
|
6
|
+
import AppInput from '~components/AppInput/AppInput.vue';
|
|
6
7
|
import ShortcutItem from './ShortcutItem.vue';
|
|
7
8
|
|
|
8
9
|
import type { Props, Action } from './index.d';
|
|
@@ -19,13 +20,29 @@
|
|
|
19
20
|
const isOpen = ref(true);
|
|
20
21
|
const isLoading = ref(false);
|
|
21
22
|
const openIndex = ref<number | null>(null);
|
|
23
|
+
const searchActionsQuery = ref<string>('');
|
|
22
24
|
const actionbar = useActionBar();
|
|
23
25
|
const contextMenu = useContextMenu();
|
|
24
26
|
const contextMenuElement = ref<HTMLDivElement>();
|
|
25
27
|
const clientComputedPosition = useClientPoint(contextMenuElement);
|
|
26
28
|
const { activate, deactivate, onEscape } = useFocusTrap(contextMenuElement);
|
|
29
|
+
const onSearchActionsQueryInput = useDebounceFn((value: string) => {
|
|
30
|
+
searchActionsQuery.value = value;
|
|
31
|
+
});
|
|
27
32
|
|
|
28
33
|
const actionsWithFallback = computed((): Action[] => props.actions ?? actionbar.actions.value);
|
|
34
|
+
const normalizedSearchQuery = computed((): string => searchActionsQuery.value?.toLowerCase().trim() || '');
|
|
35
|
+
const isSearching = computed((): boolean => normalizedSearchQuery.value?.length > 0);
|
|
36
|
+
const filteredActions = computed((): Action[] => {
|
|
37
|
+
const actions = actionsWithFallback.value;
|
|
38
|
+
const limit = props.maxList ?? actions.length;
|
|
39
|
+
|
|
40
|
+
if (!isSearching.value) return actions.slice(0, limit);
|
|
41
|
+
|
|
42
|
+
return actions
|
|
43
|
+
.filter(action => action.name.toLowerCase().includes(normalizedSearchQuery.value))
|
|
44
|
+
.slice(0, limit);
|
|
45
|
+
});
|
|
29
46
|
|
|
30
47
|
onUnmounted(deactivate);
|
|
31
48
|
|
|
@@ -84,9 +101,20 @@
|
|
|
84
101
|
class="app-context-menu fixed z-50 flex w-64 flex-col rounded-lg bg-secondary p-2 drop-shadow-card empty:hidden"
|
|
85
102
|
data-test-context-menu
|
|
86
103
|
>
|
|
104
|
+
<div v-if="enableSearch && actionsWithFallback.length > 0">
|
|
105
|
+
<AppInput
|
|
106
|
+
:model-value="searchActionsQuery"
|
|
107
|
+
:placeholder="searchPlaceholder ?? ''"
|
|
108
|
+
class="mb-2 text-zinc-700"
|
|
109
|
+
type="text"
|
|
110
|
+
data-test-driver-search
|
|
111
|
+
@update:model-value="onSearchActionsQueryInput"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
87
115
|
<div
|
|
88
116
|
v-if="contextMenu.shortcuts.value.length > 0"
|
|
89
|
-
:class="{ 'mb-2':
|
|
117
|
+
:class="{ 'mb-2': filteredActions.length > 0 }"
|
|
90
118
|
class="flex justify-between divide-x divide-zinc-600 rounded-md border border-zinc-600"
|
|
91
119
|
>
|
|
92
120
|
<ShortcutItem
|
|
@@ -99,7 +127,7 @@
|
|
|
99
127
|
</div>
|
|
100
128
|
|
|
101
129
|
<AppActionBarItem
|
|
102
|
-
v-for="(action, index) in
|
|
130
|
+
v-for="(action, index) in filteredActions"
|
|
103
131
|
:key="`action-item-${index}`"
|
|
104
132
|
:action="action"
|
|
105
133
|
:context="true"
|
|
@@ -116,5 +144,13 @@
|
|
|
116
144
|
>
|
|
117
145
|
<span class="text-base">{{ action.name }}</span>
|
|
118
146
|
</AppActionBarItem>
|
|
147
|
+
|
|
148
|
+
<p
|
|
149
|
+
v-if="isSearching && filteredActions.length === 0"
|
|
150
|
+
class="px-4 py-2 text-sm text-zinc-500"
|
|
151
|
+
data-test-context-menu-no-results
|
|
152
|
+
>
|
|
153
|
+
{{ noResultsText ?? '' }}
|
|
154
|
+
</p>
|
|
119
155
|
</div>
|
|
120
156
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { shallowMount } from '@vue/test-utils';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { flushPromises, shallowMount } from '@vue/test-utils';
|
|
3
3
|
import { useContextMenu } from '~composables';
|
|
4
4
|
import { actions as actionsMock, shortcutItem as shortcutItemMock } from '../__mocks__';
|
|
5
5
|
import AppActionBarItem from '~components/AppActionBar/AppActionBarItem.vue';
|
|
@@ -11,19 +11,28 @@ import type { Action } from '~components/AppActionBar/index.d';
|
|
|
11
11
|
class MockPointerEvent {}
|
|
12
12
|
const contextMenu = useContextMenu();
|
|
13
13
|
|
|
14
|
-
function createWrapper(actions: Action[] = actionsMock) {
|
|
14
|
+
function createWrapper(actions: Action[] = actionsMock, props: Record<string, unknown> = {}) {
|
|
15
15
|
return shallowMount(AppContextMenu, { props: {
|
|
16
16
|
actions,
|
|
17
17
|
event: new MockPointerEvent() as PointerEvent,
|
|
18
18
|
item: {},
|
|
19
|
+
...props,
|
|
19
20
|
} });
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
describe('the AppContextMenu ShortcutItem component', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.useFakeTimers();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.useRealTimers();
|
|
30
|
+
});
|
|
31
|
+
|
|
23
32
|
it('should display the context menu', () => {
|
|
24
33
|
const wrapper = createWrapper();
|
|
25
|
-
|
|
26
34
|
const menu = wrapper.find('[data-test-context-menu]');
|
|
35
|
+
|
|
27
36
|
expect(menu.exists()).toBe(true);
|
|
28
37
|
});
|
|
29
38
|
|
|
@@ -31,8 +40,8 @@ describe('the AppContextMenu ShortcutItem component', () => {
|
|
|
31
40
|
expect.assertions(3);
|
|
32
41
|
|
|
33
42
|
const wrapper = createWrapper();
|
|
34
|
-
|
|
35
43
|
let shortcutItems = wrapper.findAllComponents(ShortcutItem);
|
|
44
|
+
|
|
36
45
|
expect(shortcutItems).toHaveLength(0);
|
|
37
46
|
|
|
38
47
|
contextMenu.setShortcuts([shortcutItemMock, shortcutItemMock]);
|
|
@@ -40,6 +49,7 @@ describe('the AppContextMenu ShortcutItem component', () => {
|
|
|
40
49
|
await wrapper.vm.$nextTick();
|
|
41
50
|
|
|
42
51
|
shortcutItems = wrapper.findAllComponents(ShortcutItem);
|
|
52
|
+
|
|
43
53
|
expect(shortcutItems).toHaveLength(2);
|
|
44
54
|
expect(shortcutItems[0].props('shortcut')).toStrictEqual(shortcutItemMock);
|
|
45
55
|
});
|
|
@@ -48,8 +58,8 @@ describe('the AppContextMenu ShortcutItem component', () => {
|
|
|
48
58
|
expect.assertions(5);
|
|
49
59
|
|
|
50
60
|
const wrapper = createWrapper();
|
|
51
|
-
|
|
52
61
|
const actionBarItems = wrapper.findAllComponents(AppActionBarItem);
|
|
62
|
+
|
|
53
63
|
expect(actionBarItems).toHaveLength(2);
|
|
54
64
|
expect(actionBarItems[0].props('action')).toStrictEqual(actionsMock[0]);
|
|
55
65
|
expect(actionBarItems[1].props('action')).toStrictEqual(actionsMock[1]);
|
|
@@ -61,11 +71,144 @@ describe('the AppContextMenu ShortcutItem component', () => {
|
|
|
61
71
|
expect.assertions(1);
|
|
62
72
|
|
|
63
73
|
const wrapper = createWrapper();
|
|
64
|
-
|
|
65
74
|
const actionBarItem = wrapper.findComponent(AppActionBarItem);
|
|
66
75
|
|
|
67
76
|
actionBarItem.vm.$emit('close');
|
|
68
77
|
|
|
69
78
|
expect(wrapper.emitted()).toHaveProperty('close');
|
|
70
79
|
});
|
|
80
|
+
|
|
81
|
+
it('should not render the search input when enableSearch is false', () => {
|
|
82
|
+
const wrapper = createWrapper(actionsMock, { enableSearch: false });
|
|
83
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
84
|
+
|
|
85
|
+
expect(searchInput.exists()).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should not render the search input when enableSearch is not provided', () => {
|
|
89
|
+
const wrapper = createWrapper();
|
|
90
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
91
|
+
|
|
92
|
+
expect(searchInput.exists()).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should render the search input when enableSearch is true', () => {
|
|
96
|
+
const wrapper = createWrapper(actionsMock, { enableSearch: true });
|
|
97
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
98
|
+
|
|
99
|
+
expect(searchInput.exists()).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should use the searchPlaceholder prop as the input placeholder', () => {
|
|
103
|
+
const wrapper = createWrapper(actionsMock, {
|
|
104
|
+
enableSearch: true,
|
|
105
|
+
searchPlaceholder: 'Search actions...',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
109
|
+
|
|
110
|
+
expect(searchInput.attributes('placeholder')).toBe('Search actions...');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should default the placeholder to an empty string when searchPlaceholder is not provided', () => {
|
|
114
|
+
const wrapper = createWrapper(actionsMock, { enableSearch: true });
|
|
115
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
116
|
+
|
|
117
|
+
expect(searchInput.attributes('placeholder')).toBe('');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should display noResultsText when searching yields no results', async () => {
|
|
121
|
+
const wrapper = createWrapper(actionsMock, {
|
|
122
|
+
enableSearch: true,
|
|
123
|
+
noResultsText: 'No results found',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
127
|
+
|
|
128
|
+
await searchInput.vm.$emit('update:modelValue', 'nonexistentaction');
|
|
129
|
+
|
|
130
|
+
vi.runAllTimers();
|
|
131
|
+
await flushPromises();
|
|
132
|
+
await wrapper.vm.$nextTick();
|
|
133
|
+
|
|
134
|
+
const noResults = wrapper.find('[data-test-context-menu-no-results]');
|
|
135
|
+
|
|
136
|
+
expect(noResults.exists()).toBe(true);
|
|
137
|
+
expect(noResults.text()).toBe('No results found');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should default noResultsText to an empty string when not provided', async () => {
|
|
141
|
+
const wrapper = createWrapper(actionsMock, { enableSearch: true });
|
|
142
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
143
|
+
|
|
144
|
+
await searchInput.vm.$emit('update:modelValue', 'test_action_non_existant');
|
|
145
|
+
|
|
146
|
+
vi.runAllTimers();
|
|
147
|
+
|
|
148
|
+
await wrapper.vm.$nextTick();
|
|
149
|
+
|
|
150
|
+
const noResults = wrapper.find('[data-test-context-menu-no-results]');
|
|
151
|
+
|
|
152
|
+
expect(noResults.exists()).toBe(true);
|
|
153
|
+
expect(noResults.text()).toBe('');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should filter actions based on search query', async () => {
|
|
157
|
+
const wrapper = createWrapper(actionsMock, { enableSearch: true });
|
|
158
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
159
|
+
|
|
160
|
+
await searchInput.vm.$emit('update:modelValue', 'Action #1');
|
|
161
|
+
vi.runAllTimers();
|
|
162
|
+
await wrapper.vm.$nextTick();
|
|
163
|
+
|
|
164
|
+
const actionBarItems = wrapper.findAllComponents(AppActionBarItem);
|
|
165
|
+
|
|
166
|
+
expect(actionBarItems).toHaveLength(1);
|
|
167
|
+
expect(actionBarItems[0].props('action')).toStrictEqual(actionsMock[0]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should limit the number of displayed actions when maxList is set', () => {
|
|
171
|
+
const wrapper = createWrapper(actionsMock, { maxList: 1 });
|
|
172
|
+
const actionBarItems = wrapper.findAllComponents(AppActionBarItem);
|
|
173
|
+
|
|
174
|
+
expect(actionBarItems).toHaveLength(1);
|
|
175
|
+
expect(actionBarItems[0].props('action')).toStrictEqual(actionsMock[0]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should display all actions when maxList is not set', () => {
|
|
179
|
+
const wrapper = createWrapper(actionsMock);
|
|
180
|
+
const actionBarItems = wrapper.findAllComponents(AppActionBarItem);
|
|
181
|
+
|
|
182
|
+
expect(actionBarItems).toHaveLength(actionsMock.length);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should display all actions when maxList exceeds the number of actions', () => {
|
|
186
|
+
const wrapper = createWrapper(actionsMock, { maxList: 100 });
|
|
187
|
+
const actionBarItems = wrapper.findAllComponents(AppActionBarItem);
|
|
188
|
+
|
|
189
|
+
expect(actionBarItems).toHaveLength(actionsMock.length);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should limit filtered search results to maxList', async () => {
|
|
193
|
+
const manyActions: Action[] = [
|
|
194
|
+
{ ...actionsMock[0], name: 'Action 1' },
|
|
195
|
+
{ ...actionsMock[0], name: 'Action 2' },
|
|
196
|
+
{ ...actionsMock[0], name: 'Action 3' },
|
|
197
|
+
{ ...actionsMock[0], name: 'Action 4' },
|
|
198
|
+
{ ...actionsMock[0], name: 'Action 5' },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const wrapper = createWrapper(manyActions, { enableSearch: true, maxList: 2 });
|
|
202
|
+
const searchInput = wrapper.findComponent({ name: 'AppInput' });
|
|
203
|
+
|
|
204
|
+
await searchInput.vm.$emit('update:modelValue', 'Action');
|
|
205
|
+
|
|
206
|
+
vi.runAllTimers();
|
|
207
|
+
|
|
208
|
+
await wrapper.vm.$nextTick();
|
|
209
|
+
|
|
210
|
+
const actionBarItems = wrapper.findAllComponents(AppActionBarItem);
|
|
211
|
+
|
|
212
|
+
expect(actionBarItems).toHaveLength(2);
|
|
213
|
+
});
|
|
71
214
|
});
|
|
@@ -8,6 +8,10 @@ export interface Props {
|
|
|
8
8
|
event: PointerEvent | MouseEvent;
|
|
9
9
|
actions?: Action[];
|
|
10
10
|
confirmed?: Action['key'];
|
|
11
|
+
enableSearch?: boolean;
|
|
12
|
+
searchPlaceholder?: string;
|
|
13
|
+
noResultsText?: string;
|
|
14
|
+
maxList?: number;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export interface ContextMenuInstance {
|
|
@@ -3,7 +3,7 @@ import { get } from 'radash';
|
|
|
3
3
|
import { AppContextMenu } from '~components';
|
|
4
4
|
|
|
5
5
|
import type { App, Ref } from 'vue';
|
|
6
|
-
import type {
|
|
6
|
+
import type { ContextMenuInstance, Shortcut, Props } from '~components/AppContextMenu/index.d';
|
|
7
7
|
|
|
8
8
|
const state = {
|
|
9
9
|
instances: ref<ContextMenuInstance[]>([]),
|
|
@@ -18,7 +18,7 @@ function closeContextMenu(contextInstance: ContextMenuInstance): void {
|
|
|
18
18
|
|
|
19
19
|
interface UseContextMenu<T> {
|
|
20
20
|
setShortcuts: (shortcuts: Shortcut[]) => void;
|
|
21
|
-
open: (item: T, event: PointerEvent | MouseEvent,
|
|
21
|
+
open: (item: T, event: PointerEvent | MouseEvent, props?: Partial<Props>) => Promise<unknown>;
|
|
22
22
|
close: () => Promise<void>;
|
|
23
23
|
closeAll: () => void;
|
|
24
24
|
shortcuts: Ref<Shortcut[]>;
|
|
@@ -35,7 +35,7 @@ export function useContextMenu<T>(): UseContextMenu<T> {
|
|
|
35
35
|
state.shortcuts.value = shortcuts;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async function open(item: T, event: PointerEvent | MouseEvent,
|
|
38
|
+
async function open(item: T, event: PointerEvent | MouseEvent, props?: Partial<Props>): Promise<unknown> {
|
|
39
39
|
// @ts-expect-error parameter not matched
|
|
40
40
|
state.instances.value.forEach(closeContextMenu);
|
|
41
41
|
|
|
@@ -51,10 +51,10 @@ export function useContextMenu<T>(): UseContextMenu<T> {
|
|
|
51
51
|
contextInstance = createApp(AppContextMenu, {
|
|
52
52
|
item,
|
|
53
53
|
|
|
54
|
-
confirmed,
|
|
55
|
-
|
|
56
54
|
event,
|
|
57
55
|
|
|
56
|
+
...props,
|
|
57
|
+
|
|
58
58
|
onClose: (data: unknown) => {
|
|
59
59
|
resolve(data);
|
|
60
60
|
},
|
|
@@ -73,7 +73,11 @@ export function useEcho(listenerName: string | null = null) {
|
|
|
73
73
|
|
|
74
74
|
const channelName = channel ?? Object.keys(state.casters)[0];
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
try {
|
|
77
|
+
state.casters[channelName]?.whisper?.(event, data);
|
|
78
|
+
} catch {
|
|
79
|
+
//
|
|
80
|
+
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
function listen<T = unknown>(event: string, callback: (data: T) => void, channel?: string): void {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
<main
|
|
70
70
|
class="relative order-4 flex min-h-[calc(100vh-5rem)] shrink-0 flex-col overflow-y-auto overflow-x-hidden
|
|
71
|
-
bg-zinc-100
|
|
71
|
+
bg-zinc-100 context-menu-open:overflow-y-hidden scrollbar-gutter-stable justify-self-start
|
|
72
72
|
md:order-3 md:-mt-0 md:h-screen md:grow md:basis-0 md:overflow-y-auto md:overflow-x-hidden"
|
|
73
73
|
>
|
|
74
74
|
<AppActionBar />
|