Skip to content

Commit daef94f

Browse files
feat(folders): add multi-selection (#641)
1 parent a49b5d0 commit daef94f

File tree

15 files changed

+553
-161
lines changed

15 files changed

+553
-161
lines changed

src/renderer/components/editor/Tab.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const props = defineProps<Props>()
1414
const { selectedSnippetContent, selectedSnippet, deleteSnippetContent }
1515
= useSnippets()
1616
const { addToUpdateContentQueue } = useSnippetUpdate()
17-
const { highlightedSnippetIds, highlightedFolderId, state } = useApp()
17+
const { highlightedSnippetIds, highlightedFolderIds, state } = useApp()
1818
1919
const tabRef = ref<HTMLDivElement>()
2020
const isEdit = ref(false)
@@ -38,7 +38,7 @@ const name = computed({
3838
3939
function onClickContextMenu() {
4040
highlightedSnippetIds.value.clear()
41-
highlightedFolderId.value = undefined
41+
highlightedFolderIds.value.clear()
4242
}
4343
4444
async function onDelete() {

src/renderer/components/sidebar/folders/Tree.vue

Lines changed: 95 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import type { Ref } from 'vue'
33
import type { PerfectScrollbarExpose } from 'vue3-perfect-scrollbar'
4-
import type { Node } from './types'
4+
import type { Node, Position } from './types'
55
import { languages } from '@/components/editor/grammars/languages'
66
import * as ContextMenu from '@/components/ui/shadcn/context-menu'
77
import { useApp, useDialog, useFolders, useSnippets } from '@/composables'
@@ -20,8 +20,11 @@ interface Props {
2020
2121
interface Emits {
2222
(e: 'update:modelValue', value: Node[]): void
23-
(e: 'clickNode', value: number): void
24-
(e: 'dragNode', value: { node: Node, target: Node, position: string }): void
23+
(e: 'clickNode', value: { id: number, event?: MouseEvent }): void
24+
(
25+
e: 'dragNode',
26+
value: { nodes: Node[], target: Node, position: Position },
27+
): void
2528
(e: 'toggleNode', value: Node): void
2629
}
2730
@@ -39,6 +42,9 @@ const {
3942
updateFolder,
4043
getFolderByIdFromTree,
4144
getFolders,
45+
selectedFolderIds,
46+
clearFolderSelection,
47+
selectFolder,
4248
} = useFolders()
4349
const { state } = useApp()
4450
const { clearSnippetsState } = useSnippets()
@@ -49,13 +55,22 @@ const scrollRef = useTemplateRef<PerfectScrollbarExpose>('scrollRef')
4955
const hoveredNodeId = ref('')
5056
const isHoveredByIdDisabled = ref(false)
5157
const contextNode = ref<Node | null>(null)
58+
const isContextMultiSelection = computed(() => {
59+
if (!contextNode.value)
60+
return false
61+
62+
if (selectedFolderIds.value.length <= 1)
63+
return false
64+
65+
return selectedFolderIds.value.includes(contextNode.value.id)
66+
})
5267
53-
function clickNode(id: number) {
54-
return emit('clickNode', id)
68+
function clickNode(id: number, event?: MouseEvent) {
69+
return emit('clickNode', { id, event })
5570
}
5671
57-
function dragNode(node: Node, target: Node, position: string) {
58-
return emit('dragNode', { node, target, position })
72+
function dragNode(nodes: Node[], target: Node, position: Position) {
73+
return emit('dragNode', { nodes, target, position })
5974
}
6075
6176
function toggleNode(node: Node) {
@@ -84,38 +99,47 @@ async function onDeleteFolder() {
8499
return
85100
86101
const { confirm } = useDialog()
87-
102+
const activeBeforeDelete = state.folderId
103+
const targetIds = selectedFolderIds.value.includes(contextNode.value.id)
104+
? [...selectedFolderIds.value]
105+
: [contextNode.value.id]
88106
const folderName = getFolderByIdFromTree(
89107
folders.value,
90108
contextNode.value.id,
91109
)?.name
92110
93111
const isConfirmed = await confirm({
94-
title: i18n.t('messages:confirm.delete', { name: folderName }),
112+
title:
113+
targetIds.length > 1
114+
? i18n.t('messages:confirm.delete', {
115+
name: i18n.t('sidebar.folders'),
116+
})
117+
: i18n.t('messages:confirm.delete', { name: folderName }),
95118
description: i18n.t('messages:warning:allSnippetsMoveToTrash'),
96119
})
97120
98-
if (isConfirmed) {
99-
await deleteFolder(contextNode.value.id)
121+
if (!isConfirmed)
122+
return
100123
101-
if (contextNode.value.id === state.folderId) {
102-
state.folderId = undefined
103-
clearSnippetsState()
124+
await Promise.all(targetIds.map(id => deleteFolder(id, false)))
125+
await getFolders(false)
104126
105-
const firstFolder = folders.value?.[0]
127+
if (activeBeforeDelete && targetIds.includes(activeBeforeDelete)) {
128+
clearSnippetsState()
129+
const fallbackId = selectedFolderIds.value[0]
106130
107-
if (firstFolder) {
108-
state.folderId = firstFolder.id
109-
scrollToElement(`[id="${state.folderId}"]`)
110-
}
131+
if (fallbackId) {
132+
await selectFolder(fallbackId)
133+
scrollToElement(`[id="${fallbackId}"]`)
134+
}
135+
else {
136+
clearFolderSelection()
111137
}
112-
113-
nextTick(() => {
114-
if (scrollRef.value) {
115-
scrollRef.value.ps?.update()
116-
}
117-
})
118138
}
139+
140+
nextTick(() => {
141+
scrollRef.value?.ps?.update()
142+
})
119143
}
120144
121145
function onRenameFolder() {
@@ -210,45 +234,52 @@ provide(treeKeys, {
210234
</div>
211235
</ContextMenu.Trigger>
212236
<ContextMenu.Content>
213-
<ContextMenu.Item @click="createFolderAndSelect">
214-
{{ i18n.t("action.new.folder") }}
215-
</ContextMenu.Item>
216-
<ContextMenu.Separator />
217-
<ContextMenu.Item @click="onRenameFolder">
218-
{{ i18n.t("action.rename") }}
219-
</ContextMenu.Item>
220-
<ContextMenu.Item @click="onDeleteFolder">
221-
{{ i18n.t("action.delete.common") }}
222-
</ContextMenu.Item>
223-
<ContextMenu.Separator />
224-
<ContextMenu.Item @click="onSetCustomIcon">
225-
{{ i18n.t("action.setCustomIcon") }}
226-
</ContextMenu.Item>
227-
<ContextMenu.Item
228-
v-if="contextNode?.icon"
229-
@click="onRemoveCustomIcon"
230-
>
231-
{{ i18n.t("action.removeCustomIcon") }}
232-
</ContextMenu.Item>
233-
<ContextMenu.Separator />
234-
<ContextMenu.Sub>
235-
<ContextMenu.SubTrigger>
236-
{{ i18n.t("action.defaultLanguage") }}
237-
</ContextMenu.SubTrigger>
238-
<ContextMenu.SubContent>
239-
<PerfectScrollbar :options="{ minScrollbarLength: 20 }">
240-
<div class="max-h-[250px]">
241-
<ContextMenu.Item
242-
v-for="language in languages"
243-
:key="language.value"
244-
@click="onSelectLanguage(language.value)"
245-
>
246-
{{ language.name }}
247-
</ContextMenu.Item>
248-
</div>
249-
</PerfectScrollbar>
250-
</ContextMenu.SubContent>
251-
</ContextMenu.Sub>
237+
<template v-if="isContextMultiSelection">
238+
<ContextMenu.Item @click="onDeleteFolder">
239+
{{ i18n.t("action.delete.common") }}
240+
</ContextMenu.Item>
241+
</template>
242+
<template v-else>
243+
<ContextMenu.Item @click="createFolderAndSelect">
244+
{{ i18n.t("action.new.folder") }}
245+
</ContextMenu.Item>
246+
<ContextMenu.Separator />
247+
<ContextMenu.Item @click="onRenameFolder">
248+
{{ i18n.t("action.rename") }}
249+
</ContextMenu.Item>
250+
<ContextMenu.Item @click="onDeleteFolder">
251+
{{ i18n.t("action.delete.common") }}
252+
</ContextMenu.Item>
253+
<ContextMenu.Separator />
254+
<ContextMenu.Item @click="onSetCustomIcon">
255+
{{ i18n.t("action.setCustomIcon") }}
256+
</ContextMenu.Item>
257+
<ContextMenu.Item
258+
v-if="contextNode?.icon"
259+
@click="onRemoveCustomIcon"
260+
>
261+
{{ i18n.t("action.removeCustomIcon") }}
262+
</ContextMenu.Item>
263+
<ContextMenu.Separator />
264+
<ContextMenu.Sub>
265+
<ContextMenu.SubTrigger>
266+
{{ i18n.t("action.defaultLanguage") }}
267+
</ContextMenu.SubTrigger>
268+
<ContextMenu.SubContent>
269+
<PerfectScrollbar :options="{ minScrollbarLength: 20 }">
270+
<div class="max-h-[250px]">
271+
<ContextMenu.Item
272+
v-for="language in languages"
273+
:key="language.value"
274+
@click="onSelectLanguage(language.value)"
275+
>
276+
{{ language.name }}
277+
</ContextMenu.Item>
278+
</div>
279+
</PerfectScrollbar>
280+
</ContextMenu.SubContent>
281+
</ContextMenu.Sub>
282+
</template>
252283
</ContextMenu.Content>
253284
</ContextMenu.Root>
254285
</PerfectScrollbar>

0 commit comments

Comments
 (0)