mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
feat: create group button, fix instance selection, and increase group name max length
This commit is contained in:
@@ -150,7 +150,7 @@ const addCategory = async () => {
|
||||
|
||||
if (text.length > 0) {
|
||||
try {
|
||||
const group = await create_group(text.substring(0, 32))
|
||||
const group = await create_group(text.substring(0, 128))
|
||||
availableGroups.value.push(group)
|
||||
groupIds.value.push(group.id)
|
||||
newCategoryInput.value = ''
|
||||
@@ -383,6 +383,7 @@ const messages = defineMessages({
|
||||
<StyledInput
|
||||
v-model="newCategoryInput"
|
||||
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||
:maxlength="128"
|
||||
class="w-full max-w-[300px]"
|
||||
@submit="() => addCategory"
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<span class="px-4 py-2.5 text-base font-semibold text-contrast tabular-nums">
|
||||
{{ formatMessage(messages.selectedCount, { count: selectedLibraryInstanceIds.size }) }}
|
||||
{{ formatMessage(messages.selectedCount, { count: selectedLibraryInstances.size }) }}
|
||||
</span>
|
||||
<div class="mx-1 h-6 w-px bg-surface-5" />
|
||||
<ButtonStyled type="transparent">
|
||||
@@ -21,6 +21,12 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-0.5">
|
||||
<ButtonStyled type="transparent">
|
||||
<button type="button" :disabled="busy" @click="createGroupFromSelection">
|
||||
<PlusIcon />
|
||||
<span class="bar-label">{{ formatMessage(messages.createGroup) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="selectedGroupedInstances.length > 0" type="transparent">
|
||||
<button type="button" :disabled="busy" @click="removeSelectedInstancesFromGroups">
|
||||
<MinusIcon />
|
||||
@@ -43,13 +49,13 @@
|
||||
</FloatingActionBar>
|
||||
<ConfirmDeleteInstanceModal
|
||||
ref="confirmDeleteModal"
|
||||
:count="selectedInstanceCount"
|
||||
:count="selectedInstanceIds.size"
|
||||
@delete="deleteSelectedInstances"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MinusIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { MinusIcon, PlusIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
@@ -60,7 +66,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useLibrary } from '@/components/ui/library/use-library'
|
||||
import { getLibraryInstanceSelectionKey, useLibrary } from '@/components/ui/library/use-library'
|
||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import { edit, remove } from '@/helpers/instance'
|
||||
@@ -69,20 +75,25 @@ const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const {
|
||||
instances,
|
||||
selectedLibraryInstanceIds,
|
||||
selectedLibraryInstances,
|
||||
clearLibraryInstanceSelection,
|
||||
setSelectedLibraryInstanceIds,
|
||||
setSelectedLibraryInstances,
|
||||
openNewGroupModal,
|
||||
} = useLibrary()
|
||||
|
||||
const confirmDeleteModal = ref<InstanceType<typeof ConfirmDeleteInstanceModal>>()
|
||||
const deleting = ref(false)
|
||||
const removingFromGroup = ref(false)
|
||||
const selectedInstanceCount = computed(() => selectedLibraryInstanceIds.value.size)
|
||||
const selectedInstanceCount = computed(() => selectedLibraryInstances.value.size)
|
||||
const selectedInstanceIds = computed(
|
||||
() =>
|
||||
new Set([...selectedLibraryInstances.value.values()].map((selection) => selection.instanceId)),
|
||||
)
|
||||
const selectedGroupedInstances = computed(() =>
|
||||
instances.value.filter(
|
||||
(instance) =>
|
||||
selectedLibraryInstanceIds.value.has(instance.id) && instance.group_ids.length > 0,
|
||||
),
|
||||
[...selectedLibraryInstances.value.values()].flatMap((selection) => {
|
||||
const instance = instances.value.find((candidate) => candidate.id === selection.instanceId)
|
||||
return instance?.group_ids.includes(selection.groupId) ? [{ instance, selection }] : []
|
||||
}),
|
||||
)
|
||||
const busy = computed(() => deleting.value || removingFromGroup.value)
|
||||
|
||||
@@ -99,26 +110,59 @@ const messages = defineMessages({
|
||||
id: 'app.library.selection.deleting',
|
||||
defaultMessage: 'Deleting selected instances',
|
||||
},
|
||||
createGroup: {
|
||||
id: 'app.library.selection.create-group',
|
||||
defaultMessage: 'Create group',
|
||||
},
|
||||
removeFromGroup: {
|
||||
id: 'app.library.selection.remove-from-group',
|
||||
defaultMessage: 'Remove from group',
|
||||
},
|
||||
})
|
||||
|
||||
function createGroupFromSelection() {
|
||||
openNewGroupModal(selectedInstanceIds.value)
|
||||
}
|
||||
|
||||
async function removeSelectedInstancesFromGroups() {
|
||||
if (busy.value || selectedGroupedInstances.value.length === 0) return
|
||||
|
||||
removingFromGroup.value = true
|
||||
const selectedGroupIdsByInstanceId = new Map<string, Set<string>>()
|
||||
for (const { instance, selection } of selectedGroupedInstances.value) {
|
||||
const groupIds = selectedGroupIdsByInstanceId.get(instance.id) ?? new Set()
|
||||
groupIds.add(selection.groupId)
|
||||
selectedGroupIdsByInstanceId.set(instance.id, groupIds)
|
||||
}
|
||||
const operations = [...selectedGroupIdsByInstanceId].flatMap(([instanceId, groupIds]) => {
|
||||
const instance = instances.value.find((candidate) => candidate.id === instanceId)
|
||||
return instance ? [{ instance, groupIds }] : []
|
||||
})
|
||||
const results = await Promise.allSettled(
|
||||
selectedGroupedInstances.value.map((instance) => edit(instance.id, { group_ids: [] })),
|
||||
operations.map(({ instance, groupIds }) => {
|
||||
return edit(instance.id, {
|
||||
group_ids: instance.group_ids.filter((groupId) => !groupIds.has(groupId)),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
for (const result of results) {
|
||||
const nextSelectedInstances = new Map(selectedLibraryInstances.value)
|
||||
for (const [index, result] of results.entries()) {
|
||||
if (result.status === 'rejected') {
|
||||
handleError(toError(result.reason))
|
||||
} else {
|
||||
for (const groupId of operations[index].groupIds) {
|
||||
nextSelectedInstances.delete(
|
||||
getLibraryInstanceSelectionKey({
|
||||
instanceId: operations[index].instance.id,
|
||||
groupId,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedLibraryInstances(nextSelectedInstances.values())
|
||||
removingFromGroup.value = false
|
||||
}
|
||||
|
||||
@@ -126,19 +170,23 @@ async function deleteSelectedInstances() {
|
||||
if (busy.value || selectedInstanceCount.value === 0) return
|
||||
|
||||
deleting.value = true
|
||||
const instanceIds = [...selectedLibraryInstanceIds.value]
|
||||
const instanceIds = [...selectedInstanceIds.value]
|
||||
const results = await Promise.allSettled(instanceIds.map((instanceId) => remove(instanceId)))
|
||||
const nextSelectedInstanceIds = new Set(selectedLibraryInstanceIds.value)
|
||||
const deletedInstanceIds = new Set<string>()
|
||||
|
||||
for (const [index, result] of results.entries()) {
|
||||
if (result.status === 'rejected') {
|
||||
handleError(toError(result.reason))
|
||||
} else {
|
||||
nextSelectedInstanceIds.delete(instanceIds[index])
|
||||
deletedInstanceIds.add(instanceIds[index])
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedLibraryInstanceIds(nextSelectedInstanceIds)
|
||||
setSelectedLibraryInstances(
|
||||
[...selectedLibraryInstances.value.values()].filter(
|
||||
(selection) => !deletedInstanceIds.has(selection.instanceId),
|
||||
),
|
||||
)
|
||||
deleting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@ import InstanceGroup from '@/components/ui/library/instance-group/index.vue'
|
||||
import InstanceGroupDnd from '@/components/ui/library/instance-group/instance-group-dnd.vue'
|
||||
import LibraryToolbar from '@/components/ui/library/library-toolbar/index.vue'
|
||||
import LibrarySelectionActionBar from '@/components/ui/library/LibrarySelectionActionBar.vue'
|
||||
import { provideLibrary } from '@/components/ui/library/use-library'
|
||||
import { getLibraryInstanceSelectionKey, provideLibrary } from '@/components/ui/library/use-library'
|
||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
@@ -30,8 +30,8 @@ const {
|
||||
confirmDeleteModal,
|
||||
deleteInstance,
|
||||
handleInstanceOption,
|
||||
selectedLibraryInstanceIds,
|
||||
setSelectedLibraryInstanceIds,
|
||||
selectedLibraryInstances,
|
||||
setSelectedLibraryInstances,
|
||||
toggleLibraryInstanceSelection,
|
||||
} = provideLibrary(toRef(props, 'instances'))
|
||||
|
||||
@@ -62,36 +62,37 @@ function handleToggleInstance(groupId: string, instanceId: string, shiftKey: boo
|
||||
)
|
||||
|
||||
if (anchorIndex === -1 || targetIndex === -1) {
|
||||
toggleLibraryInstanceSelection(instanceId)
|
||||
toggleLibraryInstanceSelection({ groupId, instanceId })
|
||||
return
|
||||
}
|
||||
|
||||
const start = Math.min(anchorIndex, targetIndex)
|
||||
const end = Math.max(anchorIndex, targetIndex)
|
||||
const range = displayedInstances.slice(start, end + 1)
|
||||
const nextSelectedIds = new Set(selectedLibraryInstanceIds.value)
|
||||
const nextSelectedInstances = new Map(selectedLibraryInstances.value)
|
||||
const targetKey = getLibraryInstanceSelectionKey({ groupId, instanceId })
|
||||
|
||||
if (nextSelectedIds.has(instanceId)) {
|
||||
if (nextSelectedInstances.has(targetKey)) {
|
||||
for (const instance of range) {
|
||||
nextSelectedIds.delete(instance.instanceId)
|
||||
nextSelectedInstances.delete(getLibraryInstanceSelectionKey(instance))
|
||||
}
|
||||
} else {
|
||||
for (const instance of range) {
|
||||
nextSelectedIds.add(instance.instanceId)
|
||||
nextSelectedInstances.set(getLibraryInstanceSelectionKey(instance), instance)
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedLibraryInstanceIds(nextSelectedIds)
|
||||
setSelectedLibraryInstances(nextSelectedInstances.values())
|
||||
anchorInstance.value = null
|
||||
return
|
||||
}
|
||||
|
||||
toggleLibraryInstanceSelection(instanceId)
|
||||
toggleLibraryInstanceSelection({ groupId, instanceId })
|
||||
anchorInstance.value = { groupId, instanceId }
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectedLibraryInstanceIds.value.size,
|
||||
() => selectedLibraryInstances.value.size,
|
||||
(selectedInstanceCount) => {
|
||||
if (selectedInstanceCount === 0) {
|
||||
anchorInstance.value = null
|
||||
|
||||
@@ -44,10 +44,8 @@ const draggedInstances = computed(() => {
|
||||
const drag = activeInstanceGroupDrag.value
|
||||
if (!drag) return []
|
||||
|
||||
return drag.instanceIds.flatMap((instanceId) => {
|
||||
const instance = props.instances.find((candidate) => candidate.id === instanceId)
|
||||
return instance ? [instance] : []
|
||||
})
|
||||
const draggedInstanceIds = new Set(drag.instances.map((selection) => selection.instanceId))
|
||||
return props.instances.filter((instance) => draggedInstanceIds.has(instance.id))
|
||||
})
|
||||
const draggedInstance = computed(() => {
|
||||
const drag = activeInstanceGroupDrag.value
|
||||
|
||||
@@ -8,7 +8,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import InstanceFileIcon from '@/assets/icons/instance-file.svg'
|
||||
import { useLibrary } from '@/components/ui/library/use-library'
|
||||
import { getLibraryInstanceSelectionKey, useLibrary } from '@/components/ui/library/use-library'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
|
||||
@@ -28,9 +28,9 @@ type ProcessEventPayload = {
|
||||
const { handleError } = injectNotificationManager()
|
||||
const {
|
||||
displayState,
|
||||
selectedLibraryInstanceIds,
|
||||
selectedLibraryInstances,
|
||||
isLibraryInstanceSelectionActive,
|
||||
activeDraggedInstanceIds,
|
||||
activeDraggedInstanceKeys,
|
||||
} = useLibrary()
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -55,10 +55,16 @@ const modLoading = computed(
|
||||
)
|
||||
const installing = computed(() => props.instance.install_stage.includes('installing'))
|
||||
const installed = computed(() => props.instance.install_stage === 'installed')
|
||||
const selected = computed(() => selectedLibraryInstanceIds.value.has(props.instance.id))
|
||||
const selectionKey = computed(() =>
|
||||
getLibraryInstanceSelectionKey({
|
||||
instanceId: props.instance.id,
|
||||
groupId: props.instanceGroupId,
|
||||
}),
|
||||
)
|
||||
const selected = computed(() => selectedLibraryInstances.value.has(selectionKey.value))
|
||||
const keys = useMagicKeys()
|
||||
const holdingShift = computed(() => keys.shift.value)
|
||||
const isPartOfActiveDrag = computed(() => activeDraggedInstanceIds.value.has(props.instance.id))
|
||||
const isPartOfActiveDrag = computed(() => activeDraggedInstanceKeys.value.has(selectionKey.value))
|
||||
const { isDragging } = useDraggable({
|
||||
id: computed(() => `instance:${props.instanceGroupId}:${props.instance.id}`),
|
||||
element: instanceCard,
|
||||
|
||||
+3
-3
@@ -59,7 +59,7 @@ export function useInstanceDragGather(instances: Ref<GameInstance[]>) {
|
||||
clear()
|
||||
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
if (!drag || drag.instanceIds.length < 2 || reduceMotion) return
|
||||
if (!drag || drag.instances.length < 2 || reduceMotion) return
|
||||
|
||||
const instanceCards = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('[data-library-instance-card]'),
|
||||
@@ -78,13 +78,13 @@ export function useInstanceDragGather(instances: Ref<GameInstance[]>) {
|
||||
}
|
||||
}
|
||||
|
||||
items.value = drag.instanceIds.flatMap((instanceId) => {
|
||||
items.value = drag.instances.flatMap(({ instanceId, groupId }) => {
|
||||
const instance = instances.value.find((candidate) => candidate.id === instanceId)
|
||||
const matchingCards = instanceCards.filter(
|
||||
(card) => card.dataset.instanceId === instanceId && card.getClientRects().length > 0,
|
||||
)
|
||||
const card =
|
||||
matchingCards.find((candidate) => candidate.dataset.instanceGroup === source.fromGroup) ??
|
||||
matchingCards.find((candidate) => candidate.dataset.instanceGroup === groupId) ??
|
||||
matchingCards[0]
|
||||
if (!instance || !card) return []
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const showCreationModal = inject<() => void>('showCreationModal')
|
||||
wrapper-class="min-w-[16rem] flex-1"
|
||||
/>
|
||||
<ButtonStyled>
|
||||
<button type="button" @click="openNewGroupModal">
|
||||
<button type="button" @click="openNewGroupModal()">
|
||||
<PlusIcon />
|
||||
New group
|
||||
</button>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
ref="groupNameInput"
|
||||
v-model="newGroupName"
|
||||
placeholder="Enter group name"
|
||||
:maxlength="32"
|
||||
:maxlength="128"
|
||||
@click="groupNameInput?.select()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,16 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
import { formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { useEventListener, useStorage } from '@vueuse/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, inject, type InjectionKey, provide, type Ref, ref, watchEffect } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
type InjectionKey,
|
||||
provide,
|
||||
type Ref,
|
||||
ref,
|
||||
watch,
|
||||
watchEffect,
|
||||
} from 'vue'
|
||||
|
||||
import { get_project_v3_many } from '@/helpers/cache.js'
|
||||
import { toError } from '@/helpers/errors'
|
||||
@@ -43,12 +52,20 @@ export type InstanceGroup = {
|
||||
instances: GameInstance[]
|
||||
}
|
||||
|
||||
export type LibraryInstanceSelection = {
|
||||
instanceId: string
|
||||
groupId: string
|
||||
}
|
||||
|
||||
export type ActiveInstanceGroupDrag = {
|
||||
instanceIds: string[]
|
||||
instances: LibraryInstanceSelection[]
|
||||
primaryInstanceId: string
|
||||
fromGroup: string | null
|
||||
}
|
||||
|
||||
export const getLibraryInstanceSelectionKey = ({ instanceId, groupId }: LibraryInstanceSelection) =>
|
||||
JSON.stringify([groupId, instanceId])
|
||||
|
||||
export type InstanceCard = {
|
||||
instance: GameInstance
|
||||
playing: boolean
|
||||
@@ -88,13 +105,13 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
const newGroupName = ref('')
|
||||
const newGroupSearch = ref('')
|
||||
const selectedNewGroupInstanceIds = ref(new Set<string>())
|
||||
/** Instance-level selection shared by every group representation of an instance. */
|
||||
const selectedLibraryInstanceIds = ref(new Set<string>())
|
||||
const isLibraryInstanceSelectionActive = computed(() => selectedLibraryInstanceIds.value.size > 0)
|
||||
const selectedLibraryInstances = ref(new Map<string, LibraryInstanceSelection>())
|
||||
const isLibraryInstanceSelectionActive = computed(() => selectedLibraryInstances.value.size > 0)
|
||||
const creatingGroup = ref(false)
|
||||
const activeInstanceGroupDrag = ref<ActiveInstanceGroupDrag | null>(null)
|
||||
const activeDraggedInstanceIds = computed(
|
||||
() => new Set(activeInstanceGroupDrag.value?.instanceIds ?? []),
|
||||
const activeDraggedInstanceKeys = computed(
|
||||
() =>
|
||||
new Set(activeInstanceGroupDrag.value?.instances.map(getLibraryInstanceSelectionKey) ?? []),
|
||||
)
|
||||
const instanceGroupDragTarget = ref<string | null>(null)
|
||||
const instanceGroupDragPointer = ref({ x: 0, y: 0 })
|
||||
@@ -137,7 +154,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
const existingGroupNames = computed(
|
||||
() => new Set(['none', ...Array.from(groupNames.value, (group) => group.toLowerCase())]),
|
||||
)
|
||||
const normalizedNewGroupName = computed(() => newGroupName.value.trim().substring(0, 32))
|
||||
const normalizedNewGroupName = computed(() => newGroupName.value.trim().substring(0, 128))
|
||||
const newGroupInstances = computed(() => {
|
||||
const query = newGroupSearch.value.trim().toLowerCase()
|
||||
|
||||
@@ -360,17 +377,22 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
) => {
|
||||
if (displayState.value.group !== 'Group') return
|
||||
|
||||
const instanceIds = selectedLibraryInstanceIds.value.has(instanceId)
|
||||
const primaryInstance = { instanceId, groupId }
|
||||
const primaryInstanceKey = getLibraryInstanceSelectionKey(primaryInstance)
|
||||
const draggedInstances = selectedLibraryInstances.value.has(primaryInstanceKey)
|
||||
? [
|
||||
instanceId,
|
||||
...[...selectedLibraryInstanceIds.value].filter(
|
||||
(selectedId) => selectedId !== instanceId,
|
||||
primaryInstance,
|
||||
...[...selectedLibraryInstances.value.values()].filter(
|
||||
(selectedInstance) =>
|
||||
getLibraryInstanceSelectionKey(selectedInstance) !== primaryInstanceKey,
|
||||
),
|
||||
].filter((selectedId) => instances.value.some((instance) => instance.id === selectedId))
|
||||
: [instanceId]
|
||||
].filter((selectedInstance) =>
|
||||
instances.value.some((instance) => instance.id === selectedInstance.instanceId),
|
||||
)
|
||||
: [primaryInstance]
|
||||
|
||||
activeInstanceGroupDrag.value = {
|
||||
instanceIds,
|
||||
instances: draggedInstances,
|
||||
primaryInstanceId: instanceId,
|
||||
fromGroup: normalizeInstanceGroupId(groupId),
|
||||
}
|
||||
@@ -389,9 +411,12 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
|
||||
const getInstanceGroupDropState = (groupId: string) => {
|
||||
const drag = activeInstanceGroupDrag.value
|
||||
const draggedInstances = drag
|
||||
? instances.value.filter((instance) => drag.instanceIds.includes(instance.id))
|
||||
: []
|
||||
const draggedInstanceIds = new Set(
|
||||
drag?.instances.map((selection) => selection.instanceId) ?? [],
|
||||
)
|
||||
const draggedInstances = instances.value.filter((instance) =>
|
||||
draggedInstanceIds.has(instance.id),
|
||||
)
|
||||
const toGroup = normalizeInstanceGroupId(groupId)
|
||||
const operation = isAddingInstanceToGroup.value ? 'add' : 'move'
|
||||
const canAddToGroup = operation !== 'add' || toGroup !== null
|
||||
@@ -418,7 +443,9 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
const dropState = getInstanceGroupDropState(target)
|
||||
if (!dropState.canDrop || dropState.operation !== 'add') return
|
||||
|
||||
const count = activeInstanceGroupDrag.value?.instanceIds.length ?? 0
|
||||
const count = new Set(
|
||||
activeInstanceGroupDrag.value?.instances.map((selection) => selection.instanceId) ?? [],
|
||||
).size
|
||||
return count > 1 ? `Duplicate ${count} instances to group` : 'Duplicate into group'
|
||||
}
|
||||
|
||||
@@ -435,8 +462,9 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
if (!dropState.canDrop) return false
|
||||
|
||||
const shouldAdd = addToGroup && toGroup !== null
|
||||
const draggedInstanceIds = new Set(drag.instances.map((selection) => selection.instanceId))
|
||||
const draggedInstances = instances.value.filter((instance) =>
|
||||
drag.instanceIds.includes(instance.id),
|
||||
draggedInstanceIds.has(instance.id),
|
||||
)
|
||||
const results = await Promise.allSettled(
|
||||
draggedInstances.map((instance) => {
|
||||
@@ -475,7 +503,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
isAddingInstanceToGroup.value = false
|
||||
})
|
||||
|
||||
const openNewGroupModal = () => {
|
||||
const openNewGroupModal = (instanceIds: Iterable<string> = []) => {
|
||||
let groupNumber = groupNames.value.size + 1
|
||||
while (existingGroupNames.value.has(`group ${groupNumber}`.toLowerCase())) {
|
||||
groupNumber++
|
||||
@@ -483,7 +511,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
|
||||
newGroupName.value = `Group ${groupNumber}`
|
||||
newGroupSearch.value = ''
|
||||
selectedNewGroupInstanceIds.value = new Set()
|
||||
selectedNewGroupInstanceIds.value = new Set(instanceIds)
|
||||
isNewGroupModalOpen.value = true
|
||||
}
|
||||
|
||||
@@ -504,23 +532,28 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
}
|
||||
|
||||
const clearLibraryInstanceSelection = () => {
|
||||
selectedLibraryInstanceIds.value = new Set()
|
||||
selectedLibraryInstances.value = new Map()
|
||||
}
|
||||
|
||||
const setSelectedLibraryInstanceIds = (instanceIds: Iterable<string>) => {
|
||||
selectedLibraryInstanceIds.value = new Set(instanceIds)
|
||||
watch(() => displayState.value.group, clearLibraryInstanceSelection)
|
||||
|
||||
const setSelectedLibraryInstances = (selections: Iterable<LibraryInstanceSelection>) => {
|
||||
selectedLibraryInstances.value = new Map(
|
||||
[...selections].map((selection) => [getLibraryInstanceSelectionKey(selection), selection]),
|
||||
)
|
||||
}
|
||||
|
||||
const toggleLibraryInstanceSelection = (instanceId: string) => {
|
||||
const selectedIds = new Set(selectedLibraryInstanceIds.value)
|
||||
const toggleLibraryInstanceSelection = (selection: LibraryInstanceSelection) => {
|
||||
const selectedInstances = new Map(selectedLibraryInstances.value)
|
||||
const selectionKey = getLibraryInstanceSelectionKey(selection)
|
||||
|
||||
if (selectedIds.has(instanceId)) {
|
||||
selectedIds.delete(instanceId)
|
||||
if (selectedInstances.has(selectionKey)) {
|
||||
selectedInstances.delete(selectionKey)
|
||||
} else {
|
||||
selectedIds.add(instanceId)
|
||||
selectedInstances.set(selectionKey, selection)
|
||||
}
|
||||
|
||||
selectedLibraryInstanceIds.value = selectedIds
|
||||
selectedLibraryInstances.value = selectedInstances
|
||||
}
|
||||
|
||||
useEventListener(window, 'keydown', (event) => {
|
||||
@@ -693,10 +726,10 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
newGroupName,
|
||||
newGroupSearch,
|
||||
selectedNewGroupInstanceIds,
|
||||
selectedLibraryInstanceIds,
|
||||
selectedLibraryInstances,
|
||||
isLibraryInstanceSelectionActive,
|
||||
activeInstanceGroupDrag,
|
||||
activeDraggedInstanceIds,
|
||||
activeDraggedInstanceKeys,
|
||||
instanceGroupDragTarget,
|
||||
instanceGroupDragPointer,
|
||||
instanceGroupDragStatus,
|
||||
@@ -718,7 +751,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
|
||||
closeNewGroupModal,
|
||||
toggleNewGroupInstance,
|
||||
clearLibraryInstanceSelection,
|
||||
setSelectedLibraryInstanceIds,
|
||||
setSelectedLibraryInstances,
|
||||
toggleLibraryInstanceSelection,
|
||||
createGroup,
|
||||
deleteGroup,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::state::instances::adapters::sqlite::instance_rows;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
const MAX_GROUP_NAME_LENGTH: usize = 32;
|
||||
const MAX_GROUP_NAME_LENGTH: usize = 128;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct InstanceGroup {
|
||||
|
||||
@@ -68,10 +68,7 @@
|
||||
<component :is="currentStage?.stageContent" />
|
||||
|
||||
<template #actions>
|
||||
<div
|
||||
class="flex flex-col justify-end gap-2 sm:flex-row"
|
||||
:class="leftButtonConfig || rightButtonConfig ? 'mt-4' : ''"
|
||||
>
|
||||
<div class="flex flex-col justify-end gap-2 sm:flex-row">
|
||||
<ButtonStyled v-if="leftButtonConfig" type="outlined">
|
||||
<button
|
||||
v-tooltip="leftButtonConfig.tooltip"
|
||||
|
||||
Reference in New Issue
Block a user