mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 22:10:15 +00:00
feat: start on content tab env focused design
This commit is contained in:
@@ -105,6 +105,57 @@ export class ArchonContentV1Module extends AbstractModule {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** POST /v1/:server_id/worlds/:world_id/addons/set-enabled-server */
|
||||||
|
public async setAddonEnabledServer(
|
||||||
|
serverId: string,
|
||||||
|
worldId: string,
|
||||||
|
request: Archon.Content.v1.SetAddonEnabledRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.client.request<void>(
|
||||||
|
`/servers/${serverId}/worlds/${worldId}/addons/set-enabled-server`,
|
||||||
|
{
|
||||||
|
api: 'archon',
|
||||||
|
version: 1,
|
||||||
|
method: 'POST',
|
||||||
|
body: request,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /v1/:server_id/worlds/:world_id/addons/set-enabled-player */
|
||||||
|
public async setAddonEnabledPlayer(
|
||||||
|
serverId: string,
|
||||||
|
worldId: string,
|
||||||
|
request: Archon.Content.v1.SetAddonEnabledRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.client.request<void>(
|
||||||
|
`/servers/${serverId}/worlds/${worldId}/addons/set-enabled-player`,
|
||||||
|
{
|
||||||
|
api: 'archon',
|
||||||
|
version: 1,
|
||||||
|
method: 'POST',
|
||||||
|
body: request,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /v1/:server_id/worlds/:world_id/addons/set-locked-side-toggle */
|
||||||
|
public async setAddonSideToggleLocked(
|
||||||
|
serverId: string,
|
||||||
|
worldId: string,
|
||||||
|
request: Archon.Content.v1.SetAddonSideToggleLockedRequest,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.client.request<void>(
|
||||||
|
`/servers/${serverId}/worlds/${worldId}/addons/set-locked-side-toggle`,
|
||||||
|
{
|
||||||
|
api: 'archon',
|
||||||
|
version: 1,
|
||||||
|
method: 'POST',
|
||||||
|
body: request,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** POST /v1/:server_id/worlds/:world_id/addons/delete-many */
|
/** POST /v1/:server_id/worlds/:world_id/addons/delete-many */
|
||||||
public async deleteAddons(
|
public async deleteAddons(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
|
|||||||
@@ -301,6 +301,25 @@ export namespace Archon {
|
|||||||
environment?: Labrinth.Projects.v3.Environment | null
|
environment?: Labrinth.Projects.v3.Environment | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AddonManifestEnvironment =
|
||||||
|
| 'client_and_server'
|
||||||
|
| 'client_only'
|
||||||
|
| 'dedicated_server_only'
|
||||||
|
|
||||||
|
export type AddonManifestWarnings = {
|
||||||
|
multiple_mod_entries: number | null
|
||||||
|
malformed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddonManifest = {
|
||||||
|
platform: Modloader
|
||||||
|
name: string | null
|
||||||
|
version: string | null
|
||||||
|
environment: AddonManifestEnvironment | null
|
||||||
|
icon_embedded: boolean
|
||||||
|
warnings: AddonManifestWarnings
|
||||||
|
}
|
||||||
|
|
||||||
export type AddonStatus =
|
export type AddonStatus =
|
||||||
| 'pending'
|
| 'pending'
|
||||||
| 'installed'
|
| 'installed'
|
||||||
@@ -316,6 +335,10 @@ export namespace Archon {
|
|||||||
filesize: number
|
filesize: number
|
||||||
btime?: string
|
btime?: string
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
|
disabled_server: boolean
|
||||||
|
disabled_player: boolean
|
||||||
|
side_toggle_unlocked: boolean
|
||||||
|
manifest: AddonManifest | null
|
||||||
kind: AddonKind
|
kind: AddonKind
|
||||||
from_modpack: boolean
|
from_modpack: boolean
|
||||||
status: AddonStatus
|
status: AddonStatus
|
||||||
@@ -354,6 +377,14 @@ export namespace Archon {
|
|||||||
filename: string
|
filename: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SetAddonEnabledRequest = RemoveAddonRequest & {
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SetAddonSideToggleLockedRequest = RemoveAddonRequest & {
|
||||||
|
locked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdateAddonRequest = {
|
export type UpdateAddonRequest = {
|
||||||
filename: string
|
filename: string
|
||||||
version_id?: string | null
|
version_id?: string | null
|
||||||
@@ -1063,6 +1094,7 @@ export namespace Archon {
|
|||||||
project_id: string | null
|
project_id: string | null
|
||||||
pack_client_retained: boolean
|
pack_client_retained: boolean
|
||||||
pack_client_depends: boolean
|
pack_client_depends: boolean
|
||||||
|
manifest?: Archon.Content.v1.AddonManifest | null
|
||||||
status: Archon.Content.v1.AddonStatus
|
status: Archon.Content.v1.AddonStatus
|
||||||
filesize: number | null
|
filesize: number | null
|
||||||
name: string | null
|
name: string | null
|
||||||
|
|||||||
@@ -7,6 +7,24 @@ export class KyrosContentV1Module extends AbstractModule {
|
|||||||
return 'kyros_content_v1'
|
return 'kyros_content_v1'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** GET /v1/worlds/:world_id/content/embedded-icon */
|
||||||
|
public async getEmbeddedAddonIcon(
|
||||||
|
worldId: string,
|
||||||
|
parentDirectory: 'mods' | 'plugins',
|
||||||
|
filename: string,
|
||||||
|
): Promise<Blob> {
|
||||||
|
return this.client.request<Blob>(`/worlds/${worldId}/content/embedded-icon`, {
|
||||||
|
api: '',
|
||||||
|
version: 'v1',
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
parent_directory: parentDirectory,
|
||||||
|
filename,
|
||||||
|
},
|
||||||
|
useNodeAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload addon files to a world via multipart form data
|
* Upload addon files to a world via multipart form data
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -233,17 +233,48 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = worldContentUpdateToAddons(event)
|
const content = worldContentUpdateToAddons(event)
|
||||||
queryClient.setQueryData<Archon.Content.v1.Addons>(contentListKey(serverId), {
|
queryClient.setQueryData<Archon.Content.v1.Addons>(contentListKey(serverId), (current) => ({
|
||||||
...content,
|
...content,
|
||||||
addons: content.addons?.filter((addon) => !addon.from_modpack) ?? null,
|
addons: mergeWorldContentSideState(
|
||||||
})
|
current?.addons ?? [],
|
||||||
queryClient.setQueryData<Archon.Content.v1.Addons>(modpackContentListKey(serverId), {
|
content.addons?.filter((addon) => !addon.from_modpack) ?? [],
|
||||||
...content,
|
),
|
||||||
addons: content.addons?.filter((addon) => addon.from_modpack) ?? null,
|
}))
|
||||||
})
|
queryClient.setQueryData<Archon.Content.v1.Addons>(
|
||||||
|
modpackContentListKey(serverId),
|
||||||
|
(current) => ({
|
||||||
|
...content,
|
||||||
|
addons: mergeWorldContentSideState(
|
||||||
|
current?.addons ?? [],
|
||||||
|
content.addons?.filter((addon) => addon.from_modpack) ?? [],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
void queryClient.invalidateQueries({ queryKey: contentListKey(serverId) })
|
||||||
|
void queryClient.invalidateQueries({ queryKey: modpackContentListKey(serverId) })
|
||||||
void queryClient.invalidateQueries({ queryKey: serverV1DetailKey(serverId) })
|
void queryClient.invalidateQueries({ queryKey: serverV1DetailKey(serverId) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeWorldContentSideState(
|
||||||
|
currentAddons: Archon.Content.v1.Addon[],
|
||||||
|
incomingAddons: Archon.Content.v1.Addon[],
|
||||||
|
) {
|
||||||
|
const currentByFilename = new Map(
|
||||||
|
currentAddons.map((addon) => [normalizeAddonFilename(addon.filename), addon] as const),
|
||||||
|
)
|
||||||
|
return incomingAddons.map((incoming) => {
|
||||||
|
const current = currentByFilename.get(normalizeAddonFilename(incoming.filename))
|
||||||
|
return current
|
||||||
|
? {
|
||||||
|
...incoming,
|
||||||
|
disabled_server: current.disabled_server,
|
||||||
|
disabled_player: current.disabled_player,
|
||||||
|
side_toggle_unlocked: current.side_toggle_unlocked,
|
||||||
|
}
|
||||||
|
: incoming
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function worldContentUpdateToAddons(
|
function worldContentUpdateToAddons(
|
||||||
event: Archon.Sync.v1.WorldContentUpdateEvent,
|
event: Archon.Sync.v1.WorldContentUpdateEvent,
|
||||||
): Archon.Content.v1.Addons {
|
): Archon.Content.v1.Addons {
|
||||||
@@ -297,7 +328,11 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
|||||||
filename: item.filename,
|
filename: item.filename,
|
||||||
filesize: item.filesize ?? 0,
|
filesize: item.filesize ?? 0,
|
||||||
btime: item.btime,
|
btime: item.btime,
|
||||||
disabled: item.filename.endsWith('.disabled'),
|
disabled: false,
|
||||||
|
disabled_server: false,
|
||||||
|
disabled_player: false,
|
||||||
|
side_toggle_unlocked: false,
|
||||||
|
manifest: item.manifest ?? null,
|
||||||
kind: parentDirectoryToAddonKind(item.parent_directory),
|
kind: parentDirectoryToAddonKind(item.parent_directory),
|
||||||
from_modpack: item.from_modpack,
|
from_modpack: item.from_modpack,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface ScrollViewportOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VirtualScrollOptions {
|
export interface VirtualScrollOptions {
|
||||||
itemHeight: number
|
itemHeight: number | Ref<number>
|
||||||
bufferSize?: number
|
bufferSize?: number
|
||||||
initialItemCount?: number
|
initialItemCount?: number
|
||||||
enabled?: Ref<boolean>
|
enabled?: Ref<boolean>
|
||||||
@@ -140,6 +140,9 @@ export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptio
|
|||||||
onNearEnd,
|
onNearEnd,
|
||||||
nearEndThreshold = 0.2,
|
nearEndThreshold = 0.2,
|
||||||
} = options
|
} = options
|
||||||
|
const resolvedItemHeight = computed(() =>
|
||||||
|
typeof itemHeight === 'number' ? itemHeight : itemHeight.value,
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
listContainer,
|
listContainer,
|
||||||
@@ -152,7 +155,7 @@ export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptio
|
|||||||
onScroll: checkNearEnd,
|
onScroll: checkNearEnd,
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalHeight = computed(() => items.value.length * itemHeight)
|
const totalHeight = computed(() => items.value.length * resolvedItemHeight.value)
|
||||||
|
|
||||||
const visibleRange = computed(() => {
|
const visibleRange = computed(() => {
|
||||||
if (enabled && !enabled.value) {
|
if (enabled && !enabled.value) {
|
||||||
@@ -163,8 +166,8 @@ export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptio
|
|||||||
return { start: 0, end: Math.min(items.value.length, initialItemCount) }
|
return { start: 0, end: Math.min(items.value.length, initialItemCount) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = Math.floor(relativeScrollTop.value / itemHeight)
|
const start = Math.floor(relativeScrollTop.value / resolvedItemHeight.value)
|
||||||
const visibleCount = Math.ceil(viewportHeight.value / itemHeight)
|
const visibleCount = Math.ceil(viewportHeight.value / resolvedItemHeight.value)
|
||||||
const rangeSize = visibleCount + bufferSize * 2
|
const rangeSize = visibleCount + bufferSize * 2
|
||||||
|
|
||||||
const rangeStart = Math.min(
|
const rangeStart = Math.min(
|
||||||
@@ -180,7 +183,7 @@ export function useVirtualScroll<T>(items: Ref<T[]>, options: VirtualScrollOptio
|
|||||||
})
|
})
|
||||||
|
|
||||||
const visibleTop = computed(() =>
|
const visibleTop = computed(() =>
|
||||||
enabled && !enabled.value ? 0 : visibleRange.value.start * itemHeight,
|
enabled && !enabled.value ? 0 : visibleRange.value.start * resolvedItemHeight.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
const visibleItems = computed(() =>
|
const visibleItems = computed(() =>
|
||||||
|
|||||||
@@ -28,11 +28,16 @@ import { truncatedTooltip } from '#ui/utils/truncate'
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ClientWarningType,
|
ClientWarningType,
|
||||||
|
ContentCardEmbeddedIcon,
|
||||||
ContentCardProject,
|
ContentCardProject,
|
||||||
ContentCardVersion,
|
ContentCardVersion,
|
||||||
|
ContentEnabledForState,
|
||||||
ContentOwner,
|
ContentOwner,
|
||||||
|
ContentSide,
|
||||||
ContentSource,
|
ContentSource,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
import ContentCardItemIcon from './ContentCardItemIcon.vue'
|
||||||
|
import ContentEnabledFor from './ContentEnabledFor.vue'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
@@ -77,6 +82,8 @@ interface Props {
|
|||||||
hideDelete?: boolean
|
hideDelete?: boolean
|
||||||
hideActions?: boolean
|
hideActions?: boolean
|
||||||
inline?: boolean
|
inline?: boolean
|
||||||
|
enabledFor?: ContentEnabledForState
|
||||||
|
embeddedIcon?: ContentCardEmbeddedIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -104,12 +111,15 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
hideDelete: false,
|
hideDelete: false,
|
||||||
hideActions: false,
|
hideActions: false,
|
||||||
inline: false,
|
inline: false,
|
||||||
|
enabledFor: undefined,
|
||||||
|
embeddedIcon: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const selected = defineModel<boolean>('selected')
|
const selected = defineModel<boolean>('selected')
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:enabled': [value: boolean]
|
'update:enabled': [value: boolean]
|
||||||
|
'update:enabled-for': [side: ContentSide, value: boolean]
|
||||||
select: [value: boolean, event?: MouseEvent]
|
select: [value: boolean, event?: MouseEvent]
|
||||||
delete: [event: MouseEvent]
|
delete: [event: MouseEvent]
|
||||||
update: []
|
update: []
|
||||||
@@ -128,6 +138,7 @@ const fileNameRef = ref<HTMLElement | null>(null)
|
|||||||
|
|
||||||
const isDisabled = computed(() => props.disabled || props.installing)
|
const isDisabled = computed(() => props.disabled || props.installing)
|
||||||
const isToggleDisabled = computed(() => isDisabled.value || props.toggleDisabled)
|
const isToggleDisabled = computed(() => isDisabled.value || props.toggleDisabled)
|
||||||
|
const isEnabledForDisabled = computed(() => !props.enabledFor?.server && !props.enabledFor?.player)
|
||||||
|
|
||||||
const clientWarningMessage = computed(() => {
|
const clientWarningMessage = computed(() => {
|
||||||
switch (props.clientWarning) {
|
switch (props.clientWarning) {
|
||||||
@@ -154,16 +165,21 @@ const installTooltip = computed(() => {
|
|||||||
role="row"
|
role="row"
|
||||||
class="flex items-center justify-between"
|
class="flex items-center justify-between"
|
||||||
:class="{
|
:class="{
|
||||||
'h-[74px] gap-4 px-3': !inline,
|
'h-[74px] gap-4 px-3': !inline && !enabledFor,
|
||||||
|
'h-[72px] gap-4 px-3': !inline && enabledFor,
|
||||||
'gap-3': inline,
|
'gap-3': inline,
|
||||||
'opacity-50 grayscale': disabled && !installing,
|
'opacity-50 grayscale': disabled && !installing,
|
||||||
'opacity-50': installing,
|
'opacity-50': installing || (enabledFor && isEnabledForDisabled && !disabled),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex min-w-0 items-center gap-4"
|
class="flex min-w-0 items-center gap-4"
|
||||||
:class="
|
:class="
|
||||||
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
hideActions
|
||||||
|
? 'flex-1'
|
||||||
|
: enabledFor
|
||||||
|
? 'flex-1 @[800px]:w-[340px] @[800px]:shrink-0 @[800px]:flex-none'
|
||||||
|
: 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -177,15 +193,14 @@ const installTooltip = computed(() => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex min-w-0 items-center gap-3 transition-[filter,opacity] duration-200"
|
class="flex min-w-0 items-center gap-3 transition-[filter,opacity] duration-200"
|
||||||
:class="enabled === false && !disabled ? 'grayscale opacity-50' : ''"
|
:class="!enabledFor && enabled === false && !disabled ? 'grayscale opacity-50' : ''"
|
||||||
>
|
>
|
||||||
<div v-tooltip="installTooltip" class="relative flex shrink-0 items-center">
|
<div v-tooltip="installTooltip" class="relative flex shrink-0 items-center">
|
||||||
<Avatar
|
<ContentCardItemIcon
|
||||||
:src="project.icon_url"
|
:src="project.icon_url"
|
||||||
:alt="project.title"
|
:alt="project.title"
|
||||||
size="3rem"
|
:tint-by="project.id"
|
||||||
no-shadow
|
:embedded-icon="embeddedIcon"
|
||||||
class="rounded-2xl border border-surface-5"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="installing"
|
v-if="installing"
|
||||||
@@ -295,11 +310,27 @@ const installTooltip = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="enabledFor"
|
||||||
|
class="hidden w-[200px] shrink-0 @[800px]:block"
|
||||||
|
>
|
||||||
|
<ContentEnabledFor
|
||||||
|
:model-value="enabledFor"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
:disabled-tooltip="isDisabled ? disabledTooltip : undefined"
|
||||||
|
@update:model-value="(side, value) => emit('update:enabled-for', side, value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="hidden flex-col gap-0.5 transition-[filter,opacity] duration-200 @[800px]:flex"
|
class="hidden flex-col gap-0.5 transition-[filter,opacity] duration-200 @[800px]:flex"
|
||||||
:class="[
|
:class="[
|
||||||
hideActions ? 'flex-1' : 'flex-1 min-w-0',
|
hideActions
|
||||||
enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
? 'flex-1'
|
||||||
|
: enabledFor
|
||||||
|
? 'w-[250px] min-w-0 shrink-0'
|
||||||
|
: 'flex-1 min-w-0',
|
||||||
|
!enabledFor && enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<template v-if="version">
|
<template v-if="version">
|
||||||
@@ -335,7 +366,8 @@ const installTooltip = computed(() => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!hideActions"
|
v-if="!hideActions"
|
||||||
class="flex min-w-[160px] shrink-0 items-center justify-end gap-2 transition-colors duration-200"
|
class="flex shrink-0 items-center justify-end gap-2 transition-colors duration-200"
|
||||||
|
:class="enabledFor ? 'w-[112px]' : 'min-w-[160px]'"
|
||||||
>
|
>
|
||||||
<slot name="additionalButtonsLeft" />
|
<slot name="additionalButtonsLeft" />
|
||||||
|
|
||||||
@@ -398,7 +430,7 @@ const installTooltip = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
v-if="enabled !== undefined && !hideToggle"
|
v-if="enabled !== undefined && !hideToggle && !enabledFor"
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
isToggleDisabled && (toggleDisabledTooltip || disabledTooltip)
|
isToggleDisabled && (toggleDisabledTooltip || disabledTooltip)
|
||||||
? (toggleDisabledTooltip ?? disabledTooltip)
|
? (toggleDisabledTooltip ?? disabledTooltip)
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { computed, onScopeDispose, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import Avatar from '#ui/components/base/Avatar.vue'
|
||||||
|
|
||||||
|
import type { ContentCardEmbeddedIcon } from '../types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
src?: string | null
|
||||||
|
fallbackUrl?: string | null
|
||||||
|
embeddedIcon?: ContentCardEmbeddedIcon
|
||||||
|
alt: string
|
||||||
|
tintBy?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const queryKey = computed(
|
||||||
|
() => props.embeddedIcon?.queryKey ?? (['content', 'embedded-icon', 'disabled'] as const),
|
||||||
|
)
|
||||||
|
|
||||||
|
const embeddedIconQuery = useQuery({
|
||||||
|
queryKey,
|
||||||
|
queryFn: () => {
|
||||||
|
if (!props.embeddedIcon) throw new Error('Missing embedded icon request')
|
||||||
|
return props.embeddedIcon.queryFn()
|
||||||
|
},
|
||||||
|
enabled: computed(
|
||||||
|
() => typeof window !== 'undefined' && !props.src && props.embeddedIcon !== undefined,
|
||||||
|
),
|
||||||
|
staleTime: Infinity,
|
||||||
|
})
|
||||||
|
|
||||||
|
const embeddedIconUrl = ref<string>()
|
||||||
|
let pendingIconUrl: string | undefined
|
||||||
|
let validationId = 0
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => embeddedIconQuery.data.value,
|
||||||
|
(blob) => {
|
||||||
|
validationId += 1
|
||||||
|
const currentValidationId = validationId
|
||||||
|
if (embeddedIconUrl.value) URL.revokeObjectURL(embeddedIconUrl.value)
|
||||||
|
if (pendingIconUrl) URL.revokeObjectURL(pendingIconUrl)
|
||||||
|
embeddedIconUrl.value = undefined
|
||||||
|
pendingIconUrl = undefined
|
||||||
|
if (!blob) return
|
||||||
|
|
||||||
|
const candidateUrl = URL.createObjectURL(blob)
|
||||||
|
pendingIconUrl = candidateUrl
|
||||||
|
const image = new Image()
|
||||||
|
image.onload = () => {
|
||||||
|
if (currentValidationId !== validationId) return
|
||||||
|
pendingIconUrl = undefined
|
||||||
|
embeddedIconUrl.value = candidateUrl
|
||||||
|
}
|
||||||
|
image.onerror = () => {
|
||||||
|
if (currentValidationId !== validationId) return
|
||||||
|
pendingIconUrl = undefined
|
||||||
|
URL.revokeObjectURL(candidateUrl)
|
||||||
|
}
|
||||||
|
image.src = candidateUrl
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onScopeDispose(() => {
|
||||||
|
validationId += 1
|
||||||
|
if (embeddedIconUrl.value) URL.revokeObjectURL(embeddedIconUrl.value)
|
||||||
|
if (pendingIconUrl) URL.revokeObjectURL(pendingIconUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedSrc = computed(
|
||||||
|
() =>
|
||||||
|
props.src ??
|
||||||
|
embeddedIconUrl.value ??
|
||||||
|
props.embeddedIcon?.fallbackUrl ??
|
||||||
|
props.fallbackUrl ??
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Avatar
|
||||||
|
:src="resolvedSrc"
|
||||||
|
:alt="alt"
|
||||||
|
:tint-by="tintBy"
|
||||||
|
size="3rem"
|
||||||
|
no-shadow
|
||||||
|
class="rounded-2xl border border-surface-5"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from '@modrinth/assets'
|
import { ChevronDownIcon, ChevronUpIcon, InfoIcon } from '@modrinth/assets'
|
||||||
import { computed, getCurrentInstance, ref, toRef } from 'vue'
|
import { computed, getCurrentInstance, ref, toRef } from 'vue'
|
||||||
|
|
||||||
import Checkbox from '#ui/components/base/Checkbox.vue'
|
import Checkbox from '#ui/components/base/Checkbox.vue'
|
||||||
import { useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
import { useStickyObserver } from '#ui/composables/sticky-observer'
|
import { useStickyObserver } from '#ui/composables/sticky-observer'
|
||||||
import { useVirtualScroll } from '#ui/composables/virtual-scroll'
|
import { useVirtualScroll } from '#ui/composables/virtual-scroll'
|
||||||
import { commonMessages } from '#ui/utils/common-messages'
|
import { commonMessages } from '#ui/utils/common-messages'
|
||||||
@@ -17,6 +17,22 @@ import ContentCardItem from './ContentCardItem.vue'
|
|||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
enabledFor: {
|
||||||
|
id: 'content.enabled-for.label',
|
||||||
|
defaultMessage: 'Enabled for',
|
||||||
|
},
|
||||||
|
enabledForDescription: {
|
||||||
|
id: 'content.enabled-for.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Choose whether this content runs on the server, is sent to players, or is disabled in both places.',
|
||||||
|
},
|
||||||
|
sortEnabledFor: {
|
||||||
|
id: 'content.enabled-for.sort',
|
||||||
|
defaultMessage: 'Sort by where content is enabled',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: ContentCardTableItem[]
|
items: ContentCardTableItem[]
|
||||||
showSelection?: boolean
|
showSelection?: boolean
|
||||||
@@ -28,6 +44,8 @@ interface Props {
|
|||||||
hideHeader?: boolean
|
hideHeader?: boolean
|
||||||
flat?: boolean
|
flat?: boolean
|
||||||
showItemActions?: boolean
|
showItemActions?: boolean
|
||||||
|
showEnabledForColumn?: boolean
|
||||||
|
enabledForSortDirection?: ContentCardTableSortDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -40,6 +58,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
hideHeader: false,
|
hideHeader: false,
|
||||||
flat: false,
|
flat: false,
|
||||||
showItemActions: false,
|
showItemActions: false,
|
||||||
|
showEnabledForColumn: false,
|
||||||
|
enabledForSortDirection: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const stickyHeaderRef = ref<HTMLElement | null>(null)
|
const stickyHeaderRef = ref<HTMLElement | null>(null)
|
||||||
@@ -49,10 +69,12 @@ const selectedIds = defineModel<string[]>('selectedIds', { default: () => [] })
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:enabled': [id: string, value: boolean]
|
'update:enabled': [id: string, value: boolean]
|
||||||
|
'update:enabled-for': [id: string, side: 'server' | 'player', value: boolean]
|
||||||
delete: [id: string, event: MouseEvent]
|
delete: [id: string, event: MouseEvent]
|
||||||
update: [id: string]
|
update: [id: string]
|
||||||
switchVersion: [id: string]
|
switchVersion: [id: string]
|
||||||
sort: [column: ContentCardTableSortColumn, direction: ContentCardTableSortDirection]
|
sort: [column: ContentCardTableSortColumn, direction: ContentCardTableSortDirection]
|
||||||
|
'sort-enabled-for': [direction: ContentCardTableSortDirection]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Check if any actions are available
|
// Check if any actions are available
|
||||||
@@ -65,6 +87,9 @@ const hasSwitchVersionListener = computed(
|
|||||||
const hasEnabledListener = computed(
|
const hasEnabledListener = computed(
|
||||||
() => typeof instance?.vnode.props?.['onUpdate:enabled'] === 'function',
|
() => typeof instance?.vnode.props?.['onUpdate:enabled'] === 'function',
|
||||||
)
|
)
|
||||||
|
const hasEnabledForColumn = computed(
|
||||||
|
() => props.showEnabledForColumn || props.items.some((item) => item.enabledFor !== undefined),
|
||||||
|
)
|
||||||
|
|
||||||
const hasAnyActions = computed(() => {
|
const hasAnyActions = computed(() => {
|
||||||
// Check if there are listeners for actions
|
// Check if there are listeners for actions
|
||||||
@@ -80,17 +105,18 @@ const hasAnyActions = computed(() => {
|
|||||||
(item) =>
|
(item) =>
|
||||||
(item.overflowOptions && item.overflowOptions.length > 0) ||
|
(item.overflowOptions && item.overflowOptions.length > 0) ||
|
||||||
item.hasUpdate ||
|
item.hasUpdate ||
|
||||||
item.enabled !== undefined,
|
(item.enabled !== undefined && !item.hideToggle),
|
||||||
)
|
)
|
||||||
|
|
||||||
return hasListeners || hasItemActions || props.showItemActions
|
return hasListeners || hasItemActions || props.showItemActions
|
||||||
})
|
})
|
||||||
|
|
||||||
// Virtualization
|
// Virtualization
|
||||||
|
const itemHeight = computed(() => (hasEnabledForColumn.value ? 72 : 74))
|
||||||
const { listContainer, totalHeight, visibleRange, visibleTop, visibleItems } = useVirtualScroll(
|
const { listContainer, totalHeight, visibleRange, visibleTop, visibleItems } = useVirtualScroll(
|
||||||
toRef(props, 'items'),
|
toRef(props, 'items'),
|
||||||
{
|
{
|
||||||
itemHeight: 74,
|
itemHeight,
|
||||||
bufferSize: 5,
|
bufferSize: 5,
|
||||||
initialItemCount: 20,
|
initialItemCount: 20,
|
||||||
enabled: toRef(props, 'virtualized'),
|
enabled: toRef(props, 'virtualized'),
|
||||||
@@ -167,6 +193,12 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
|
|
||||||
emit('sort', column, newDirection)
|
emit('sort', column, newDirection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleEnabledForSort() {
|
||||||
|
const newDirection: ContentCardTableSortDirection =
|
||||||
|
props.enabledForSortDirection === 'desc' ? 'asc' : 'desc'
|
||||||
|
emit('sort-enabled-for', newDirection)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -191,7 +223,11 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
role="row"
|
role="row"
|
||||||
class="flex min-w-0 items-center gap-4"
|
class="flex min-w-0 items-center gap-4"
|
||||||
:class="
|
:class="
|
||||||
hasAnyActions ? 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none' : 'flex-1'
|
hasAnyActions
|
||||||
|
? hasEnabledForColumn
|
||||||
|
? 'flex-1 @[800px]:w-[340px] @[800px]:shrink-0 @[800px]:flex-none'
|
||||||
|
: 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||||
|
: 'flex-1'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -225,7 +261,53 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden @[800px]:flex" :class="hasAnyActions ? 'flex-1 min-w-0' : 'flex-1'">
|
<div
|
||||||
|
v-if="hasEnabledForColumn"
|
||||||
|
role="columnheader"
|
||||||
|
:aria-sort="
|
||||||
|
enabledForSortDirection
|
||||||
|
? enabledForSortDirection === 'asc'
|
||||||
|
? 'ascending'
|
||||||
|
: 'descending'
|
||||||
|
: 'none'
|
||||||
|
"
|
||||||
|
class="hidden w-[200px] shrink-0 items-center gap-1.5 font-semibold text-secondary @[800px]:flex"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cursor-pointer border-0 bg-transparent p-0 font-semibold text-secondary"
|
||||||
|
@click="handleEnabledForSort"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.enabledFor) }}
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-tooltip="formatMessage(messages.enabledForDescription)"
|
||||||
|
class="inline-flex size-4 cursor-help items-center justify-center"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<InfoIcon class="size-4" />
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex cursor-pointer items-center border-0 bg-transparent p-0 text-secondary"
|
||||||
|
:aria-label="formatMessage(messages.sortEnabledFor)"
|
||||||
|
@click="handleEnabledForSort"
|
||||||
|
>
|
||||||
|
<ChevronUpIcon v-if="enabledForSortDirection === 'asc'" class="size-4" />
|
||||||
|
<ChevronDownIcon v-else class="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="hidden @[800px]:flex"
|
||||||
|
:class="
|
||||||
|
hasAnyActions
|
||||||
|
? hasEnabledForColumn
|
||||||
|
? 'w-[250px] min-w-0 shrink-0'
|
||||||
|
: 'flex-1 min-w-0'
|
||||||
|
: 'flex-1'
|
||||||
|
"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-if="sortable"
|
v-if="sortable"
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
@@ -247,7 +329,12 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasAnyActions" role="columnheader" class="min-w-[160px] shrink-0 text-right">
|
<div
|
||||||
|
v-if="hasAnyActions"
|
||||||
|
role="columnheader"
|
||||||
|
class="shrink-0 text-right"
|
||||||
|
:class="hasEnabledForColumn ? 'w-[112px]' : 'min-w-[160px]'"
|
||||||
|
>
|
||||||
<span class="font-semibold text-secondary">{{
|
<span class="font-semibold text-secondary">{{
|
||||||
formatMessage(commonMessages.actionsLabel)
|
formatMessage(commonMessages.actionsLabel)
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -288,6 +375,8 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
:toggle-disabled="item.toggleDisabled"
|
:toggle-disabled="item.toggleDisabled"
|
||||||
:toggle-disabled-tooltip="item.toggleDisabledTooltip"
|
:toggle-disabled-tooltip="item.toggleDisabledTooltip"
|
||||||
:hide-toggle="item.hideToggle"
|
:hide-toggle="item.hideToggle"
|
||||||
|
:enabled-for="item.enabledFor"
|
||||||
|
:embedded-icon="item.embeddedIcon"
|
||||||
:show-checkbox="showSelection"
|
:show-checkbox="showSelection"
|
||||||
:hide-delete="hideDelete || item.hideDelete"
|
:hide-delete="hideDelete || item.hideDelete"
|
||||||
:hide-actions="!hasAnyActions"
|
:hide-actions="!hasAnyActions"
|
||||||
@@ -306,6 +395,9 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
toggleItemSelection(item.id, val ?? false, visibleRange.start + idx, event)
|
toggleItemSelection(item.id, val ?? false, visibleRange.start + idx, event)
|
||||||
"
|
"
|
||||||
@update:enabled="(val) => emit('update:enabled', item.id, val)"
|
@update:enabled="(val) => emit('update:enabled', item.id, val)"
|
||||||
|
@update:enabled-for="
|
||||||
|
(side, val) => emit('update:enabled-for', item.id, side, val)
|
||||||
|
"
|
||||||
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
|
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
|
||||||
@update="emit('update', item.id)"
|
@update="emit('update', item.id)"
|
||||||
v-on="
|
v-on="
|
||||||
@@ -356,6 +448,8 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
:toggle-disabled="item.toggleDisabled"
|
:toggle-disabled="item.toggleDisabled"
|
||||||
:toggle-disabled-tooltip="item.toggleDisabledTooltip"
|
:toggle-disabled-tooltip="item.toggleDisabledTooltip"
|
||||||
:hide-toggle="item.hideToggle"
|
:hide-toggle="item.hideToggle"
|
||||||
|
:enabled-for="item.enabledFor"
|
||||||
|
:embedded-icon="item.embeddedIcon"
|
||||||
:show-checkbox="showSelection"
|
:show-checkbox="showSelection"
|
||||||
:hide-delete="hideDelete || item.hideDelete"
|
:hide-delete="hideDelete || item.hideDelete"
|
||||||
:hide-actions="!hasAnyActions"
|
:hide-actions="!hasAnyActions"
|
||||||
@@ -371,6 +465,9 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
]"
|
]"
|
||||||
@select="(val, event) => toggleItemSelection(item.id, val ?? false, index, event)"
|
@select="(val, event) => toggleItemSelection(item.id, val ?? false, index, event)"
|
||||||
@update:enabled="(val) => emit('update:enabled', item.id, val)"
|
@update:enabled="(val) => emit('update:enabled', item.id, val)"
|
||||||
|
@update:enabled-for="
|
||||||
|
(side, val) => emit('update:enabled-for', item.id, side, val)
|
||||||
|
"
|
||||||
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
|
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
|
||||||
@update="emit('update', item.id)"
|
@update="emit('update', item.id)"
|
||||||
@switch-version="emit('switchVersion', item.id)"
|
@switch-version="emit('switchVersion', item.id)"
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { LockIcon, TriangleAlertIcon } from '@modrinth/assets'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
|
|
||||||
|
import type { ContentEnabledForState, ContentSide } from '../types'
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
server: {
|
||||||
|
id: 'content.enabled-for.server',
|
||||||
|
defaultMessage: 'Server',
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
id: 'content.enabled-for.player',
|
||||||
|
defaultMessage: 'Player',
|
||||||
|
},
|
||||||
|
locked: {
|
||||||
|
id: 'content.enabled-for.locked',
|
||||||
|
defaultMessage: 'Unlock the environment controls from the row menu to change this selection.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: ContentEnabledForState
|
||||||
|
disabled?: boolean
|
||||||
|
disabledTooltip?: string | null
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
disabledTooltip: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:model-value': [side: ContentSide, value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const sides: ContentSide[] = ['server', 'player']
|
||||||
|
const disabledSides = computed(() => new Set(props.modelValue.disabledSides ?? []))
|
||||||
|
|
||||||
|
function isSelected(side: ContentSide) {
|
||||||
|
return props.modelValue[side]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDisabled(side: ContentSide) {
|
||||||
|
return props.disabled || disabledSides.value.has(side)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(side: ContentSide) {
|
||||||
|
if (isDisabled(side)) return
|
||||||
|
emit('update:model-value', side, !isSelected(side))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex w-[200px] shrink-0 items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
v-for="side in sides"
|
||||||
|
:key="side"
|
||||||
|
v-tooltip="
|
||||||
|
isDisabled(side)
|
||||||
|
? (disabledTooltip ?? modelValue.disabledTooltip ?? formatMessage(messages.locked))
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
type="button"
|
||||||
|
class="flex h-8 items-center rounded-xl border border-solid px-3 text-sm font-medium transition-[background-color,border-color,color,opacity,transform] duration-100 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-brand-shadow"
|
||||||
|
:class="[
|
||||||
|
isSelected(side)
|
||||||
|
? 'border-brand bg-brand-highlight text-brand'
|
||||||
|
: 'border-surface-5 bg-transparent text-primary hover:bg-surface-3',
|
||||||
|
isDisabled(side) ? 'cursor-not-allowed opacity-50' : 'cursor-pointer active:scale-[0.97]',
|
||||||
|
]"
|
||||||
|
:aria-pressed="isSelected(side)"
|
||||||
|
:aria-disabled="isDisabled(side)"
|
||||||
|
@click="toggle(side)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages[side]) }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="modelValue.locked"
|
||||||
|
v-tooltip="modelValue.disabledTooltip ?? formatMessage(messages.locked)"
|
||||||
|
class="inline-flex size-5 shrink-0 cursor-help items-center justify-center text-secondary"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<LockIcon class="size-5" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="modelValue.warningTooltip"
|
||||||
|
v-tooltip="modelValue.warningTooltip"
|
||||||
|
class="inline-flex size-5 shrink-0 cursor-help items-center justify-center"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<TriangleAlertIcon class="size-5 text-orange" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+71
-11
@@ -30,7 +30,7 @@ import {
|
|||||||
} from '#ui/utils/common-messages'
|
} from '#ui/utils/common-messages'
|
||||||
|
|
||||||
import { getClientWarningType } from '../../composables/content-filtering'
|
import { getClientWarningType } from '../../composables/content-filtering'
|
||||||
import type { ContentCardProject, ContentCardTableItem, ContentItem } from '../../types'
|
import type { ContentCardProject, ContentCardTableItem, ContentItem, ContentSide } from '../../types'
|
||||||
import ContentCardTable from '../ContentCardTable.vue'
|
import ContentCardTable from '../ContentCardTable.vue'
|
||||||
import ContentSelectionBar from '../ContentSelectionBar.vue'
|
import ContentSelectionBar from '../ContentSelectionBar.vue'
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ interface Props {
|
|||||||
sourceName?: string
|
sourceName?: string
|
||||||
sourceIconUrl?: string
|
sourceIconUrl?: string
|
||||||
enableToggle?: boolean
|
enableToggle?: boolean
|
||||||
|
enableEnabledFor?: boolean
|
||||||
actionDisabled?: boolean
|
actionDisabled?: boolean
|
||||||
actionDisabledTooltip?: string | null
|
actionDisabledTooltip?: string | null
|
||||||
getOverflowOptions?: (item: ContentItem) => OverflowMenuOption[]
|
getOverflowOptions?: (item: ContentItem) => OverflowMenuOption[]
|
||||||
@@ -54,6 +55,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
sourceName: undefined,
|
sourceName: undefined,
|
||||||
sourceIconUrl: undefined,
|
sourceIconUrl: undefined,
|
||||||
enableToggle: false,
|
enableToggle: false,
|
||||||
|
enableEnabledFor: false,
|
||||||
actionDisabled: false,
|
actionDisabled: false,
|
||||||
actionDisabledTooltip: undefined,
|
actionDisabledTooltip: undefined,
|
||||||
getOverflowOptions: undefined,
|
getOverflowOptions: undefined,
|
||||||
@@ -63,6 +65,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:enabled': [item: ContentItem, value: boolean]
|
'update:enabled': [item: ContentItem, value: boolean]
|
||||||
|
'update:enabled-for': [item: ContentItem, side: ContentSide, value: boolean]
|
||||||
'bulk:enable': [items: ContentItem[]]
|
'bulk:enable': [items: ContentItem[]]
|
||||||
'bulk:disable': [items: ContentItem[]]
|
'bulk:disable': [items: ContentItem[]]
|
||||||
hide: []
|
hide: []
|
||||||
@@ -109,6 +112,14 @@ const messages = defineMessages({
|
|||||||
id: 'instances.managed-content-modal.download-file',
|
id: 'instances.managed-content-modal.download-file',
|
||||||
defaultMessage: 'Download File',
|
defaultMessage: 'Download File',
|
||||||
},
|
},
|
||||||
|
enabledFor: {
|
||||||
|
id: 'content.enabled-for.label',
|
||||||
|
defaultMessage: 'Enabled for',
|
||||||
|
},
|
||||||
|
pleaseWait: {
|
||||||
|
id: 'content.enabled-for.please-wait',
|
||||||
|
defaultMessage: 'Please wait',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface ManagedContentModalState {
|
export interface ManagedContentModalState {
|
||||||
@@ -262,6 +273,7 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
|
|||||||
icon_url: item.embedded_metadata?.icon_url ?? null,
|
icon_url: item.embedded_metadata?.icon_url ?? null,
|
||||||
},
|
},
|
||||||
projectLink: !item.external && item.project?.id ? `/project/${item.project.id}` : undefined,
|
projectLink: !item.external && item.project?.id ? `/project/${item.project.id}` : undefined,
|
||||||
|
embeddedIcon: item.embeddedIcon,
|
||||||
version: item.version ?? {
|
version: item.version ?? {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
version_number: contentVersionLabel(item),
|
version_number: contentVersionLabel(item),
|
||||||
@@ -283,15 +295,24 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
...(props.enableToggle ? { enabled: item.enabled } : {}),
|
...(props.enableToggle ? { enabled: item.enabled } : {}),
|
||||||
|
...(props.enableEnabledFor ? { enabledFor: item.enabledFor } : {}),
|
||||||
locked: item.locked,
|
locked: item.locked,
|
||||||
installing: item.installing === true,
|
installing: item.installing === true,
|
||||||
toggleDisabled: props.actionDisabled,
|
toggleDisabled: props.actionDisabled,
|
||||||
toggleDisabledTooltip: props.actionDisabled ? props.actionDisabledTooltip : undefined,
|
toggleDisabledTooltip: props.actionDisabled ? props.actionDisabledTooltip : undefined,
|
||||||
isClientOnly: getClientWarningType(item, props.showEnvironmentWarnings) !== null,
|
isClientOnly:
|
||||||
clientWarning: getClientWarningType(item, props.showEnvironmentWarnings),
|
!props.enableEnabledFor &&
|
||||||
|
getClientWarningType(item, props.showEnvironmentWarnings) !== null,
|
||||||
|
clientWarning: props.enableEnabledFor
|
||||||
|
? null
|
||||||
|
: getClientWarningType(item, props.showEnvironmentWarnings),
|
||||||
disabled:
|
disabled:
|
||||||
props.actionDisabled || disabledIds.value.has(item.file_name) || item.installing === true,
|
props.actionDisabled || disabledIds.value.has(item.file_name) || item.installing === true,
|
||||||
disabledTooltip: props.actionDisabled ? props.actionDisabledTooltip : undefined,
|
disabledTooltip: props.actionDisabled
|
||||||
|
? props.actionDisabledTooltip
|
||||||
|
: disabledIds.value.has(item.file_name)
|
||||||
|
? formatMessage(messages.pleaseWait)
|
||||||
|
: undefined,
|
||||||
overflowOptions: [
|
overflowOptions: [
|
||||||
...(props.switchVersion && !item.locked
|
...(props.switchVersion && !item.locked
|
||||||
? [
|
? [
|
||||||
@@ -329,7 +350,9 @@ const externalUrls = computed(() => {
|
|||||||
return urls
|
return urls
|
||||||
})
|
})
|
||||||
const hasExternalSlicerUrls = computed(() => Object.keys(externalSlicerUrls.value).length > 0)
|
const hasExternalSlicerUrls = computed(() => Object.keys(externalSlicerUrls.value).length > 0)
|
||||||
const showTableActions = computed(() => props.enableToggle || hasExternalSlicerUrls.value)
|
const showTableActions = computed(
|
||||||
|
() => props.enableToggle || props.enableEnabledFor || hasExternalSlicerUrls.value,
|
||||||
|
)
|
||||||
|
|
||||||
function getTypeIcon(type: string) {
|
function getTypeIcon(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -372,6 +395,13 @@ function handleEnabledChange(id: string, value: boolean) {
|
|||||||
emit('update:enabled', item, value)
|
emit('update:enabled', item, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleEnabledForChange(id: string, side: ContentSide, value: boolean) {
|
||||||
|
if (props.actionDisabled || !props.enableEnabledFor) return
|
||||||
|
const item = items.value.find((item) => item.id === id)
|
||||||
|
if (!item) return
|
||||||
|
emit('update:enabled-for', item, side, value)
|
||||||
|
}
|
||||||
|
|
||||||
function bulkEnable() {
|
function bulkEnable() {
|
||||||
if (props.actionDisabled) return
|
if (props.actionDisabled) return
|
||||||
emit('bulk:enable', [...toggleableSelectedItems.value])
|
emit('bulk:enable', [...toggleableSelectedItems.value])
|
||||||
@@ -474,8 +504,16 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
|||||||
<template>
|
<template>
|
||||||
<NewModal
|
<NewModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:max-width="'min(928px, calc(95vw - 10rem))'"
|
:max-width="
|
||||||
:width="'min(928px, calc(95vw - 10rem))'"
|
props.enableEnabledFor
|
||||||
|
? 'min(1080px, calc(95vw - 4rem))'
|
||||||
|
: 'min(928px, calc(95vw - 10rem))'
|
||||||
|
"
|
||||||
|
:width="
|
||||||
|
props.enableEnabledFor
|
||||||
|
? 'min(1080px, calc(95vw - 4rem))'
|
||||||
|
: 'min(928px, calc(95vw - 10rem))'
|
||||||
|
"
|
||||||
:on-hide="handleHide"
|
:on-hide="handleHide"
|
||||||
no-padding
|
no-padding
|
||||||
>
|
>
|
||||||
@@ -545,7 +583,9 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
|||||||
class="flex min-w-0 items-center gap-4"
|
class="flex min-w-0 items-center gap-4"
|
||||||
:class="
|
:class="
|
||||||
showTableActions
|
showTableActions
|
||||||
? 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
? props.enableEnabledFor
|
||||||
|
? 'flex-1 @[800px]:w-[340px] @[800px]:shrink-0 @[800px]:flex-none'
|
||||||
|
: 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||||
: 'flex-1'
|
: 'flex-1'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -561,15 +601,33 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
|||||||
formatMessage(commonMessages.projectLabel)
|
formatMessage(commonMessages.projectLabel)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.enableEnabledFor"
|
||||||
|
class="hidden w-[200px] shrink-0 @[800px]:flex"
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-secondary">{{
|
||||||
|
formatMessage(messages.enabledFor)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="hidden @[800px]:flex"
|
class="hidden @[800px]:flex"
|
||||||
:class="showTableActions ? 'flex-1 min-w-0' : 'flex-1'"
|
:class="
|
||||||
|
showTableActions
|
||||||
|
? props.enableEnabledFor
|
||||||
|
? 'w-[250px] min-w-0 shrink-0'
|
||||||
|
: 'flex-1 min-w-0'
|
||||||
|
: 'flex-1'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-secondary">{{
|
<span class="font-semibold text-secondary">{{
|
||||||
formatMessage(commonMessages.versionLabel)
|
formatMessage(commonMessages.versionLabel)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showTableActions" class="min-w-[160px] shrink-0 text-right">
|
<div
|
||||||
|
v-if="showTableActions"
|
||||||
|
class="shrink-0 text-right"
|
||||||
|
:class="props.enableEnabledFor ? 'w-[112px]' : 'min-w-[160px]'"
|
||||||
|
>
|
||||||
<span class="font-semibold text-secondary">{{
|
<span class="font-semibold text-secondary">{{
|
||||||
formatMessage(commonMessages.actionsLabel)
|
formatMessage(commonMessages.actionsLabel)
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -580,7 +638,8 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
|||||||
v-model:selected-ids="selectedIds"
|
v-model:selected-ids="selectedIds"
|
||||||
:items="tableItems"
|
:items="tableItems"
|
||||||
:show-selection="props.enableToggle"
|
:show-selection="props.enableToggle"
|
||||||
:show-item-actions="hasExternalSlicerUrls"
|
:show-enabled-for-column="props.enableEnabledFor"
|
||||||
|
:show-item-actions="props.enableEnabledFor || hasExternalSlicerUrls"
|
||||||
hide-delete
|
hide-delete
|
||||||
hide-header
|
hide-header
|
||||||
flat
|
flat
|
||||||
@@ -589,6 +648,7 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
|
|||||||
? { 'update:enabled': (id: string, val: boolean) => handleEnabledChange(id, val) }
|
? { 'update:enabled': (id: string, val: boolean) => handleEnabledChange(id, val) }
|
||||||
: {}
|
: {}
|
||||||
"
|
"
|
||||||
|
@update:enabled-for="handleEnabledForChange"
|
||||||
>
|
>
|
||||||
<template #itemTitleBadges="{ item }">
|
<template #itemTitleBadges="{ item }">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
OrganizationIcon,
|
OrganizationIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
ServerIcon,
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
TextCursorInputIcon,
|
TextCursorInputIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
@@ -110,6 +111,14 @@ const messages = defineMessages({
|
|||||||
id: 'content.page-layout.sort.date-added-oldest',
|
id: 'content.page-layout.sort.date-added-oldest',
|
||||||
defaultMessage: 'Oldest first',
|
defaultMessage: 'Oldest first',
|
||||||
},
|
},
|
||||||
|
sortEnabledForDescending: {
|
||||||
|
id: 'content.page-layout.sort.enabled-for-descending',
|
||||||
|
defaultMessage: 'Enabled for (most first)',
|
||||||
|
},
|
||||||
|
sortEnabledForAscending: {
|
||||||
|
id: 'content.page-layout.sort.enabled-for-ascending',
|
||||||
|
defaultMessage: 'Enabled for (least first)',
|
||||||
|
},
|
||||||
filter: {
|
filter: {
|
||||||
id: 'content.page-layout.filter.add',
|
id: 'content.page-layout.filter.add',
|
||||||
defaultMessage: 'Filter',
|
defaultMessage: 'Filter',
|
||||||
@@ -179,44 +188,76 @@ function getItemId(item: ContentItem) {
|
|||||||
return ctx.getItemId?.(item) ?? item.file_path ?? item.file_name ?? item.id
|
return ctx.getItemId?.(item) ?? item.file_path ?? item.file_name ?? item.id
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'date-added-newest' | 'date-added-oldest'
|
type SortMode =
|
||||||
|
| 'alphabetical-asc'
|
||||||
|
| 'alphabetical-desc'
|
||||||
|
| 'date-added-newest'
|
||||||
|
| 'date-added-oldest'
|
||||||
|
| 'enabled-for-desc'
|
||||||
|
| 'enabled-for-asc'
|
||||||
|
const defaultSortMode: SortMode = ctx.setEnabledFor ? 'enabled-for-desc' : 'alphabetical-asc'
|
||||||
const sortMode = ctx.filterPersistKey
|
const sortMode = ctx.filterPersistKey
|
||||||
? useSessionStorage<SortMode>(`content-sort:${ctx.filterPersistKey}`, 'alphabetical-asc')
|
? useSessionStorage<SortMode>(`content-sort:${ctx.filterPersistKey}`, defaultSortMode)
|
||||||
: ref<SortMode>('alphabetical-asc')
|
: ref<SortMode>(defaultSortMode)
|
||||||
|
|
||||||
const sortLabels: Record<SortMode, () => string> = {
|
const sortLabels: Record<SortMode, () => string> = {
|
||||||
'alphabetical-asc': () => formatMessage(messages.sortAlphabeticalAscending),
|
'alphabetical-asc': () => formatMessage(messages.sortAlphabeticalAscending),
|
||||||
'alphabetical-desc': () => formatMessage(messages.sortAlphabeticalDescending),
|
'alphabetical-desc': () => formatMessage(messages.sortAlphabeticalDescending),
|
||||||
'date-added-newest': () => formatMessage(messages.sortDateAddedNewest),
|
'date-added-newest': () => formatMessage(messages.sortDateAddedNewest),
|
||||||
'date-added-oldest': () => formatMessage(messages.sortDateAddedOldest),
|
'date-added-oldest': () => formatMessage(messages.sortDateAddedOldest),
|
||||||
|
'enabled-for-desc': () => formatMessage(messages.sortEnabledForDescending),
|
||||||
|
'enabled-for-asc': () => formatMessage(messages.sortEnabledForAscending),
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortOptions = computed<OverflowMenuOption[]>(() => [
|
const sortOptions = computed<OverflowMenuOption[]>(() => {
|
||||||
{
|
const options: OverflowMenuOption[] = [
|
||||||
id: 'alphabetical-asc',
|
{
|
||||||
label: formatMessage(messages.sortAlphabeticalAscending),
|
id: 'alphabetical-asc',
|
||||||
icon: ArrowDownAZIcon,
|
label: formatMessage(messages.sortAlphabeticalAscending),
|
||||||
action: () => (sortMode.value = 'alphabetical-asc'),
|
icon: ArrowDownAZIcon,
|
||||||
},
|
action: () => (sortMode.value = 'alphabetical-asc'),
|
||||||
{
|
},
|
||||||
id: 'alphabetical-desc',
|
{
|
||||||
label: formatMessage(messages.sortAlphabeticalDescending),
|
id: 'alphabetical-desc',
|
||||||
icon: ArrowUpZAIcon,
|
label: formatMessage(messages.sortAlphabeticalDescending),
|
||||||
action: () => (sortMode.value = 'alphabetical-desc'),
|
icon: ArrowUpZAIcon,
|
||||||
},
|
action: () => (sortMode.value = 'alphabetical-desc'),
|
||||||
{
|
},
|
||||||
id: 'date-added-newest',
|
{
|
||||||
label: formatMessage(messages.sortDateAddedNewest),
|
id: 'date-added-newest',
|
||||||
icon: ClockArrowDownIcon,
|
label: formatMessage(messages.sortDateAddedNewest),
|
||||||
action: () => (sortMode.value = 'date-added-newest'),
|
icon: ClockArrowDownIcon,
|
||||||
},
|
action: () => (sortMode.value = 'date-added-newest'),
|
||||||
{
|
},
|
||||||
id: 'date-added-oldest',
|
{
|
||||||
label: formatMessage(messages.sortDateAddedOldest),
|
id: 'date-added-oldest',
|
||||||
icon: ClockArrowUpIcon,
|
label: formatMessage(messages.sortDateAddedOldest),
|
||||||
action: () => (sortMode.value = 'date-added-oldest'),
|
icon: ClockArrowUpIcon,
|
||||||
},
|
action: () => (sortMode.value = 'date-added-oldest'),
|
||||||
])
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (ctx.setEnabledFor) {
|
||||||
|
options.push(
|
||||||
|
{
|
||||||
|
id: 'enabled-for-desc',
|
||||||
|
label: formatMessage(messages.sortEnabledForDescending),
|
||||||
|
action: () => (sortMode.value = 'enabled-for-desc'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'enabled-for-asc',
|
||||||
|
label: formatMessage(messages.sortEnabledForAscending),
|
||||||
|
action: () => (sortMode.value = 'enabled-for-asc'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
function getEnabledForRank(item: ContentItem) {
|
||||||
|
return Number(item.enabledFor?.server) * 2 + Number(item.enabledFor?.player)
|
||||||
|
}
|
||||||
|
|
||||||
const sortedItems = computed(() => {
|
const sortedItems = computed(() => {
|
||||||
const items = [...ctx.items.value]
|
const items = [...ctx.items.value]
|
||||||
@@ -242,6 +283,16 @@ const sortedItems = computed(() => {
|
|||||||
const dateB = b.date_added ?? ''
|
const dateB = b.date_added ?? ''
|
||||||
return dateA.localeCompare(dateB) || a.file_name.localeCompare(b.file_name)
|
return dateA.localeCompare(dateB) || a.file_name.localeCompare(b.file_name)
|
||||||
})
|
})
|
||||||
|
case 'enabled-for-desc':
|
||||||
|
return items.sort(
|
||||||
|
(a, b) =>
|
||||||
|
getEnabledForRank(b) - getEnabledForRank(a) || a.file_name.localeCompare(b.file_name),
|
||||||
|
)
|
||||||
|
case 'enabled-for-asc':
|
||||||
|
return items.sort(
|
||||||
|
(a, b) =>
|
||||||
|
getEnabledForRank(a) - getEnabledForRank(b) || a.file_name.localeCompare(b.file_name),
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return items.sort((a, b) => {
|
return items.sort((a, b) => {
|
||||||
const nameA = a.project?.title ?? a.file_name
|
const nameA = a.project?.title ?? a.file_name
|
||||||
@@ -424,14 +475,22 @@ const tableItems = computed<ContentCardTableItem[]>(() => {
|
|||||||
const base = ctx.mapToTableItem(item)
|
const base = ctx.mapToTableItem(item)
|
||||||
const id = getItemId(item)
|
const id = getItemId(item)
|
||||||
const locked = base.locked ?? item.locked ?? false
|
const locked = base.locked ?? item.locked ?? false
|
||||||
const clientWarning = getClientWarningType(item, ctx.showEnvironmentWarnings)
|
const clientWarning = base.enabledFor
|
||||||
|
? null
|
||||||
|
: getClientWarningType(item, ctx.showEnvironmentWarnings)
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
id,
|
id,
|
||||||
locked,
|
locked,
|
||||||
disabled:
|
disabled:
|
||||||
isChanging(id) || ctx.isBusy.value || isBulkOperating.value || item.installing === true,
|
isChanging(id) || ctx.isBusy.value || isBulkOperating.value || item.installing === true,
|
||||||
disabledTooltip: ctx.isBusy.value ? (ctx.busyMessage?.value ?? null) : null,
|
disabledTooltip: ctx.isBusy.value
|
||||||
|
? (ctx.busyMessage?.value ?? null)
|
||||||
|
: isChanging(id)
|
||||||
|
? formatMessage(messages.pleaseWait)
|
||||||
|
: item.installing
|
||||||
|
? formatMessage(commonMessages.installingLabel)
|
||||||
|
: null,
|
||||||
toggleDisabled: ctx.isBusy.value || base.toggleDisabled,
|
toggleDisabled: ctx.isBusy.value || base.toggleDisabled,
|
||||||
toggleDisabledTooltip: ctx.isBusy.value
|
toggleDisabledTooltip: ctx.isBusy.value
|
||||||
? (ctx.busyMessage?.value ?? null)
|
? (ctx.busyMessage?.value ?? null)
|
||||||
@@ -505,7 +564,10 @@ function canDeleteItem(item: ContentItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canToggleItem(item: ContentItem) {
|
function canToggleItem(item: ContentItem) {
|
||||||
return ctx.canToggleItem?.(item) ?? true
|
return (
|
||||||
|
ctx.canToggleItem?.(item) ??
|
||||||
|
!!(ctx.toggleEnabled || ctx.bulkEnableItems || ctx.bulkDisableItems)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletableSelectedItems = computed(() => selectedItems.value.filter(canDeleteItem))
|
const deletableSelectedItems = computed(() => selectedItems.value.filter(canDeleteItem))
|
||||||
@@ -617,6 +679,7 @@ async function disableItemsWithoutWarning(items: ContentItem[]) {
|
|||||||
await ctx.bulkDisableItems(items)
|
await ctx.bulkDisableItems(items)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!ctx.toggleEnabled) return
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const id = getItemId(item)
|
const id = getItemId(item)
|
||||||
@@ -730,7 +793,7 @@ async function confirmDisable() {
|
|||||||
if (ctx.bulkDisableItems) {
|
if (ctx.bulkDisableItems) {
|
||||||
await ctx.bulkDisableItems(itemsToDisable)
|
await ctx.bulkDisableItems(itemsToDisable)
|
||||||
} else {
|
} else {
|
||||||
await ctx.toggleEnabled(item)
|
await ctx.toggleEnabled?.(item)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
unmarkChanging(id)
|
unmarkChanging(id)
|
||||||
@@ -744,7 +807,7 @@ async function confirmDisable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggleEnabledById(id: string, _value: boolean) {
|
async function handleToggleEnabledById(id: string, _value: boolean) {
|
||||||
if (ctx.isBusy.value) return
|
if (ctx.isBusy.value || !ctx.toggleEnabled) return
|
||||||
const item = ctx.items.value.find((i) => getItemId(i) === id)
|
const item = ctx.items.value.find((i) => getItemId(i) === id)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
if (!canToggleItem(item)) return
|
if (!canToggleItem(item)) return
|
||||||
@@ -782,7 +845,8 @@ async function bulkEnable() {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await runBulk('enable', items, (item) => ctx.toggleEnabled(item), { onComplete: clearSelection })
|
if (!ctx.toggleEnabled) return
|
||||||
|
await runBulk('enable', items, (item) => ctx.toggleEnabled!(item), { onComplete: clearSelection })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkDisable() {
|
async function bulkDisable() {
|
||||||
@@ -805,6 +869,27 @@ function handleSwitchVersionById(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSetEnabledForById(
|
||||||
|
id: string,
|
||||||
|
side: 'server' | 'player',
|
||||||
|
enabled: boolean,
|
||||||
|
) {
|
||||||
|
if (ctx.isBusy.value || !ctx.setEnabledFor) return
|
||||||
|
const item = ctx.items.value.find((candidate) => getItemId(candidate) === id)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
markChanging(id)
|
||||||
|
try {
|
||||||
|
await ctx.setEnabledFor(item, side, enabled)
|
||||||
|
} finally {
|
||||||
|
unmarkChanging(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnabledForSort(direction: 'asc' | 'desc') {
|
||||||
|
sortMode.value = direction === 'asc' ? 'enabled-for-asc' : 'enabled-for-desc'
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk updating
|
// Bulk updating
|
||||||
const confirmBulkUpdateModal = ref<InstanceType<typeof ConfirmBulkUpdateModal>>()
|
const confirmBulkUpdateModal = ref<InstanceType<typeof ConfirmBulkUpdateModal>>()
|
||||||
const pendingBulkUpdateItems = ref<ContentItem[]>([])
|
const pendingBulkUpdateItems = ref<ContentItem[]>([])
|
||||||
@@ -1006,6 +1091,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
|||||||
<ArrowUpZAIcon v-if="sortMode === 'alphabetical-desc'" />
|
<ArrowUpZAIcon v-if="sortMode === 'alphabetical-desc'" />
|
||||||
<ClockArrowDownIcon v-else-if="sortMode === 'date-added-newest'" />
|
<ClockArrowDownIcon v-else-if="sortMode === 'date-added-newest'" />
|
||||||
<ClockArrowUpIcon v-else-if="sortMode === 'date-added-oldest'" />
|
<ClockArrowUpIcon v-else-if="sortMode === 'date-added-oldest'" />
|
||||||
|
<ServerIcon v-else-if="sortMode.startsWith('enabled-for')" />
|
||||||
<ArrowDownAZIcon v-else />
|
<ArrowDownAZIcon v-else />
|
||||||
{{ sortLabels[sortMode]() }}
|
{{ sortLabels[sortMode]() }}
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
@@ -1219,7 +1305,17 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
|||||||
v-model:selected-ids="selectedIds"
|
v-model:selected-ids="selectedIds"
|
||||||
:items="tableItems"
|
:items="tableItems"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
|
:show-enabled-for-column="!!ctx.setEnabledFor"
|
||||||
|
:enabled-for-sort-direction="
|
||||||
|
sortMode === 'enabled-for-asc'
|
||||||
|
? 'asc'
|
||||||
|
: sortMode === 'enabled-for-desc'
|
||||||
|
? 'desc'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
@update:enabled="handleToggleEnabledById"
|
@update:enabled="handleToggleEnabledById"
|
||||||
|
@update:enabled-for="handleSetEnabledForById"
|
||||||
|
@sort-enabled-for="handleEnabledForSort"
|
||||||
@delete="handleDeleteById"
|
@delete="handleDeleteById"
|
||||||
@update="handleUpdateById"
|
@update="handleUpdateById"
|
||||||
@switch-version="handleSwitchVersionById"
|
@switch-version="handleSwitchVersionById"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
ContentActionWarning,
|
ContentActionWarning,
|
||||||
ContentCardTableItem,
|
ContentCardTableItem,
|
||||||
ContentItem,
|
ContentItem,
|
||||||
|
ContentSide,
|
||||||
ManagedContentCardData,
|
ManagedContentCardData,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
@@ -46,7 +47,8 @@ export interface ContentManagerContext {
|
|||||||
contentTypeLabel: Ref<string> | ComputedRef<string>
|
contentTypeLabel: Ref<string> | ComputedRef<string>
|
||||||
|
|
||||||
// Core actions
|
// Core actions
|
||||||
toggleEnabled: (item: ContentItem) => Promise<void>
|
toggleEnabled?: (item: ContentItem) => Promise<void>
|
||||||
|
setEnabledFor?: (item: ContentItem, side: ContentSide, enabled: boolean) => Promise<void>
|
||||||
deleteItem: (item: ContentItem) => Promise<void>
|
deleteItem: (item: ContentItem) => Promise<void>
|
||||||
refresh: () => Promise<void>
|
refresh: () => Promise<void>
|
||||||
browse: () => void
|
browse: () => void
|
||||||
|
|||||||
@@ -50,6 +50,23 @@ export interface EmbeddedContentMetadata {
|
|||||||
icon_url?: string | null
|
icon_url?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ContentSide = 'server' | 'player'
|
||||||
|
|
||||||
|
export interface ContentEnabledForState {
|
||||||
|
server: boolean
|
||||||
|
player: boolean
|
||||||
|
locked: boolean
|
||||||
|
disabledSides?: ContentSide[]
|
||||||
|
disabledTooltip?: string | null
|
||||||
|
warningTooltip?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentCardEmbeddedIcon {
|
||||||
|
queryKey: readonly unknown[]
|
||||||
|
queryFn: () => Promise<Blob>
|
||||||
|
fallbackUrl?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContentCardTableItem {
|
export interface ContentCardTableItem {
|
||||||
id: string
|
id: string
|
||||||
project: ContentCardProject
|
project: ContentCardProject
|
||||||
@@ -74,6 +91,8 @@ export interface ContentCardTableItem {
|
|||||||
hideDelete?: boolean
|
hideDelete?: boolean
|
||||||
hideSwitchVersion?: boolean
|
hideSwitchVersion?: boolean
|
||||||
overflowOptions?: OverflowMenuOption[]
|
overflowOptions?: OverflowMenuOption[]
|
||||||
|
enabledFor?: ContentEnabledForState
|
||||||
|
embeddedIcon?: ContentCardEmbeddedIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContentCardTableSortColumn = 'project' | 'version'
|
export type ContentCardTableSortColumn = 'project' | 'version'
|
||||||
|
|||||||
@@ -500,6 +500,26 @@ async function uploadLocalModpackWithSoftOverride() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function disableAddonsEverywhere(addons: Archon.Content.v1.Addon[]) {
|
||||||
|
await Promise.all(
|
||||||
|
addons.flatMap((addon) => {
|
||||||
|
const request: Archon.Content.v1.SetAddonEnabledRequest = {
|
||||||
|
kind: addon.kind,
|
||||||
|
filename: addon.filename,
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...(!addon.disabled_server
|
||||||
|
? [client.archon.content_v1.setAddonEnabledServer(serverId, worldId.value!, request)]
|
||||||
|
: []),
|
||||||
|
...(!addon.disabled_player
|
||||||
|
? [client.archon.content_v1.setAddonEnabledPlayer(serverId, worldId.value!, request)]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
provideInstallationSettings({
|
provideInstallationSettings({
|
||||||
closeSettings: serverSettings.closeModal,
|
closeSettings: serverSettings.closeModal,
|
||||||
afterSave: async () => {
|
afterSave: async () => {
|
||||||
@@ -899,12 +919,12 @@ provideInstallationSettings({
|
|||||||
if (setupActionDisabled.value) return
|
if (setupActionDisabled.value) return
|
||||||
debug('disableAllContent: fetching all addons')
|
debug('disableAllContent: fetching all addons')
|
||||||
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
|
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
|
||||||
const items = (addons.addons ?? [])
|
const activeAddons = (addons.addons ?? []).filter(
|
||||||
.filter((a) => !a.disabled)
|
(addon) => !addon.disabled_server || !addon.disabled_player,
|
||||||
.map((a) => ({ kind: a.kind, filename: a.filename }))
|
)
|
||||||
if (items.length > 0) {
|
if (activeAddons.length > 0) {
|
||||||
debug('disableAllContent: disabling', items.length, 'addons')
|
debug('disableAllContent: disabling', activeAddons.length, 'addons')
|
||||||
await client.archon.content_v1.disableAddons(serverId, worldId.value!, items)
|
await disableAddonsEverywhere(activeAddons)
|
||||||
}
|
}
|
||||||
debug('disableAllContent: done')
|
debug('disableAllContent: done')
|
||||||
},
|
},
|
||||||
@@ -913,7 +933,9 @@ provideInstallationSettings({
|
|||||||
if (setupActionDisabled.value) return
|
if (setupActionDisabled.value) return
|
||||||
debug('disableIncompatibleContent: fetching addons')
|
debug('disableIncompatibleContent: fetching addons')
|
||||||
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
|
const addons = await client.archon.content_v1.getAddons(serverId, worldId.value!)
|
||||||
const activeAddons = (addons.addons ?? []).filter((a) => !a.disabled)
|
const activeAddons = (addons.addons ?? []).filter(
|
||||||
|
(addon) => !addon.disabled_server || !addon.disabled_player,
|
||||||
|
)
|
||||||
|
|
||||||
const modrinthAddons = activeAddons.filter((a) => a.version?.id)
|
const modrinthAddons = activeAddons.filter((a) => a.version?.id)
|
||||||
const customAddons = activeAddons.filter((a) => !a.version?.id)
|
const customAddons = activeAddons.filter((a) => !a.version?.id)
|
||||||
@@ -936,7 +958,14 @@ provideInstallationSettings({
|
|||||||
|
|
||||||
if (incompatibleItems.length > 0) {
|
if (incompatibleItems.length > 0) {
|
||||||
debug('disableIncompatibleContent: disabling', incompatibleItems.length, 'addons')
|
debug('disableIncompatibleContent: disabling', incompatibleItems.length, 'addons')
|
||||||
await client.archon.content_v1.disableAddons(serverId, worldId.value!, incompatibleItems)
|
const incompatibleKeys = new Set(
|
||||||
|
incompatibleItems.map((item) => `${item.kind}:${item.filename}`),
|
||||||
|
)
|
||||||
|
await disableAddonsEverywhere(
|
||||||
|
activeAddons.filter((addon) =>
|
||||||
|
incompatibleKeys.has(`${addon.kind}:${addon.filename}`),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
debug('disableIncompatibleContent: done')
|
debug('disableIncompatibleContent: done')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Archon, type Labrinth, ModrinthApiError } from '@modrinth/api-client'
|
import { type Archon, type Labrinth, ModrinthApiError } from '@modrinth/api-client'
|
||||||
import { ClipboardCopyIcon } from '@modrinth/assets'
|
import { ClipboardCopyIcon, LockIcon, LockOpenIcon } from '@modrinth/assets'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import ReadyTransition from '#ui/components/base/ReadyTransition.vue'
|
import ReadyTransition from '#ui/components/base/ReadyTransition.vue'
|
||||||
|
import type { OverflowMenuOption } from '#ui/components/base/buttons'
|
||||||
import UnknownFileWarningModal from '#ui/components/modal/UnknownFileWarningModal.vue'
|
import UnknownFileWarningModal from '#ui/components/modal/UnknownFileWarningModal.vue'
|
||||||
import { useUploadSessionUpload } from '#ui/composables/hosting/kyros-session-upload'
|
import { useUploadSessionUpload } from '#ui/composables/hosting/kyros-session-upload'
|
||||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
@@ -34,7 +35,7 @@ import ContentUpdaterModal from '../../../shared/content-tab/components/modals/c
|
|||||||
import ContentPageLayout from '../../../shared/content-tab/layout.vue'
|
import ContentPageLayout from '../../../shared/content-tab/layout.vue'
|
||||||
import type { ManagedContentData } from '../../../shared/content-tab/providers/content-manager'
|
import type { ManagedContentData } from '../../../shared/content-tab/providers/content-manager'
|
||||||
import { provideContentManager } from '../../../shared/content-tab/providers/content-manager'
|
import { provideContentManager } from '../../../shared/content-tab/providers/content-manager'
|
||||||
import type { ContentItem } from '../../../shared/content-tab/types'
|
import type { ContentItem, ContentSide } from '../../../shared/content-tab/types'
|
||||||
import { summarizeManagedContent } from '../../../shared/content-tab/utils/managed-content'
|
import { summarizeManagedContent } from '../../../shared/content-tab/utils/managed-content'
|
||||||
|
|
||||||
type AddonWithUiState = Archon.Content.v1.Addon & { installing?: boolean }
|
type AddonWithUiState = Archon.Content.v1.Addon & { installing?: boolean }
|
||||||
@@ -63,9 +64,13 @@ const messages = defineMessages({
|
|||||||
id: 'hosting.content.failed-to-remove',
|
id: 'hosting.content.failed-to-remove',
|
||||||
defaultMessage: 'Failed to remove content',
|
defaultMessage: 'Failed to remove content',
|
||||||
},
|
},
|
||||||
failedToToggle: {
|
failedToSetEnabledFor: {
|
||||||
id: 'hosting.content.failed-to-toggle',
|
id: 'hosting.content.failed-to-set-enabled-for',
|
||||||
defaultMessage: 'Failed to toggle {name}',
|
defaultMessage: 'Failed to change where {name} is enabled',
|
||||||
|
},
|
||||||
|
failedToSetSideLock: {
|
||||||
|
id: 'hosting.content.failed-to-set-side-lock',
|
||||||
|
defaultMessage: 'Failed to change the environment lock for {name}',
|
||||||
},
|
},
|
||||||
failedToUpload: {
|
failedToUpload: {
|
||||||
id: 'hosting.content.failed-to-upload',
|
id: 'hosting.content.failed-to-upload',
|
||||||
@@ -91,14 +96,6 @@ const messages = defineMessages({
|
|||||||
id: 'hosting.content.failed-to-bulk-delete',
|
id: 'hosting.content.failed-to-bulk-delete',
|
||||||
defaultMessage: 'Failed to delete content',
|
defaultMessage: 'Failed to delete content',
|
||||||
},
|
},
|
||||||
failedToBulkEnable: {
|
|
||||||
id: 'hosting.content.failed-to-bulk-enable',
|
|
||||||
defaultMessage: 'Failed to enable content',
|
|
||||||
},
|
|
||||||
failedToBulkDisable: {
|
|
||||||
id: 'hosting.content.failed-to-bulk-disable',
|
|
||||||
defaultMessage: 'Failed to disable content',
|
|
||||||
},
|
|
||||||
failedToBulkUpdate: {
|
failedToBulkUpdate: {
|
||||||
id: 'hosting.content.failed-to-bulk-update',
|
id: 'hosting.content.failed-to-bulk-update',
|
||||||
defaultMessage: 'Failed to update content',
|
defaultMessage: 'Failed to update content',
|
||||||
@@ -107,6 +104,24 @@ const messages = defineMessages({
|
|||||||
id: 'hosting.content.failed-to-install',
|
id: 'hosting.content.failed-to-install',
|
||||||
defaultMessage: 'Failed to install content',
|
defaultMessage: 'Failed to install content',
|
||||||
},
|
},
|
||||||
|
sideLocked: {
|
||||||
|
id: 'hosting.content.enabled-for.side-locked',
|
||||||
|
defaultMessage:
|
||||||
|
'This option conflicts with the detected environment. Unlock the environment controls from the row menu to change it.',
|
||||||
|
},
|
||||||
|
unknownEnvironment: {
|
||||||
|
id: 'hosting.content.enabled-for.unknown-environment',
|
||||||
|
defaultMessage:
|
||||||
|
'We could not determine where this content should run. Check that the selected environments are correct.',
|
||||||
|
},
|
||||||
|
lockEnvironment: {
|
||||||
|
id: 'hosting.content.enabled-for.lock-environment',
|
||||||
|
defaultMessage: 'Lock detected environment',
|
||||||
|
},
|
||||||
|
unlockEnvironment: {
|
||||||
|
id: 'hosting.content.enabled-for.unlock-environment',
|
||||||
|
defaultMessage: 'Unlock environment controls',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
@@ -299,12 +314,46 @@ const managedContent = computed<ManagedContentData | null>(() => {
|
|||||||
|
|
||||||
function friendlyAddonName(addon: Archon.Content.v1.Addon): string {
|
function friendlyAddonName(addon: Archon.Content.v1.Addon): string {
|
||||||
if (addon.name) return addon.name
|
if (addon.name) return addon.name
|
||||||
|
if (addon.manifest?.name) return addon.manifest.name
|
||||||
let cleanName = addon.filename
|
let cleanName = addon.filename
|
||||||
const lastDotIndex = cleanName.lastIndexOf('.')
|
const lastDotIndex = cleanName.lastIndexOf('.')
|
||||||
if (lastDotIndex !== -1) cleanName = cleanName.substring(0, lastDotIndex)
|
if (lastDotIndex !== -1) cleanName = cleanName.substring(0, lastDotIndex)
|
||||||
return cleanName
|
return cleanName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAddonEnvironment(
|
||||||
|
addon: Archon.Content.v1.Addon,
|
||||||
|
): Labrinth.Projects.v3.Environment | undefined {
|
||||||
|
return addon.version?.environment ?? addon.manifest?.environment ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDetectedEnvironment(addon: Archon.Content.v1.Addon) {
|
||||||
|
const environment = getAddonEnvironment(addon)
|
||||||
|
return environment !== undefined && environment !== 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLockedSides(addon: Archon.Content.v1.Addon): ContentSide[] {
|
||||||
|
if (addon.side_toggle_unlocked || !hasDetectedEnvironment(addon)) return []
|
||||||
|
|
||||||
|
switch (getAddonEnvironment(addon)) {
|
||||||
|
case 'client_only':
|
||||||
|
case 'singleplayer_only':
|
||||||
|
return ['server']
|
||||||
|
case 'server_only':
|
||||||
|
case 'dedicated_server_only':
|
||||||
|
return ['player']
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnabledForWarning(addon: Archon.Content.v1.Addon) {
|
||||||
|
if (addon.pack_client_retained) return formatMessage(commonMessages.clientRetainedWarning)
|
||||||
|
if (addon.pack_client_depends) return formatMessage(commonMessages.clientDependsWarning)
|
||||||
|
if (!hasDetectedEnvironment(addon)) return formatMessage(messages.unknownEnvironment)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const modpackAddons = ref<Archon.Content.v1.Addon[]>([])
|
const modpackAddons = ref<Archon.Content.v1.Addon[]>([])
|
||||||
|
|
||||||
const addonLookup = computed(() => {
|
const addonLookup = computed(() => {
|
||||||
@@ -312,12 +361,23 @@ const addonLookup = computed(() => {
|
|||||||
for (const addon of contentQuery.data.value?.addons ?? []) {
|
for (const addon of contentQuery.data.value?.addons ?? []) {
|
||||||
map.set(addon.filename, addon)
|
map.set(addon.filename, addon)
|
||||||
}
|
}
|
||||||
for (const addon of modpackAddons.value) {
|
|
||||||
map.set(addon.filename, addon)
|
|
||||||
}
|
|
||||||
return map
|
return map
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const modpackAddonLookup = computed(
|
||||||
|
() => new Map(modpackAddons.value.map((addon) => [addon.filename, addon])),
|
||||||
|
)
|
||||||
|
|
||||||
|
function getAddonForItem(item: ContentItem) {
|
||||||
|
return item.source_kind === 'modrinth_modpack'
|
||||||
|
? modpackAddonLookup.value.get(item.file_name)
|
||||||
|
: addonLookup.value.get(item.file_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAddonQueryKey(addon: Archon.Content.v1.Addon) {
|
||||||
|
return addon.from_modpack ? modpackContentQueryKey.value : queryKey.value
|
||||||
|
}
|
||||||
|
|
||||||
const projectMetadataBatchSize = 800
|
const projectMetadataBatchSize = 800
|
||||||
const contentProjectIds = computed(() =>
|
const contentProjectIds = computed(() =>
|
||||||
[...(contentQuery.data.value?.addons ?? []), ...modpackAddons.value]
|
[...(contentQuery.data.value?.addons ?? []), ...modpackAddons.value]
|
||||||
@@ -557,44 +617,124 @@ const deleteMutation = useMutation({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleMutation = useMutation({
|
type SetEnabledForVariables = {
|
||||||
mutationFn: async ({ addon }: { addon: Archon.Content.v1.Addon }) => {
|
addon: Archon.Content.v1.Addon
|
||||||
const request: Archon.Content.v1.RemoveAddonRequest = {
|
side: ContentSide
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const setEnabledForMutation = useMutation({
|
||||||
|
mutationFn: async ({ addon, side, enabled }: SetEnabledForVariables) => {
|
||||||
|
const request: Archon.Content.v1.SetAddonEnabledRequest = {
|
||||||
filename: addon.filename,
|
filename: addon.filename,
|
||||||
kind: addon.kind,
|
kind: addon.kind,
|
||||||
|
enabled,
|
||||||
}
|
}
|
||||||
if (addon.disabled) {
|
if (side === 'server') {
|
||||||
await client.archon.content_v1.enableAddon(serverId, worldId.value!, request)
|
await client.archon.content_v1.setAddonEnabledServer(serverId, worldId.value!, request)
|
||||||
} else {
|
} else {
|
||||||
await client.archon.content_v1.disableAddon(serverId, worldId.value!, request)
|
await client.archon.content_v1.setAddonEnabledPlayer(serverId, worldId.value!, request)
|
||||||
}
|
}
|
||||||
return { filename: addon.filename, newDisabled: !addon.disabled }
|
|
||||||
},
|
},
|
||||||
onSuccess: ({ filename, newDisabled }) => {
|
onMutate: async ({ addon, side, enabled }) => {
|
||||||
queryClient.setQueryData(queryKey.value, (oldData: Archon.Content.v1.Addons | undefined) => {
|
const targetQueryKey = getAddonQueryKey(addon)
|
||||||
|
await queryClient.cancelQueries({ queryKey: targetQueryKey })
|
||||||
|
const previousData = queryClient.getQueryData<Archon.Content.v1.Addons>(targetQueryKey)
|
||||||
|
queryClient.setQueryData(targetQueryKey, (oldData: Archon.Content.v1.Addons | undefined) => {
|
||||||
if (!oldData) return oldData
|
if (!oldData) return oldData
|
||||||
return {
|
return {
|
||||||
...oldData,
|
...oldData,
|
||||||
addons: (oldData.addons ?? []).map((a) =>
|
addons: (oldData.addons ?? []).map((candidate) => {
|
||||||
a.filename === filename ? { ...a, disabled: newDisabled } : a,
|
if (candidate.filename !== addon.filename || candidate.kind !== addon.kind) return candidate
|
||||||
),
|
return side === 'server'
|
||||||
|
? { ...candidate, disabled_server: !enabled }
|
||||||
|
: { ...candidate, disabled_player: !enabled }
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({ queryKey: queryKey.value })
|
return { previousData, targetQueryKey }
|
||||||
},
|
},
|
||||||
onError: (_err, { addon }) => {
|
onError: (error, { addon }, context) => {
|
||||||
|
if (context?.previousData) {
|
||||||
|
queryClient.setQueryData(context.targetQueryKey, context.previousData)
|
||||||
|
}
|
||||||
addNotification({
|
addNotification({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: formatMessage(messages.failedToToggle, { name: friendlyAddonName(addon) }),
|
title: formatMessage(messages.failedToSetEnabledFor, {
|
||||||
|
name: friendlyAddonName(addon),
|
||||||
|
}),
|
||||||
|
text: error instanceof Error ? error.message : undefined,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||||
|
void queryClient.invalidateQueries({ queryKey: modpackContentQueryKey.value })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleToggleEnabled(item: ContentItem) {
|
const setSideLockMutation = useMutation({
|
||||||
|
mutationFn: async ({ addon, locked }: { addon: Archon.Content.v1.Addon; locked: boolean }) => {
|
||||||
|
await client.archon.content_v1.setAddonSideToggleLocked(serverId, worldId.value!, {
|
||||||
|
filename: addon.filename,
|
||||||
|
kind: addon.kind,
|
||||||
|
locked,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onMutate: async ({ addon, locked }) => {
|
||||||
|
const targetQueryKey = getAddonQueryKey(addon)
|
||||||
|
await queryClient.cancelQueries({ queryKey: targetQueryKey })
|
||||||
|
const previousData = queryClient.getQueryData<Archon.Content.v1.Addons>(targetQueryKey)
|
||||||
|
queryClient.setQueryData(targetQueryKey, (oldData: Archon.Content.v1.Addons | undefined) => {
|
||||||
|
if (!oldData) return oldData
|
||||||
|
return {
|
||||||
|
...oldData,
|
||||||
|
addons: (oldData.addons ?? []).map((candidate) =>
|
||||||
|
candidate.filename === addon.filename && candidate.kind === addon.kind
|
||||||
|
? { ...candidate, side_toggle_unlocked: !locked }
|
||||||
|
: candidate,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { previousData, targetQueryKey }
|
||||||
|
},
|
||||||
|
onError: (error, { addon }, context) => {
|
||||||
|
if (context?.previousData) {
|
||||||
|
queryClient.setQueryData(context.targetQueryKey, context.previousData)
|
||||||
|
}
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: formatMessage(messages.failedToSetSideLock, {
|
||||||
|
name: friendlyAddonName(addon),
|
||||||
|
}),
|
||||||
|
text: error instanceof Error ? error.message : undefined,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||||
|
void queryClient.invalidateQueries({ queryKey: modpackContentQueryKey.value })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleSetEnabledFor(item: ContentItem, side: ContentSide, enabled: boolean) {
|
||||||
if (contentActionDisabled.value) return
|
if (contentActionDisabled.value) return
|
||||||
const addon = addonLookup.value.get(item.file_name)
|
const addon = getAddonForItem(item)
|
||||||
if (!addon) return
|
if (!addon) return
|
||||||
await toggleMutation.mutateAsync({ addon })
|
await setEnabledForMutation.mutateAsync({ addon, side, enabled })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleModpackSetEnabledFor(
|
||||||
|
item: ContentItem,
|
||||||
|
side: ContentSide,
|
||||||
|
enabled: boolean,
|
||||||
|
) {
|
||||||
|
modpackContentModal.value?.updateItem(item.file_name, { disabled: true })
|
||||||
|
try {
|
||||||
|
await handleSetEnabledFor(item, side, enabled)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
modpackContentModal.value?.updateItem(item.file_name, { disabled: false })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteItem(item: ContentItem) {
|
async function handleDeleteItem(item: ContentItem) {
|
||||||
@@ -629,38 +769,6 @@ async function handleBulkDelete(items: ContentItem[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBulkEnable(items: ContentItem[]) {
|
|
||||||
if (contentActionDisabled.value) return
|
|
||||||
const requests = itemsToAddonRequests(items)
|
|
||||||
if (requests.length === 0) return
|
|
||||||
try {
|
|
||||||
await client.archon.content_v1.enableAddons(serverId, worldId.value!, requests)
|
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKey.value })
|
|
||||||
} catch (err) {
|
|
||||||
addNotification({
|
|
||||||
type: 'error',
|
|
||||||
title: formatMessage(messages.failedToBulkEnable),
|
|
||||||
text: err instanceof Error ? err.message : undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleBulkDisable(items: ContentItem[]) {
|
|
||||||
if (contentActionDisabled.value) return
|
|
||||||
const requests = itemsToAddonRequests(items)
|
|
||||||
if (requests.length === 0) return
|
|
||||||
try {
|
|
||||||
await client.archon.content_v1.disableAddons(serverId, worldId.value!, requests)
|
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKey.value })
|
|
||||||
} catch (err) {
|
|
||||||
addNotification({
|
|
||||||
type: 'error',
|
|
||||||
title: formatMessage(messages.failedToBulkDisable),
|
|
||||||
text: err instanceof Error ? err.message : undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const modpackUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
const modpackUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||||
const modpackContentModal = ref<InstanceType<typeof ManagedContentModal>>()
|
const modpackContentModal = ref<InstanceType<typeof ManagedContentModal>>()
|
||||||
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal>>()
|
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal>>()
|
||||||
@@ -813,17 +921,44 @@ function addonToContentItem(addon: AddonWithUiState): ContentItem {
|
|||||||
const projectMetadata = addon.project_id
|
const projectMetadata = addon.project_id
|
||||||
? contentProjectsById.value.get(addon.project_id)
|
? contentProjectsById.value.get(addon.project_id)
|
||||||
: undefined
|
: undefined
|
||||||
|
const environment = getAddonEnvironment(addon)
|
||||||
|
const embeddedIcon =
|
||||||
|
!addon.icon_url &&
|
||||||
|
addon.manifest?.icon_embedded &&
|
||||||
|
worldId.value &&
|
||||||
|
(addon.kind === 'mod' || addon.kind === 'plugin')
|
||||||
|
? {
|
||||||
|
queryKey: [
|
||||||
|
'kyros',
|
||||||
|
'content',
|
||||||
|
'embedded-icon',
|
||||||
|
worldId.value,
|
||||||
|
addon.kind,
|
||||||
|
addon.filename,
|
||||||
|
] as const,
|
||||||
|
queryFn: () =>
|
||||||
|
client.kyros.content_v1.getEmbeddedAddonIcon(
|
||||||
|
worldId.value!,
|
||||||
|
addon.kind === 'mod' ? 'mods' : 'plugins',
|
||||||
|
addon.filename,
|
||||||
|
),
|
||||||
|
fallbackUrl: projectMetadata?.icon_url,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
const serverEnabled = !addon.disabled_server
|
||||||
|
const playerEnabled = !addon.disabled_player
|
||||||
return {
|
return {
|
||||||
project: {
|
project: {
|
||||||
...(projectMetadata ?? {}),
|
...(projectMetadata ?? {}),
|
||||||
id: addon.project_id ?? addon.filename,
|
id: addon.project_id ?? addon.filename,
|
||||||
slug: projectMetadata?.slug ?? addon.project_id ?? addon.filename,
|
slug: projectMetadata?.slug ?? addon.project_id ?? addon.filename,
|
||||||
title: projectMetadata?.title ?? friendlyAddonName(addon),
|
title: projectMetadata?.title ?? friendlyAddonName(addon),
|
||||||
icon_url: addon.icon_url ?? projectMetadata?.icon_url ?? undefined,
|
icon_url: addon.icon_url ?? (embeddedIcon ? undefined : projectMetadata?.icon_url) ?? undefined,
|
||||||
},
|
},
|
||||||
version: {
|
version: {
|
||||||
id: addon.version?.id ?? addon.filename,
|
id: addon.version?.id ?? addon.filename,
|
||||||
version_number: addon.version?.name ?? formatMessage(commonMessages.unknownLabel),
|
version_number:
|
||||||
|
addon.version?.name ?? addon.manifest?.version ?? formatMessage(commonMessages.unknownLabel),
|
||||||
file_name: addon.filename,
|
file_name: addon.filename,
|
||||||
},
|
},
|
||||||
owner: addon.owner
|
owner: addon.owner
|
||||||
@@ -836,13 +971,24 @@ function addonToContentItem(addon: AddonWithUiState): ContentItem {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
id: addon.id ?? addon.filename,
|
id: addon.id ?? addon.filename,
|
||||||
enabled: !addon.disabled,
|
external: !addon.project_id,
|
||||||
|
source_kind: addon.from_modpack ? 'modrinth_modpack' : undefined,
|
||||||
|
enabled: serverEnabled || playerEnabled,
|
||||||
|
enabledFor: {
|
||||||
|
server: serverEnabled,
|
||||||
|
player: playerEnabled,
|
||||||
|
locked: hasDetectedEnvironment(addon) && !addon.side_toggle_unlocked,
|
||||||
|
disabledSides: getLockedSides(addon),
|
||||||
|
disabledTooltip: formatMessage(messages.sideLocked),
|
||||||
|
warningTooltip: getEnabledForWarning(addon),
|
||||||
|
},
|
||||||
|
embeddedIcon,
|
||||||
file_name: addon.filename,
|
file_name: addon.filename,
|
||||||
date_added: addon.btime,
|
date_added: addon.btime,
|
||||||
project_type: addon.kind,
|
project_type: addon.kind,
|
||||||
has_update: !!addon.has_update,
|
has_update: !!addon.has_update,
|
||||||
update_version_id: addon.has_update,
|
update_version_id: addon.has_update,
|
||||||
environment: addon.version?.environment ?? undefined,
|
environment,
|
||||||
pack_client_retained: addon.pack_client_retained,
|
pack_client_retained: addon.pack_client_retained,
|
||||||
pack_client_depends: addon.pack_client_depends,
|
pack_client_depends: addon.pack_client_depends,
|
||||||
installing: addon.installing ?? addon.status === 'pending',
|
installing: addon.installing ?? addon.status === 'pending',
|
||||||
@@ -877,86 +1023,6 @@ async function handleViewModpackContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleModpackContentToggle(item: ContentItem) {
|
|
||||||
if (contentActionDisabled.value) return
|
|
||||||
const addon = addonLookup.value.get(item.file_name)
|
|
||||||
if (!addon) return
|
|
||||||
modpackContentModal.value?.updateItem(item.file_name, { disabled: true })
|
|
||||||
try {
|
|
||||||
await toggleMutation.mutateAsync({ addon })
|
|
||||||
modpackAddons.value = modpackAddons.value.map((a) =>
|
|
||||||
a.filename === addon.filename ? { ...a, disabled: !addon.disabled } : a,
|
|
||||||
)
|
|
||||||
queryClient.setQueryData(
|
|
||||||
modpackContentQueryKey.value,
|
|
||||||
(oldData: Archon.Content.v1.Addons | undefined) =>
|
|
||||||
oldData
|
|
||||||
? {
|
|
||||||
...oldData,
|
|
||||||
addons: (oldData.addons ?? []).map((a) =>
|
|
||||||
a.filename === addon.filename ? { ...a, disabled: !addon.disabled } : a,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: oldData,
|
|
||||||
)
|
|
||||||
modpackContentModal.value?.updateItem(item.file_name, {
|
|
||||||
enabled: !item.enabled,
|
|
||||||
disabled: false,
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
modpackContentModal.value?.updateItem(item.file_name, { disabled: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleModpackBulkToggle(items: ContentItem[], enable: boolean) {
|
|
||||||
if (contentActionDisabled.value) return
|
|
||||||
const requests = itemsToAddonRequests(items)
|
|
||||||
if (requests.length === 0) return
|
|
||||||
|
|
||||||
// Optimistic update
|
|
||||||
for (const item of items) {
|
|
||||||
modpackAddons.value = modpackAddons.value.map((a) =>
|
|
||||||
a.filename === item.file_name ? { ...a, disabled: !enable } : a,
|
|
||||||
)
|
|
||||||
modpackContentModal.value?.updateItem(item.file_name, { enabled: enable })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (enable) {
|
|
||||||
await client.archon.content_v1.enableAddons(serverId, worldId.value!, requests)
|
|
||||||
} else {
|
|
||||||
await client.archon.content_v1.disableAddons(serverId, worldId.value!, requests)
|
|
||||||
}
|
|
||||||
queryClient.setQueryData(
|
|
||||||
modpackContentQueryKey.value,
|
|
||||||
(oldData: Archon.Content.v1.Addons | undefined) =>
|
|
||||||
oldData
|
|
||||||
? {
|
|
||||||
...oldData,
|
|
||||||
addons: (oldData.addons ?? []).map((addon) =>
|
|
||||||
items.some((item) => item.file_name === addon.filename)
|
|
||||||
? { ...addon, disabled: !enable }
|
|
||||||
: addon,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: oldData,
|
|
||||||
)
|
|
||||||
await queryClient.invalidateQueries({ queryKey: queryKey.value })
|
|
||||||
} catch (err) {
|
|
||||||
for (const item of items) {
|
|
||||||
modpackAddons.value = modpackAddons.value.map((a) =>
|
|
||||||
a.filename === item.file_name ? { ...a, disabled: enable } : a,
|
|
||||||
)
|
|
||||||
modpackContentModal.value?.updateItem(item.file_name, { enabled: !enable })
|
|
||||||
}
|
|
||||||
addNotification({
|
|
||||||
type: 'error',
|
|
||||||
title: formatMessage(enable ? messages.failedToBulkEnable : messages.failedToBulkDisable),
|
|
||||||
text: err instanceof Error ? err.message : undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleModpackUnlink() {
|
function handleModpackUnlink() {
|
||||||
modpackUnlinkModal.value?.show()
|
modpackUnlinkModal.value?.show()
|
||||||
}
|
}
|
||||||
@@ -1175,12 +1241,22 @@ function handleModpackUpdateCancel() {
|
|||||||
pendingModpackUpdateVersion.value = null
|
pendingModpackUpdateVersion.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOverflowOptions(item: ContentItem) {
|
async function handleToggleSideLock(addon: Archon.Content.v1.Addon) {
|
||||||
const options: { id: string; icon?: typeof ClipboardCopyIcon; action: () => void }[] = []
|
if (contentActionDisabled.value) return
|
||||||
|
await setSideLockMutation.mutateAsync({
|
||||||
|
addon,
|
||||||
|
locked: addon.side_toggle_unlocked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (item.project?.slug) {
|
function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
|
||||||
|
const options: OverflowMenuOption[] = []
|
||||||
|
const addon = getAddonForItem(item)
|
||||||
|
|
||||||
|
if (addon?.project_id && item.project?.slug) {
|
||||||
options.push({
|
options.push({
|
||||||
id: formatMessage(commonMessages.copyLinkButton),
|
id: 'copy-link',
|
||||||
|
label: formatMessage(commonMessages.copyLinkButton),
|
||||||
icon: ClipboardCopyIcon,
|
icon: ClipboardCopyIcon,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
@@ -1190,6 +1266,18 @@ function getOverflowOptions(item: ContentItem) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (addon && hasDetectedEnvironment(addon)) {
|
||||||
|
if (options.length > 0) options.push({ type: 'divider' })
|
||||||
|
options.push({
|
||||||
|
id: 'toggle-side-lock',
|
||||||
|
label: formatMessage(
|
||||||
|
addon.side_toggle_unlocked ? messages.lockEnvironment : messages.unlockEnvironment,
|
||||||
|
),
|
||||||
|
icon: addon.side_toggle_unlocked ? LockIcon : LockOpenIcon,
|
||||||
|
action: () => handleToggleSideLock(addon),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1204,11 +1292,9 @@ provideContentManager({
|
|||||||
disableAddContent: computed(() => !canSetup.value),
|
disableAddContent: computed(() => !canSetup.value),
|
||||||
disableAddContentTooltip: permissionDeniedMessage.value,
|
disableAddContentTooltip: permissionDeniedMessage.value,
|
||||||
contentTypeLabel: type,
|
contentTypeLabel: type,
|
||||||
toggleEnabled: handleToggleEnabled,
|
setEnabledFor: handleSetEnabledFor,
|
||||||
deleteItem: handleDeleteItem,
|
deleteItem: handleDeleteItem,
|
||||||
bulkDeleteItems: handleBulkDelete,
|
bulkDeleteItems: handleBulkDelete,
|
||||||
bulkEnableItems: handleBulkEnable,
|
|
||||||
bulkDisableItems: handleBulkDisable,
|
|
||||||
refresh: async () => {
|
refresh: async () => {
|
||||||
await contentQuery.refetch()
|
await contentQuery.refetch()
|
||||||
},
|
},
|
||||||
@@ -1245,6 +1331,9 @@ provideContentManager({
|
|||||||
: undefined,
|
: undefined,
|
||||||
external: item.external ?? !hasModrinthProject,
|
external: item.external ?? !hasModrinthProject,
|
||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
|
enabledFor: item.enabledFor,
|
||||||
|
embeddedIcon: item.embeddedIcon,
|
||||||
|
hideToggle: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterPersistKey: `server:${serverId}:${worldId.value}`,
|
filterPersistKey: `server:${serverId}:${worldId.value}`,
|
||||||
@@ -1274,13 +1363,12 @@ provideContentManager({
|
|||||||
:source-name="managedContent?.card.manager.name"
|
:source-name="managedContent?.card.manager.name"
|
||||||
:source-icon-url="managedContent?.card.manager.iconUrl"
|
:source-icon-url="managedContent?.card.manager.iconUrl"
|
||||||
:header="formatMessage(messages.modpackContent)"
|
:header="formatMessage(messages.modpackContent)"
|
||||||
enable-toggle
|
enable-enabled-for
|
||||||
show-environment-warnings
|
show-environment-warnings
|
||||||
|
:get-overflow-options="getOverflowOptions"
|
||||||
:action-disabled="contentActionDisabled"
|
:action-disabled="contentActionDisabled"
|
||||||
:action-disabled-tooltip="contentActionBusyMessage ?? undefined"
|
:action-disabled-tooltip="contentActionBusyMessage ?? undefined"
|
||||||
@update:enabled="handleModpackContentToggle"
|
@update:enabled-for="handleModpackSetEnabledFor"
|
||||||
@bulk:enable="handleModpackBulkToggle($event, true)"
|
|
||||||
@bulk:disable="handleModpackBulkToggle($event, false)"
|
|
||||||
@hide="isModpackContentModalOpen = false"
|
@hide="isModpackContentModalOpen = false"
|
||||||
/>
|
/>
|
||||||
<ContentUpdaterModal
|
<ContentUpdaterModal
|
||||||
|
|||||||
Reference in New Issue
Block a user