refactor: instance groups to be identified by ID instead of name, names become display data

This commit is contained in:
tdgao
2026-07-29 14:43:03 -07:00
parent 5ad5fa829f
commit 6bb69c8d52
28 changed files with 237 additions and 1303 deletions
@@ -19,8 +19,10 @@ import { useRouter } from 'vue-router'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { toError } from '@/helpers/errors'
import { install_duplicate_instance } from '@/helpers/install'
import { edit, edit_icon, list, remove } from '@/helpers/instance'
import { edit, edit_icon, remove } from '@/helpers/instance'
import { create_group, list_groups } from '@/helpers/instance-groups'
import { injectInstanceSettings } from '@/providers/instance-settings'
import type { GameInstance } from '../../../helpers/types'
@@ -38,7 +40,7 @@ const releaseChannelOptions: ReleaseChannel[] = ['release', 'beta', 'alpha']
const title = ref(instance.value.name)
const icon: Ref<string | undefined> = ref(instance.value.icon_path)
const groups = ref([...instance.value.groups])
const groupIds = ref([...instance.value.group_ids])
const savingReleaseChannel = ref(false)
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.update_channel)
const releaseChannelDisabledItems = computed<ReleaseChannel[]>(() =>
@@ -57,10 +59,7 @@ async function duplicateInstance() {
})
}
const allInstances = ref((await list()) as GameInstance[])
const availableGroups = computed(() => [
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
])
const availableGroups = ref(await list_groups())
function formatReleaseChannelLabel(channel: ReleaseChannel) {
switch (channel) {
@@ -135,28 +134,34 @@ async function setIcon() {
const editInstanceObject = computed(() => ({
name: title.value.trim().substring(0, 32) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
group_ids: groupIds.value,
}))
const toggleGroup = (group: string) => {
if (groups.value.includes(group)) {
groups.value = groups.value.filter((x) => x !== group)
const toggleGroup = (groupId: string) => {
if (groupIds.value.includes(groupId)) {
groupIds.value = groupIds.value.filter((id) => id !== groupId)
} else {
groups.value.push(group)
groupIds.value.push(groupId)
}
}
const addCategory = () => {
const addCategory = async () => {
const text = newCategoryInput.value.trim()
if (text.length > 0) {
groups.value.push(text.substring(0, 32))
newCategoryInput.value = ''
try {
const group = await create_group(text.substring(0, 32))
availableGroups.value.push(group)
groupIds.value.push(group.id)
newCategoryInput.value = ''
} catch (error) {
handleError(toError(error))
}
}
}
watch(
[title, groups, groups],
[title, groupIds],
async () => {
if (removing.value) return
await edit(instance.value.id, editInstanceObject.value).catch(handleError)
@@ -369,10 +374,10 @@ const messages = defineMessages({
<div class="flex flex-col gap-1">
<Checkbox
v-for="group in availableGroups"
:key="group"
:model-value="groups.includes(group)"
:label="group"
@click="toggleGroup(group)"
:key="group.id"
:model-value="groupIds.includes(group.id)"
:label="group.name"
@click="toggleGroup(group.id)"
/>
<div class="flex gap-2 items-center">
<StyledInput
@@ -80,7 +80,8 @@ const removingFromGroup = ref(false)
const selectedInstanceCount = computed(() => selectedLibraryInstanceIds.value.size)
const selectedGroupedInstances = computed(() =>
instances.value.filter(
(instance) => selectedLibraryInstanceIds.value.has(instance.id) && instance.groups.length > 0,
(instance) =>
selectedLibraryInstanceIds.value.has(instance.id) && instance.group_ids.length > 0,
),
)
const busy = computed(() => deleting.value || removingFromGroup.value)
@@ -109,7 +110,7 @@ async function removeSelectedInstancesFromGroups() {
removingFromGroup.value = true
const results = await Promise.allSettled(
selectedGroupedInstances.value.map((instance) => edit(instance.id, { groups: [] })),
selectedGroupedInstances.value.map((instance) => edit(instance.id, { group_ids: [] })),
)
for (const result of results) {
@@ -40,7 +40,6 @@ const {
isSectionCollapsed,
setSectionCollapsed,
deleteGroup,
isValidGroupName,
renameGroup,
handleInstanceContextMenu,
displayState,
@@ -57,7 +56,7 @@ const groupNameInput = ref<InstanceType<typeof InlineEditableText>>()
const confirmDeleteGroupModal = ref<InstanceType<typeof NewModal>>()
const deletingGroup = ref(false)
const groupName = ref(props.instanceGroup.key)
const isUngrouped = computed(() => props.instanceGroup.key === 'None')
const isUngrouped = computed(() => props.instanceGroup.id === 'group:none')
const groupContextMenuOpen = ref(false)
const isGroupToggleBlocked = computed(
() => groupContextMenuOpen.value || Boolean(groupNameInput.value?.isEditing),
@@ -74,7 +73,7 @@ useDroppable({
element: groupDropTarget,
disabled: computed(() => displayState.value.group !== 'Group'),
data: computed(() => ({
groupName: props.instanceGroup.key,
groupId: props.instanceGroup.id,
})),
})
@@ -107,21 +106,17 @@ const messages = defineMessages({
id: 'app.library.group.name-reserved',
defaultMessage: '"None" is reserved and cannot be used as a group name.',
},
groupNameDuplicate: {
id: 'app.library.group.name-duplicate',
defaultMessage: 'A group with this name already exists.',
},
deleteGroupDescription: {
id: 'app.library.group.delete-description',
defaultMessage: 'Instances in this group will be ungrouped.',
},
})
function openInstanceContextMenu(event: MouseEvent, instanceId: string, instanceGroupName: string) {
function openInstanceContextMenu(event: MouseEvent, instanceId: string, instanceGroupId: string) {
const instanceComponent = instanceComponents.get(instanceId)
if (!instanceComponent) return
handleInstanceContextMenu(event, instanceComponent, instanceGroupName)
handleInstanceContextMenu(event, instanceComponent, instanceGroupId)
}
function setInstanceComponent(instanceId: string, component: unknown) {
@@ -136,7 +131,7 @@ async function removeGroup() {
if (deletingGroup.value) return
deletingGroup.value = true
const deleted = await deleteGroup(props.instanceGroup.key)
const deleted = await deleteGroup(props.instanceGroup.id)
deletingGroup.value = false
if (deleted) {
@@ -217,8 +212,6 @@ function validateGroupName(value: string) {
reason = formatMessage(messages.groupNameTooLong)
} else if (normalizedGroupName.toLowerCase() === 'none') {
reason = formatMessage(messages.groupNameReserved)
} else if (!isValidGroupName(normalizedGroupName, props.instanceGroup.key)) {
reason = formatMessage(messages.groupNameDuplicate)
}
if (reason) {
@@ -234,7 +227,7 @@ function validateGroupName(value: string) {
}
async function updateGroupName(value: string) {
return await renameGroup(props.instanceGroup.id, props.instanceGroup.key, value)
return await renameGroup(props.instanceGroup.id, value)
}
watch(
@@ -272,8 +265,8 @@ watch(
<div
v-if="
activeInstanceGroupDrag &&
instanceGroupDragTarget === instanceGroup.key &&
getInstanceGroupDropState(instanceGroup.key).canDrop
instanceGroupDragTarget === instanceGroup.id &&
getInstanceGroupDropState(instanceGroup.id).canDrop
"
class="pointer-events-none absolute -inset-2 inset-y-0 z-20 rounded-xl border-2 opacity-50 border-dashed border-brand bg-transparent"
/>
@@ -344,10 +337,10 @@ watch(
</div>
<Accordion
ref="groupAccordion"
:open-by-default="hideHeader || !isSectionCollapsed(instanceGroup.key)"
:open-by-default="hideHeader || !isSectionCollapsed(instanceGroup.id)"
class="w-full"
@on-open="setSectionCollapsed(instanceGroup.key, false)"
@on-close="setSectionCollapsed(instanceGroup.key, true)"
@on-open="setSectionCollapsed(instanceGroup.id, false)"
@on-close="setSectionCollapsed(instanceGroup.id, true)"
>
<section
class="grid min-h-[45px] mt-2.5 w-full grid-cols-[repeat(auto-fill,minmax(20rem,22rem))] gap-3 overflow-y-auto scroll-smooth"
@@ -356,13 +349,13 @@ watch(
<Instance
:ref="(component: unknown) => setInstanceComponent(instance.id, component)"
:instance="instance"
:instance-group-name="instanceGroup.key"
:instance-group-id="instanceGroup.id"
:is-selection-anchor="selectionAnchorInstanceId === instance.id"
@toggle-selection="
(shiftKey: boolean) => emit('toggle-selection', instance.id, shiftKey)
"
@contextmenu.prevent.stop="
(event: MouseEvent) => openInstanceContextMenu(event, instance.id, instanceGroup.key)
(event: MouseEvent) => openInstanceContextMenu(event, instance.id, instanceGroup.id)
"
/>
</div>
@@ -21,7 +21,7 @@ type InstanceDragData = {
}
type InstanceGroupDndDropData = {
groupName: string
groupId: string
}
const props = defineProps<{
@@ -134,15 +134,15 @@ function handleDragMove(event: DragMoveEvent) {
function handleDragOver(event: DragOverEvent) {
const targetData = event.operation.target?.data as InstanceGroupDndDropData | undefined
setInstanceGroupDragTarget(targetData?.groupName ?? null)
setInstanceGroupDragTarget(targetData?.groupId ?? null)
}
function handleDragEnd(event: DragEndEvent) {
const targetData = event.operation.target?.data as InstanceGroupDndDropData | undefined
if (!event.canceled && targetData) {
const dropState = getInstanceGroupDropState(targetData.groupName)
const dropState = getInstanceGroupDropState(targetData.groupId)
if (dropState.canDrop) {
void moveDraggedInstancesToGroup(targetData.groupName, dropState.operation === 'add')
void moveDraggedInstancesToGroup(targetData.groupId, dropState.operation === 'add')
}
}
@@ -35,7 +35,7 @@ const {
const props = defineProps<{
instance: GameInstance
instanceGroupName: string
instanceGroupId: string
isSelectionAnchor?: boolean
}>()
@@ -60,12 +60,12 @@ const keys = useMagicKeys()
const holdingShift = computed(() => keys.shift.value)
const isPartOfActiveDrag = computed(() => activeDraggedInstanceIds.value.has(props.instance.id))
const { isDragging } = useDraggable({
id: computed(() => `instance:${props.instanceGroupName}:${props.instance.id}`),
id: computed(() => `instance:${props.instanceGroupId}:${props.instance.id}`),
element: instanceCard,
disabled: computed(() => displayState.value.group !== 'Group'),
data: computed(() => ({
instanceId: props.instance.id,
fromGroup: props.instanceGroupName,
fromGroup: props.instanceGroupId,
})),
})
@@ -218,7 +218,7 @@ onUnmounted(() => unlisten())
}"
data-library-instance-card
:data-instance-id="instance.id"
:data-instance-group="instanceGroupName"
:data-instance-group="instanceGroupId"
role="button"
tabindex="0"
:aria-label="
@@ -22,9 +22,6 @@
:maxlength="32"
@click="groupNameInput?.select()"
/>
<span v-if="newGroupNameExists" class="text-sm font-medium text-red">
A group with this name already exists.
</span>
</div>
<div class="h-px bg-divider" />
@@ -62,8 +59,8 @@
/>
<div class="flex min-w-0 items-center gap-2">
<span class="truncate font-semibold text-contrast">{{ instance.name }}</span>
<TagItem v-if="instance.groups[0]" class="shrink-0">
{{ instance.groups[0] }}
<TagItem v-if="instance.group_ids[0]" class="shrink-0">
{{ groupNamesById.get(instance.group_ids[0]) ?? 'Unknown group' }}
</TagItem>
</div>
</div>
@@ -74,10 +71,10 @@
<CheckIcon v-if="selectedNewGroupInstanceIds.has(instance.id)" />
{{
selectedNewGroupInstanceIds.has(instance.id)
? instance.groups.length > 0
? instance.group_ids.length > 0
? 'Moved'
: 'Added'
: instance.groups.length > 0
: instance.group_ids.length > 0
? 'Move'
: 'Add'
}}
@@ -111,17 +108,17 @@
import { CheckIcon, PlusIcon, SearchIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, NewModal, StyledInput, TagItem } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useLibrary } from '@/components/ui/library/use-library'
const {
isNewGroupModalOpen,
libraryGroups,
newGroupName,
newGroupSearch,
selectedNewGroupInstanceIds,
creatingGroup,
newGroupNameExists,
newGroupInstances,
canCreateGroup,
closeNewGroupModal,
@@ -131,6 +128,9 @@ const {
const modal = ref<InstanceType<typeof NewModal>>()
const groupNameInput = ref<InstanceType<typeof StyledInput>>()
const groupNamesById = computed(
() => new Map(libraryGroups.value.map((group) => [group.id, group.name])),
)
watch(isNewGroupModalOpen, (open) => {
if (open) {
@@ -84,7 +84,6 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
})
const serverProjectIds = ref(new Set<string>())
const libraryGroups = ref<InstanceGroupDefinition[]>([])
const groupIdsByName = ref(new Map<string, string>())
const isNewGroupModalOpen = ref(false)
const newGroupName = ref('')
const newGroupSearch = ref('')
@@ -102,7 +101,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
const isAddingInstanceToGroup = ref(false)
const instanceOptions = ref<InstanceContextMenu | null>(null)
const currentDeleteInstanceId = ref<string | null>(null)
const currentContextGroupName = ref<string | null>(null)
const currentContextGroupId = ref<string | null>(null)
const confirmDeleteModal = ref<ConfirmDeleteModal | null>(null)
const displayState = useStorage<{
@@ -129,10 +128,8 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
const groupNames = computed(
() =>
new Set(
[
...libraryGroups.value.map((group) => group.name),
...instances.value.flatMap((instance) => instance.groups),
]
libraryGroups.value
.map((group) => group.name)
.map((group) => group.trim())
.filter((group) => group && group.toLowerCase() !== 'none'),
),
@@ -141,9 +138,6 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
() => new Set(['none', ...Array.from(groupNames.value, (group) => group.toLowerCase())]),
)
const normalizedNewGroupName = computed(() => newGroupName.value.trim().substring(0, 32))
const newGroupNameExists = computed(() =>
existingGroupNames.value.has(normalizedNewGroupName.value.toLowerCase()),
)
const newGroupInstances = computed(() => {
const query = newGroupSearch.value.trim().toLowerCase()
@@ -151,25 +145,19 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
.filter((instance) => !query || instance.name.toLowerCase().includes(query))
.slice()
.sort((a, b) => {
const groupedDifference = Number(a.groups.length > 0) - Number(b.groups.length > 0)
const groupedDifference = Number(a.group_ids.length > 0) - Number(b.group_ids.length > 0)
if (groupedDifference !== 0) return groupedDifference
return a.name.localeCompare(b.name)
})
})
const canCreateGroup = computed(
() =>
normalizedNewGroupName.value.length > 0 && !newGroupNameExists.value && !creatingGroup.value,
() => normalizedNewGroupName.value.length > 0 && !creatingGroup.value,
)
const refreshGroups = async () => {
try {
const groups = await listInstanceGroups()
libraryGroups.value = groups
groupIdsByName.value = new Map([
...groupIdsByName.value,
...groups.map((group) => [group.name, group.id] as const),
])
libraryGroups.value = await listInstanceGroups()
} catch (error) {
handleError(toError(error))
}
@@ -259,66 +247,61 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
break
}
const groupedInstances = new Map<string, GameInstance[]>()
const addToGroup = (key: string, instance: GameInstance) => {
const group = groupedInstances.get(key) ?? []
group.push(instance)
groupedInstances.set(key, group)
const groupedInstances = new Map<string, { name: string; instances: GameInstance[] }>()
const addToGroup = (id: string, name: string, instance: GameInstance) => {
const group = groupedInstances.get(id) ?? { name, instances: [] }
group.instances.push(instance)
groupedInstances.set(id, group)
}
const groupsById = new Map(libraryGroups.value.map((group) => [group.id, group]))
for (const instance of visibleInstances) {
switch (displayState.value.group) {
case 'Loader':
addToGroup(formatLoader(formatMessage, instance.loader), instance)
{
const name = formatLoader(formatMessage, instance.loader)
addToGroup(`Loader:${name}`, name, instance)
}
break
case 'Game version':
addToGroup(instance.game_version, instance)
addToGroup(`Game version:${instance.game_version}`, instance.game_version, instance)
break
case 'Group':
for (const group of instance.groups.length > 0 ? instance.groups : ['None']) {
addToGroup(group, instance)
if (instance.group_ids.length === 0) {
addToGroup('group:none', 'None', instance)
} else {
for (const groupId of instance.group_ids) {
const group = groupsById.get(groupId)
addToGroup(groupId, group?.name ?? groupId, instance)
}
}
break
case 'None':
addToGroup('None', instance)
addToGroup('None:None', 'None', instance)
break
}
}
const resolveGroupId = (groupName: string) =>
groupIdsByName.value.get(groupName) ?? `group-name:${groupName}`
if (displayState.value.group === 'Group') {
if (!groupedInstances.has('None')) {
groupedInstances.set('None', [])
if (!groupedInstances.has('group:none')) {
groupedInstances.set('group:none', { name: 'None', instances: [] })
}
const populatedGroupIds = new Set(
instances.value.flatMap((instance) =>
instance.groups.map((groupName) => resolveGroupId(groupName)),
),
)
for (const group of libraryGroups.value) {
if (!populatedGroupIds.has(group.id) && !groupedInstances.has(group.name)) {
groupedInstances.set(group.name, [])
if (!groupedInstances.has(group.id)) {
groupedInstances.set(group.id, { name: group.name, instances: [] })
}
}
}
const groups = Array.from(groupedInstances, ([key, groupInstances]) => ({
id:
displayState.value.group === 'Group'
? key === 'None'
? 'group:none'
: resolveGroupId(key)
: `${displayState.value.group}:${key}`,
key,
instances: groupInstances,
const groups = Array.from(groupedInstances, ([id, group]) => ({
id,
key: group.name,
instances: group.instances,
}))
if (displayState.value.sortBy === 'Name') {
groups.sort((a, b) => a.key.localeCompare(b.key))
groups.sort((a, b) => a.key.localeCompare(b.key) || a.id.localeCompare(b.id))
}
if (displayState.value.group === 'Game version') {
@@ -329,20 +312,20 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
groups.sort((a, b) => {
if (a.key === 'None') return 1
if (b.key === 'None') return -1
return a.key.localeCompare(b.key)
return a.key.localeCompare(b.key) || a.id.localeCompare(b.id)
})
}
return groups
})
const getSectionKey = (sectionName: string) => `${displayState.value.group}:${sectionName}`
const getSectionKey = (sectionId: string) => `${displayState.value.group}:${sectionId}`
const isSectionCollapsed = (sectionName: string) =>
collapsedSectionKeys.value.has(getSectionKey(sectionName))
const isSectionCollapsed = (sectionId: string) =>
collapsedSectionKeys.value.has(getSectionKey(sectionId))
const setSectionCollapsed = (sectionName: string, collapsed: boolean) => {
const sectionKey = getSectionKey(sectionName)
const setSectionCollapsed = (sectionId: string, collapsed: boolean) => {
const sectionKey = getSectionKey(sectionId)
const collapsedSections = new Set(displayState.value.collapsedGroups)
if (collapsed) {
@@ -354,8 +337,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
displayState.value.collapsedGroups = [...collapsedSections]
}
const normalizeInstanceGroupName = (groupName: string) =>
groupName === 'None' ? null : groupName
const normalizeInstanceGroupId = (groupId: string) => (groupId === 'group:none' ? null : groupId)
const updateInstanceGroupDrag = (
pointer?: { x: number; y: number },
@@ -372,7 +354,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
const startInstanceGroupDrag = (
instanceId: string,
groupName: string,
groupId: string,
pointer?: { x: number; y: number },
altKey = false,
) => {
@@ -390,7 +372,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
activeInstanceGroupDrag.value = {
instanceIds,
primaryInstanceId: instanceId,
fromGroup: normalizeInstanceGroupName(groupName),
fromGroup: normalizeInstanceGroupId(groupId),
}
updateInstanceGroupDrag(pointer, altKey)
}
@@ -401,26 +383,26 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
isAddingInstanceToGroup.value = false
}
const setInstanceGroupDragTarget = (groupName: string | null) => {
instanceGroupDragTarget.value = groupName
const setInstanceGroupDragTarget = (groupId: string | null) => {
instanceGroupDragTarget.value = groupId
}
const getInstanceGroupDropState = (groupName: string) => {
const getInstanceGroupDropState = (groupId: string) => {
const drag = activeInstanceGroupDrag.value
const draggedInstances = drag
? instances.value.filter((instance) => drag.instanceIds.includes(instance.id))
: []
const toGroup = normalizeInstanceGroupName(groupName)
const toGroup = normalizeInstanceGroupId(groupId)
const operation = isAddingInstanceToGroup.value ? 'add' : 'move'
const canAddToGroup = operation !== 'add' || toGroup !== null
const hasChanges = draggedInstances.some((instance) => {
if (operation === 'add') {
return toGroup !== null && !instance.groups.includes(toGroup)
return toGroup !== null && !instance.group_ids.includes(toGroup)
}
return toGroup === null
? instance.groups.length > 0
: instance.groups.length !== 1 || instance.groups[0] !== toGroup
? instance.group_ids.length > 0
: instance.group_ids.length !== 1 || instance.group_ids[0] !== toGroup
})
return {
@@ -444,12 +426,12 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
return
})
const moveDraggedInstancesToGroup = async (groupName: string, addToGroup = false) => {
const moveDraggedInstancesToGroup = async (groupId: string, addToGroup = false) => {
const drag = activeInstanceGroupDrag.value
const toGroup = normalizeInstanceGroupName(groupName)
const toGroup = normalizeInstanceGroupId(groupId)
if (!drag) return false
const dropState = getInstanceGroupDropState(groupName)
const dropState = getInstanceGroupDropState(groupId)
if (!dropState.canDrop) return false
const shouldAdd = addToGroup && toGroup !== null
@@ -458,12 +440,12 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
)
const results = await Promise.allSettled(
draggedInstances.map((instance) => {
const nextGroups = shouldAdd ? [...instance.groups] : toGroup ? [toGroup] : []
if (shouldAdd && toGroup && !nextGroups.includes(toGroup)) {
nextGroups.push(toGroup)
const nextGroupIds = shouldAdd ? [...instance.group_ids] : toGroup ? [toGroup] : []
if (shouldAdd && toGroup && !nextGroupIds.includes(toGroup)) {
nextGroupIds.push(toGroup)
}
return edit(instance.id, { groups: nextGroups })
return edit(instance.id, { group_ids: nextGroupIds })
}),
)
let movedInstanceCount = 0
@@ -562,14 +544,13 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
...libraryGroups.value.filter((existingGroup) => existingGroup.id !== group.id),
group,
]
groupIdsByName.value = new Map(groupIdsByName.value).set(group.name, group.id)
await Promise.all(
instances.value
.filter((instance) => selectedNewGroupInstanceIds.value.has(instance.id))
.map((instance) =>
edit(instance.id, {
groups: [group.name],
group_ids: [group.id],
}),
),
)
@@ -583,13 +564,10 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
}
}
const deleteGroup = async (groupName: string) => {
const deleteGroup = async (groupId: string) => {
try {
await deleteInstanceGroup(groupName)
libraryGroups.value = libraryGroups.value.filter((group) => group.name !== groupName)
const nextGroupIdsByName = new Map(groupIdsByName.value)
nextGroupIdsByName.delete(groupName)
groupIdsByName.value = nextGroupIdsByName
await deleteInstanceGroup(groupId)
libraryGroups.value = libraryGroups.value.filter((group) => group.id !== groupId)
return true
} catch (error) {
handleError(toError(error))
@@ -598,60 +576,19 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
}
}
const isValidGroupName = (groupName: string, currentGroupName: string) => {
const normalizedGroupName = groupName.trim()
if (
normalizedGroupName.length === 0 ||
normalizedGroupName.length > 32 ||
normalizedGroupName.toLowerCase() === 'none'
) {
return false
}
return !Array.from(groupNames.value).some(
(existingGroupName) =>
existingGroupName.toLowerCase() === normalizedGroupName.toLowerCase() &&
existingGroupName !== currentGroupName,
)
}
const renameGroup = async (groupId: string, oldName: string, newName: string) => {
const renameGroup = async (groupId: string, newName: string) => {
const normalizedNewName = newName.trim()
if (oldName === normalizedNewName) return true
if (!isValidGroupName(normalizedNewName, oldName)) return false
const previousNewNameId = groupIdsByName.value.get(normalizedNewName)
groupIdsByName.value = new Map(groupIdsByName.value).set(normalizedNewName, groupId)
const currentGroup = libraryGroups.value.find((group) => group.id === groupId)
if (currentGroup?.name === normalizedNewName) return true
try {
const renamedGroup = await renameInstanceGroup(oldName, normalizedNewName)
const renamedGroup = await renameInstanceGroup(groupId, normalizedNewName)
libraryGroups.value = [
...libraryGroups.value.filter((group) => group.id !== groupId),
renamedGroup,
]
groupIdsByName.value = new Map(groupIdsByName.value).set(renamedGroup.name, renamedGroup.id)
const oldSectionKey = getSectionKey(oldName)
const newSectionKey = getSectionKey(renamedGroup.name)
if (collapsedSectionKeys.value.has(oldSectionKey)) {
displayState.value.collapsedGroups = displayState.value.collapsedGroups.map((sectionKey) =>
sectionKey === oldSectionKey ? newSectionKey : sectionKey,
)
}
if (currentContextGroupName.value === oldName) {
currentContextGroupName.value = renamedGroup.name
}
return true
} catch (error) {
const nextGroupIdsByName = new Map(groupIdsByName.value)
if (previousNewNameId) {
nextGroupIdsByName.set(normalizedNewName, previousNewNameId)
} else {
nextGroupIdsByName.delete(normalizedNewName)
}
groupIdsByName.value = nextGroupIdsByName
handleError(toError(error))
await refreshGroups()
return false
@@ -672,11 +609,11 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
const handleInstanceContextMenu = (
event: MouseEvent,
item: InstanceCard,
instanceGroupName: string,
instanceGroupId: string,
) => {
currentContextGroupName.value =
displayState.value.group === 'Group' && instanceGroupName !== 'None'
? instanceGroupName
currentContextGroupId.value =
displayState.value.group === 'Group' && instanceGroupId !== 'group:none'
? instanceGroupId
: null
const baseOptions = [
@@ -685,7 +622,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
{ name: 'duplicate' },
{ name: 'open' },
{ name: 'copy' },
...(currentContextGroupName.value
...(currentContextGroupId.value
? [{ name: 'remove_from_group' }, { type: 'divider' }]
: [{ type: 'divider' }]),
{ name: 'delete', color: 'danger' },
@@ -729,10 +666,12 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
await navigator.clipboard.writeText(item.instance.id)
break
case 'remove_from_group':
if (currentContextGroupName.value) {
const groupName = currentContextGroupName.value
if (currentContextGroupId.value) {
const groupId = currentContextGroupId.value
await edit(item.instance.id, {
groups: item.instance.groups.filter((group) => group !== groupName),
group_ids: item.instance.group_ids.filter(
(instanceGroupId) => instanceGroupId !== groupId,
),
}).catch((error) => handleError(toError(error)))
}
break
@@ -745,6 +684,7 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
return {
instances,
libraryGroups,
search,
filters,
displayState,
@@ -762,7 +702,6 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
instanceGroupDragStatus,
isAddingInstanceToGroup,
creatingGroup,
newGroupNameExists,
newGroupInstances,
canCreateGroup,
instanceOptions,
@@ -783,7 +722,6 @@ function createLibraryState(instances: Ref<GameInstance[]>) {
toggleLibraryInstanceSelection,
createGroup,
deleteGroup,
isValidGroupName,
renameGroup,
deleteInstance,
handleInstanceContextMenu,
@@ -13,13 +13,10 @@ export async function create_group(name: string): Promise<InstanceGroupDefinitio
return await invoke('plugin:instance|instance_create_group', { name })
}
export async function rename_group(
oldName: string,
newName: string,
): Promise<InstanceGroupDefinition> {
return await invoke('plugin:instance|instance_rename_group', { oldName, newName })
export async function rename_group(id: string, newName: string): Promise<InstanceGroupDefinition> {
return await invoke('plugin:instance|instance_rename_group', { id, newName })
}
export async function delete_group(name: string): Promise<void> {
return await invoke('plugin:instance|instance_delete_group', { name })
export async function delete_group(id: string): Promise<void> {
return await invoke('plugin:instance|instance_delete_group', { id })
}
+1 -1
View File
@@ -14,7 +14,7 @@ export type GameInstance = {
loader: InstanceLoader
loader_version?: string
groups: string[]
group_ids: string[]
link?: InstanceLink | null
shared_instance?: SharedInstanceAttachment | null
+10 -9
View File
@@ -83,7 +83,7 @@ pub struct Instance {
pub protocol_version: Option<u32>,
pub loader: ModLoader,
pub loader_version: Option<String>,
pub groups: Vec<String>,
pub group_ids: Vec<String>,
pub link: Option<InstanceLink>,
pub shared_instance: Option<SharedInstanceAttachment>,
pub quarantined: bool,
@@ -179,7 +179,7 @@ pub struct EditInstance {
)]
pub loader_version: Option<Option<String>>,
pub groups: Option<Vec<String>>,
pub group_ids: Option<Vec<String>>,
#[serde(
default,
@@ -246,7 +246,7 @@ impl From<InstanceMetadata> for Instance {
protocol_version: metadata.applied_content_set.protocol_version,
loader: metadata.applied_content_set.loader,
loader_version: metadata.applied_content_set.loader_version,
groups: metadata.groups,
group_ids: metadata.group_ids,
link: InstanceLink::from_core(metadata.link),
shared_instance: metadata.shared_instance.map(Into::into),
quarantined: metadata.quarantined,
@@ -414,7 +414,7 @@ fn edit_to_core(edit_instance: EditInstance) -> Result<CoreEditInstance> {
name: edit_instance.name,
icon_path: None,
update_channel: edit_instance.update_channel,
groups: edit_instance.groups,
group_ids: edit_instance.group_ids,
link: edit_instance
.link
.map(|link| match link {
@@ -479,7 +479,8 @@ pub async fn instance_list() -> Result<Vec<Instance>> {
}
#[tauri::command]
pub async fn instance_list_groups() -> Result<Vec<theseus::instance::InstanceGroup>> {
pub async fn instance_list_groups()
-> Result<Vec<theseus::instance::InstanceGroup>> {
Ok(theseus::instance::list_groups().await?)
}
@@ -492,15 +493,15 @@ pub async fn instance_create_group(
#[tauri::command]
pub async fn instance_rename_group(
old_name: String,
id: String,
new_name: String,
) -> Result<theseus::instance::InstanceGroup> {
Ok(theseus::instance::rename_group(old_name, new_name).await?)
Ok(theseus::instance::rename_group(id, new_name).await?)
}
#[tauri::command]
pub async fn instance_delete_group(name: String) -> Result<()> {
Ok(theseus::instance::delete_group(name).await?)
pub async fn instance_delete_group(id: String) -> Result<()> {
Ok(theseus::instance::delete_group(id).await?)
}
#[tauri::command]
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tDELETE FROM instance_groups\n\t\tWHERE instance_id = ?\n\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "073e4c6fbf434f94c8015076b50c099d57f217b4c1f4a63761b5e07129eb1f84"
}
@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tSELECT group_name\n\t\tFROM instance_groups\n\t\tWHERE instance_id = ?\n\t\tORDER BY group_name\n\t\t",
"describe": {
"columns": [
{
"name": "group_name",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "0812e019f53d81d527ea15cbf7a6416ad15182131cfb6de3f2dab153c74a553c"
}
@@ -1,296 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n i.id AS \"id!: String\",\n i.path AS \"path!: String\",\n i.applied_content_set_id AS \"applied_content_set_id?: String\",\n i.install_stage AS \"install_stage!: String\",\n i.launcher_feature_version AS \"launcher_feature_version!: String\",\n i.update_channel AS \"update_channel!: String\",\n i.name AS \"name!: String\",\n i.icon_path AS \"icon_path?: String\",\n i.created AS \"created!: i64\",\n i.modified AS \"modified!: i64\",\n i.last_played AS \"last_played?: i64\",\n i.submitted_time_played AS \"submitted_time_played!: i64\",\n i.recent_time_played AS \"recent_time_played!: i64\",\n cs.id AS \"content_set_id?: String\",\n cs.instance_id AS \"content_set_instance_id?: String\",\n cs.name AS \"content_set_name?: String\",\n cs.source_kind AS \"content_set_source_kind?: String\",\n cs.status AS \"content_set_status?: String\",\n cs.game_version AS \"content_set_game_version?: String\",\n cs.protocol_version AS \"content_set_protocol_version?: i64\",\n cs.loader AS \"content_set_loader?: String\",\n cs.loader_version AS \"content_set_loader_version?: String\",\n cs.created AS \"content_set_created?: i64\",\n cs.modified AS \"content_set_modified?: i64\",\n COALESCE(link.link_kind, 'unmanaged') AS \"link_kind!: String\",\n link.modrinth_project_id AS \"modrinth_project_id?: String\",\n link.modrinth_version_id AS \"modrinth_version_id?: String\",\n link.server_project_id AS \"server_project_id?: String\",\n link.content_project_id AS \"content_project_id?: String\",\n link.content_version_id AS \"content_version_id?: String\",\n link.hosting_server_id AS \"hosting_server_id?: String\",\n json(link.hosting_instance_ids) AS \"hosting_instance_ids?: String\",\n link.hosting_active_instance_id AS \"hosting_active_instance_id?: String\",\n link.shared_instance_id AS \"shared_instance_id?: String\",\n link.shared_instance_role AS \"shared_instance_role?: String\",\n link.shared_instance_manager_id AS \"shared_instance_manager_id?: String\",\n link.shared_instance_server_manager_name AS \"shared_instance_server_manager_name?: String\",\n link.shared_instance_server_manager_icon_url AS \"shared_instance_server_manager_icon_url?: String\",\n link.shared_instance_linked_user_id AS \"shared_instance_linked_user_id?: String\",\n sync.applied_update_id AS \"shared_sync_applied_update_id?: String\",\n sync.latest_available_update_id AS \"shared_sync_latest_available_update_id?: String\",\n sync.status AS \"shared_sync_status?: String\",\n link.imported_name AS \"imported_name?: String\",\n link.imported_version_number AS \"imported_version_number?: String\",\n link.imported_filename AS \"imported_filename?: String\",\n COALESCE((\n SELECT json_group_array(name)\n FROM (\n SELECT groups.name\n FROM instance_group_memberships memberships\n INNER JOIN instance_groups groups\n ON groups.id = memberships.group_id\n WHERE memberships.instance_id = i.id\n ORDER BY groups.name\n )\n ), '[]') AS \"groups!: String\",\n json(overrides.overrides) AS \"launch_overrides?: String\"\n FROM instances i\n LEFT JOIN instance_content_sets cs\n ON cs.id = i.applied_content_set_id\n AND cs.instance_id = i.id\n LEFT JOIN instance_links link\n ON link.instance_id = i.id\n LEFT JOIN instance_content_set_sync_state sync\n ON sync.content_set_id = cs.id\n AND sync.provider = 'shared_instance'\n LEFT JOIN instance_launch_overrides overrides\n ON overrides.instance_id = i.id\n WHERE i.id = ?",
"describe": {
"columns": [
{
"name": "id!: String",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "path!: String",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "applied_content_set_id?: String",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "install_stage!: String",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "launcher_feature_version!: String",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "update_channel!: String",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "name!: String",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "icon_path?: String",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "created!: i64",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "modified!: i64",
"ordinal": 9,
"type_info": "Integer"
},
{
"name": "last_played?: i64",
"ordinal": 10,
"type_info": "Integer"
},
{
"name": "submitted_time_played!: i64",
"ordinal": 11,
"type_info": "Integer"
},
{
"name": "recent_time_played!: i64",
"ordinal": 12,
"type_info": "Integer"
},
{
"name": "content_set_id?: String",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "content_set_instance_id?: String",
"ordinal": 14,
"type_info": "Text"
},
{
"name": "content_set_name?: String",
"ordinal": 15,
"type_info": "Text"
},
{
"name": "content_set_source_kind?: String",
"ordinal": 16,
"type_info": "Text"
},
{
"name": "content_set_status?: String",
"ordinal": 17,
"type_info": "Text"
},
{
"name": "content_set_game_version?: String",
"ordinal": 18,
"type_info": "Text"
},
{
"name": "content_set_protocol_version?: i64",
"ordinal": 19,
"type_info": "Integer"
},
{
"name": "content_set_loader?: String",
"ordinal": 20,
"type_info": "Text"
},
{
"name": "content_set_loader_version?: String",
"ordinal": 21,
"type_info": "Text"
},
{
"name": "content_set_created?: i64",
"ordinal": 22,
"type_info": "Integer"
},
{
"name": "content_set_modified?: i64",
"ordinal": 23,
"type_info": "Integer"
},
{
"name": "link_kind!: String",
"ordinal": 24,
"type_info": "Null"
},
{
"name": "modrinth_project_id?: String",
"ordinal": 25,
"type_info": "Text"
},
{
"name": "modrinth_version_id?: String",
"ordinal": 26,
"type_info": "Text"
},
{
"name": "server_project_id?: String",
"ordinal": 27,
"type_info": "Text"
},
{
"name": "content_project_id?: String",
"ordinal": 28,
"type_info": "Text"
},
{
"name": "content_version_id?: String",
"ordinal": 29,
"type_info": "Text"
},
{
"name": "hosting_server_id?: String",
"ordinal": 30,
"type_info": "Text"
},
{
"name": "hosting_instance_ids?: String",
"ordinal": 31,
"type_info": "Null"
},
{
"name": "hosting_active_instance_id?: String",
"ordinal": 32,
"type_info": "Text"
},
{
"name": "shared_instance_id?: String",
"ordinal": 33,
"type_info": "Text"
},
{
"name": "shared_instance_role?: String",
"ordinal": 34,
"type_info": "Text"
},
{
"name": "shared_instance_manager_id?: String",
"ordinal": 35,
"type_info": "Text"
},
{
"name": "shared_instance_server_manager_name?: String",
"ordinal": 36,
"type_info": "Text"
},
{
"name": "shared_instance_server_manager_icon_url?: String",
"ordinal": 37,
"type_info": "Text"
},
{
"name": "shared_instance_linked_user_id?: String",
"ordinal": 38,
"type_info": "Text"
},
{
"name": "shared_sync_applied_update_id?: String",
"ordinal": 39,
"type_info": "Text"
},
{
"name": "shared_sync_latest_available_update_id?: String",
"ordinal": 40,
"type_info": "Text"
},
{
"name": "shared_sync_status?: String",
"ordinal": 41,
"type_info": "Text"
},
{
"name": "imported_name?: String",
"ordinal": 42,
"type_info": "Text"
},
{
"name": "imported_version_number?: String",
"ordinal": 43,
"type_info": "Text"
},
{
"name": "imported_filename?: String",
"ordinal": 44,
"type_info": "Text"
},
{
"name": "groups!: String",
"ordinal": 45,
"type_info": "Null"
},
{
"name": "launch_overrides?: String",
"ordinal": 46,
"type_info": "Null"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
true,
false,
false,
false,
false,
true,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false,
null,
true,
true,
true,
true,
true,
true,
null,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
true,
null,
null
]
},
"hash": "16ccee4c11eca353fa347990d6d2af1df5da09605a20a48e3a580d10b6a704ab"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n i.id AS \"id!: String\",\n i.path AS \"path!: String\",\n i.applied_content_set_id AS \"applied_content_set_id?: String\",\n i.install_stage AS \"install_stage!: String\",\n i.launcher_feature_version AS \"launcher_feature_version!: String\",\n i.update_channel AS \"update_channel!: String\",\n i.name AS \"name!: String\",\n i.icon_path AS \"icon_path?: String\",\n i.created AS \"created!: i64\",\n i.modified AS \"modified!: i64\",\n i.last_played AS \"last_played?: i64\",\n i.submitted_time_played AS \"submitted_time_played!: i64\",\n i.recent_time_played AS \"recent_time_played!: i64\",\n cs.id AS \"content_set_id?: String\",\n cs.instance_id AS \"content_set_instance_id?: String\",\n cs.name AS \"content_set_name?: String\",\n cs.source_kind AS \"content_set_source_kind?: String\",\n cs.status AS \"content_set_status?: String\",\n cs.game_version AS \"content_set_game_version?: String\",\n cs.protocol_version AS \"content_set_protocol_version?: i64\",\n cs.loader AS \"content_set_loader?: String\",\n cs.loader_version AS \"content_set_loader_version?: String\",\n cs.created AS \"content_set_created?: i64\",\n cs.modified AS \"content_set_modified?: i64\",\n COALESCE(link.link_kind, 'unmanaged') AS \"link_kind!: String\",\n link.modrinth_project_id AS \"modrinth_project_id?: String\",\n link.modrinth_version_id AS \"modrinth_version_id?: String\",\n link.server_project_id AS \"server_project_id?: String\",\n link.content_project_id AS \"content_project_id?: String\",\n link.content_version_id AS \"content_version_id?: String\",\n link.hosting_server_id AS \"hosting_server_id?: String\",\n json(link.hosting_instance_ids) AS \"hosting_instance_ids?: String\",\n link.hosting_active_instance_id AS \"hosting_active_instance_id?: String\",\n link.shared_instance_id AS \"shared_instance_id?: String\",\n link.shared_instance_role AS \"shared_instance_role?: String\",\n link.shared_instance_manager_id AS \"shared_instance_manager_id?: String\",\n link.shared_instance_server_manager_name AS \"shared_instance_server_manager_name?: String\",\n link.shared_instance_server_manager_icon_url AS \"shared_instance_server_manager_icon_url?: String\",\n link.shared_instance_linked_user_id AS \"shared_instance_linked_user_id?: String\",\n sync.applied_update_id AS \"shared_sync_applied_update_id?: String\",\n sync.latest_available_update_id AS \"shared_sync_latest_available_update_id?: String\",\n sync.status AS \"shared_sync_status?: String\",\n link.imported_name AS \"imported_name?: String\",\n link.imported_version_number AS \"imported_version_number?: String\",\n link.imported_filename AS \"imported_filename?: String\",\n COALESCE((\n SELECT json_group_array(group_name)\n FROM (\n SELECT group_name\n FROM instance_groups\n WHERE instance_id = i.id\n ORDER BY group_name\n )\n ), '[]') AS \"groups!: String\",\n json(overrides.overrides) AS \"launch_overrides?: String\"\n FROM instances i\n LEFT JOIN instance_content_sets cs\n ON cs.id = i.applied_content_set_id\n AND cs.instance_id = i.id\n LEFT JOIN instance_links link\n ON link.instance_id = i.id\n LEFT JOIN instance_content_set_sync_state sync\n ON sync.content_set_id = cs.id\n AND sync.provider = 'shared_instance'\n LEFT JOIN instance_launch_overrides overrides\n ON overrides.instance_id = i.id\n WHERE i.id = ?",
"query": "\n SELECT\n i.id AS \"id!: String\",\n i.path AS \"path!: String\",\n i.applied_content_set_id AS \"applied_content_set_id?: String\",\n i.install_stage AS \"install_stage!: String\",\n i.launcher_feature_version AS \"launcher_feature_version!: String\",\n i.update_channel AS \"update_channel!: String\",\n i.name AS \"name!: String\",\n i.icon_path AS \"icon_path?: String\",\n i.created AS \"created!: i64\",\n i.modified AS \"modified!: i64\",\n i.last_played AS \"last_played?: i64\",\n i.submitted_time_played AS \"submitted_time_played!: i64\",\n i.recent_time_played AS \"recent_time_played!: i64\",\n cs.id AS \"content_set_id?: String\",\n cs.instance_id AS \"content_set_instance_id?: String\",\n cs.name AS \"content_set_name?: String\",\n cs.source_kind AS \"content_set_source_kind?: String\",\n cs.status AS \"content_set_status?: String\",\n cs.game_version AS \"content_set_game_version?: String\",\n cs.protocol_version AS \"content_set_protocol_version?: i64\",\n cs.loader AS \"content_set_loader?: String\",\n cs.loader_version AS \"content_set_loader_version?: String\",\n cs.created AS \"content_set_created?: i64\",\n cs.modified AS \"content_set_modified?: i64\",\n COALESCE(link.link_kind, 'unmanaged') AS \"link_kind!: String\",\n link.modrinth_project_id AS \"modrinth_project_id?: String\",\n link.modrinth_version_id AS \"modrinth_version_id?: String\",\n link.server_project_id AS \"server_project_id?: String\",\n link.content_project_id AS \"content_project_id?: String\",\n link.content_version_id AS \"content_version_id?: String\",\n link.hosting_server_id AS \"hosting_server_id?: String\",\n json(link.hosting_instance_ids) AS \"hosting_instance_ids?: String\",\n link.hosting_active_instance_id AS \"hosting_active_instance_id?: String\",\n link.shared_instance_id AS \"shared_instance_id?: String\",\n link.shared_instance_role AS \"shared_instance_role?: String\",\n link.shared_instance_manager_id AS \"shared_instance_manager_id?: String\",\n link.shared_instance_server_manager_name AS \"shared_instance_server_manager_name?: String\",\n link.shared_instance_server_manager_icon_url AS \"shared_instance_server_manager_icon_url?: String\",\n link.shared_instance_linked_user_id AS \"shared_instance_linked_user_id?: String\",\n sync.applied_update_id AS \"shared_sync_applied_update_id?: String\",\n sync.latest_available_update_id AS \"shared_sync_latest_available_update_id?: String\",\n sync.status AS \"shared_sync_status?: String\",\n link.imported_name AS \"imported_name?: String\",\n link.imported_version_number AS \"imported_version_number?: String\",\n link.imported_filename AS \"imported_filename?: String\",\n COALESCE((\n SELECT json_group_array(id)\n FROM (\n SELECT groups.id\n FROM instance_group_memberships memberships\n INNER JOIN instance_groups groups\n ON groups.id = memberships.group_id\n WHERE memberships.instance_id = i.id\n ORDER BY groups.name\n )\n ), '[]') AS \"group_ids!: String\",\n json(overrides.overrides) AS \"launch_overrides?: String\"\n FROM instances i\n LEFT JOIN instance_content_sets cs\n ON cs.id = i.applied_content_set_id\n AND cs.instance_id = i.id\n LEFT JOIN instance_links link\n ON link.instance_id = i.id\n LEFT JOIN instance_content_set_sync_state sync\n ON sync.content_set_id = cs.id\n AND sync.provider = 'shared_instance'\n LEFT JOIN instance_launch_overrides overrides\n ON overrides.instance_id = i.id\n WHERE i.id = ?",
"describe": {
"columns": [
{
@@ -229,7 +229,7 @@
"type_info": "Text"
},
{
"name": "groups!: String",
"name": "group_ids!: String",
"ordinal": 45,
"type_info": "Null"
},
@@ -292,5 +292,5 @@
null
]
},
"hash": "0a708a6410e4d7d4cbdb07fa6ea32383be454526177686a0685a988af747db0f"
"hash": "2171aca7d44d86f0e0ec5bf1dec37da14fc8bafa72fdf2b589663c15cb047a68"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n\t\tSELECT groups.id\n\t\tFROM instance_group_memberships memberships\n\t\tINNER JOIN instance_groups groups\n\t\t\tON groups.id = memberships.group_id\n\t\tWHERE memberships.instance_id = ?\n\t\tORDER BY groups.name\n\t\t",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "73a5b7a4b61a75705f6ed3ea370ec66677b42cdd09b8ae87cee0ef89d3875681"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT OR IGNORE INTO instance_groups (instance_id, group_name)\n VALUES (?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "7790039c6bfeba24c9a0f515b12d06e8ca910f05a73f2b31fe80eef9033873d7"
}
File diff suppressed because one or more lines are too long
@@ -1,296 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n i.id AS \"id!: String\",\n i.path AS \"path!: String\",\n i.applied_content_set_id AS \"applied_content_set_id?: String\",\n i.install_stage AS \"install_stage!: String\",\n i.launcher_feature_version AS \"launcher_feature_version!: String\",\n i.update_channel AS \"update_channel!: String\",\n i.name AS \"name!: String\",\n i.icon_path AS \"icon_path?: String\",\n i.created AS \"created!: i64\",\n i.modified AS \"modified!: i64\",\n i.last_played AS \"last_played?: i64\",\n i.submitted_time_played AS \"submitted_time_played!: i64\",\n i.recent_time_played AS \"recent_time_played!: i64\",\n cs.id AS \"content_set_id?: String\",\n cs.instance_id AS \"content_set_instance_id?: String\",\n cs.name AS \"content_set_name?: String\",\n cs.source_kind AS \"content_set_source_kind?: String\",\n cs.status AS \"content_set_status?: String\",\n cs.game_version AS \"content_set_game_version?: String\",\n cs.protocol_version AS \"content_set_protocol_version?: i64\",\n cs.loader AS \"content_set_loader?: String\",\n cs.loader_version AS \"content_set_loader_version?: String\",\n cs.created AS \"content_set_created?: i64\",\n cs.modified AS \"content_set_modified?: i64\",\n COALESCE(link.link_kind, 'unmanaged') AS \"link_kind!: String\",\n link.modrinth_project_id AS \"modrinth_project_id?: String\",\n link.modrinth_version_id AS \"modrinth_version_id?: String\",\n link.server_project_id AS \"server_project_id?: String\",\n link.content_project_id AS \"content_project_id?: String\",\n link.content_version_id AS \"content_version_id?: String\",\n link.hosting_server_id AS \"hosting_server_id?: String\",\n json(link.hosting_instance_ids) AS \"hosting_instance_ids?: String\",\n link.hosting_active_instance_id AS \"hosting_active_instance_id?: String\",\n link.shared_instance_id AS \"shared_instance_id?: String\",\n link.shared_instance_role AS \"shared_instance_role?: String\",\n link.shared_instance_manager_id AS \"shared_instance_manager_id?: String\",\n link.shared_instance_server_manager_name AS \"shared_instance_server_manager_name?: String\",\n link.shared_instance_server_manager_icon_url AS \"shared_instance_server_manager_icon_url?: String\",\n link.shared_instance_linked_user_id AS \"shared_instance_linked_user_id?: String\",\n sync.applied_update_id AS \"shared_sync_applied_update_id?: String\",\n sync.latest_available_update_id AS \"shared_sync_latest_available_update_id?: String\",\n sync.status AS \"shared_sync_status?: String\",\n link.imported_name AS \"imported_name?: String\",\n link.imported_version_number AS \"imported_version_number?: String\",\n link.imported_filename AS \"imported_filename?: String\",\n COALESCE((\n SELECT json_group_array(group_name)\n FROM (\n SELECT group_name\n FROM instance_groups\n WHERE instance_id = i.id\n ORDER BY group_name\n )\n ), '[]') AS \"groups!: String\",\n json(overrides.overrides) AS \"launch_overrides?: String\"\n FROM instances i\n LEFT JOIN instance_content_sets cs\n ON cs.id = i.applied_content_set_id\n AND cs.instance_id = i.id\n LEFT JOIN instance_links link\n ON link.instance_id = i.id\n LEFT JOIN instance_content_set_sync_state sync\n ON sync.content_set_id = cs.id\n AND sync.provider = 'shared_instance'\n LEFT JOIN instance_launch_overrides overrides\n ON overrides.instance_id = i.id\n WHERE 1 = ?",
"describe": {
"columns": [
{
"name": "id!: String",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "path!: String",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "applied_content_set_id?: String",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "install_stage!: String",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "launcher_feature_version!: String",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "update_channel!: String",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "name!: String",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "icon_path?: String",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "created!: i64",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "modified!: i64",
"ordinal": 9,
"type_info": "Integer"
},
{
"name": "last_played?: i64",
"ordinal": 10,
"type_info": "Integer"
},
{
"name": "submitted_time_played!: i64",
"ordinal": 11,
"type_info": "Integer"
},
{
"name": "recent_time_played!: i64",
"ordinal": 12,
"type_info": "Integer"
},
{
"name": "content_set_id?: String",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "content_set_instance_id?: String",
"ordinal": 14,
"type_info": "Text"
},
{
"name": "content_set_name?: String",
"ordinal": 15,
"type_info": "Text"
},
{
"name": "content_set_source_kind?: String",
"ordinal": 16,
"type_info": "Text"
},
{
"name": "content_set_status?: String",
"ordinal": 17,
"type_info": "Text"
},
{
"name": "content_set_game_version?: String",
"ordinal": 18,
"type_info": "Text"
},
{
"name": "content_set_protocol_version?: i64",
"ordinal": 19,
"type_info": "Integer"
},
{
"name": "content_set_loader?: String",
"ordinal": 20,
"type_info": "Text"
},
{
"name": "content_set_loader_version?: String",
"ordinal": 21,
"type_info": "Text"
},
{
"name": "content_set_created?: i64",
"ordinal": 22,
"type_info": "Integer"
},
{
"name": "content_set_modified?: i64",
"ordinal": 23,
"type_info": "Integer"
},
{
"name": "link_kind!: String",
"ordinal": 24,
"type_info": "Text"
},
{
"name": "modrinth_project_id?: String",
"ordinal": 25,
"type_info": "Text"
},
{
"name": "modrinth_version_id?: String",
"ordinal": 26,
"type_info": "Text"
},
{
"name": "server_project_id?: String",
"ordinal": 27,
"type_info": "Text"
},
{
"name": "content_project_id?: String",
"ordinal": 28,
"type_info": "Text"
},
{
"name": "content_version_id?: String",
"ordinal": 29,
"type_info": "Text"
},
{
"name": "hosting_server_id?: String",
"ordinal": 30,
"type_info": "Text"
},
{
"name": "hosting_instance_ids?: String",
"ordinal": 31,
"type_info": "Null"
},
{
"name": "hosting_active_instance_id?: String",
"ordinal": 32,
"type_info": "Text"
},
{
"name": "shared_instance_id?: String",
"ordinal": 33,
"type_info": "Text"
},
{
"name": "shared_instance_role?: String",
"ordinal": 34,
"type_info": "Text"
},
{
"name": "shared_instance_manager_id?: String",
"ordinal": 35,
"type_info": "Text"
},
{
"name": "shared_instance_server_manager_name?: String",
"ordinal": 36,
"type_info": "Text"
},
{
"name": "shared_instance_server_manager_icon_url?: String",
"ordinal": 37,
"type_info": "Text"
},
{
"name": "shared_instance_linked_user_id?: String",
"ordinal": 38,
"type_info": "Text"
},
{
"name": "shared_sync_applied_update_id?: String",
"ordinal": 39,
"type_info": "Text"
},
{
"name": "shared_sync_latest_available_update_id?: String",
"ordinal": 40,
"type_info": "Text"
},
{
"name": "shared_sync_status?: String",
"ordinal": 41,
"type_info": "Text"
},
{
"name": "imported_name?: String",
"ordinal": 42,
"type_info": "Text"
},
{
"name": "imported_version_number?: String",
"ordinal": 43,
"type_info": "Text"
},
{
"name": "imported_filename?: String",
"ordinal": 44,
"type_info": "Text"
},
{
"name": "groups!: String",
"ordinal": 45,
"type_info": "Text"
},
{
"name": "launch_overrides?: String",
"ordinal": 46,
"type_info": "Null"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
true,
false,
false,
false,
false,
true,
false,
false,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
null,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
null
]
},
"hash": "aaf6e6a3de2dd9cb1c9db5dd9de2a6590b89c4f72449fdd34a7f140e2a11bf04"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n i.id AS \"id!: String\",\n i.path AS \"path!: String\",\n i.applied_content_set_id AS \"applied_content_set_id?: String\",\n i.install_stage AS \"install_stage!: String\",\n i.launcher_feature_version AS \"launcher_feature_version!: String\",\n i.update_channel AS \"update_channel!: String\",\n i.name AS \"name!: String\",\n i.icon_path AS \"icon_path?: String\",\n i.created AS \"created!: i64\",\n i.modified AS \"modified!: i64\",\n i.last_played AS \"last_played?: i64\",\n i.submitted_time_played AS \"submitted_time_played!: i64\",\n i.recent_time_played AS \"recent_time_played!: i64\",\n cs.id AS \"content_set_id?: String\",\n cs.instance_id AS \"content_set_instance_id?: String\",\n cs.name AS \"content_set_name?: String\",\n cs.source_kind AS \"content_set_source_kind?: String\",\n cs.status AS \"content_set_status?: String\",\n cs.game_version AS \"content_set_game_version?: String\",\n cs.protocol_version AS \"content_set_protocol_version?: i64\",\n cs.loader AS \"content_set_loader?: String\",\n cs.loader_version AS \"content_set_loader_version?: String\",\n cs.created AS \"content_set_created?: i64\",\n cs.modified AS \"content_set_modified?: i64\",\n COALESCE(link.link_kind, 'unmanaged') AS \"link_kind!: String\",\n link.modrinth_project_id AS \"modrinth_project_id?: String\",\n link.modrinth_version_id AS \"modrinth_version_id?: String\",\n link.server_project_id AS \"server_project_id?: String\",\n link.content_project_id AS \"content_project_id?: String\",\n link.content_version_id AS \"content_version_id?: String\",\n link.hosting_server_id AS \"hosting_server_id?: String\",\n json(link.hosting_instance_ids) AS \"hosting_instance_ids?: String\",\n link.hosting_active_instance_id AS \"hosting_active_instance_id?: String\",\n link.shared_instance_id AS \"shared_instance_id?: String\",\n link.shared_instance_role AS \"shared_instance_role?: String\",\n link.shared_instance_manager_id AS \"shared_instance_manager_id?: String\",\n link.shared_instance_server_manager_name AS \"shared_instance_server_manager_name?: String\",\n link.shared_instance_server_manager_icon_url AS \"shared_instance_server_manager_icon_url?: String\",\n link.shared_instance_linked_user_id AS \"shared_instance_linked_user_id?: String\",\n sync.applied_update_id AS \"shared_sync_applied_update_id?: String\",\n sync.latest_available_update_id AS \"shared_sync_latest_available_update_id?: String\",\n sync.status AS \"shared_sync_status?: String\",\n link.imported_name AS \"imported_name?: String\",\n link.imported_version_number AS \"imported_version_number?: String\",\n link.imported_filename AS \"imported_filename?: String\",\n COALESCE((\n SELECT json_group_array(name)\n FROM (\n SELECT groups.name\n FROM instance_group_memberships memberships\n INNER JOIN instance_groups groups\n ON groups.id = memberships.group_id\n WHERE memberships.instance_id = i.id\n ORDER BY groups.name\n )\n ), '[]') AS \"groups!: String\",\n json(overrides.overrides) AS \"launch_overrides?: String\"\n FROM instances i\n LEFT JOIN instance_content_sets cs\n ON cs.id = i.applied_content_set_id\n AND cs.instance_id = i.id\n LEFT JOIN instance_links link\n ON link.instance_id = i.id\n LEFT JOIN instance_content_set_sync_state sync\n ON sync.content_set_id = cs.id\n AND sync.provider = 'shared_instance'\n LEFT JOIN instance_launch_overrides overrides\n ON overrides.instance_id = i.id\n WHERE 1 = ?",
"query": "\n SELECT\n i.id AS \"id!: String\",\n i.path AS \"path!: String\",\n i.applied_content_set_id AS \"applied_content_set_id?: String\",\n i.install_stage AS \"install_stage!: String\",\n i.launcher_feature_version AS \"launcher_feature_version!: String\",\n i.update_channel AS \"update_channel!: String\",\n i.name AS \"name!: String\",\n i.icon_path AS \"icon_path?: String\",\n i.created AS \"created!: i64\",\n i.modified AS \"modified!: i64\",\n i.last_played AS \"last_played?: i64\",\n i.submitted_time_played AS \"submitted_time_played!: i64\",\n i.recent_time_played AS \"recent_time_played!: i64\",\n cs.id AS \"content_set_id?: String\",\n cs.instance_id AS \"content_set_instance_id?: String\",\n cs.name AS \"content_set_name?: String\",\n cs.source_kind AS \"content_set_source_kind?: String\",\n cs.status AS \"content_set_status?: String\",\n cs.game_version AS \"content_set_game_version?: String\",\n cs.protocol_version AS \"content_set_protocol_version?: i64\",\n cs.loader AS \"content_set_loader?: String\",\n cs.loader_version AS \"content_set_loader_version?: String\",\n cs.created AS \"content_set_created?: i64\",\n cs.modified AS \"content_set_modified?: i64\",\n COALESCE(link.link_kind, 'unmanaged') AS \"link_kind!: String\",\n link.modrinth_project_id AS \"modrinth_project_id?: String\",\n link.modrinth_version_id AS \"modrinth_version_id?: String\",\n link.server_project_id AS \"server_project_id?: String\",\n link.content_project_id AS \"content_project_id?: String\",\n link.content_version_id AS \"content_version_id?: String\",\n link.hosting_server_id AS \"hosting_server_id?: String\",\n json(link.hosting_instance_ids) AS \"hosting_instance_ids?: String\",\n link.hosting_active_instance_id AS \"hosting_active_instance_id?: String\",\n link.shared_instance_id AS \"shared_instance_id?: String\",\n link.shared_instance_role AS \"shared_instance_role?: String\",\n link.shared_instance_manager_id AS \"shared_instance_manager_id?: String\",\n link.shared_instance_server_manager_name AS \"shared_instance_server_manager_name?: String\",\n link.shared_instance_server_manager_icon_url AS \"shared_instance_server_manager_icon_url?: String\",\n link.shared_instance_linked_user_id AS \"shared_instance_linked_user_id?: String\",\n sync.applied_update_id AS \"shared_sync_applied_update_id?: String\",\n sync.latest_available_update_id AS \"shared_sync_latest_available_update_id?: String\",\n sync.status AS \"shared_sync_status?: String\",\n link.imported_name AS \"imported_name?: String\",\n link.imported_version_number AS \"imported_version_number?: String\",\n link.imported_filename AS \"imported_filename?: String\",\n COALESCE((\n SELECT json_group_array(id)\n FROM (\n SELECT groups.id\n FROM instance_group_memberships memberships\n INNER JOIN instance_groups groups\n ON groups.id = memberships.group_id\n WHERE memberships.instance_id = i.id\n ORDER BY groups.name\n )\n ), '[]') AS \"group_ids!: String\",\n json(overrides.overrides) AS \"launch_overrides?: String\"\n FROM instances i\n LEFT JOIN instance_content_sets cs\n ON cs.id = i.applied_content_set_id\n AND cs.instance_id = i.id\n LEFT JOIN instance_links link\n ON link.instance_id = i.id\n LEFT JOIN instance_content_set_sync_state sync\n ON sync.content_set_id = cs.id\n AND sync.provider = 'shared_instance'\n LEFT JOIN instance_launch_overrides overrides\n ON overrides.instance_id = i.id\n WHERE 1 = ?",
"describe": {
"columns": [
{
@@ -229,7 +229,7 @@
"type_info": "Text"
},
{
"name": "groups!: String",
"name": "group_ids!: String",
"ordinal": 45,
"type_info": "Text"
},
@@ -292,5 +292,5 @@
null
]
},
"hash": "3b9ff6d0c4dfa410b343bf7f4e01cad57a56093c1950027c190ca8e751bd0ca2"
"hash": "c4c754b4682e5132c3ac07764082c8dbf64f1633a8a00d925e66d005de29dd3d"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\t\tINSERT OR IGNORE INTO instance_groups (instance_id, group_name)\n\t\t\tVALUES (?, ?)\n\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "e26d2ce1fdaaef4ef4648b57faf0ea2940ba9ada7b39f3c2fa973a36ee4cba2a"
}
@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "\n\t\tSELECT groups.name\n\t\tFROM instance_group_memberships memberships\n\t\tINNER JOIN instance_groups groups\n\t\t\tON groups.id = memberships.group_id\n\t\tWHERE memberships.instance_id = ?\n\t\tORDER BY groups.name\n\t\t",
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "ff28a1341b04afc4af46cd7110cb1daa7a669b3e7363fc27bbf4e66e82c98413"
}
@@ -4,8 +4,7 @@ CREATE TABLE instance_groups (
id TEXT NOT NULL,
name TEXT NOT NULL,
PRIMARY KEY (id),
UNIQUE (name)
PRIMARY KEY (id)
);
CREATE TABLE instance_group_memberships (
@@ -31,6 +30,22 @@ SELECT
FROM legacy_instance_group_memberships legacy
INNER JOIN instance_groups groups ON groups.name = legacy.group_name;
UPDATE install_jobs
SET state = json_remove(
json_set(
state,
'$.rollback.instance.group_ids',
json(COALESCE((
SELECT json_group_array(groups.id)
FROM json_each(state, '$.rollback.instance.groups') legacy_groups
INNER JOIN instance_groups groups
ON groups.name = legacy_groups.value
), '[]'))
),
'$.rollback.instance.groups'
)
WHERE json_type(state, '$.rollback.instance.groups') = 'array';
DROP TABLE legacy_instance_group_memberships;
CREATE INDEX instance_groups_name ON instance_groups(name);
+20 -68
View File
@@ -52,24 +52,6 @@ pub async fn list_groups() -> crate::Result<Vec<InstanceGroup>> {
pub async fn create_group(name: String) -> crate::Result<InstanceGroup> {
let name = validate_group_name(&name)?;
let state = State::get().await?;
let existing_group = sqlx::query_scalar::<_, String>(
"
SELECT name
FROM instance_groups
WHERE lower(name) = lower(?)
",
)
.bind(name)
.fetch_optional(&state.pool)
.await?;
if existing_group.is_some() {
return Err(crate::ErrorKind::InputError(
"A group with this name already exists".to_string(),
)
.into());
}
let id = Uuid::new_v4().to_string();
instance_rows::create_instance_group(&id, name, &state.pool).await?;
@@ -80,47 +62,12 @@ pub async fn create_group(name: String) -> crate::Result<InstanceGroup> {
}
pub async fn rename_group(
old_name: String,
id: String,
new_name: String,
) -> crate::Result<InstanceGroup> {
let new_name = validate_group_name(&new_name)?;
let state = State::get().await?;
let mut tx = state.pool.begin().await?;
let group_id = sqlx::query_scalar::<_, String>(
"
SELECT id
FROM instance_groups
WHERE name = ?
",
)
.bind(&old_name)
.fetch_optional(&mut *tx)
.await?
.ok_or_else(|| {
crate::Error::from(crate::ErrorKind::InputError(format!(
"Unknown instance group {old_name}"
)))
})?;
let existing_group = sqlx::query_scalar::<_, String>(
"
SELECT id
FROM instance_groups
WHERE lower(name) = lower(?)
AND id != ?
",
)
.bind(new_name)
.bind(&group_id)
.fetch_optional(&mut *tx)
.await?;
if existing_group.is_some() {
return Err(crate::ErrorKind::InputError(
"A group with this name already exists".to_string(),
)
.into());
}
let instance_ids = sqlx::query_scalar::<_, String>(
"
@@ -129,11 +76,11 @@ pub async fn rename_group(
WHERE group_id = ?
",
)
.bind(&group_id)
.bind(&id)
.fetch_all(&mut *tx)
.await?;
sqlx::query(
let result = sqlx::query(
"
UPDATE instance_groups
SET name = ?
@@ -141,10 +88,17 @@ pub async fn rename_group(
",
)
.bind(new_name)
.bind(&group_id)
.bind(&id)
.execute(&mut *tx)
.await?;
if result.rows_affected() == 0 {
return Err(crate::ErrorKind::InputError(format!(
"Unknown instance group {id}"
))
.into());
}
tx.commit().await?;
for instance_id in instance_ids {
@@ -152,40 +106,38 @@ pub async fn rename_group(
}
Ok(InstanceGroup {
id: group_id,
id,
name: new_name.to_string(),
})
}
pub async fn delete_group(name: String) -> crate::Result<()> {
pub async fn delete_group(id: String) -> crate::Result<()> {
let state = State::get().await?;
let mut tx = state.pool.begin().await?;
let instance_ids = sqlx::query_scalar::<_, String>(
"
SELECT memberships.instance_id
FROM instance_group_memberships memberships
INNER JOIN instance_groups groups
ON groups.id = memberships.group_id
WHERE groups.name = ?
SELECT instance_id
FROM instance_group_memberships
WHERE group_id = ?
",
)
.bind(&name)
.bind(&id)
.fetch_all(&mut *tx)
.await?;
let result = sqlx::query(
"
DELETE FROM instance_groups
WHERE name = ?
WHERE id = ?
",
)
.bind(&name)
.bind(&id)
.execute(&mut *tx)
.await?;
if result.rows_affected() == 0 {
return Err(crate::ErrorKind::InputError(format!(
"Unknown instance group {name}"
"Unknown instance group {id}"
))
.into());
}
+1 -1
View File
@@ -154,7 +154,7 @@ async fn restore_instance_metadata(
.await?;
instance_rows::replace_instance_groups(
&metadata.instance.id,
&metadata.groups,
&metadata.group_ids,
&mut tx,
)
.await?;
@@ -164,7 +164,7 @@ pub(crate) struct InstanceMetadataRecord {
pub applied_content_set: ContentSet,
pub link: InstanceLink,
pub shared_instance: Option<SharedInstanceAttachment>,
pub groups: Vec<String>,
pub group_ids: Vec<String>,
pub launch_overrides: InstanceLaunchOverrides,
}
@@ -221,7 +221,7 @@ struct InstanceMetadataRow {
imported_name: Option<String>,
imported_version_number: Option<String>,
imported_filename: Option<String>,
groups: String,
group_ids: String,
launch_overrides: Option<String>,
}
@@ -333,7 +333,7 @@ impl InstanceMetadataRow {
self.shared_sync_applied_update_id,
self.shared_sync_latest_available_update_id,
)?;
let groups = parse_groups(self.groups)?;
let group_ids = parse_group_ids(self.group_ids)?;
let launch_overrides =
launch_overrides_from_json(instance_id, self.launch_overrides)?;
@@ -342,7 +342,7 @@ impl InstanceMetadataRow {
applied_content_set,
link,
shared_instance,
groups,
group_ids,
launch_overrides,
})
}
@@ -576,16 +576,16 @@ macro_rules! query_instance_metadata {
link.imported_version_number AS "imported_version_number?: String",
link.imported_filename AS "imported_filename?: String",
COALESCE((
SELECT json_group_array(name)
SELECT json_group_array(id)
FROM (
SELECT groups.name
SELECT groups.id
FROM instance_group_memberships memberships
INNER JOIN instance_groups groups
ON groups.id = memberships.group_id
WHERE memberships.instance_id = i.id
ORDER BY groups.name
)
), '[]') AS "groups!: String",
), '[]') AS "group_ids!: String",
json(overrides.overrides) AS "launch_overrides?: String"
"#
+ $from
@@ -748,7 +748,7 @@ where
{
let rows = sqlx::query_scalar!(
"
SELECT groups.name
SELECT groups.id
FROM instance_group_memberships memberships
INNER JOIN instance_groups groups
ON groups.id = memberships.group_id
@@ -770,7 +770,7 @@ pub(crate) async fn list_instance_groups(
"
SELECT id, name
FROM instance_groups
ORDER BY name
ORDER BY name, id
",
)
.fetch_all(pool)
@@ -1071,7 +1071,7 @@ pub(crate) async fn set_shared_instance_attachment(
pub(crate) async fn replace_instance_groups(
instance_id: &str,
groups: &[String],
group_ids: &[String],
tx: &mut Transaction<'_, Sqlite>,
) -> crate::Result<()> {
sqlx::query!(
@@ -1084,35 +1084,7 @@ pub(crate) async fn replace_instance_groups(
.execute(&mut **tx)
.await?;
for group in groups {
let group_id = match sqlx::query_scalar::<_, String>(
"
SELECT id
FROM instance_groups
WHERE name = ?
",
)
.bind(group)
.fetch_optional(&mut **tx)
.await?
{
Some(group_id) => group_id,
None => {
let group_id = Uuid::new_v4().to_string();
sqlx::query(
"
INSERT INTO instance_groups (id, name)
VALUES (?, ?)
",
)
.bind(&group_id)
.bind(group)
.execute(&mut **tx)
.await?;
group_id
}
};
for group_id in group_ids {
sqlx::query(
"
INSERT OR IGNORE INTO instance_group_memberships (
@@ -1334,7 +1306,7 @@ fn required_i64(value: Option<i64>, column: &str) -> crate::Result<i64> {
})
}
fn parse_groups(value: String) -> crate::Result<Vec<String>> {
fn parse_group_ids(value: String) -> crate::Result<Vec<String>> {
serde_json::from_str(&value).map_err(|err| {
crate::ErrorKind::InputError(format!(
"Invalid instance groups JSON: {err}"
@@ -22,7 +22,7 @@ pub struct EditInstance {
)]
pub icon_path: Option<Option<String>>,
pub update_channel: Option<ReleaseChannel>,
pub groups: Option<Vec<String>>,
pub group_ids: Option<Vec<String>>,
pub link: Option<InstanceLink>,
pub launch_overrides: Option<InstanceLaunchOverridesPatch>,
pub content_set_patch: Option<AppliedContentSetPatch>,
@@ -174,9 +174,13 @@ pub(crate) async fn edit_instance(
.await?;
}
if let Some(groups) = &patch.groups {
instance_rows::replace_instance_groups(&instance.id, groups, &mut tx)
.await?;
if let Some(group_ids) = &patch.group_ids {
instance_rows::replace_instance_groups(
&instance.id,
group_ids,
&mut tx,
)
.await?;
}
if let Some(overrides) = launch_overrides.as_mut() {
@@ -13,7 +13,7 @@ pub struct InstanceMetadata {
pub shared_instance: Option<SharedInstanceAttachment>,
#[serde(default)]
pub quarantined: bool,
pub groups: Vec<String>,
pub group_ids: Vec<String>,
pub launch_overrides: InstanceLaunchOverrides,
}
@@ -83,7 +83,7 @@ fn instance_metadata(
link: record.link,
shared_instance: record.shared_instance,
quarantined,
groups: record.groups,
group_ids: record.group_ids,
launch_overrides: record.launch_overrides,
}
}