mirror of
https://github.com/modrinth/code.git
synced 2026-08-01 13:45:53 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d28d9793f0 | ||
|
|
759173e116 | ||
|
|
eb98bc99fe | ||
|
|
d83a161901 | ||
|
|
385c7c9632 | ||
|
|
2102598e99 | ||
|
|
fdfb343eef | ||
|
|
850deca7b3 | ||
|
|
f21f632833 | ||
|
|
a61313eb0f | ||
|
|
965fa0269c | ||
|
|
7ad57bf715 | ||
|
|
3825325323 | ||
|
|
597f620711 | ||
|
|
42432bb519 | ||
|
|
85e5357f49 | ||
|
|
76a4c8ebd6 | ||
|
|
3b9e0a03eb |
@@ -5,16 +5,15 @@
|
||||
:game-versions="gameVersions"
|
||||
:versions="versions"
|
||||
:project="project"
|
||||
:show-environment-column="themeStore.featureFlags.show_version_environment_column"
|
||||
:version-link="(version) => buildProjectHref(`/project/${project.id}/version/${version.id}`)"
|
||||
>
|
||||
<template #actions="{ version }">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<ButtonStyled circular type="transparent" color="green">
|
||||
<button
|
||||
v-tooltip="`Install`"
|
||||
:class="{
|
||||
'group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted':
|
||||
!installed || version.id !== installedVersion,
|
||||
}"
|
||||
v-tooltip="
|
||||
!installed ? 'Install' : version.id !== installedVersion ? 'Swap version' : ''
|
||||
"
|
||||
:disabled="installing || (installed && version.id === installedVersion)"
|
||||
@click.stop="() => install(version.id)"
|
||||
>
|
||||
@@ -26,7 +25,6 @@
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
v-if="false"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:options="[
|
||||
{
|
||||
id: 'install-elsewhere',
|
||||
@@ -52,7 +50,6 @@
|
||||
<a
|
||||
v-else
|
||||
v-tooltip="`Open in browser`"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:href="`https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -77,6 +74,9 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
defineProps({
|
||||
project: {
|
||||
|
||||
@@ -8,6 +8,7 @@ export const DEFAULT_FEATURE_FLAGS = {
|
||||
worlds_tab: false,
|
||||
worlds_in_home: true,
|
||||
server_project_qa: false,
|
||||
show_version_environment_column: false,
|
||||
server_ram_as_bytes_always_on: false,
|
||||
always_show_app_controls: false,
|
||||
skip_non_essential_warnings: false,
|
||||
|
||||
@@ -21,6 +21,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
developerMode: false,
|
||||
demoMode: false,
|
||||
showVersionFilesInTable: false,
|
||||
showVersionEnvironmentColumn: false,
|
||||
showAdsWithPlus: false,
|
||||
alwaysShowChecklistAsPopup: true,
|
||||
testTaxForm: false,
|
||||
|
||||
@@ -914,18 +914,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "يرجى مراجعة بيانات تعريف البيئة"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "صعوداً"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "نزولا"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "الإسم"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "الحالة"
|
||||
},
|
||||
"dashboard.revenue.available-now": {
|
||||
"message": "متاح الآن"
|
||||
},
|
||||
|
||||
@@ -1346,9 +1346,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Načítání oznámení..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Více projektů můžete upravit najednou jejich výběrem níže."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverové projekty nepodporují hromadné úpravy"
|
||||
},
|
||||
@@ -1400,24 +1397,6 @@
|
||||
"dashboard.projects.project.moderator-message-aria": {
|
||||
"message": "Projekt má zprávu od moderátorů. Další informace zobrazíte v projektu."
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Vzestupně"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Sestupně"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Jméno"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Stav"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Typ"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikona"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Benachrichtigungen werden geladen..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Du kannst mehrere Projekte gleichzeitig bearbeiten, indem du sie unten auswählst."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverprojekte understützen Massenbearbeitung nicht"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Bitte überprüfe die Umgebungsmetadaten"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Aufsteigend"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Absteigend"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Art"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Benachrichtigungen werden geladen..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Du kannst mehrere Projekte gleichzeitig bearbeiten, indem du sie unten auswählst."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverprojekte unterstützen Massenbearbeitung nicht"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Bitte überprüfe die Umgebungsmetadaten"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Aufsteigend"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Absteigend"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Typ"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Loading notifications..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "You can edit multiple projects at once by selecting them below."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Server projects do not support bulk editing"
|
||||
},
|
||||
@@ -1712,23 +1709,11 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Please review environment metadata"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Ascending"
|
||||
"dashboard.projects.selection-bar.aria-label": {
|
||||
"message": "Selected projects actions"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Descending"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Type"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icon"
|
||||
"dashboard.projects.selection-bar.selected-count": {
|
||||
"message": "{count, plural, one {# project selected} other {# projects selected}}"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
@@ -1736,6 +1721,9 @@
|
||||
"dashboard.projects.table.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"dashboard.projects.table.select-all-bulk-editable": {
|
||||
"message": "Select all projects that support bulk editing"
|
||||
},
|
||||
"dashboard.projects.table.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Cargando notificaciones..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Puedes editar varios proyectos a la vez seleccionándolos a continuación."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Los proyectos no son compatible con la edición masiva de servidores"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Por favor revisa los metadatos de entorno"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Ascendente"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Descendente"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nombre"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Estado"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icono"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1517,9 +1517,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Cargando notificaciones..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Puedes editar múltiples proyectos a la vez seleccionándolos abajo."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Proyectos de servidor no soportan edición masiva"
|
||||
},
|
||||
@@ -1592,24 +1589,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Por favor revise el entorno del metadata"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Ascendiendo"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Descendiendo"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nombre"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Estado"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icono"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -611,24 +611,6 @@
|
||||
"dashboard.projects.project.icon-alt": {
|
||||
"message": "Kuvake projektille {title}"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Nouseva"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Laskeva"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nimi"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Tila"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tyyppi"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Kuvake"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -998,18 +998,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Pakisuri sa metadata sa environment"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Pangalan"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Estado"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Uri"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikono"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Chargement des notifications..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Vous pouvez modifier plusieurs projets en même temps en les sélectionnant ci-dessous."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Les projets du serveur ne prennent pas en charge l'édition en masse"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Merci de revoir les métadonnées environnementales"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Ascendant"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Descendant"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nom"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Statut"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Type"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icône"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1382,9 +1382,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Értesítések betöltése..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Több projektet is szerkeszthetsz egyszerre, ha kiválasztod őket alább."
|
||||
},
|
||||
"dashboard.projects.empty": {
|
||||
"message": "Még nincsenek projektjeid. Kattints a fenti zöld gombra a kezdéshez."
|
||||
},
|
||||
@@ -1445,24 +1442,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Kérlek ellenőrizd a környezeti metaadatokat"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Növekvő"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Csökkenő"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Név"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Állapot"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Típus"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "Azonosító"
|
||||
},
|
||||
|
||||
@@ -1628,9 +1628,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Caricando le notifiche..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Puoi modificare più progetti contemporaneamente selezionandoli qui sotto."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Non puoi modificare più server contemporaneamente"
|
||||
},
|
||||
@@ -1703,24 +1700,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Si prega di rivedere i metadati d'ambiente"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Crescente"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Decrescente"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nome"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Stato"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Icona"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1211,9 +1211,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "通知を読み込み中…"
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "以下で選択することで、複数のプロジェクトを一括で編集できます。"
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "サーバープロジェクトでは一括編集を利用できません"
|
||||
},
|
||||
@@ -1286,24 +1283,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "環境メタデータを確認してください"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "昇順"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "降順"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "名前"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "ステータス"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "種類"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "アイコン"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -998,9 +998,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "알림 불러오는 중..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "아래에서 여러 프로젝트를 선택하면 한 번에 편집할 수 있습니다."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "서버 프로젝트는 일괄 편집을 지원하지 않습니다"
|
||||
},
|
||||
@@ -1070,24 +1067,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "환경 메타데이터를 검토해 주세요"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "오름차순"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "내림차순"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "이름"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "상태"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "유형"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "아이콘"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1367,9 +1367,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Sedang memuat pemberitahuan..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Anda boleh menyunting berbilang projek sekaligus dengan memilihnya di bawah."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Projek pelayan tidak menyokong penyuntingan pukal"
|
||||
},
|
||||
@@ -1439,24 +1436,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Sila semak metadata persekitaran"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Menaik"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Menurun"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nama"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Jenis"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1358,9 +1358,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Notificaties aan het laden..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "U kunt meerdere projecten tegelijkertijd bijwerken door ze hieronder te selecteren."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Massa wijzigingen worden niet ondersteund door server projecten"
|
||||
},
|
||||
@@ -1412,12 +1409,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Controller project metadata alstublieft"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Naam"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.table.name": {
|
||||
"message": "Naam"
|
||||
},
|
||||
|
||||
@@ -851,9 +851,6 @@
|
||||
"dashboard.organizations.title": {
|
||||
"message": "Organiseringer"
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Du kan redigere flere prosjekt samtidig ved å velge dem nedafor."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Serverprosjekt støtter ikke massredigering"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Ładowanie powiadomień..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Możesz edytować kilka projektów naraz, zaznaczając je poniżej."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Projekty serwerów nie wspierają grupowej edycji"
|
||||
},
|
||||
@@ -1709,24 +1706,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Sprawdź metadane środowiska"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Rosnąco"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Malejąco"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nazwa"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Rodzaj"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikona"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1631,9 +1631,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Carregando notificações..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Você pode editar vários projetos em simultâneo, selecionando-os abaixo."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Os projetos de servidor não suportam edição em massa"
|
||||
},
|
||||
@@ -1709,24 +1706,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Por favor, revise os metadados do ambiente"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Crescente"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Decrescente"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nome"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ícone"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -821,18 +821,6 @@
|
||||
"dashboard.projects.links.wiki-page.label": {
|
||||
"message": "Página da Wiki"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Nome"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tipo"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ícone"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1628,9 +1628,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Загрузка уведомлений..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Можно редактировать сразу несколько проектов, выбрав их ниже."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Серверные проекты не поддерживают массовое редактирование"
|
||||
},
|
||||
@@ -1706,24 +1703,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Проверьте метаданные среды"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "По возрастанию"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "По убыванию"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Название"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Статус"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Тип"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Иконка"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1514,24 +1514,6 @@
|
||||
"dashboard.projects.project.icon-alt": {
|
||||
"message": "Ikon för {title}"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Stigande"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Fallande"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Namn"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Status"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Typ"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Ikon"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1574,9 +1574,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Bildirimler yükleniyor..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Aşağıdan seçerek birden fazla projeyi düzenleyebilirsiniz."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Sunucu projeleri toplu düzenlemeyi desteklemez"
|
||||
},
|
||||
@@ -1652,24 +1649,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Lütfen ortam meta verilerini inceleyin"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Artan"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Azalan sırada sırala"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "İsim"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Durum"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Tip"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Simge"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "Kimlik"
|
||||
},
|
||||
|
||||
@@ -1301,9 +1301,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Завантаження сповіщень…"
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Ви можете редагувати кілька проєктів одночасно, вибравши їх нижче."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Проєкти сервера не підтримують масове редагування"
|
||||
},
|
||||
@@ -1376,24 +1373,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Перегляньте метадані середовища"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Зростання"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Спадання"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Назва"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Статус"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Тип"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "Значок"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1478,9 +1478,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "Đang tải thông báo..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "Bạn có thể sửa đổi nhiều dự án cùng một lúc bằng cách chọn chúng ở dưới."
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "Dự án máy chủ không hỗ trợ sửa đổi nhiều mục cùng một lúc"
|
||||
},
|
||||
@@ -1556,24 +1553,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "Vui lòng xem lại thông tin dữ liệu"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "Từ dưới lên"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "Từ trên xuống"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "Tên"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "Trạng thái"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "Loại"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "B.tượng"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1631,9 +1631,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "加载通知…"
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "你可以通过下方复选框选择多个项目来一次性编辑。"
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "服务器项目不支持批量编辑"
|
||||
},
|
||||
@@ -1709,24 +1706,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "请复核运行环境元数据"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "正序"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "降序"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "名称"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "状态"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "类型"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "图标"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1634,9 +1634,6 @@
|
||||
"dashboard.overview.notifications.loading": {
|
||||
"message": "正在載入通知..."
|
||||
},
|
||||
"dashboard.projects.bulk-edit-hint": {
|
||||
"message": "你可以透過在下方選擇專案來同時編輯多個專案。"
|
||||
},
|
||||
"dashboard.projects.bulk-edit.server-disabled": {
|
||||
"message": "伺服器專案不支援大量編輯"
|
||||
},
|
||||
@@ -1712,24 +1709,6 @@
|
||||
"dashboard.projects.project.review-environment-metadata": {
|
||||
"message": "請審查環境詮釋資料"
|
||||
},
|
||||
"dashboard.projects.sort.ascending": {
|
||||
"message": "遞增"
|
||||
},
|
||||
"dashboard.projects.sort.descending": {
|
||||
"message": "遞減"
|
||||
},
|
||||
"dashboard.projects.sort.option.name": {
|
||||
"message": "名稱"
|
||||
},
|
||||
"dashboard.projects.sort.option.status": {
|
||||
"message": "狀態"
|
||||
},
|
||||
"dashboard.projects.sort.option.type": {
|
||||
"message": "類型"
|
||||
},
|
||||
"dashboard.projects.table.icon": {
|
||||
"message": "圖示"
|
||||
},
|
||||
"dashboard.projects.table.id": {
|
||||
"message": "ID"
|
||||
},
|
||||
|
||||
@@ -1017,7 +1017,7 @@
|
||||
</div>
|
||||
|
||||
<div class="normal-page__content">
|
||||
<div class="overflow-x-auto"><NavTabs :links="navLinks" replace class="mb-4" /></div>
|
||||
<div class="mb-3 overflow-x-auto"><NavTabs :links="navLinks" replace class="mb-1" /></div>
|
||||
<NuxtPage @on-download="triggerDownloadAnimation" @delete-version="deleteVersion" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
:project="project"
|
||||
:versions="versionsWithDisplayUrl"
|
||||
:show-files="flags.showVersionFilesInTable"
|
||||
:show-environment-column="flags.showVersionEnvironmentColumn"
|
||||
:current-member="!!currentMember"
|
||||
:loaders="tags.loaders"
|
||||
:game-versions="tags.gameVersions"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
:project="project"
|
||||
:versions="versions"
|
||||
:show-files="flags.showVersionFilesInTable"
|
||||
:show-environment-column="flags.showVersionEnvironmentColumn"
|
||||
:current-member="!!currentMember"
|
||||
:loaders="tags.loaders"
|
||||
:game-versions="tags.gameVersions"
|
||||
|
||||
@@ -157,181 +157,215 @@
|
||||
</div>
|
||||
</NewModal>
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<section class="universal-card">
|
||||
<div class="header__row">
|
||||
<h2 class="header__title text-2xl">{{ formatMessage(messages.headTitle) }}</h2>
|
||||
<div class="input-group">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="$refs.modal_creation.show($event)">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(commonMessages.createAProjectButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="projects.length < 1">
|
||||
{{ formatMessage(messages.noProjectsYet) }}
|
||||
</p>
|
||||
<template v-else>
|
||||
<p>{{ formatMessage(messages.bulkEditHint) }}</p>
|
||||
<div class="input-group">
|
||||
<ButtonStyled>
|
||||
<button :disabled="selectedProjects.length === 0" @click="$refs.editLinksModal.show()">
|
||||
<EditIcon />
|
||||
{{ formatMessage(messages.editLinksButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="push-right">
|
||||
<div class="labeled-control-row">
|
||||
{{ formatMessage(commonMessages.sortByLabel) }}
|
||||
<Combobox
|
||||
v-model="sortBy"
|
||||
:searchable="false"
|
||||
class="small-select"
|
||||
:options="sortOptions"
|
||||
@update:model-value="projects = updateSort(projects, sortBy, descending)"
|
||||
/>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="formatMessage(descending ? messages.descending : messages.ascending)"
|
||||
@click="updateDescending()"
|
||||
>
|
||||
<SortDescIcon v-if="descending" />
|
||||
<SortAscIcon v-else />
|
||||
<section class="relative overflow-hidden rounded-2xl">
|
||||
<Table
|
||||
v-model:sort-column="sortColumn"
|
||||
v-model:sort-direction="sortDirection"
|
||||
:columns="projectTableColumns"
|
||||
:data="sortedProjects"
|
||||
row-key="id"
|
||||
table-min-width="50rem"
|
||||
table-layout="auto"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<h2 class="m-0 text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.headTitle) }}
|
||||
</h2>
|
||||
<div class="flex w-full flex-wrap items-center gap-2 md:w-auto">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="$refs.modal_creation.show($event)">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(commonMessages.createAProjectButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-table">
|
||||
<div class="grid-table__row grid-table__header">
|
||||
<div>
|
||||
<Checkbox
|
||||
:model-value="allBulkEditableProjectsSelected"
|
||||
@update:model-value="toggleAllBulkEditableProjects()"
|
||||
/>
|
||||
</div>
|
||||
<div>{{ formatMessage(messages.iconHeader) }}</div>
|
||||
<div>{{ formatMessage(messages.nameHeader) }}</div>
|
||||
<div>{{ formatMessage(messages.idHeader) }}</div>
|
||||
<div>{{ formatMessage(messages.typeHeader) }}</div>
|
||||
<div>{{ formatMessage(messages.statusHeader) }}</div>
|
||||
<div />
|
||||
</template>
|
||||
<template #empty-state>
|
||||
<div class="flex h-64 items-center justify-center text-secondary">
|
||||
{{ formatMessage(messages.noProjectsYet) }}
|
||||
</div>
|
||||
<div v-for="project in projects" :key="`project-${project.id}`" class="grid-table__row">
|
||||
<div>
|
||||
<Checkbox
|
||||
v-tooltip="getBulkEditDisabledTooltip(project)"
|
||||
:disabled="isProjectBulkEditDisabled(project)"
|
||||
:model-value="selectedProjects.includes(project)"
|
||||
@update:model-value="toggleProjectSelection(project)"
|
||||
</template>
|
||||
<template #header-select>
|
||||
<div v-tooltip="formatMessage(messages.selectAllBulkEditableProjects)">
|
||||
<Checkbox
|
||||
:model-value="allBulkEditableProjectsSelected"
|
||||
@update:model-value="toggleAllBulkEditableProjects()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-select="{ row: project }">
|
||||
<div>
|
||||
<Checkbox
|
||||
v-tooltip="getBulkEditDisabledTooltip(project)"
|
||||
:disabled="isProjectBulkEditDisabled(project)"
|
||||
:model-value="isProjectSelected(project)"
|
||||
@update:model-value="toggleProjectSelection(project)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-name="{ row: project }">
|
||||
<nuxt-link class="project-name-cell" :to="getProjectUrl(project)">
|
||||
<span
|
||||
v-tooltip="project.title"
|
||||
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded text-primary"
|
||||
>
|
||||
<img
|
||||
v-if="project.icon_url"
|
||||
:src="project.icon_url"
|
||||
:alt="formatMessage(messages.projectIconAlt, { title: project.title })"
|
||||
class="h-6 w-6 rounded object-cover"
|
||||
/>
|
||||
<BoxIcon v-else class="h-full w-full" />
|
||||
</span>
|
||||
<span class="project-title">
|
||||
<IssuesIcon
|
||||
v-if="project.moderator_message"
|
||||
:aria-label="formatMessage(messages.projectModeratorMessageAriaLabel)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<nuxt-link
|
||||
tabindex="-1"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
>
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
aria-hidden="true"
|
||||
:alt="formatMessage(messages.projectIconAlt, { title: project.title })"
|
||||
no-shadow
|
||||
/>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="project-title">
|
||||
<IssuesIcon
|
||||
v-if="project.moderator_message"
|
||||
:aria-label="formatMessage(messages.projectModeratorMessageAriaLabel)"
|
||||
/>
|
||||
|
||||
<nuxt-link
|
||||
class="hover-link wrap-as-needed"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
>
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
<span class="project-title-link wrap-as-needed">
|
||||
{{ project.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CopyCode :text="project.id" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ formatProjectType(getProjectTypeForUrl(project.project_type, project.loaders)) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ProjectStatusBadge v-if="project.status" :status="project.status" />
|
||||
</div>
|
||||
|
||||
<div class="flex !flex-row items-center !justify-end gap-2">
|
||||
<ButtonStyled
|
||||
v-if="projectsWithMigrationWarning.includes(project.id)"
|
||||
circular
|
||||
color="orange"
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template #cell-id="{ row: project }">
|
||||
<div class="flex items-center">
|
||||
<CopyCode :text="project.id" />
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-type="{ row: project }">
|
||||
<div class="flex items-center">
|
||||
{{ getProjectDisplayType(project) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-status="{ row: project }">
|
||||
<div class="flex items-center">
|
||||
<ProjectStatusBadge v-if="project.status" :status="project.status" />
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-actions="{ row: project }">
|
||||
<div class="flex !flex-row items-center !justify-end gap-2">
|
||||
<ButtonStyled
|
||||
v-if="projectsWithMigrationWarning.includes(project.id)"
|
||||
circular
|
||||
color="orange"
|
||||
>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(messages.reviewEnvironmentMetadata)"
|
||||
:to="`${getProjectUrl(project)}?showEnvironmentMigrationWarning=true`"
|
||||
>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(messages.reviewEnvironmentMetadata)"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}?showEnvironmentMigrationWarning=true`"
|
||||
>
|
||||
<TriangleAlertIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(commonMessages.settingsLabel)"
|
||||
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<TriangleAlertIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(commonMessages.settingsLabel)"
|
||||
:to="`${getProjectUrl(project)}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
</section>
|
||||
<FloatingActionBar
|
||||
:shown="selectedProjects.length > 0"
|
||||
:aria-label="formatMessage(messages.selectionActionBarAriaLabel)"
|
||||
hide-when-modal-open
|
||||
>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div
|
||||
class="relative h-8 shrink-0"
|
||||
:style="{ width: `${selectedProjectIconStackWidth}px` }"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
v-for="(project, index) in visibleSelectedProjects"
|
||||
:key="project.id"
|
||||
v-tooltip="project.title"
|
||||
class="absolute top-0 flex h-8 w-8 items-center justify-center overflow-hidden rounded-lg border-[1.5px] border-solid border-surface-3 bg-surface-4"
|
||||
:style="{
|
||||
left: `${index * SELECTED_PROJECT_ICON_STACK_OFFSET}px`,
|
||||
zIndex: visibleSelectedProjects.length - index,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="project.icon_url"
|
||||
:src="project.icon_url"
|
||||
:alt="formatMessage(messages.projectIconAlt, { title: project.title })"
|
||||
class="h-full w-full rounded-lg object-cover"
|
||||
/>
|
||||
<BoxIcon v-else class="h-full w-full text-primary" />
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedProjectOverflowCount > 0"
|
||||
class="absolute top-0 flex h-8 w-8 items-center justify-center rounded-lg border-[1.5px] border-solid border-surface-3 bg-surface-4 text-xs font-bold text-contrast"
|
||||
:style="{
|
||||
left: `${visibleSelectedProjects.length * SELECTED_PROJECT_ICON_STACK_OFFSET}px`,
|
||||
zIndex: 0,
|
||||
}"
|
||||
>
|
||||
+{{ selectedProjectOverflowCount }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<span class="px-3 py-2 text-base font-semibold tabular-nums text-contrast">
|
||||
{{ formatMessage(messages.selectedProjectsCount, { count: selectedProjects.length }) }}
|
||||
</span>
|
||||
<div class="mx-0.5 h-6 w-px bg-surface-5" />
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.clearButton)"
|
||||
class="!text-primary"
|
||||
@click="clearProjectSelection()"
|
||||
>
|
||||
<XIcon class="cq-show-icon hidden" />
|
||||
<span class="bar-label">{{ formatMessage(commonMessages.clearButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-0.5">
|
||||
<ButtonStyled color="brand">
|
||||
<button v-tooltip="formatMessage(messages.editLinksButton)" @click="showEditLinksModal()">
|
||||
<EditIcon />
|
||||
<span class="bar-label">{{ formatMessage(messages.editLinksButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</FloatingActionBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BoxIcon,
|
||||
EditIcon,
|
||||
IssuesIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
SettingsIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
TrashIcon,
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
commonMessages,
|
||||
CopyCode,
|
||||
defineMessages,
|
||||
FloatingActionBar,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
NewModal,
|
||||
ProjectStatusBadge,
|
||||
StyledInput,
|
||||
Table,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
@@ -360,6 +394,14 @@ const messages = defineMessages({
|
||||
id: 'dashboard.projects.links.button.edit',
|
||||
defaultMessage: 'Edit links',
|
||||
},
|
||||
selectedProjectsCount: {
|
||||
id: 'dashboard.projects.selection-bar.selected-count',
|
||||
defaultMessage: '{count, plural, one {# project selected} other {# projects selected}}',
|
||||
},
|
||||
selectionActionBarAriaLabel: {
|
||||
id: 'dashboard.projects.selection-bar.aria-label',
|
||||
defaultMessage: 'Selected projects actions',
|
||||
},
|
||||
editLinksHeader: {
|
||||
id: 'dashboard.projects.links.header.edit',
|
||||
defaultMessage: 'Edit links',
|
||||
@@ -434,34 +476,6 @@ const messages = defineMessages({
|
||||
id: 'dashboard.projects.empty',
|
||||
defaultMessage: "You don't have any projects yet. Click the green button above to begin.",
|
||||
},
|
||||
bulkEditHint: {
|
||||
id: 'dashboard.projects.bulk-edit-hint',
|
||||
defaultMessage: 'You can edit multiple projects at once by selecting them below.',
|
||||
},
|
||||
ascending: {
|
||||
id: 'dashboard.projects.sort.ascending',
|
||||
defaultMessage: 'Ascending',
|
||||
},
|
||||
descending: {
|
||||
id: 'dashboard.projects.sort.descending',
|
||||
defaultMessage: 'Descending',
|
||||
},
|
||||
sortOptionName: {
|
||||
id: 'dashboard.projects.sort.option.name',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
sortOptionStatus: {
|
||||
id: 'dashboard.projects.sort.option.status',
|
||||
defaultMessage: 'Status',
|
||||
},
|
||||
sortOptionType: {
|
||||
id: 'dashboard.projects.sort.option.type',
|
||||
defaultMessage: 'Type',
|
||||
},
|
||||
iconHeader: {
|
||||
id: 'dashboard.projects.table.icon',
|
||||
defaultMessage: 'Icon',
|
||||
},
|
||||
nameHeader: {
|
||||
id: 'dashboard.projects.table.name',
|
||||
defaultMessage: 'Name',
|
||||
@@ -478,6 +492,10 @@ const messages = defineMessages({
|
||||
id: 'dashboard.projects.table.status',
|
||||
defaultMessage: 'Status',
|
||||
},
|
||||
selectAllBulkEditableProjects: {
|
||||
id: 'dashboard.projects.table.select-all-bulk-editable',
|
||||
defaultMessage: 'Select all projects that support bulk editing',
|
||||
},
|
||||
projectIconAlt: {
|
||||
id: 'dashboard.projects.project.icon-alt',
|
||||
defaultMessage: 'Icon for {title}',
|
||||
@@ -505,14 +523,9 @@ useHead({ title: () => `${formatMessage(messages.headTitle)} - Modrinth` })
|
||||
const user = await useUser()
|
||||
const projects = ref([])
|
||||
const projectsWithMigrationWarning = ref([])
|
||||
const selectedProjects = ref([])
|
||||
const sortBy = ref('Name')
|
||||
const sortOptions = computed(() => [
|
||||
{ value: 'Name', label: formatMessage(messages.sortOptionName) },
|
||||
{ value: 'Status', label: formatMessage(messages.sortOptionStatus) },
|
||||
{ value: 'Type', label: formatMessage(messages.sortOptionType) },
|
||||
])
|
||||
const descending = ref(false)
|
||||
const selectedProjectIds = ref([])
|
||||
const sortColumn = ref('name')
|
||||
const sortDirection = ref('asc')
|
||||
const editLinks = reactive({
|
||||
showAffected: false,
|
||||
source: { val: '', clear: false },
|
||||
@@ -522,7 +535,62 @@ const editLinks = reactive({
|
||||
})
|
||||
|
||||
const editLinksModal = ref(null)
|
||||
const modal_creation = ref(null)
|
||||
const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
|
||||
const SELECTED_PROJECT_ICON_STACK_OFFSET = 24
|
||||
|
||||
const projectTableColumns = computed(() => [
|
||||
{
|
||||
key: 'select',
|
||||
width: '3rem',
|
||||
headerClass: '!text-center',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: formatMessage(messages.nameHeader),
|
||||
enableSorting: true,
|
||||
defaultSortDirection: 'asc',
|
||||
width: '22rem',
|
||||
},
|
||||
{
|
||||
key: 'id',
|
||||
label: formatMessage(messages.idHeader),
|
||||
width: '13rem',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: formatMessage(messages.typeHeader),
|
||||
enableSorting: true,
|
||||
width: '11rem',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: formatMessage(messages.statusHeader),
|
||||
enableSorting: true,
|
||||
width: '10rem',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
width: '6rem',
|
||||
align: 'right',
|
||||
cellClass: '!overflow-visible',
|
||||
},
|
||||
])
|
||||
|
||||
const sortedProjects = computed(() => {
|
||||
const direction = sortDirection.value === 'desc' ? -1 : 1
|
||||
|
||||
return projects.value.slice().sort((left, right) => {
|
||||
const result = sortCollator.compare(
|
||||
getProjectSortValue(left, sortColumn.value),
|
||||
getProjectSortValue(right, sortColumn.value),
|
||||
)
|
||||
|
||||
return result * direction
|
||||
})
|
||||
})
|
||||
|
||||
function getLinkInputPlaceholder(clearLink, isDiscord = false) {
|
||||
if (clearLink) {
|
||||
@@ -545,16 +613,35 @@ const bulkEditableProjects = computed(() =>
|
||||
projects.value.filter((project) => !isProjectBulkEditDisabled(project)),
|
||||
)
|
||||
|
||||
const selectedProjects = computed(() =>
|
||||
projects.value.filter((project) => selectedProjectIds.value.includes(project.id)),
|
||||
)
|
||||
const visibleSelectedProjects = computed(() => selectedProjects.value.slice(0, 3))
|
||||
const selectedProjectOverflowCount = computed(() => Math.max(0, selectedProjects.value.length - 3))
|
||||
const selectedProjectIconStackWidth = computed(() => {
|
||||
if (selectedProjects.value.length === 0) return 0
|
||||
|
||||
return (
|
||||
32 +
|
||||
(visibleSelectedProjects.value.length - 1 + (selectedProjectOverflowCount.value > 0 ? 1 : 0)) *
|
||||
SELECTED_PROJECT_ICON_STACK_OFFSET
|
||||
)
|
||||
})
|
||||
|
||||
const allBulkEditableProjectsSelected = computed(
|
||||
() =>
|
||||
bulkEditableProjects.value.length > 0 &&
|
||||
bulkEditableProjects.value.every((project) => selectedProjects.value.includes(project)),
|
||||
bulkEditableProjects.value.every((project) => selectedProjectIds.value.includes(project.id)),
|
||||
)
|
||||
|
||||
function toggleAllBulkEditableProjects() {
|
||||
selectedProjects.value = allBulkEditableProjectsSelected.value
|
||||
selectedProjectIds.value = allBulkEditableProjectsSelected.value
|
||||
? []
|
||||
: bulkEditableProjects.value.slice()
|
||||
: bulkEditableProjects.value.map((project) => project.id)
|
||||
}
|
||||
|
||||
function isProjectSelected(project) {
|
||||
return selectedProjectIds.value.includes(project.id)
|
||||
}
|
||||
|
||||
function toggleProjectSelection(project) {
|
||||
@@ -562,12 +649,24 @@ function toggleProjectSelection(project) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedProjects.value.includes(project)) {
|
||||
selectedProjects.value = selectedProjects.value.filter((it) => it !== project)
|
||||
if (isProjectSelected(project)) {
|
||||
selectedProjectIds.value = selectedProjectIds.value.filter((id) => id !== project.id)
|
||||
return
|
||||
}
|
||||
|
||||
selectedProjects.value = [...selectedProjects.value, project]
|
||||
selectedProjectIds.value = [...selectedProjectIds.value, project.id]
|
||||
}
|
||||
|
||||
function clearProjectSelection() {
|
||||
selectedProjectIds.value = []
|
||||
}
|
||||
|
||||
function showEditLinksModal() {
|
||||
if (selectedProjects.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
editLinksModal.value?.show()
|
||||
}
|
||||
|
||||
function getBulkEditDisabledTooltip(project) {
|
||||
@@ -578,40 +677,28 @@ function getBulkEditDisabledTooltip(project) {
|
||||
return ''
|
||||
}
|
||||
|
||||
function updateSort(list, sort, desc) {
|
||||
let sortedArray = list
|
||||
switch (sort) {
|
||||
case 'Name':
|
||||
sortedArray = list.slice().sort((a, b) => a.title.localeCompare(b.title))
|
||||
break
|
||||
case 'Status':
|
||||
sortedArray = list.slice().sort((a, b) => {
|
||||
if (a.status < b.status) return -1
|
||||
if (a.status > b.status) return 1
|
||||
return 0
|
||||
})
|
||||
break
|
||||
case 'Type':
|
||||
sortedArray = list.slice().sort((a, b) => {
|
||||
if (a.project_type < b.project_type) return -1
|
||||
if (a.project_type > b.project_type) return 1
|
||||
return 0
|
||||
})
|
||||
break
|
||||
function getProjectUrlType(project) {
|
||||
return getProjectTypeForUrl(project.project_type, project.loaders)
|
||||
}
|
||||
|
||||
function getProjectDisplayType(project) {
|
||||
return formatProjectType(getProjectUrlType(project))
|
||||
}
|
||||
|
||||
function getProjectUrl(project) {
|
||||
return `/${getProjectUrlType(project)}/${project.slug ? project.slug : project.id}`
|
||||
}
|
||||
|
||||
function getProjectSortValue(project, column) {
|
||||
switch (column) {
|
||||
case 'type':
|
||||
return getProjectDisplayType(project)
|
||||
case 'status':
|
||||
return project.status ?? ''
|
||||
case 'name':
|
||||
default:
|
||||
break
|
||||
return project.title ?? ''
|
||||
}
|
||||
if (desc) sortedArray = sortedArray.reverse()
|
||||
return sortedArray
|
||||
}
|
||||
|
||||
function resort() {
|
||||
projects.value = updateSort(projects.value, sortBy.value, descending.value)
|
||||
}
|
||||
|
||||
function updateDescending() {
|
||||
descending.value = !descending.value
|
||||
resort()
|
||||
}
|
||||
|
||||
async function bulkEditLinks() {
|
||||
@@ -635,7 +722,7 @@ async function bulkEditLinks() {
|
||||
text: formatMessage(messages.bulkEditSuccessText),
|
||||
type: 'success',
|
||||
})
|
||||
selectedProjects.value = []
|
||||
selectedProjectIds.value = []
|
||||
|
||||
editLinks.issues.val = ''
|
||||
editLinks.source.val = ''
|
||||
@@ -656,7 +743,7 @@ async function bulkEditLinks() {
|
||||
|
||||
await initUserProjects()
|
||||
if (user.value?.projects) {
|
||||
projects.value = updateSort(user.value.projects, 'Name', false)
|
||||
projects.value = user.value.projects.slice()
|
||||
|
||||
// minecraft_java_server type determined from component on projectV3
|
||||
projects.value = projects.value.map((project) => {
|
||||
@@ -677,126 +764,20 @@ if (user.value?.projects) {
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.grid-table {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) minmax(min-content, 1fr) minmax(min-content, 1fr) min-content;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
overflow: hidden;
|
||||
margin-top: var(--spacing-card-md);
|
||||
outline: 1px solid transparent;
|
||||
|
||||
.grid-table__row {
|
||||
display: contents;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-card-sm);
|
||||
|
||||
// Left edge of table
|
||||
&:first-child {
|
||||
padding-left: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
// Right edge of table
|
||||
&:last-child {
|
||||
padding-right: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) > div {
|
||||
background-color: var(--color-table-alternate-row);
|
||||
}
|
||||
|
||||
&.grid-table__header > div {
|
||||
background-color: var(--color-bg);
|
||||
font-weight: bold;
|
||||
color: var(--color-text-dark);
|
||||
padding-top: var(--spacing-card-bg);
|
||||
padding-bottom: var(--spacing-card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.grid-table__row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name type settings' 'checkbox icon id status settings';
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(1) {
|
||||
grid-area: checkbox;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
:nth-child(3) {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
:nth-child(4) {
|
||||
grid-area: id;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:nth-child(5) {
|
||||
grid-area: type;
|
||||
}
|
||||
|
||||
:nth-child(6) {
|
||||
grid-area: status;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:nth-child(7) {
|
||||
grid-area: settings;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__header {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
|
||||
:nth-child(2),
|
||||
:nth-child(3),
|
||||
:nth-child(4),
|
||||
:nth-child(5),
|
||||
:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.grid-table__row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
|
||||
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(5) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-table__header {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
}
|
||||
}
|
||||
.project-name-cell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 3.5rem;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-sm);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 0;
|
||||
gap: var(--spacing-card-xs);
|
||||
|
||||
svg {
|
||||
@@ -804,29 +785,11 @@ if (user.value?.projects) {
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: var(--spacing-card-xs);
|
||||
}
|
||||
|
||||
.hover-link:hover {
|
||||
.project-name-cell:hover .project-title-link,
|
||||
.project-name-cell:focus-visible .project-title-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.labeled-control-row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-md);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.small-select {
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.label-button[data-active='true'] {
|
||||
--background-color: var(--color-red);
|
||||
--text-color: var(--color-brand-inverted);
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
</div>
|
||||
<div class="overflow-x-auto overflow-y-hidden">
|
||||
<table
|
||||
class="w-full table-fixed border-separate border-spacing-0 border-surface-4"
|
||||
class="w-full border-separate border-spacing-0 border-surface-4"
|
||||
:class="tableLayout === 'auto' ? 'table-auto' : 'table-fixed'"
|
||||
:style="tableMinWidth ? { minWidth: tableMinWidth } : undefined"
|
||||
>
|
||||
<colgroup>
|
||||
@@ -36,6 +37,7 @@
|
||||
:class="[
|
||||
`text-${column.align ?? 'left'}`,
|
||||
column.enableSorting ? 'cursor-pointer select-none' : '',
|
||||
column.headerClass,
|
||||
]"
|
||||
:style="column.width ? { width: column.width } : undefined"
|
||||
@click="column.enableSorting ? handleSort(column.key) : undefined"
|
||||
@@ -80,7 +82,8 @@
|
||||
<tr
|
||||
v-for="(row, rowIndex) in renderedRows"
|
||||
:key="getRowRenderKey(row, getAbsoluteRowIndex(rowIndex))"
|
||||
:class="getRowClass(getAbsoluteRowIndex(rowIndex))"
|
||||
:class="getRowClass(row, getAbsoluteRowIndex(rowIndex))"
|
||||
@click="handleRowClick(row, getAbsoluteRowIndex(rowIndex), $event)"
|
||||
>
|
||||
<td
|
||||
v-if="showSelection"
|
||||
@@ -96,7 +99,7 @@
|
||||
v-for="column in columns"
|
||||
:key="column.key"
|
||||
class="text-secondary h-14 overflow-hidden first:pl-4 last:pr-4 border-solid border-0 border-t border-surface-4"
|
||||
:class="`text-${column.align ?? 'left'}`"
|
||||
:class="[`text-${column.align ?? 'left'}`, column.cellClass]"
|
||||
>
|
||||
<slot
|
||||
:name="`cell-${column.key}`"
|
||||
@@ -132,7 +135,8 @@
|
||||
<tr
|
||||
v-for="(row, rowIndex) in renderedRows"
|
||||
:key="getRowRenderKey(row, getAbsoluteRowIndex(rowIndex))"
|
||||
:class="getRowClass(getAbsoluteRowIndex(rowIndex))"
|
||||
:class="getRowClass(row, getAbsoluteRowIndex(rowIndex))"
|
||||
@click="handleRowClick(row, getAbsoluteRowIndex(rowIndex), $event)"
|
||||
>
|
||||
<td
|
||||
v-if="showSelection"
|
||||
@@ -148,7 +152,7 @@
|
||||
v-for="column in columns"
|
||||
:key="column.key"
|
||||
class="text-secondary h-14 overflow-hidden first:pl-4 last:pr-4 border-solid border-0 border-t border-surface-4"
|
||||
:class="`text-${column.align ?? 'left'}`"
|
||||
:class="[`text-${column.align ?? 'left'}`, column.cellClass]"
|
||||
>
|
||||
<slot
|
||||
:name="`cell-${column.key}`"
|
||||
@@ -188,6 +192,7 @@ import Checkbox from './Checkbox.vue'
|
||||
|
||||
export type TableColumnAlign = 'left' | 'center' | 'right'
|
||||
export type SortDirection = 'asc' | 'desc'
|
||||
export type TableLayout = 'fixed' | 'auto'
|
||||
|
||||
/**
|
||||
* Defines a table column configuration.
|
||||
@@ -204,6 +209,8 @@ export interface TableColumn<K extends string = string> {
|
||||
* Accepts any valid CSS width (e.g., '200px', '20%', '10rem', 'auto', 'fit-content').
|
||||
*/
|
||||
width?: string
|
||||
headerClass?: string
|
||||
cellClass?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -223,10 +230,14 @@ const props = withDefaults(
|
||||
* Sets a minimum width for the table content, allowing horizontal overflow below that width.
|
||||
*/
|
||||
tableMinWidth?: string
|
||||
tableLayout?: TableLayout
|
||||
rowClass?: string | ((row: T, index: number) => string)
|
||||
rowClickable?: boolean | ((row: T, index: number) => boolean)
|
||||
}>(),
|
||||
{
|
||||
showSelection: false,
|
||||
rowKey: 'id' as keyof T,
|
||||
tableLayout: 'fixed',
|
||||
virtualized: false,
|
||||
virtualRowHeight: 56,
|
||||
virtualBufferSize: 5,
|
||||
@@ -267,6 +278,7 @@ const bottomSpacerHeight = computed(() => {
|
||||
|
||||
const emit = defineEmits<{
|
||||
sort: [column: string, direction: SortDirection]
|
||||
rowClick: [row: T, index: number, event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const selectableRows = computed(() => props.selectionData ?? props.data)
|
||||
@@ -319,8 +331,37 @@ function getRowRenderKey(row: T, rowIndex: number): PropertyKey {
|
||||
return rowIndex
|
||||
}
|
||||
|
||||
function getRowClass(rowIndex: number): string {
|
||||
return rowIndex % 2 === 0 ? 'bg-surface-2' : 'bg-surface-1.5'
|
||||
function getRowClass(row: T, rowIndex: number): string[] {
|
||||
const baseClass = rowIndex % 2 === 0 ? 'bg-surface-2' : 'bg-surface-1.5'
|
||||
const customClass =
|
||||
typeof props.rowClass === 'function' ? props.rowClass(row, rowIndex) : props.rowClass
|
||||
|
||||
return customClass ? [baseClass, customClass] : [baseClass]
|
||||
}
|
||||
|
||||
function isRowClickable(row: T, rowIndex: number): boolean {
|
||||
return typeof props.rowClickable === 'function'
|
||||
? props.rowClickable(row, rowIndex)
|
||||
: props.rowClickable === true
|
||||
}
|
||||
|
||||
function isNoRowClickTarget(event: MouseEvent): boolean {
|
||||
const target = event.target
|
||||
const currentTarget = event.currentTarget
|
||||
if (!(target instanceof Element) || !(currentTarget instanceof Element)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const noRowClickTarget = target.closest('[data-no-row-click]')
|
||||
return noRowClickTarget !== null && noRowClickTarget !== currentTarget
|
||||
}
|
||||
|
||||
function handleRowClick(row: T, rowIndex: number, event: MouseEvent) {
|
||||
if (!isRowClickable(row, rowIndex) || isNoRowClickTarget(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('rowClick', row, rowIndex, event)
|
||||
}
|
||||
|
||||
function isSelected(row: T): boolean {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 mb-3">
|
||||
<div class="mb-3 flex flex-col gap-3">
|
||||
<div class="flex flex-wrap justify-between gap-2">
|
||||
<VersionFilterControl
|
||||
ref="versionFilters"
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<div
|
||||
v-if="openModal && filteredVersions.length > pageSize"
|
||||
class="flex flex-wrap justify-between items-center gap-2"
|
||||
class="flex flex-wrap items-center justify-between gap-2"
|
||||
>
|
||||
<span>
|
||||
Showing {{ (currentPage - 1) * pageSize + 1 }} to
|
||||
@@ -40,219 +40,62 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
<Table
|
||||
v-if="versions.length > 0"
|
||||
class="flex flex-col gap-4 rounded-2xl bg-bg-raised px-6 pb-8 pt-4 supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[1fr_min-content] sm:px-8 supports-[grid-template-columns:subgrid]:sm:grid-cols-[min-content_auto_auto_auto_min-content]"
|
||||
:class="[
|
||||
hasMultipleEnvironments
|
||||
? 'supports-[grid-template-columns:subgrid]:xl:grid-cols-[min-content_auto_auto_auto_auto_auto_auto_min-content] has-environment'
|
||||
: 'supports-[grid-template-columns:subgrid]:xl:grid-cols-[min-content_auto_auto_auto_auto_auto_min-content] no-environment',
|
||||
]"
|
||||
class="hidden sm:block"
|
||||
:columns="versionColumns"
|
||||
:data="currentVersionRows"
|
||||
row-key="id"
|
||||
:row-class="getVersionRowClass"
|
||||
:row-clickable="!!versionLink"
|
||||
table-layout="auto"
|
||||
@row-click="openVersionRow"
|
||||
>
|
||||
<div class="versions-grid-row">
|
||||
<div class="w-9 max-sm:hidden"></div>
|
||||
<div class="text-sm font-bold text-contrast max-sm:hidden">Name</div>
|
||||
<div
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Game version
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Platforms
|
||||
</div>
|
||||
<div
|
||||
v-if="hasMultipleEnvironments"
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Environment
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Published
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Downloads
|
||||
</div>
|
||||
<div class="text-sm font-bold text-contrast max-sm:hidden xl:collapse xl:hidden">
|
||||
Compatibility
|
||||
</div>
|
||||
<div class="text-sm font-bold text-contrast max-sm:hidden xl:collapse xl:hidden">Stats</div>
|
||||
<div class="w-9 max-sm:hidden"></div>
|
||||
</div>
|
||||
<template v-for="(version, index) in currentVersions" :key="index">
|
||||
<!-- Row divider -->
|
||||
<div
|
||||
class="versions-grid-row h-px w-full bg-surface-5"
|
||||
:class="{
|
||||
'max-sm:!hidden': index === 0,
|
||||
}"
|
||||
></div>
|
||||
<div class="versions-grid-row group relative">
|
||||
<AutoLink
|
||||
v-if="!!versionLink"
|
||||
class="absolute inset-[calc(-1rem-2px)_-2rem] before:absolute before:inset-0 before:transition-all before:content-[''] hover:before:backdrop-brightness-110"
|
||||
:to="versionLink?.(version)"
|
||||
<template #cell-channel="{ row: version }">
|
||||
<div class="flex items-center justify-center">
|
||||
<VersionChannelIndicator
|
||||
v-tooltip="`Toggle filter for ${version.version_type}`"
|
||||
:channel="version.version_type"
|
||||
class="cursor-pointer"
|
||||
data-no-row-click
|
||||
@click.stop="versionFilters?.toggleFilter('channel', version.version_type)"
|
||||
/>
|
||||
<div class="flex flex-col justify-center gap-2 sm:contents">
|
||||
<div class="flex flex-row items-center gap-2 sm:contents">
|
||||
<div class="self-center">
|
||||
<div class="relative z-[1] cursor-pointer">
|
||||
<VersionChannelIndicator
|
||||
v-tooltip="`Toggle filter for ${version.version_type}`"
|
||||
:channel="version.version_type"
|
||||
@click="versionFilters?.toggleFilter('channel', version.version_type)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none relative z-[1] flex flex-col gap-1 justify-center overflow-hidden min-w-32"
|
||||
:class="{
|
||||
'group-hover:underline': !!versionLink,
|
||||
}"
|
||||
title="`${version.version_number} - ${version.name}`"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="font-bold text-contrast text-ellipsis overflow-hidden">
|
||||
{{ version.version_number }}
|
||||
</div>
|
||||
<div
|
||||
v-if="version.files_missing_attribution"
|
||||
v-tooltip="formatMessage(messages.withheldTooltip)"
|
||||
class="z-[1]"
|
||||
:style="{
|
||||
'--_bg-color': 'var(--color-orange-bg)',
|
||||
'--_color': 'var(--color-orange)',
|
||||
}"
|
||||
>
|
||||
<TagItem> <CircleAlertIcon /> {{ formatMessage(messages.withheld) }}</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs font-medium text-ellipsis overflow-hidden">
|
||||
{{ version.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center gap-2 sm:contents">
|
||||
<div class="flex flex-row flex-wrap items-center gap-1 xl:contents">
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<TagItem
|
||||
v-for="gameVersion in formatVersionsForDisplay(
|
||||
version.game_versions,
|
||||
gameVersions,
|
||||
).slice(0, maxGameVersionTags)"
|
||||
:key="`version-tag-${gameVersion}`"
|
||||
v-tooltip="`Toggle filter for ${gameVersion}`"
|
||||
class="z-[1]"
|
||||
:action="
|
||||
() => versionFilters?.toggleFilters('gameVersion', version.game_versions)
|
||||
"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
</TagItem>
|
||||
<Menu
|
||||
v-if="
|
||||
formatVersionsForDisplay(version.game_versions, gameVersions).length >
|
||||
maxGameVersionTags
|
||||
"
|
||||
:delay="{ hide: 50, show: 0 }"
|
||||
no-auto-focus
|
||||
class="z-[1] cursor-default"
|
||||
>
|
||||
<TagItem tabindex="0">
|
||||
+{{
|
||||
formatVersionsForDisplay(version.game_versions, gameVersions).length -
|
||||
maxGameVersionTags
|
||||
}}
|
||||
</TagItem>
|
||||
<template #popper>
|
||||
<div class="flex gap-1 flex-wrap max-w-[20rem]">
|
||||
<TagItem
|
||||
v-for="gameVersion in formatVersionsForDisplay(
|
||||
version.game_versions,
|
||||
gameVersions,
|
||||
).slice(maxGameVersionTags)"
|
||||
:key="`overflow-version-tag-${gameVersion}`"
|
||||
:action="
|
||||
() =>
|
||||
versionFilters?.toggleFilters('gameVersion', version.game_versions)
|
||||
"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template v-if="version.noModLoader">
|
||||
<TagItem class="z-[1] border !border-solid border-surface-5">
|
||||
No mod loader
|
||||
</TagItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<TagItem
|
||||
v-for="platform in version.loaders"
|
||||
:key="`platform-tag-${platform}`"
|
||||
v-tooltip="`Toggle filter for ${platform}`"
|
||||
class="z-[1]"
|
||||
:style="`--_color: var(--color-platform-${platform})`"
|
||||
:action="() => versionFilters?.toggleFilter('platform', platform)"
|
||||
>
|
||||
<component :is="getLoaderIcon(platform)" v-if="getLoaderIcon(platform)" />
|
||||
<FormattedTag :tag="platform" enforce-type="loader" />
|
||||
</TagItem>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasMultipleEnvironments" class="flex items-center">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<TagItem
|
||||
v-for="(tag, tagIdx) in getEnvironmentTags(version.environment)"
|
||||
:key="`env-tag-${tagIdx}`"
|
||||
class="z-[1] text-center"
|
||||
>
|
||||
<component :is="tag.icon" />
|
||||
{{ formatMessage(tag.label).replace('and', '&') }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col justify-center gap-1 max-sm:flex-row max-sm:justify-start max-sm:gap-3 xl:contents"
|
||||
>
|
||||
<div
|
||||
v-tooltip="formatDateTime(version.date_published)"
|
||||
class="z-[1] flex cursor-help items-center gap-1 text-nowrap font-medium xl:self-center"
|
||||
>
|
||||
<CalendarIcon class="xl:hidden" />
|
||||
{{ formatRelativeTime(new Date(version.date_published)) }}
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none z-[1] flex items-center gap-1 font-medium xl:self-center"
|
||||
>
|
||||
<DownloadIcon class="xl:hidden" />
|
||||
{{ formatCompactNumber(version.downloads) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-start justify-end gap-1 sm:items-center z-[1] max-[400px]:flex-col max-[400px]:justify-start"
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-name="{ row: version }">
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
<AutoLink
|
||||
v-tooltip="`${version.version_number} - ${version.name}`"
|
||||
:to="versionLink?.(version)"
|
||||
class="flex min-w-0 flex-col gap-1 w-fit"
|
||||
:link-class="versionLink ? 'focus-visible:underline' : ''"
|
||||
:title="`${version.version_number} - ${version.name}`"
|
||||
>
|
||||
<slot name="actions" :version="version"></slot>
|
||||
</div>
|
||||
<div v-if="showFiles" class="tag-list pointer-events-none relative z-[1] col-span-full">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div
|
||||
class="overflow-hidden text-ellipsis font-medium text-contrast"
|
||||
:class="versionLink ? 'version-row-name' : ''"
|
||||
>
|
||||
{{ version.version_number }}
|
||||
</div>
|
||||
<div
|
||||
v-if="version.files_missing_attribution"
|
||||
v-tooltip="formatMessage(messages.withheldTooltip)"
|
||||
:style="{
|
||||
'--_bg-color': 'var(--color-orange-bg)',
|
||||
'--_color': 'var(--color-orange)',
|
||||
}"
|
||||
>
|
||||
<TagItem> <CircleAlertIcon /> {{ formatMessage(messages.withheld) }}</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
</AutoLink>
|
||||
<div v-if="showFiles" class="tag-list">
|
||||
<div
|
||||
v-for="(file, fileIdx) in version.files"
|
||||
:key="`platform-tag-${fileIdx}`"
|
||||
:key="`file-tag-${fileIdx}`"
|
||||
:class="`flex items-center gap-1 text-wrap rounded-full bg-button-bg px-2 py-0.5 text-xs font-medium ${file.primary || fileIdx === 0 ? 'bg-brand-highlight text-contrast' : 'text-primary'}`"
|
||||
>
|
||||
<StarIcon v-if="file.primary || fileIdx === 0" class="shrink-0" />
|
||||
@@ -261,8 +104,306 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-gameVersions="{ row: version }">
|
||||
<div class="flex flex-wrap gap-1 w-fit">
|
||||
<TagItem
|
||||
v-for="gameVersion in getDisplayGameVersions(version).slice(0, MAX_GAME_VERSION_TAGS)"
|
||||
:key="`version-tag-${gameVersion}`"
|
||||
v-tooltip="`Toggle filter for ${gameVersion}`"
|
||||
data-no-row-click
|
||||
:action="() => versionFilters?.toggleFilters('gameVersion', version.game_versions)"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
</TagItem>
|
||||
<Menu
|
||||
v-if="getDisplayGameVersions(version).length > MAX_GAME_VERSION_TAGS"
|
||||
data-no-row-click
|
||||
:delay="{ hide: 50, show: 0 }"
|
||||
no-auto-focus
|
||||
class="cursor-default"
|
||||
>
|
||||
<TagItem tabindex="0">
|
||||
+{{ getDisplayGameVersions(version).length - MAX_GAME_VERSION_TAGS }}
|
||||
</TagItem>
|
||||
<template #popper>
|
||||
<div class="flex max-w-[20rem] flex-wrap gap-1">
|
||||
<TagItem
|
||||
v-for="gameVersion in getDisplayGameVersions(version).slice(MAX_GAME_VERSION_TAGS)"
|
||||
:key="`overflow-version-tag-${gameVersion}`"
|
||||
:action="() => versionFilters?.toggleFilters('gameVersion', version.game_versions)"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-platforms="{ row: version }">
|
||||
<div class="flex flex-wrap gap-1 w-fit">
|
||||
<template v-if="version.noModLoader">
|
||||
<TagItem class="border !border-solid border-surface-5"> No mod loader </TagItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<TagItem
|
||||
v-for="platform in version.loaders.slice(0, MAX_PLATFORM_TAGS)"
|
||||
:key="`platform-tag-${platform}`"
|
||||
v-tooltip="`Toggle filter for ${platform}`"
|
||||
data-no-row-click
|
||||
:style="`--_color: var(--color-platform-${platform})`"
|
||||
:action="() => versionFilters?.toggleFilter('platform', platform)"
|
||||
>
|
||||
<component :is="getLoaderIcon(platform)" v-if="getLoaderIcon(platform)" />
|
||||
<FormattedTag :tag="platform" enforce-type="loader" />
|
||||
</TagItem>
|
||||
<Menu
|
||||
v-if="version.loaders.length > MAX_PLATFORM_TAGS"
|
||||
data-no-row-click
|
||||
:delay="{ hide: 50, show: 0 }"
|
||||
no-auto-focus
|
||||
class="cursor-default"
|
||||
>
|
||||
<TagItem tabindex="0"> +{{ version.loaders.length - MAX_PLATFORM_TAGS }} </TagItem>
|
||||
<template #popper>
|
||||
<div class="flex max-w-[20rem] flex-wrap gap-1">
|
||||
<TagItem
|
||||
v-for="platform in version.loaders.slice(MAX_PLATFORM_TAGS)"
|
||||
:key="`overflow-platform-tag-${platform}`"
|
||||
:style="`--_color: var(--color-platform-${platform})`"
|
||||
:action="() => versionFilters?.toggleFilter('platform', platform)"
|
||||
>
|
||||
<component :is="getLoaderIcon(platform)" v-if="getLoaderIcon(platform)" />
|
||||
<FormattedTag :tag="platform" enforce-type="loader" />
|
||||
</TagItem>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="showEnvironmentColumn" #cell-environment="{ row: version }">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<TagItem
|
||||
v-for="(tag, tagIdx) in getEnvironmentTags(version.environment)"
|
||||
:key="`env-tag-${tagIdx}`"
|
||||
data-no-row-click
|
||||
class="text-center"
|
||||
>
|
||||
<component :is="tag.icon" />
|
||||
{{ formatMessage(tag.label).replace('and', '&') }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-published="{ row: version }">
|
||||
<div
|
||||
v-tooltip="formatDateTime(version.date_published)"
|
||||
class="flex items-center gap-1 text-nowrap font-medium w-max cursor-default"
|
||||
data-no-row-click
|
||||
>
|
||||
{{ formatRelativeTime(new Date(version.date_published)) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-downloads="{ row: version }">
|
||||
<div
|
||||
v-tooltip="`${version.downloads} downloads`"
|
||||
class="flex items-center gap-1 font-medium w-max text-nowrap cursor-default"
|
||||
data-no-row-click
|
||||
>
|
||||
{{ formatCompactNumber(version.downloads) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row: version }">
|
||||
<div
|
||||
class="flex h-full w-max items-center justify-end gap-0.5 whitespace-nowrap cursor-default"
|
||||
data-no-row-click
|
||||
>
|
||||
<slot name="actions" :version="version"></slot>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<!-- MOBILE VERSIONS TABLE/LIST -->
|
||||
<div
|
||||
v-if="versions.length > 0"
|
||||
class="flex flex-col gap-4 rounded-2xl bg-bg-raised p-5 sm:hidden"
|
||||
>
|
||||
<template v-for="(version, index) in currentVersions" :key="version.id ?? index">
|
||||
<div
|
||||
class="h-px w-[calc(100%+2.5rem)] bg-surface-5 -ml-5"
|
||||
:class="{
|
||||
hidden: index === 0,
|
||||
}"
|
||||
></div>
|
||||
<SmartClickable class="group">
|
||||
<template v-if="versionLink" #clickable>
|
||||
<AutoLink
|
||||
:to="versionLink(version)"
|
||||
class="rounded-xl outline-none no-click-animation custom-focus-indicator"
|
||||
:title="`${version.version_number} - ${version.name}`"
|
||||
></AutoLink>
|
||||
</template>
|
||||
<div
|
||||
class="flex flex-col justify-center gap-1.5 rounded-xl transition-colors smart-clickable:outline-on-focus"
|
||||
:class="{
|
||||
'cursor-pointer': !!versionLink,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="self-center">
|
||||
<VersionChannelIndicator
|
||||
v-tooltip="`Toggle filter for ${version.version_type}`"
|
||||
:channel="version.version_type"
|
||||
class="cursor-pointer smart-clickable:allow-pointer-events"
|
||||
size="sm"
|
||||
@click="versionFilters?.toggleFilter('channel', version.version_type)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden text-ellipsis font-medium text-base text-contrast">
|
||||
{{ version.version_number }}
|
||||
</div>
|
||||
<div
|
||||
v-if="version.files_missing_attribution"
|
||||
v-tooltip="formatMessage(messages.withheldTooltip)"
|
||||
:style="{
|
||||
'--_bg-color': 'var(--color-orange-bg)',
|
||||
'--_color': 'var(--color-orange)',
|
||||
}"
|
||||
>
|
||||
<TagItem> <CircleAlertIcon /> {{ formatMessage(messages.withheld) }}</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-start justify-end gap-1 max-[400px]:flex-col max-[400px]:justify-start smart-clickable:allow-pointer-events"
|
||||
>
|
||||
<slot name="actions" :version="version"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center gap-3">
|
||||
<div class="flex flex-row flex-wrap items-center gap-1.5">
|
||||
<TagItem
|
||||
v-for="gameVersion in getDisplayGameVersions(version).slice(
|
||||
0,
|
||||
MAX_GAME_VERSION_TAGS,
|
||||
)"
|
||||
:key="`version-tag-${gameVersion}`"
|
||||
v-tooltip="`Toggle filter for ${gameVersion}`"
|
||||
class="smart-clickable:allow-pointer-events"
|
||||
:action="() => versionFilters?.toggleFilters('gameVersion', version.game_versions)"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
</TagItem>
|
||||
<Menu
|
||||
v-if="getDisplayGameVersions(version).length > MAX_GAME_VERSION_TAGS"
|
||||
:delay="{ hide: 50, show: 0 }"
|
||||
no-auto-focus
|
||||
class="cursor-default smart-clickable:allow-pointer-events"
|
||||
>
|
||||
<TagItem tabindex="0">
|
||||
+{{ getDisplayGameVersions(version).length - MAX_GAME_VERSION_TAGS }}
|
||||
</TagItem>
|
||||
<template #popper>
|
||||
<div class="flex max-w-[20rem] flex-wrap gap-1">
|
||||
<TagItem
|
||||
v-for="gameVersion in getDisplayGameVersions(version).slice(
|
||||
MAX_GAME_VERSION_TAGS,
|
||||
)"
|
||||
:key="`overflow-version-tag-${gameVersion}`"
|
||||
:action="
|
||||
() => versionFilters?.toggleFilters('gameVersion', version.game_versions)
|
||||
"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
<template v-if="version.noModLoader">
|
||||
<TagItem class="border !border-solid border-surface-5"> No mod loader </TagItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<TagItem
|
||||
v-for="platform in version.loaders.slice(0, MAX_PLATFORM_TAGS)"
|
||||
:key="`platform-tag-${platform}`"
|
||||
v-tooltip="`Toggle filter for ${platform}`"
|
||||
class="smart-clickable:allow-pointer-events"
|
||||
:style="`--_color: var(--color-platform-${platform})`"
|
||||
:action="() => versionFilters?.toggleFilter('platform', platform)"
|
||||
>
|
||||
<component :is="getLoaderIcon(platform)" v-if="getLoaderIcon(platform)" />
|
||||
<FormattedTag :tag="platform" enforce-type="loader" />
|
||||
</TagItem>
|
||||
<Menu
|
||||
v-if="version.loaders.length > MAX_PLATFORM_TAGS"
|
||||
:delay="{ hide: 50, show: 0 }"
|
||||
no-auto-focus
|
||||
class="cursor-default smart-clickable:allow-pointer-events"
|
||||
>
|
||||
<TagItem tabindex="0">
|
||||
+{{ version.loaders.length - MAX_PLATFORM_TAGS }}
|
||||
</TagItem>
|
||||
<template #popper>
|
||||
<div class="flex max-w-[20rem] flex-wrap gap-1">
|
||||
<TagItem
|
||||
v-for="platform in version.loaders.slice(MAX_PLATFORM_TAGS)"
|
||||
:key="`overflow-platform-tag-${platform}`"
|
||||
:style="`--_color: var(--color-platform-${platform})`"
|
||||
:action="() => versionFilters?.toggleFilter('platform', platform)"
|
||||
>
|
||||
<component :is="getLoaderIcon(platform)" v-if="getLoaderIcon(platform)" />
|
||||
<FormattedTag :tag="platform" enforce-type="loader" />
|
||||
</TagItem>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</template>
|
||||
<template v-if="showEnvironmentColumn">
|
||||
<TagItem
|
||||
v-for="(tag, tagIdx) in getEnvironmentTags(version.environment)"
|
||||
:key="`env-tag-${tagIdx}`"
|
||||
class="text-center"
|
||||
>
|
||||
<component :is="tag.icon" />
|
||||
{{ formatMessage(tag.label).replace('and', '&') }}
|
||||
</TagItem>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex flex-row justify-start gap-3">
|
||||
<div class="flex cursor-help items-center gap-1 text-nowrap font-medium">
|
||||
<CalendarIcon />
|
||||
{{ formatRelativeTime(new Date(version.date_published)) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 font-medium">
|
||||
<DownloadIcon />
|
||||
{{ formatCompactNumber(version.downloads) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showFiles" class="tag-list">
|
||||
<div
|
||||
v-for="(file, fileIdx) in version.files"
|
||||
:key="`file-tag-${fileIdx}`"
|
||||
:class="`flex items-center gap-1 text-wrap rounded-full bg-button-bg px-2 py-0.5 text-xs font-medium ${file.primary || fileIdx === 0 ? 'bg-brand-highlight text-contrast' : 'text-primary'}`"
|
||||
>
|
||||
<StarIcon v-if="file.primary || fileIdx === 0" class="shrink-0" />
|
||||
{{ file.filename }} - {{ formatBytes(file.size) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex mt-3">
|
||||
|
||||
<div class="mt-3 flex">
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
class="ml-auto"
|
||||
@@ -286,6 +427,9 @@ import {
|
||||
ButtonStyled,
|
||||
FormattedTag,
|
||||
Pagination,
|
||||
SmartClickable,
|
||||
Table,
|
||||
type TableColumn,
|
||||
TagItem,
|
||||
useCompactNumber,
|
||||
useFormatBytes,
|
||||
@@ -303,7 +447,7 @@ import { defineMessages, useVIntl } from '../../composables/i18n'
|
||||
import { getEnvironmentTags } from './settings/environment/environments'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const formatRelativeTime = useRelativeTime({ style: 'narrow' })
|
||||
const { formatCompactNumber } = useCompactNumber()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
@@ -311,6 +455,9 @@ const formatDateTime = useFormatDateTime({
|
||||
})
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const MAX_GAME_VERSION_TAGS = 5
|
||||
const MAX_PLATFORM_TAGS = 3
|
||||
|
||||
type VersionWithDisplayUrlEnding = Version & {
|
||||
displayUrlEnding: string
|
||||
environment?: Labrinth.Projects.v3.Environment
|
||||
@@ -319,8 +466,20 @@ type VersionWithDisplayUrlEnding = Version & {
|
||||
|
||||
type DisplayVersion = VersionWithDisplayUrlEnding & {
|
||||
noModLoader: boolean
|
||||
files_missing_attribution?: boolean
|
||||
}
|
||||
|
||||
type VersionTableColumn =
|
||||
| 'channel'
|
||||
| 'name'
|
||||
| 'gameVersions'
|
||||
| 'platforms'
|
||||
| 'environment'
|
||||
| 'published'
|
||||
| 'downloads'
|
||||
| 'actions'
|
||||
type VersionTableRow = DisplayVersion & Record<string, unknown>
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
baseId?: string
|
||||
@@ -331,6 +490,7 @@ const props = withDefaults(
|
||||
}
|
||||
versions: VersionWithDisplayUrlEnding[]
|
||||
showFiles?: boolean
|
||||
showEnvironmentColumn?: boolean
|
||||
currentMember?: boolean
|
||||
loaders: Labrinth.Tags.v2.Loader[]
|
||||
gameVersions: GameVersionTag[]
|
||||
@@ -341,11 +501,72 @@ const props = withDefaults(
|
||||
{
|
||||
baseId: undefined,
|
||||
showFiles: false,
|
||||
showEnvironmentColumn: false,
|
||||
currentMember: false,
|
||||
versionLink: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const visibleCellClass = '!overflow-visible py-3 align-middle pr-2.5'
|
||||
|
||||
const versionColumns = computed<TableColumn<VersionTableColumn>[]>(() => {
|
||||
const columns: TableColumn<VersionTableColumn>[] = [
|
||||
{
|
||||
key: 'channel',
|
||||
width: '4.5rem',
|
||||
headerClass: 'text-secondary',
|
||||
cellClass: visibleCellClass,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
cellClass: '!overflow-visible py-3 pr-4 min-w-[7rem]',
|
||||
},
|
||||
{
|
||||
key: 'gameVersions',
|
||||
label: 'Game version',
|
||||
cellClass: '!overflow-visible py-3 align-middle pr-2.5 w-fit max-w-[10rem]',
|
||||
},
|
||||
{
|
||||
key: 'platforms',
|
||||
label: 'Platforms',
|
||||
cellClass: '!overflow-visible py-3 align-middle pr-2.5 w-fit max-w-[10rem]',
|
||||
},
|
||||
]
|
||||
|
||||
if (props.showEnvironmentColumn) {
|
||||
columns.push({
|
||||
key: 'environment',
|
||||
label: 'Environment',
|
||||
cellClass: visibleCellClass,
|
||||
})
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
key: 'published',
|
||||
label: 'Published',
|
||||
cellClass: '!overflow-visible align-middle pr-2.5 w-max',
|
||||
width: '12%',
|
||||
},
|
||||
{
|
||||
key: 'downloads',
|
||||
label: 'Downloads',
|
||||
cellClass: '!overflow-visible align-middle',
|
||||
width: '12%',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
align: 'right',
|
||||
headerClass: 'text-secondary',
|
||||
width: '1%',
|
||||
cellClass: '!overflow-visible align-middle',
|
||||
},
|
||||
)
|
||||
|
||||
return columns
|
||||
})
|
||||
|
||||
function getModpackLoaders(version: VersionWithDisplayUrlEnding): string[] {
|
||||
const loaders = Array.isArray(version.loaders) ? version.loaders : []
|
||||
|
||||
@@ -374,6 +595,10 @@ function hasNoModLoader(loaders: string[]): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function getDisplayGameVersions(version: DisplayVersion): string[] {
|
||||
return formatVersionsForDisplay(version.game_versions, props.gameVersions)
|
||||
}
|
||||
|
||||
const normalizedVersions = computed<DisplayVersion[]>(() =>
|
||||
props.versions.map((version) => {
|
||||
const loaders = getModpackLoaders(version)
|
||||
@@ -389,8 +614,6 @@ const normalizedVersions = computed<DisplayVersion[]>(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
const maxGameVersionTags = 6
|
||||
|
||||
const currentPage: Ref<number> = ref(1)
|
||||
const pageSize: Ref<number> = ref(20)
|
||||
const versionFilters: Ref<InstanceType<typeof VersionFilterControl> | null> = ref(null)
|
||||
@@ -403,11 +626,6 @@ const selectedPlatforms: Ref<string[]> = computed(
|
||||
)
|
||||
const selectedChannels: Ref<string[]> = computed(() => versionFilters.value?.selectedChannels ?? [])
|
||||
|
||||
const hasMultipleEnvironments = computed(() => {
|
||||
const environments = new Set(currentVersions.value.map((v) => v.environment).filter(Boolean))
|
||||
return environments.size > 1
|
||||
})
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
return normalizedVersions.value.filter(
|
||||
(version) =>
|
||||
@@ -431,6 +649,9 @@ const currentVersions = computed(() =>
|
||||
currentPage.value * pageSize.value,
|
||||
),
|
||||
)
|
||||
const currentVersionRows = computed<VersionTableRow[]>(
|
||||
() => currentVersions.value as VersionTableRow[],
|
||||
)
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -452,6 +673,18 @@ function switchPage(page: number) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function getVersionRowClass(): string {
|
||||
return props.versionLink
|
||||
? 'group version-row-link cursor-pointer transition-[filter] [&:hover:not(:has([data-no-row-click]:hover))]:brightness-[115%]'
|
||||
: 'group'
|
||||
}
|
||||
|
||||
function openVersionRow(version: VersionTableRow) {
|
||||
const link = props.versionLink?.(version)
|
||||
if (!link) return
|
||||
router.push(link)
|
||||
}
|
||||
|
||||
function updateQuery(newQueries: Record<string, string | string[] | undefined | null>) {
|
||||
if (newQueries.page) {
|
||||
currentPage.value = Number(newQueries.page)
|
||||
@@ -478,16 +711,9 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.versions-grid-row {
|
||||
@apply grid grid-cols-[1fr_min-content] gap-4 supports-[grid-template-columns:subgrid]:col-span-full supports-[grid-template-columns:subgrid]:!grid-cols-subgrid sm:grid-cols-[min-content_1fr_1fr_1fr_min-content];
|
||||
}
|
||||
|
||||
.has-environment .versions-grid-row {
|
||||
@apply xl:grid-cols-[min-content_1fr_1fr_1fr_1fr_1fr_1fr_min-content];
|
||||
}
|
||||
|
||||
.no-environment .versions-grid-row {
|
||||
@apply xl:grid-cols-[min-content_1fr_1fr_1fr_1fr_1fr_min-content];
|
||||
:deep(.version-row-link:hover:not(:has([data-no-row-click]:hover)) .version-row-name) {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,43 +1,84 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<ManySelect
|
||||
v-model="selectedPlatforms"
|
||||
:options="filterOptions.platform"
|
||||
:dropdown-id="`${baseId}-platform`"
|
||||
@change="updateFilters"
|
||||
<MultiSelect
|
||||
v-if="filterOptions.platform.length > 1"
|
||||
:model-value="selectedPlatforms"
|
||||
:options="platformOptions"
|
||||
fit-content
|
||||
:dropdown-min-width="180"
|
||||
trigger-class="!min-h-9 !px-3 !py-0"
|
||||
@update:model-value="updateSelectedPlatforms"
|
||||
>
|
||||
<FilterIcon class="h-5 w-5 text-secondary" />
|
||||
Platform
|
||||
<template #option="{ option }">
|
||||
<FormattedTag :tag="option" enforce-type="loader" />
|
||||
<template #input-content="{ isOpen, openDirection }">
|
||||
<div class="flex items-center gap-2">
|
||||
<FilterIcon class="h-5 w-5 text-secondary" />
|
||||
<span class="font-semibold text-primary">Platforms</span>
|
||||
<ChevronLeftIcon
|
||||
class="h-5 w-5 text-secondary transition-transform duration-150"
|
||||
:class="
|
||||
isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ManySelect>
|
||||
<ManySelect
|
||||
v-model="selectedGameVersions"
|
||||
:options="filterOptions.gameVersion"
|
||||
:dropdown-id="`${baseId}-game-version`"
|
||||
search
|
||||
@change="updateFilters"
|
||||
</MultiSelect>
|
||||
<MultiSelect
|
||||
v-if="availableGameVersions.length > 1"
|
||||
:model-value="selectedGameVersions"
|
||||
:options="gameVersionOptions"
|
||||
searchable
|
||||
search-placeholder="Search..."
|
||||
fit-content
|
||||
:dropdown-min-width="240"
|
||||
trigger-class="!min-h-9 !px-3 !py-0"
|
||||
@update:model-value="updateSelectedGameVersions"
|
||||
>
|
||||
<FilterIcon class="h-5 w-5 text-secondary" />
|
||||
Game versions
|
||||
<template #footer>
|
||||
<Checkbox v-model="showSnapshots" class="mx-1" :label="`Show all versions`" />
|
||||
<template #input-content="{ isOpen, openDirection }">
|
||||
<div class="flex items-center gap-2">
|
||||
<FilterIcon class="h-5 w-5 text-secondary" />
|
||||
<span class="font-semibold text-primary">Game versions</span>
|
||||
<ChevronLeftIcon
|
||||
class="h-5 w-5 text-secondary transition-transform duration-150"
|
||||
:class="
|
||||
isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ManySelect>
|
||||
<ManySelect
|
||||
v-model="selectedChannels"
|
||||
:options="filterOptions.channel"
|
||||
:dropdown-id="`${baseId}-channel`"
|
||||
@change="updateFilters"
|
||||
<template v-if="hasAnyNonReleaseGameVersions" #bottom>
|
||||
<div class="border-0 border-t border-solid border-t-surface-5 px-3 py-3">
|
||||
<Checkbox
|
||||
:model-value="showSnapshots"
|
||||
class="mx-1"
|
||||
:label="`Show all versions`"
|
||||
@update:model-value="updateShowSnapshots"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</MultiSelect>
|
||||
<MultiSelect
|
||||
v-if="filterOptions.channel.length > 1"
|
||||
:model-value="selectedChannels"
|
||||
:options="channelOptions"
|
||||
fit-content
|
||||
:dropdown-min-width="180"
|
||||
trigger-class="!min-h-9 !px-3 !py-0"
|
||||
@update:model-value="updateSelectedChannels"
|
||||
>
|
||||
<FilterIcon class="h-5 w-5 text-secondary" />
|
||||
Channels
|
||||
<template #option="{ option }">
|
||||
{{ option === 'release' ? 'Release' : option === 'beta' ? 'Beta' : 'Alpha' }}
|
||||
<template #input-content="{ isOpen, openDirection }">
|
||||
<div class="flex items-center gap-2">
|
||||
<FilterIcon class="h-5 w-5 text-secondary" />
|
||||
<span class="font-semibold text-primary">Channels</span>
|
||||
<ChevronLeftIcon
|
||||
class="h-5 w-5 text-secondary transition-transform duration-150"
|
||||
:class="
|
||||
isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ManySelect>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1 empty:hidden">
|
||||
<TagItem
|
||||
@@ -79,10 +120,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FilterIcon, XCircleIcon, XIcon } from '@modrinth/assets'
|
||||
import { Checkbox, FormattedTag, ManySelect, TagItem } from '@modrinth/ui'
|
||||
import { ChevronLeftIcon, FilterIcon, XCircleIcon, XIcon } from '@modrinth/assets'
|
||||
import type { MultiSelectOption } from '@modrinth/ui'
|
||||
import { Checkbox, formatLoader, FormattedTag, MultiSelect, TagItem, useVIntl } from '@modrinth/ui'
|
||||
import type { GameVersionTag, Version } from '@modrinth/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { LocationQueryValue } from 'vue-router'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -93,6 +136,8 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits(['update:query'])
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const allChannels = ref(['release', 'beta', 'alpha'])
|
||||
|
||||
const route = useRoute()
|
||||
@@ -102,6 +147,34 @@ const showSnapshots = ref(false)
|
||||
type FilterType = 'channel' | 'gameVersion' | 'platform'
|
||||
type Filter = string
|
||||
|
||||
const gameVersionTags = computed(() => new Map(props.gameVersions.map((x) => [x.version, x])))
|
||||
|
||||
const availableGameVersions = computed(() => {
|
||||
const gameVersionSet = new Set<Filter>()
|
||||
|
||||
for (const version of props.versions) {
|
||||
for (const gameVersion of Array.isArray(version.game_versions) ? version.game_versions : []) {
|
||||
gameVersionSet.add(gameVersion)
|
||||
}
|
||||
}
|
||||
|
||||
const knownGameVersions = props.gameVersions.filter((x) => gameVersionSet.has(x.version))
|
||||
const knownGameVersionSet = new Set(knownGameVersions.map((x) => x.version))
|
||||
const unknownGameVersions = Array.from(gameVersionSet).filter(
|
||||
(version) => !knownGameVersionSet.has(version),
|
||||
)
|
||||
|
||||
return [...knownGameVersions.map((x) => x.version), ...unknownGameVersions]
|
||||
})
|
||||
|
||||
const hasAnyReleaseGameVersions = computed(() =>
|
||||
availableGameVersions.value.some((version) => isReleaseGameVersion(version)),
|
||||
)
|
||||
|
||||
const hasAnyNonReleaseGameVersions = computed(() =>
|
||||
availableGameVersions.value.some((version) => !isReleaseGameVersion(version)),
|
||||
)
|
||||
|
||||
const filterOptions = computed(() => {
|
||||
const filters: Record<FilterType, Filter[]> = {
|
||||
channel: [],
|
||||
@@ -110,16 +183,12 @@ const filterOptions = computed(() => {
|
||||
}
|
||||
|
||||
const platformSet = new Set<Filter>()
|
||||
const gameVersionSet = new Set<Filter>()
|
||||
const channelSet = new Set<Filter>()
|
||||
|
||||
for (const version of props.versions) {
|
||||
for (const loader of Array.isArray(version.loaders) ? version.loaders : []) {
|
||||
platformSet.add(loader)
|
||||
}
|
||||
for (const gameVersion of Array.isArray(version.game_versions) ? version.game_versions : []) {
|
||||
gameVersionSet.add(gameVersion)
|
||||
}
|
||||
channelSet.add(version.version_type)
|
||||
}
|
||||
|
||||
@@ -127,12 +196,12 @@ const filterOptions = computed(() => {
|
||||
filters.channel = Array.from(channelSet) as Filter[]
|
||||
filters.channel.sort((a, b) => allChannels.value.indexOf(a) - allChannels.value.indexOf(b))
|
||||
}
|
||||
if (gameVersionSet.size > 0) {
|
||||
const gameVersions = props.gameVersions.filter((x) => gameVersionSet.has(x.version))
|
||||
|
||||
filters.gameVersion = gameVersions
|
||||
.filter((x) => (showSnapshots.value ? true : x.version_type === 'release'))
|
||||
.map((x) => x.version)
|
||||
if (availableGameVersions.value.length > 0) {
|
||||
filters.gameVersion = availableGameVersions.value.filter((version) =>
|
||||
showSnapshots.value || !hasAnyReleaseGameVersions.value
|
||||
? true
|
||||
: isReleaseGameVersion(version),
|
||||
)
|
||||
}
|
||||
if (platformSet.size > 0) {
|
||||
filters.platform = Array.from(platformSet) as Filter[]
|
||||
@@ -141,6 +210,27 @@ const filterOptions = computed(() => {
|
||||
return filters
|
||||
})
|
||||
|
||||
const gameVersionOptions = computed<MultiSelectOption<string>[]>(() =>
|
||||
filterOptions.value.gameVersion.map((version) => ({
|
||||
value: version,
|
||||
label: version,
|
||||
})),
|
||||
)
|
||||
|
||||
const channelOptions = computed<MultiSelectOption<string>[]>(() =>
|
||||
filterOptions.value.channel.map((channel) => ({
|
||||
value: channel,
|
||||
label: getChannelLabel(channel),
|
||||
})),
|
||||
)
|
||||
|
||||
const platformOptions = computed<MultiSelectOption<string>[]>(() =>
|
||||
filterOptions.value.platform.map((platform) => ({
|
||||
value: platform,
|
||||
label: formatLoader(formatMessage, platform),
|
||||
})),
|
||||
)
|
||||
|
||||
const selectedChannels = ref<string[]>([])
|
||||
const selectedGameVersions = ref<string[]>([])
|
||||
const selectedPlatforms = ref<string[]>([])
|
||||
@@ -149,6 +239,10 @@ selectedChannels.value = route.query.c ? getArrayOrString(route.query.c) : []
|
||||
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : []
|
||||
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : []
|
||||
|
||||
if (selectedGameVersions.value.some((version) => !isReleaseGameVersion(version))) {
|
||||
showSnapshots.value = true
|
||||
}
|
||||
|
||||
async function toggleFilters(type: FilterType, filters: Filter[]) {
|
||||
for (const filter of filters) {
|
||||
await toggleFilter(type, filter, true)
|
||||
@@ -176,6 +270,38 @@ async function toggleFilter(type: FilterType, filter: Filter, bulk = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectedGameVersions(versions: string[]) {
|
||||
selectedGameVersions.value = versions
|
||||
updateFilters()
|
||||
}
|
||||
|
||||
function updateSelectedChannels(channels: string[]) {
|
||||
selectedChannels.value = channels
|
||||
updateFilters()
|
||||
}
|
||||
|
||||
function updateSelectedPlatforms(platforms: string[]) {
|
||||
selectedPlatforms.value = platforms
|
||||
updateFilters()
|
||||
}
|
||||
|
||||
function updateShowSnapshots(value: boolean, _event?: MouseEvent) {
|
||||
showSnapshots.value = value
|
||||
|
||||
if (value || !hasAnyReleaseGameVersions.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedReleaseGameVersions = selectedGameVersions.value.filter((version) =>
|
||||
isReleaseGameVersion(version),
|
||||
)
|
||||
|
||||
if (selectedReleaseGameVersions.length !== selectedGameVersions.value.length) {
|
||||
selectedGameVersions.value = selectedReleaseGameVersions
|
||||
updateFilters()
|
||||
}
|
||||
}
|
||||
|
||||
async function clearFilters() {
|
||||
selectedChannels.value = []
|
||||
selectedGameVersions.value = []
|
||||
@@ -201,11 +327,22 @@ defineExpose({
|
||||
selectedPlatforms,
|
||||
})
|
||||
|
||||
function getArrayOrString(x: string | string[]): string[] {
|
||||
function getArrayOrString(x: LocationQueryValue | LocationQueryValue[]): string[] {
|
||||
if (x === null) {
|
||||
return []
|
||||
}
|
||||
if (typeof x === 'string') {
|
||||
return [x]
|
||||
} else {
|
||||
return x
|
||||
}
|
||||
|
||||
return x.filter((value): value is string => value !== null)
|
||||
}
|
||||
|
||||
function getChannelLabel(channel: string) {
|
||||
return channel === 'release' ? 'Release' : channel === 'beta' ? 'Beta' : 'Alpha'
|
||||
}
|
||||
|
||||
function isReleaseGameVersion(version: string) {
|
||||
return gameVersionTags.value.get(version)?.version_type === 'release'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -100,6 +100,32 @@ export const HorizontalOverflow: StoryObj = {
|
||||
}),
|
||||
}
|
||||
|
||||
export const CustomClasses: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
components: { Table },
|
||||
setup() {
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name', cellClass: '!overflow-visible py-3' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
headerClass: 'text-center',
|
||||
cellClass: 'text-center',
|
||||
},
|
||||
{ key: 'role', label: 'Role' },
|
||||
]
|
||||
const data = sampleUsers
|
||||
const rowClass = (_row: User, index: number) => (index === 0 ? 'font-semibold' : '')
|
||||
return { columns, data, rowClass }
|
||||
},
|
||||
template: /* html */ `
|
||||
<Table :columns="columns" :data="data" :row-class="rowClass" />
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithSelection: StoryObj = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { DownloadIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||
import type { GameVersionTag, Version } from '@modrinth/utils'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||
import ProjectPageVersions from '../../components/project/ProjectPageVersions.vue'
|
||||
|
||||
type StoryVersion = Version & {
|
||||
displayUrlEnding: string
|
||||
environment?: Labrinth.Projects.v3.Environment
|
||||
mrpack_loaders?: string[]
|
||||
files_missing_attribution?: string[]
|
||||
}
|
||||
|
||||
const gameVersions: GameVersionTag[] = [
|
||||
{ version: '1.21.4', version_type: 'release', date: '2024-12-03', major: true },
|
||||
{ version: '1.21.3', version_type: 'release', date: '2024-10-23', major: false },
|
||||
{ version: '1.21.1', version_type: 'release', date: '2024-08-08', major: true },
|
||||
{ version: '1.20.6', version_type: 'release', date: '2024-04-29', major: false },
|
||||
{ version: '1.20.4', version_type: 'release', date: '2023-12-07', major: true },
|
||||
{ version: '1.20.1', version_type: 'release', date: '2023-06-12', major: true },
|
||||
{ version: '1.19.4', version_type: 'release', date: '2023-03-14', major: true },
|
||||
]
|
||||
|
||||
const loaders: Labrinth.Tags.v2.Loader[] = [
|
||||
{ icon: '', name: 'fabric', supported_project_types: ['mod', 'modpack'] },
|
||||
{ icon: '', name: 'forge', supported_project_types: ['mod', 'modpack'] },
|
||||
{ icon: '', name: 'neoforge', supported_project_types: ['mod', 'modpack'] },
|
||||
{ icon: '', name: 'quilt', supported_project_types: ['mod', 'modpack'] },
|
||||
]
|
||||
|
||||
const versions: StoryVersion[] = [
|
||||
{
|
||||
id: 'version-1',
|
||||
project_id: 'project-1',
|
||||
author_id: 'author-1',
|
||||
name: 'Performance improvements and bug fixes',
|
||||
version_number: 'mc1.21.4-0.6.13',
|
||||
displayUrlEnding: 'mc1.21.4-0.6.13',
|
||||
changelog: '',
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.4', '1.21.3', '1.21.1'],
|
||||
version_type: 'release',
|
||||
loaders: ['fabric', 'quilt', 'forge', 'neoforge'],
|
||||
featured: true,
|
||||
status: 'listed',
|
||||
date_published: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(),
|
||||
downloads: 1258400,
|
||||
environment: 'client_only',
|
||||
files: [
|
||||
{
|
||||
hashes: { sha512: 'sha512-1', sha1: 'sha1-1' },
|
||||
url: 'https://cdn.modrinth.com/data/story/version-1.jar',
|
||||
filename: 'sodium-fabric-0.6.13.jar',
|
||||
primary: true,
|
||||
size: 1248200,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'version-2',
|
||||
project_id: 'project-1',
|
||||
author_id: 'author-1',
|
||||
name: 'NeoForge compatibility',
|
||||
version_number: 'mc1.20.6-0.5.11',
|
||||
displayUrlEnding: 'mc1.20.6-0.5.11',
|
||||
changelog: '',
|
||||
dependencies: [],
|
||||
game_versions: ['1.20.6', '1.20.4', '1.20.1', '1.19.4'],
|
||||
version_type: 'beta',
|
||||
loaders: ['neoforge'],
|
||||
featured: false,
|
||||
status: 'listed',
|
||||
date_published: new Date(Date.now() - 1000 * 60 * 60 * 24 * 16).toISOString(),
|
||||
downloads: 84200,
|
||||
environment: 'client_and_server',
|
||||
files_missing_attribution: ['bundled-library.jar'],
|
||||
files: [
|
||||
{
|
||||
hashes: { sha512: 'sha512-2', sha1: 'sha1-2' },
|
||||
url: 'https://cdn.modrinth.com/data/story/version-2.jar',
|
||||
filename: 'sodium-neoforge-0.5.11.jar',
|
||||
primary: true,
|
||||
size: 1424200,
|
||||
},
|
||||
{
|
||||
hashes: { sha512: 'sha512-3', sha1: 'sha1-3' },
|
||||
url: 'https://cdn.modrinth.com/data/story/version-2-sources.jar',
|
||||
filename: 'sodium-neoforge-0.5.11-sources.jar',
|
||||
primary: false,
|
||||
size: 624200,
|
||||
file_type: 'sources-jar',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'version-3',
|
||||
project_id: 'project-1',
|
||||
author_id: 'author-1',
|
||||
name: 'Server pack with no mod loader',
|
||||
version_number: 'server-pack-1.0.0',
|
||||
displayUrlEnding: 'server-pack-1.0.0',
|
||||
changelog: '',
|
||||
dependencies: [],
|
||||
game_versions: ['1.21.4'],
|
||||
version_type: 'alpha',
|
||||
loaders: ['minecraft'],
|
||||
mrpack_loaders: [],
|
||||
featured: false,
|
||||
status: 'listed',
|
||||
date_published: new Date(Date.now() - 1000 * 60 * 60 * 24 * 45).toISOString(),
|
||||
downloads: 1200,
|
||||
environment: 'server_only',
|
||||
files: [
|
||||
{
|
||||
hashes: { sha512: 'sha512-4', sha1: 'sha1-4' },
|
||||
url: 'https://cdn.modrinth.com/data/story/version-3.mrpack',
|
||||
filename: 'server-pack-1.0.0.mrpack',
|
||||
primary: true,
|
||||
size: 2048200,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: 'Project/ProjectPageVersions',
|
||||
component: ProjectPageVersions,
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div class="p-4"><story /></div>',
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
project: {
|
||||
project_type: 'mod',
|
||||
slug: 'sodium',
|
||||
id: 'project-1',
|
||||
},
|
||||
versions,
|
||||
loaders,
|
||||
gameVersions,
|
||||
baseId: 'project-page-versions-story',
|
||||
showFiles: false,
|
||||
showEnvironmentColumn: false,
|
||||
versionLink: (version: Version) => `https://modrinth.com/mod/sodium/version/${version.id}`,
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { ButtonStyled, DownloadIcon, MoreVerticalIcon, ProjectPageVersions },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: /* html */ `
|
||||
<ProjectPageVersions v-bind="args">
|
||||
<template #actions="{ version }">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<a
|
||||
v-tooltip="'Download'"
|
||||
:href="version.files[0]?.url"
|
||||
:download="version.files[0]?.filename"
|
||||
aria-label="Download"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button v-tooltip="'More options'" aria-label="More options">
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectPageVersions>
|
||||
`,
|
||||
}),
|
||||
} satisfies Meta<typeof ProjectPageVersions>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const WithFiles: Story = {
|
||||
args: {
|
||||
showFiles: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithEnvironmentColumn: Story = {
|
||||
args: {
|
||||
showEnvironmentColumn: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const MobileWidth: Story = {
|
||||
parameters: {
|
||||
viewport: {
|
||||
defaultViewport: 'mobile1',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
showFiles: true,
|
||||
showEnvironmentColumn: true,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user