Files
drop/components/GameEditor/Version.vue
DecDuck 63ac2b8ffc Depot API & v4 (#298)
* feat: nginx + torrential basics & services system

* fix: lint + i18n

* fix: update torrential to remove openssl

* feat: add torrential to Docker build

* feat: move to self hosted runner

* fix: move off self-hosted runner

* fix: update nginx.conf

* feat: torrential cache invalidation

* fix: update torrential for cache invalidation

* feat: integrity check task

* fix: lint

* feat: move to version ids

* fix: client fixes and client-side checks

* feat: new depot apis and version id fixes

* feat: update torrential

* feat: droplet bump and remove unsafe update functions

* fix: lint

* feat: v4 featureset: emulators, multi-launch commands

* fix: lint

* fix: mobile ui for game editor

* feat: launch options

* fix: lint

* fix: remove axios, use $fetch

* feat: metadata and task api improvements

* feat: task actions

* fix: slight styling issue

* feat: fix style and lints

* feat: totp backend routes

* feat: oidc groups

* fix: update drop-base

* feat: creation of passkeys & totp

* feat: totp signin

* feat: webauthn mfa/signin

* feat: launch selecting ui

* fix: manually running tasks

* feat: update add company game modal to use new SelectorGame

* feat: executor selector

* fix(docker): update rust to rust nightly for torrential build (#305)

* feat: new version ui

* feat: move package lookup to build time to allow for deno dev

* fix: lint

* feat: localisation cleanup

* feat: apply localisation cleanup

* feat: potential i18n refactor logic

* feat: remove args from commands

* fix: lint

* fix: lockfile

---------

Co-authored-by: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
2026-01-13 15:32:39 +11:00

261 lines
8.8 KiB
Vue

<!-- eslint-disable vue/no-v-html -->
<template>
<div v-if="game && unimportedVersions" class="px-4 sm:px-6 lg:px-8 py-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-white">Versions</h1>
<p class="mt-2 text-sm text-gray-300">
Versions versions version, versions versions. Versions.
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<NuxtLink
:href="canImport ? `/admin/library/${game.id}/import` : ''"
type="button"
:class="[
canImport ? 'bg-blue-600 hover:bg-blue-700' : 'bg-blue-800/50',
'inline-flex w-fit items-center gap-x-2 rounded-md px-3 py-1 text-sm font-semibold font-display text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600',
]"
>
{{
canImport
? $t("library.admin.import.version.import")
: $t("library.admin.import.version.noVersions")
}}
</NuxtLink>
</div>
</div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="relative min-w-full divide-y divide-white/15">
<thead>
<tr>
<th></th>
<th
scope="col"
class="py-3 pr-3 pl-4 text-left text-xs font-medium tracking-wide text-gray-400 uppercase sm:pl-0"
>
Name (ID)
</th>
<th
scope="col"
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
>
Path
</th>
<th
scope="col"
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
>
Setup Configurations
</th>
<th
scope="col"
class="px-3 py-3 text-left text-xs font-medium tracking-wide text-gray-400 uppercase"
>
Launch Configurations
</th>
<th scope="col" class="py-3 pr-4 pl-3 sm:pr-0">
<span class="sr-only">Edit</span>
</th>
</tr>
</thead>
<draggable
:list="game.versions"
handle=".handle"
class="divide-y divide-white/10"
tag="tbody"
@update="() => updateVersionOrder()"
>
<template #item="{ element: version }: { element: VersionType }">
<tr :key="version.versionId">
<td>
<Bars3Icon
class="cursor-move w-6 h-6 text-zinc-400 handle"
/>
</td>
<td class="py-4 pr-3 pl-4 sm:pl-0">
<div class="flex flex-col">
<span
class="text-sm font-medium whitespace-nowrap text-white"
>{{ version.displayName ?? version.versionPath }}</span
>
<span class="text-xs text-zinc-500 mono">{{
version.versionId
}}</span>
</div>
</td>
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
{{ version.versionPath }}
</td>
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
<ul class="space-y-2">
<GameEditorVersionConfig
v-for="config in version.setups"
:key="config.setupId"
:config="config"
/>
<li
v-if="version.setups.length == 0"
class="text-xs uppercase font-display text-zinc-700 font-semibold"
>
No setups configured.
</li>
</ul>
</td>
<td class="px-3 py-4 text-sm whitespace-nowrap text-gray-400">
<div v-if="version.onlySetup">
Version configured as in setup-only mode.
</div>
<ul v-else class="space-y-2">
<GameEditorVersionConfig
v-for="config in version.launches"
:key="config.launchId"
:config="config"
/>
</ul>
</td>
<td
class="py-4 pr-4 pl-3 text-right text-sm font-medium whitespace-nowrap sm:pr-0 space-x-2"
>
<!--
<button class="text-blue-400 hover:text-blue-300">
Edit<span class="sr-only"
>,
{{ version.displayName ?? version.versionPath }}</span
>
</button>
-->
<button
class="text-red-400 hover:text-red-300"
@click="() => deleteVersion(version.versionId)"
>
Delete<span class="sr-only"
>,
{{ version.displayName ?? version.versionPath }}</span
>
</button>
</td>
</tr></template
>
</draggable>
</table>
</div>
</div>
</div>
</div>
<div v-else class="grow w-full flex items-center justify-center">
<div class="flex flex-col items-center">
<ExclamationCircleIcon
class="h-12 w-12 text-red-600"
aria-hidden="true"
/>
<div class="mt-3 text-center sm:mt-5">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
{{ $t("library.admin.offlineTitle") }}
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-md">
{{ $t("library.admin.offline") }}
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { SerializeObject } from "nitropack";
import type { H3Error } from "h3";
import { ExclamationCircleIcon, Bars3Icon } from "@heroicons/vue/24/outline";
import type { AdminFetchGameType } from "~/server/api/v1/admin/game/[id]/index.get";
// TODO implement UI for this page
const props = defineProps<{ unimportedVersions: string[] }>();
const { t } = useI18n();
const hasDeleted = ref(false);
const canImport = computed(
() => hasDeleted.value || props.unimportedVersions.length > 0,
);
const game = defineModel<SerializeObject<AdminFetchGameType>>({
required: true,
});
if (!game.value)
throw createError({
statusCode: 500,
statusMessage: "Game not provided to editor component",
});
type VersionType = (typeof game.value.versions)[number];
async function updateVersionOrder() {
try {
const newVersionOrder = await $dropFetch(
"/api/v1/admin/game/:id/versions",
{
method: "PATCH",
body: {
versions: game.value.versions.map((e) => e.versionId),
},
params: {
id: game.value.id,
},
},
);
const newVersions = newVersionOrder.map(
(id) => game.value.versions.find((k) => k.versionId == id)!,
);
game.value.versions = newVersions;
} catch (e) {
createModal(
ModalType.Notification,
{
title: t("errors.version.order.title"),
description: t("errors.version.order.desc", {
error: (e as H3Error)?.statusMessage ?? t("errors.unknown"),
}),
buttonText: t("common.close"),
},
(e, c) => c(),
);
}
}
async function deleteVersion(versionId: string) {
try {
await $dropFetch("/api/v1/admin/game/:id/versions", {
method: "DELETE",
body: {
version: versionId,
},
params: {
id: game.value.id,
},
});
game.value.versions.splice(
game.value.versions.findIndex((e) => e.versionId === versionId),
1,
);
hasDeleted.value = true;
} catch (e) {
createModal(
ModalType.Notification,
{
title: t("errors.version.delete.title"),
description: t("errors.version.delete.desc", {
error: (e as H3Error)?.statusMessage ?? t("errors.unknown"),
}),
buttonText: t("common.close"),
},
(e, c) => c(),
);
}
}
</script>