added platform versions support

This commit is contained in:
Zurdi
2024-01-19 13:17:52 +01:00
parent 6e665af8dd
commit e231d1b73e
19 changed files with 520 additions and 34 deletions

1
.gitignore vendored
View File

@@ -48,6 +48,7 @@ romm_mock
# testing
backend/romm_test/resources
backend/romm_test/logs
backend/romm_test/config
.pytest_cache
# service worker

View File

@@ -36,6 +36,7 @@ class Config:
EXCLUDED_MULTI_PARTS_EXT: list[str]
EXCLUDED_MULTI_PARTS_FILES: list[str]
PLATFORMS_BINDING: dict[str, str]
PLATFORMS_VERSIONS: dict[str, str]
ROMS_FOLDER_NAME: str
SAVES_FOLDER_NAME: str
STATES_FOLDER_NAME: str
@@ -128,6 +129,7 @@ class ConfigManager:
self._raw_config, "exclude.roms.multi_file.parts.names", []
),
PLATFORMS_BINDING=pydash.get(self._raw_config, "system.platforms", {}),
PLATFORMS_VERSIONS=pydash.get(self._raw_config, "system.versions", {}),
ROMS_FOLDER_NAME=pydash.get(
self._raw_config, "filesystem.roms_folder", "roms"
),
@@ -189,6 +191,17 @@ class ConfigManager:
)
sys.exit(3)
if not isinstance(self.config.PLATFORMS_VERSIONS, dict):
log.critical("Invalid config.yml: system.versions must be a dictionary")
sys.exit(3)
else:
for fs_slug, slug in self.config.PLATFORMS_VERSIONS.items():
if slug is None:
log.critical(
f"Invalid config.yml: system.versions.{fs_slug} must be a string"
)
sys.exit(3)
if not isinstance(self.config.ROMS_FOLDER_NAME, str):
log.critical("Invalid config.yml: filesystem.roms_folder must be a string")
sys.exit(3)
@@ -276,6 +289,25 @@ class ConfigManager:
pass
self.update_config()
def add_version(self, fs_slug: str, slug: str) -> None:
try:
_ = self._raw_config["system"]
except KeyError:
self._raw_config = {"system": {"versions": {}}}
try:
_ = self._raw_config["system"]["versions"]
except KeyError:
self._raw_config["system"]["versions"] = {}
self._raw_config["system"]["versions"][fs_slug] = slug
self.update_config()
def remove_version(self, fs_slug: str) -> None:
try:
del self._raw_config["system"]["versions"][fs_slug]
except KeyError:
pass
self.update_config()
# def _get_exclude_path(self, exclude):
# exclude_base = self._raw_config["exclude"]
# exclusions = {

View File

@@ -67,6 +67,42 @@ async def delete_platform_binding(request: Request, fs_slug: str) -> MessageResp
return {"msg": f"{fs_slug} bind removed successfully!"}
@protected_route(router.post, "/config/system/versions", ["platforms.write"])
async def add_platform_version(request: Request) -> MessageResponse:
"""Add platform version to the configuration"""
data = await request.json()
fs_slug = data["fs_slug"]
slug = data["slug"]
try:
cm.add_version(fs_slug, slug)
except ConfigNotWritableException as e:
log.critical(e.message)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.message
)
return {"msg": f"Added {fs_slug} as version of: {slug} successfully!"}
@protected_route(
router.delete, "/config/system/versions/{fs_slug}", ["platforms.write"]
)
async def delete_platform_version(request: Request, fs_slug: str) -> MessageResponse:
"""Delete platform version from the configuration"""
try:
cm.remove_version(fs_slug)
except ConfigNotWritableException as e:
log.critical(e.message)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.message
)
return {"msg": f"{fs_slug} version removed successfully!"}
# @protected_route(router.post, "/config/exclude", ["platforms.write"])
# async def add_exclusion(request: Request) -> MessageResponse:
# """Add platform binding to the configuration"""

View File

@@ -9,6 +9,7 @@ class ConfigResponse(TypedDict):
EXCLUDED_MULTI_PARTS_EXT: list[str]
EXCLUDED_MULTI_PARTS_FILES: list[str]
PLATFORMS_BINDING: dict[str, str]
PLATFORMS_VERSIONS: dict[str, str]
ROMS_FOLDER_NAME: str
SAVES_FOLDER_NAME: str
STATES_FOLDER_NAME: str

View File

@@ -1,7 +1,7 @@
from handler.igdb_handler import IGDBRomType
from handler.igdb_handler import IGDBRom
from typing_extensions import TypedDict
class RomSearchResponse(TypedDict):
msg: str
roms: list[IGDBRomType]
roms: list[IGDBRom]

View File

@@ -43,12 +43,12 @@ SWITCH_PRODUCT_ID_FILE: Final = os.path.join(
MAME_XML_FILE: Final = os.path.join(os.path.dirname(__file__), "fixtures", "mame.xml")
class IGDBPlatformType(TypedDict):
class IGDBPlatform(TypedDict):
igdb_id: int
name: str
class IGDBRomType(TypedDict):
class IGDBRom(TypedDict):
igdb_id: int
slug: str
name: str
@@ -60,6 +60,7 @@ class IGDBRomType(TypedDict):
class IGDBHandler:
def __init__(self) -> None:
self.platform_url = "https://api.igdb.com/v4/platforms/"
self.platform_version_url = "https://api.igdb.com/v4/platform_versions/"
self.games_url = "https://api.igdb.com/v4/games/"
self.covers_url = "https://api.igdb.com/v4/covers/"
self.screenshots_url = "https://api.igdb.com/v4/screenshots/"
@@ -263,23 +264,36 @@ class IGDBHandler:
return search_term
@check_twitch_token
def get_platform(self, slug: str) -> IGDBPlatformType:
def get_platform(self, slug: str) -> IGDBPlatform:
platforms = self._request(
self.platform_url,
data=f'fields id, name; where slug="{slug.lower()}";',
)
platform = pydash.get(platforms, "[0]", None)
if not platform:
return IGDBPlatformType(igdb_id=None, name=slug.replace("-", " ").title())
return IGDBPlatformType(
# Check if platform is a version if not found
if not platform:
platform_versions = self._request(
self.platform_version_url,
data=f'fields id, name; where slug="{slug.lower()}";',
)
version = pydash.get(platform_versions, "[0]", None)
if not version:
return IGDBPlatform(igdb_id=None, igdb_id_base_platform=None, name=slug.replace("-", " ").title())
return IGDBPlatform(
igdb_id=version["id"],
name=version["name"],
)
return IGDBPlatform(
igdb_id=platform["id"],
name=platform["name"],
)
@check_twitch_token
async def get_rom(self, file_name: str, platform_idgb_id: int) -> IGDBRomType:
async def get_rom(self, file_name: str, platform_idgb_id: int) -> IGDBRom:
from handler import fs_rom_handler
search_term = fs_rom_handler.get_file_name_with_no_tags(file_name)
@@ -314,7 +328,7 @@ class IGDBHandler:
)
igdb_id = res.get("id", None)
rom = IGDBRomType(
rom = IGDBRom(
igdb_id=igdb_id,
slug=res.get("slug", ""),
name=res.get("name", search_term),
@@ -330,7 +344,7 @@ class IGDBHandler:
return rom
@check_twitch_token
def get_rom_by_id(self, igdb_id: int) -> IGDBRomType:
def get_rom_by_id(self, igdb_id: int) -> IGDBRom:
roms = self._request(
self.games_url,
f"fields slug, name, summary; where id={igdb_id};",
@@ -347,7 +361,7 @@ class IGDBHandler:
}
@check_twitch_token
def get_matched_roms_by_id(self, igdb_id: int) -> list[IGDBRomType]:
def get_matched_roms_by_id(self, igdb_id: int) -> list[IGDBRom]:
matched_rom = self.get_rom_by_id(igdb_id)
matched_rom.update(
url_cover=matched_rom["url_cover"].replace("t_thumb", "t_cover_big"),
@@ -357,7 +371,7 @@ class IGDBHandler:
@check_twitch_token
def get_matched_roms_by_name(
self, search_term: str, platform_idgb_id: int
) -> list[IGDBRomType]:
) -> list[IGDBRom]:
if not platform_idgb_id:
return []
@@ -371,7 +385,7 @@ class IGDBHandler:
)
return [
IGDBRomType(
IGDBRom(
igdb_id=rom["id"],
slug=rom["slug"],
name=rom["name"],

View File

@@ -1,17 +1,38 @@
import os
from typing import Any
import emoji
import emoji
from config.config_manager import config_manager as cm
from handler import fs_asset_handler, igdb_handler, fs_resource_handler, fs_rom_handler, db_platform_handler
from handler import (
db_platform_handler,
fs_asset_handler,
fs_resource_handler,
fs_rom_handler,
igdb_handler,
)
from logger.logger import log
from models.assets import Save, Screenshot, State
from models.platform import Platform
from models.rom import Rom
from models.assets import Save, Screenshot, State
SWAPPED_PLATFORM_BINDINGS = dict(
(v, k) for k, v in cm.config.PLATFORMS_BINDING.items()
)
SWAPPED_PLATFORM_BINDINGS = dict((v, k) for k, v in cm.config.PLATFORMS_BINDING.items())
def _get_main_platform_igdb_id(platform: Platform):
if platform.fs_slug in cm.config.PLATFORMS_VERSIONS.keys():
main_platform_slug = cm.config.PLATFORMS_VERSIONS[platform.fs_slug]
main_platform = db_platform_handler.get_platform_by_slug(main_platform_slug)
if main_platform:
main_platform_igdb_id = main_platform.igdb_id
else:
main_platform_igdb_id = igdb_handler.get_platform(main_platform_slug)[
"igdb_id"
]
if not main_platform_igdb_id:
main_platform_igdb_id = platform.igdb_id
else:
main_platform_igdb_id = platform.igdb_id
return main_platform_igdb_id
def scan_platform(fs_slug: str, fs_platforms) -> Platform:
@@ -51,7 +72,9 @@ def scan_platform(fs_slug: str, fs_platforms) -> Platform:
if platform["igdb_id"]:
log.info(emoji.emojize(f" Identified as {platform['name']} :video_game:"))
else:
log.warning(emoji.emojize(f" {platform_attrs['slug']} not found in IGDB :cross_mark:"))
log.warning(
emoji.emojize(f" {platform_attrs['slug']} not found in IGDB :cross_mark:")
)
platform_attrs.update(platform)
@@ -85,8 +108,12 @@ async def scan_rom(
"platform_id": platform.id,
"file_path": roms_path,
"file_name": rom_attrs["file_name"],
"file_name_no_tags": fs_rom_handler.get_file_name_with_no_tags(rom_attrs["file_name"]),
"file_extension": fs_rom_handler.parse_file_extension(rom_attrs["file_name"]),
"file_name_no_tags": fs_rom_handler.get_file_name_with_no_tags(
rom_attrs["file_name"]
),
"file_extension": fs_rom_handler.parse_file_extension(
rom_attrs["file_name"]
),
"file_size_bytes": file_size,
"multi": rom_attrs["multi"],
"regions": regs,
@@ -96,11 +123,13 @@ async def scan_rom(
}
)
main_platform_igdb_id = _get_main_platform_igdb_id(platform)
# Search in IGDB
igdb_handler_rom = (
igdb_handler.get_rom_by_id(int(r_igbd_id_search))
if r_igbd_id_search
else await igdb_handler.get_rom(rom_attrs["file_name"], platform.igdb_id)
else await igdb_handler.get_rom(rom_attrs["file_name"], main_platform_igdb_id)
)
rom_attrs.update(igdb_handler_rom)
@@ -108,11 +137,15 @@ async def scan_rom(
# Return early if not found in IGDB
if not igdb_handler_rom["igdb_id"]:
log.warning(
emoji.emojize(f"\t {r_igbd_id_search or rom_attrs['file_name']} not found in IGDB :cross_mark:")
emoji.emojize(
f"\t {r_igbd_id_search or rom_attrs['file_name']} not found in IGDB :cross_mark:"
)
)
return Rom(**rom_attrs)
log.info(emoji.emojize(f"\t Identified as {igdb_handler_rom['name']} :alien_monster:"))
log.info(
emoji.emojize(f"\t Identified as {igdb_handler_rom['name']} :alien_monster:")
)
# Update properties from IGDB
rom_attrs.update(
@@ -180,6 +213,4 @@ def scan_screenshot(file_name: str, platform_slug: Platform = None) -> Screensho
if platform_slug:
return Screenshot(**_scan_asset(file_name, screenshots_path))
return Screenshot(
**_scan_asset(file_name, cm.config.SCREENSHOTS_FOLDER_NAME)
)
return Screenshot(**_scan_asset(file_name, cm.config.SCREENSHOTS_FOLDER_NAME))

View File

@@ -41,7 +41,7 @@ services:
volumes:
- "/path/to/library:/romm/library"
- "/path/to/resources:/romm/resources" # [Optional] Path where roms metadata (covers) are stored
- "/path/to/config.yml:/romm/config.yml" # [Optional] Path where config is stored
- "/path/to/config:/romm/config" # [Optional] Path where config is stored
- "/path/to/database:/romm/database" # [Optional] Only needed if ROMM_DB_DRIVER=sqlite or not set
- "/path/to/logs:/romm/logs" # [Optional] Path where logs are stored
ports:

View File

@@ -15,7 +15,7 @@ export type { CursorPage_RomSchema_ } from './models/CursorPage_RomSchema_';
export type { EnhancedRomSchema } from './models/EnhancedRomSchema';
export type { HeartbeatResponse } from './models/HeartbeatResponse';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { IGDBRomType } from './models/IGDBRomType';
export type { IGDBRom } from './models/IGDBRom';
export type { MessageResponse } from './models/MessageResponse';
export type { PlatformSchema } from './models/PlatformSchema';
export type { Role } from './models/Role';

View File

@@ -11,6 +11,7 @@ export type ConfigResponse = {
EXCLUDED_MULTI_PARTS_EXT: Array<string>;
EXCLUDED_MULTI_PARTS_FILES: Array<string>;
PLATFORMS_BINDING: Record<string, string>;
PLATFORMS_VERSIONS: Record<string, string>;
ROMS_FOLDER_NAME: string;
SAVES_FOLDER_NAME: string;
STATES_FOLDER_NAME: string;

View File

@@ -3,7 +3,7 @@
/* tslint:disable */
/* eslint-disable */
export type IGDBRomType = {
export type IGDBRom = {
igdb_id: number;
slug: string;
name: string;

View File

@@ -3,10 +3,10 @@
/* tslint:disable */
/* eslint-disable */
import type { IGDBRomType } from './IGDBRomType';
import type { IGDBRom } from './IGDBRom';
export type RomSearchResponse = {
msg: string;
roms: Array<IGDBRomType>;
roms: Array<IGDBRom>;
};

View File

@@ -0,0 +1,104 @@
<script setup lang="ts">
import api_config from "@/services/api_config";
import storeConfig from "@/stores/config";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";
// Props
const show = ref(false);
const configStore = storeConfig();
const emitter = inject<Emitter<Events>>("emitter");
const fsSlugToCreate = ref();
const slugToCreate = ref();
emitter?.on("showCreatePlatformVersionDialog", ({ fsSlug = "", slug = "" }) => {
fsSlugToCreate.value = fsSlug;
slugToCreate.value = slug;
show.value = true;
});
// Functions
function addVersionPlatform() {
api_config
.addPlatformVersionConfig({
fsSlug: fsSlugToCreate.value,
slug: slugToCreate.value,
})
.then(() => {
configStore.addPlatformVersion(fsSlugToCreate.value, slugToCreate.value);
})
.catch(({ response, message }) => {
emitter?.emit("snackbarShow", {
msg: `${response?.data?.detail || response?.statusText || message}`,
icon: "mdi-close-circle",
color: "red",
timeout: 4000,
});
});
closeDialog();
}
function closeDialog() {
show.value = false;
}
</script>
<template>
<v-dialog v-model="show" max-width="500px" :scrim="true">
<v-card>
<v-toolbar density="compact" class="bg-terciary">
<v-row class="align-center" no-gutters>
<v-col cols="10">
<v-icon icon="mdi-gamepad-variant" class="ml-5" />
<v-icon icon="mdi-approximately-equal" class="ml-1 text-romm-gray" />
<v-icon icon="mdi-controller" class="ml-1 text-romm-accent-1" />
</v-col>
<v-col>
<v-btn
@click="closeDialog"
class="bg-terciary"
rounded="0"
variant="text"
icon="mdi-close"
block
/>
</v-col>
</v-row>
</v-toolbar>
<v-divider class="border-opacity-25" :thickness="1" />
<v-card-text>
<v-row class="pa-2 align-center" no-gutters>
<v-text-field
@keyup.enter=""
v-model="fsSlugToCreate"
label="Platform version"
variant="outlined"
required
hide-details
/>
<v-icon icon="mdi-menu-right" class="mx-2 text-romm-gray" />
<v-text-field
class="text-romm-accent-1"
@keyup.enter=""
v-model="slugToCreate"
label="Main platform"
color="romm-accent-1"
base-color="romm-accent-1"
variant="outlined"
required
hide-details
/>
</v-row>
<v-row class="justify-center pa-2" no-gutters>
<v-btn @click="closeDialog" class="bg-terciary">Cancel</v-btn>
<v-btn
@click="addVersionPlatform()"
class="text-romm-green bg-terciary ml-5"
>
Confirm
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>

View File

@@ -0,0 +1,97 @@
<script setup lang="ts">
import api_config from "@/services/api_config";
import storeConfig from "@/stores/config";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";
// Props
const show = ref(false);
const emitter = inject<Emitter<Events>>("emitter");
const configStore = storeConfig();
const fsSlugToDelete = ref();
const slugToDelete = ref();
emitter?.on("showDeletePlatformVersionDialog", ({ fsSlug, slug }) => {
fsSlugToDelete.value = fsSlug;
slugToDelete.value = slug;
show.value = true;
});
// Functions
function removeVersionPlatform() {
api_config
.deletePlatformVersionConfig({ fsSlug: fsSlugToDelete.value })
.then(() => {
configStore.removePlatformVersion(fsSlugToDelete.value);
})
.catch(({ response, message }) => {
emitter?.emit("snackbarShow", {
msg: `${response?.data?.detail || response?.statusText || message}`,
icon: "mdi-close-circle",
color: "red",
timeout: 4000,
});
});
closeDialog();
}
function closeDialog() {
show.value = false;
}
</script>
<template>
<v-dialog
v-model="show"
width="auto"
@click:outside="closeDialog"
@keydown.esc="closeDialog"
no-click-animation
persistent
:scrim="true"
>
<v-card>
<v-toolbar density="compact" class="bg-terciary">
<v-row class="align-center" no-gutters>
<v-col cols="10">
<v-icon icon="mdi-delete" class="ml-5 mr-2" />
</v-col>
<v-col>
<v-btn
@click="closeDialog"
class="bg-terciary"
rounded="0"
variant="text"
icon="mdi-close"
block
/>
</v-col>
</v-row>
</v-toolbar>
<v-divider class="border-opacity-25" :thickness="1" />
<v-card-text>
<v-row class="justify-center pa-2" no-gutters>
<span class="mr-1">Deleting platform version [</span>
<span class="text-romm-accent-1 mr-1">{{
fsSlugToDelete
}}</span>
<span>:</span>
<span class="text-romm-accent-1 ml-1">{{
slugToDelete
}}</span
><span class="ml-1">].</span>
<span class="ml-1">Do you confirm?</span>
</v-row>
<v-row class="justify-center pa-2" no-gutters>
<v-btn @click="closeDialog" class="bg-terciary">Cancel</v-btn>
<v-btn
@click="removeVersionPlatform()"
class="text-romm-red bg-terciary ml-5"
>
Confirm
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>

View File

@@ -0,0 +1,133 @@
<script setup lang="ts">
import CreatePlatformVersionDialog from "@/components/Dialog/Config/CreatePlatformVersion.vue";
import DeletePlatformVersionDialog from "@/components/Dialog/Config/DeletePlatformVersion.vue";
import PlatformIcon from "@/components/Platform/PlatformIcon.vue";
import storeAuth from "@/stores/auth";
import storeConfig from "@/stores/config";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";
// Props
const emitter = inject<Emitter<Events>>("emitter");
const configStore = storeConfig();
const authStore = storeAuth();
const platformsVersions = configStore.value.PLATFORMS_VERSIONS;
const editable = ref(false);
</script>
<template>
<v-card rounded="0">
<v-toolbar class="bg-terciary" density="compact">
<v-toolbar-title class="text-button">
<v-icon class="mr-3">mdi-gamepad-variant</v-icon>
Platforms Versions
</v-toolbar-title>
<v-btn
v-if="authStore.scopes.includes('platforms.write')"
class="ma-2"
rounded="0"
size="small"
variant="text"
@click="editable = !editable"
icon="mdi-cog"
>
</v-btn>
</v-toolbar>
<v-divider class="border-opacity-25" />
<v-card-text class="pa-1">
<v-row no-gutters class="align-center">
<v-col
cols="6"
sm="4"
md="3"
lg="2"
xl="2"
v-for="(slug, fsSlug) in platformsVersions"
:key="slug"
:title="slug"
>
<v-list-item class="bg-terciary ma-1 pa-1 text-truncate">
<template v-slot:prepend>
<v-avatar :rounded="0" size="40" class="mx-2">
<platform-icon class="platform-icon" :key="slug" :slug="slug" />
</v-avatar>
</template>
<v-list-item class="bg-primary pr-2 pl-2">
<span>{{ fsSlug }}</span>
<template v-slot:append>
<v-slide-x-reverse-transition>
<v-btn
v-if="
authStore.scopes.includes('platforms.write') && editable
"
rounded="0"
variant="text"
size="x-small"
icon="mdi-pencil"
@click="
emitter?.emit('showCreatePlatformVersionDialog', {
fsSlug,
slug,
})
"
class="ml-2"
/>
</v-slide-x-reverse-transition>
<v-slide-x-reverse-transition>
<v-btn
v-if="
authStore.scopes.includes('platforms.write') && editable
"
rounded="0"
variant="text"
size="x-small"
icon="mdi-delete"
@click="
emitter?.emit('showDeletePlatformVersionDialog', {
fsSlug,
slug,
})
"
class="text-romm-red"
/>
</v-slide-x-reverse-transition>
</template>
</v-list-item>
</v-list-item>
</v-col>
<v-col cols="6" sm="4" md="3" lg="2" xl="2" class="px-1">
<v-expand-transition>
<v-btn
v-if="authStore.scopes.includes('platforms.write') && editable"
block
rounded="0"
size="large"
prepend-icon="mdi-plus"
variant="outlined"
class="text-romm-accent-1"
@click="
emitter?.emit('showCreatePlatformVersionDialog', {
fsSlug: '',
slug: '',
})
"
>
Add
</v-btn>
</v-expand-transition>
</v-col>
</v-row>
</v-card-text>
</v-card>
<create-platform-version-dialog />
<delete-platform-version-dialog />
</template>
<style scoped>
.platform-icon {
cursor: pointer;
}
</style>

View File

@@ -21,6 +21,24 @@ async function deletePlatformBindConfig({
return api.delete(`/config/system/platforms/${fsSlug}`);
}
async function addPlatformVersionConfig({
fsSlug,
slug,
}: {
fsSlug: string;
slug: string;
}): Promise<{ data: MessageResponse }> {
return api.post("/config/system/versions", { fs_slug: fsSlug, slug: slug });
}
async function deletePlatformVersionConfig({
fsSlug,
}: {
fsSlug: string;
}): Promise<{ data: MessageResponse }> {
return api.delete(`/config/system/versions/${fsSlug}`);
}
async function addExclusion({
exclude,
exclusion,
@@ -37,5 +55,7 @@ async function addExclusion({
export default {
addPlatformBindConfig,
deletePlatformBindConfig,
addPlatformVersionConfig,
deletePlatformVersionConfig,
addExclusion,
};

View File

@@ -17,5 +17,11 @@ export default defineStore("config", {
removePlatformBinding(fsSlug: string) {
delete this.value.PLATFORMS_BINDING[fsSlug];
},
addPlatformVersion(fsSlug: string, slug: string) {
this.value.PLATFORMS_VERSIONS[fsSlug] = slug;
},
removePlatformVersion(fsSlug: string) {
delete this.value.PLATFORMS_VERSIONS[fsSlug];
},
},
});

View File

@@ -30,6 +30,14 @@ export type Events = {
fsSlug: string;
slug: string;
};
showCreatePlatformVersionDialog: {
fsSlug: string;
slug: string;
};
showDeletePlatformVersionDialog: {
fsSlug: string;
slug: string;
};
showCreateExclusionDialog: { exclude: string };
showCreateUserDialog: null;
showEditUserDialog: UserItem;

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import ExclusionsCard from "@/components/Settings/Config/ExclusionsCard.vue";
import PlatformBinding from "@/components/Settings/Config/PlatformBindingCard.vue";
import PlatformVersions from "@/components/Settings/Config/PlatformVersionsCard.vue";
</script>
<template>
<platform-binding />
<platform-versions />
<exclusions-card class="mt-1" />
</template>