mirror of
https://github.com/modrinth/code.git
synced 2026-09-03 13:36:48 +00:00
feat: implement dependency deletion modal for content tab (#6410)
* feat: implement dependency deletion modal for content tab * fix: handle bulk delete state * fix: lint
This commit is contained in:
@@ -99,7 +99,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { get_project_versions, get_version } from '@/helpers/cache.js'
|
import { get_project_versions, get_version, get_version_many } from '@/helpers/cache.js'
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
|
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
|
||||||
import {
|
import {
|
||||||
@@ -451,6 +451,63 @@ async function removeMod(mod: ContentItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBreakingDependency(dependency: Labrinth.Versions.v2.Dependency) {
|
||||||
|
return dependency.dependency_type === 'required' || dependency.dependency_type === 'embedded'
|
||||||
|
}
|
||||||
|
|
||||||
|
function dependencyTargetsItem(dependency: Labrinth.Versions.v2.Dependency, item: ContentItem) {
|
||||||
|
return (
|
||||||
|
(!!dependency.project_id && dependency.project_id === item.project?.id) ||
|
||||||
|
('version_id' in dependency &&
|
||||||
|
!!dependency.version_id &&
|
||||||
|
dependency.version_id === item.version?.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDeleteDependencyWarning(items: ContentItem[]) {
|
||||||
|
if (props.isServerInstance) return null
|
||||||
|
|
||||||
|
const deletingIds = new Set(items.map(getContentItemId))
|
||||||
|
const remainingItems = projects.value.filter((item) => !deletingIds.has(getContentItemId(item)))
|
||||||
|
const versionIds = [
|
||||||
|
...new Set(remainingItems.map((item) => item.version?.id).filter((id): id is string => !!id)),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (versionIds.length === 0) return null
|
||||||
|
|
||||||
|
const versions = (await get_version_many(versionIds).catch((err) => {
|
||||||
|
handleError(err as Error)
|
||||||
|
return null
|
||||||
|
})) as Labrinth.Versions.v2.Version[] | null
|
||||||
|
|
||||||
|
if (!versions) return null
|
||||||
|
|
||||||
|
const versionsById = new Map(versions.map((version) => [version.id, version]))
|
||||||
|
|
||||||
|
const dependents = remainingItems
|
||||||
|
.map((candidate) => {
|
||||||
|
const version = candidate.version?.id ? versionsById.get(candidate.version.id) : null
|
||||||
|
if (!version) return null
|
||||||
|
|
||||||
|
const dependencies = items.filter((item) => {
|
||||||
|
if (!item.project?.id && !item.version?.id) return false
|
||||||
|
|
||||||
|
return version.dependencies?.some(
|
||||||
|
(dependency) =>
|
||||||
|
isBreakingDependency(dependency) && dependencyTargetsItem(dependency, item),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return dependencies.length > 0 ? { item: candidate, dependencies } : null
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(dependent): dependent is { item: ContentItem; dependencies: ContentItem[] } =>
|
||||||
|
dependent !== null,
|
||||||
|
)
|
||||||
|
|
||||||
|
return dependents.length > 0 ? { items, dependents } : null
|
||||||
|
}
|
||||||
|
|
||||||
async function updateProject(mod: ContentItem) {
|
async function updateProject(mod: ContentItem) {
|
||||||
if (!mod.file_path) return
|
if (!mod.file_path) return
|
||||||
const operation = beginContentOperation(mod)
|
const operation = beginContentOperation(mod)
|
||||||
@@ -1081,6 +1138,7 @@ provideContentManager({
|
|||||||
deleteItem: removeMod,
|
deleteItem: removeMod,
|
||||||
bulkDeleteItems: (items: ContentItem[]) =>
|
bulkDeleteItems: (items: ContentItem[]) =>
|
||||||
Promise.all(items.map((item) => removeMod(item))).then(() => {}),
|
Promise.all(items.map((item) => removeMod(item))).then(() => {}),
|
||||||
|
getDeleteDependencyWarning,
|
||||||
refresh: () => initProjects('must_revalidate'),
|
refresh: () => initProjects('must_revalidate'),
|
||||||
browse: handleBrowseContent,
|
browse: handleBrowseContent,
|
||||||
uploadFiles: handleUploadFiles,
|
uploadFiles: handleUploadFiles,
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ interface Props {
|
|||||||
showCheckbox?: boolean
|
showCheckbox?: boolean
|
||||||
hideDelete?: boolean
|
hideDelete?: boolean
|
||||||
hideActions?: boolean
|
hideActions?: boolean
|
||||||
|
inline?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -82,12 +83,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
showCheckbox: false,
|
showCheckbox: false,
|
||||||
hideDelete: false,
|
hideDelete: false,
|
||||||
hideActions: false,
|
hideActions: false,
|
||||||
|
inline: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const selected = defineModel<boolean>('selected')
|
const selected = defineModel<boolean>('selected')
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:enabled': [value: boolean]
|
'update:enabled': [value: boolean]
|
||||||
|
select: [value: boolean, event?: MouseEvent]
|
||||||
delete: [event: MouseEvent]
|
delete: [event: MouseEvent]
|
||||||
update: []
|
update: []
|
||||||
switchVersion: []
|
switchVersion: []
|
||||||
@@ -124,8 +127,10 @@ const deleteHovered = ref(false)
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
role="row"
|
role="row"
|
||||||
class="flex h-[74px] items-center justify-between gap-4 px-3"
|
class="flex items-center justify-between"
|
||||||
:class="{
|
:class="{
|
||||||
|
'h-[74px] gap-4 px-3': !inline,
|
||||||
|
'gap-3': inline,
|
||||||
'opacity-50 grayscale': disabled && !installing,
|
'opacity-50 grayscale': disabled && !installing,
|
||||||
'opacity-50': installing,
|
'opacity-50': installing,
|
||||||
}"
|
}"
|
||||||
@@ -141,7 +146,7 @@ const deleteHovered = ref(false)
|
|||||||
:model-value="selected ?? false"
|
:model-value="selected ?? false"
|
||||||
:aria-label="formatMessage(messages.selectProject, { project: project.title })"
|
:aria-label="formatMessage(messages.selectProject, { project: project.title })"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
@update:model-value="selected = $event"
|
@update:model-value="(value, event) => emit('select', value, event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -150,7 +155,7 @@ const deleteHovered = ref(false)
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-tooltip="installing ? formatMessage(commonMessages.installingLabel) : undefined"
|
v-tooltip="installing ? formatMessage(commonMessages.installingLabel) : undefined"
|
||||||
class="relative shrink-0"
|
class="relative flex shrink-0 items-center"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="project.icon_url"
|
:src="project.icon_url"
|
||||||
@@ -180,6 +185,7 @@ const deleteHovered = ref(false)
|
|||||||
>
|
>
|
||||||
{{ project.title }}
|
{{ project.title }}
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
|
<slot name="title-badges" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
v-if="isClientOnly"
|
v-if="isClientOnly"
|
||||||
theme="dismissable-prompt"
|
theme="dismissable-prompt"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from '@modrinth/assets'
|
import { ChevronDownIcon, ChevronUpIcon } from '@modrinth/assets'
|
||||||
import { computed, getCurrentInstance, onMounted, onUnmounted, 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 { useVIntl } from '#ui/composables/i18n'
|
||||||
@@ -119,26 +119,14 @@ function toggleSelectAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lastSelectedIndex = ref<number | null>(null)
|
const lastSelectedIndex = ref<number | null>(null)
|
||||||
const shiftHeld = ref(false)
|
|
||||||
|
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
function toggleItemSelection(
|
||||||
if (e.key === 'Shift') shiftHeld.value = true
|
itemId: string,
|
||||||
}
|
selected: boolean,
|
||||||
function onKeyUp(e: KeyboardEvent) {
|
index?: number,
|
||||||
if (e.key === 'Shift') shiftHeld.value = false
|
event?: MouseEvent,
|
||||||
}
|
) {
|
||||||
|
if (selected && event?.shiftKey && lastSelectedIndex.value !== null && index !== undefined) {
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('keydown', onKeyDown)
|
|
||||||
window.addEventListener('keyup', onKeyUp)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('keydown', onKeyDown)
|
|
||||||
window.removeEventListener('keyup', onKeyUp)
|
|
||||||
})
|
|
||||||
|
|
||||||
function toggleItemSelection(itemId: string, selected: boolean, index?: number) {
|
|
||||||
if (selected && shiftHeld.value && lastSelectedIndex.value !== null && index !== undefined) {
|
|
||||||
const start = Math.min(lastSelectedIndex.value, index)
|
const start = Math.min(lastSelectedIndex.value, index)
|
||||||
const end = Math.max(lastSelectedIndex.value, index)
|
const end = Math.max(lastSelectedIndex.value, index)
|
||||||
const rangeIds = props.items.slice(start, end + 1).map((item) => item.id)
|
const rangeIds = props.items.slice(start, end + 1).map((item) => item.id)
|
||||||
@@ -297,8 +285,9 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
'border-0 border-t border-solid border-surface-4',
|
'border-0 border-t border-solid border-surface-4',
|
||||||
visibleRange.start + idx === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
|
visibleRange.start + idx === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
|
||||||
]"
|
]"
|
||||||
@update:selected="
|
@select="
|
||||||
(val) => toggleItemSelection(item.id, val ?? false, visibleRange.start + idx)
|
(val, 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)"
|
||||||
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
|
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
|
||||||
@@ -355,7 +344,7 @@ function handleSort(column: ContentCardTableSortColumn) {
|
|||||||
'border-0 border-t border-solid border-surface-4',
|
'border-0 border-t border-solid border-surface-4',
|
||||||
index === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
|
index === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
|
||||||
]"
|
]"
|
||||||
@update:selected="(val) => toggleItemSelection(item.id, val ?? false, index)"
|
@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)"
|
||||||
@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)"
|
||||||
|
|||||||
+3
-6
@@ -6,15 +6,12 @@
|
|||||||
itemType: formatContentTypeSentence(formatMessage, visibleItemType, visibleCount),
|
itemType: formatContentTypeSentence(formatMessage, visibleItemType, visibleCount),
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
:fade="props.variant === 'server' ? 'warning' : 'danger'"
|
fade="warning"
|
||||||
max-width="500px"
|
max-width="500px"
|
||||||
:on-hide="() => backupCreator?.cancelBackup()"
|
:on-hide="() => backupCreator?.cancelBackup()"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
<Admonition
|
<Admonition type="warning" :header="formatMessage(messages.admonitionHeader)">
|
||||||
:type="props.variant === 'server' ? 'warning' : 'critical'"
|
|
||||||
:header="formatMessage(messages.admonitionHeader)"
|
|
||||||
>
|
|
||||||
{{ formatMessage(messages.admonitionBody) }}
|
{{ formatMessage(messages.admonitionBody) }}
|
||||||
</Admonition>
|
</Admonition>
|
||||||
<InlineBackupCreator
|
<InlineBackupCreator
|
||||||
@@ -32,7 +29,7 @@
|
|||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled :color="props.variant === 'server' ? 'orange' : 'red'">
|
<ButtonStyled color="orange">
|
||||||
<button
|
<button
|
||||||
v-tooltip="props.actionDisabled ? props.actionDisabledTooltip : undefined"
|
v-tooltip="props.actionDisabled ? props.actionDisabledTooltip : undefined"
|
||||||
:disabled="buttonsDisabled || props.actionDisabled"
|
:disabled="buttonsDisabled || props.actionDisabled"
|
||||||
|
|||||||
+371
@@ -0,0 +1,371 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal
|
||||||
|
ref="modal"
|
||||||
|
:header="formatMessage(messages.header)"
|
||||||
|
fade="danger"
|
||||||
|
max-width="560px"
|
||||||
|
:on-hide="() => backupCreator?.cancelBackup()"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<Admonition type="critical" :header="formatMessage(messages.admonitionHeader)">
|
||||||
|
{{
|
||||||
|
visibleItems.length === 1
|
||||||
|
? formatMessage(messages.singleAdmonitionBody, {
|
||||||
|
project:
|
||||||
|
visibleItems[0]?.project.title ?? formatMessage(commonMessages.unknownLabel),
|
||||||
|
context: contextLabel,
|
||||||
|
})
|
||||||
|
: formatMessage(messages.bulkAdmonitionBody, { context: contextLabel })
|
||||||
|
}}
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<div v-if="visibleItems.length > 0" class="flex flex-col gap-2">
|
||||||
|
<span class="font-semibold text-contrast">{{ formatMessage(messages.deletingLabel) }}</span>
|
||||||
|
<div class="relative">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 max-h-0"
|
||||||
|
enter-to-class="opacity-100 max-h-2"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="opacity-100 max-h-2"
|
||||||
|
leave-to-class="opacity-0 max-h-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="showDeletingTopFade"
|
||||||
|
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-2 bg-gradient-to-b from-bg-raised to-transparent"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
<div
|
||||||
|
ref="deletingListRef"
|
||||||
|
class="flex flex-col gap-2 overflow-y-auto max-h-[212px]"
|
||||||
|
@scroll="checkDeletingScrollState"
|
||||||
|
>
|
||||||
|
<div v-for="item in visibleItems" :key="item.id" :class="modalContentCardClasses">
|
||||||
|
<ContentCardItem
|
||||||
|
:project="item.project"
|
||||||
|
:project-link="item.projectLink"
|
||||||
|
:version="item.version"
|
||||||
|
:version-link="item.versionLink"
|
||||||
|
:owner="item.owner"
|
||||||
|
hide-actions
|
||||||
|
inline
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 max-h-0"
|
||||||
|
enter-to-class="opacity-100 max-h-2"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="opacity-100 max-h-2"
|
||||||
|
leave-to-class="opacity-0 max-h-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="showDeletingBottomFade"
|
||||||
|
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-2 bg-gradient-to-t from-bg-raised to-transparent"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="visibleDependents.length > 0" class="flex flex-col gap-2">
|
||||||
|
<span class="font-semibold text-contrast">{{
|
||||||
|
formatMessage(messages.affectedDependentsLabel, { count: visibleDependents.length })
|
||||||
|
}}</span>
|
||||||
|
<div class="relative">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 max-h-0"
|
||||||
|
enter-to-class="opacity-100 max-h-2"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="opacity-100 max-h-2"
|
||||||
|
leave-to-class="opacity-0 max-h-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="showDependentTopFade"
|
||||||
|
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-2 bg-gradient-to-b from-bg-raised to-transparent"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
<div
|
||||||
|
ref="dependentListRef"
|
||||||
|
class="flex max-h-[212px] flex-col gap-2 overflow-y-auto"
|
||||||
|
@scroll="checkDependentScrollState"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="dependent in visibleDependents"
|
||||||
|
:key="dependent.item.id"
|
||||||
|
:class="modalContentCardClasses"
|
||||||
|
>
|
||||||
|
<ContentCardItem
|
||||||
|
:project="dependent.item.project"
|
||||||
|
:project-link="dependent.item.projectLink"
|
||||||
|
:version="dependent.item.version"
|
||||||
|
:version-link="dependent.item.versionLink"
|
||||||
|
:owner="dependent.item.owner"
|
||||||
|
hide-actions
|
||||||
|
inline
|
||||||
|
>
|
||||||
|
<template #title-badges>
|
||||||
|
<span class="flex min-w-0 flex-wrap items-center gap-1">
|
||||||
|
<span
|
||||||
|
v-for="dependency in dependent.dependencies"
|
||||||
|
:key="dependency.id"
|
||||||
|
:title="dependency.project.title"
|
||||||
|
>
|
||||||
|
<span class="truncate text-xs text-secondary mr-0.5"
|
||||||
|
>({{ dependency.project.title }})</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</ContentCardItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 max-h-0"
|
||||||
|
enter-to-class="opacity-100 max-h-2"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="opacity-100 max-h-2"
|
||||||
|
leave-to-class="opacity-0 max-h-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="showDependentBottomFade"
|
||||||
|
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-2 bg-gradient-to-t from-bg-raised to-transparent"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="font-semibold text-contrast">{{
|
||||||
|
formatMessage(messages.whatHappensLabel)
|
||||||
|
}}</span>
|
||||||
|
<ul class="m-0 list-disc pl-6 text-primary">
|
||||||
|
<li class="leading-6 marker:text-secondary">
|
||||||
|
{{ formatMessage(messages.effectDependentContent) }}
|
||||||
|
</li>
|
||||||
|
<li class="leading-6 marker:text-secondary">
|
||||||
|
{{ formatMessage(messages.effectInstance, { context: contextLabel }) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Checkbox
|
||||||
|
v-model="disableDependentsAfterDeleting"
|
||||||
|
:label="formatMessage(messages.disableDependentsLabel)"
|
||||||
|
label-class="font-medium text-primary"
|
||||||
|
class="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<InlineBackupCreator
|
||||||
|
ref="backupCreator"
|
||||||
|
:backup-name="backupName"
|
||||||
|
hide-shift-click-hint
|
||||||
|
@update:buttons-disabled="buttonsDisabled = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<ButtonStyled type="outlined">
|
||||||
|
<button class="!border !border-surface-5" @click="hide">
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="red">
|
||||||
|
<button
|
||||||
|
v-tooltip="props.actionDisabled ? props.actionDisabledTooltip : undefined"
|
||||||
|
:disabled="buttonsDisabled || props.actionDisabled"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
<TrashIcon aria-hidden="true" />
|
||||||
|
{{ deleteButtonLabel }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { TrashIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
|
import Admonition from '#ui/components/base/Admonition.vue'
|
||||||
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||||
|
import Checkbox from '#ui/components/base/Checkbox.vue'
|
||||||
|
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||||
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
|
import { useScrollIndicator } from '#ui/composables/scroll-indicator'
|
||||||
|
import { commonMessages, formatContentTypeSentence } from '#ui/utils/common-messages'
|
||||||
|
|
||||||
|
import type { ContentCardTableItem } from '../../types'
|
||||||
|
import ContentCardItem from '../ContentCardItem.vue'
|
||||||
|
import InlineBackupCreator from './InlineBackupCreator.vue'
|
||||||
|
|
||||||
|
export interface ContentDependencyWarningDependent {
|
||||||
|
item: ContentCardTableItem
|
||||||
|
dependencies: ContentCardTableItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
items?: ContentCardTableItem[]
|
||||||
|
dependents?: ContentDependencyWarningDependent[]
|
||||||
|
itemType: string
|
||||||
|
variant?: 'instance' | 'server'
|
||||||
|
backupTip?: string
|
||||||
|
actionDisabled?: boolean
|
||||||
|
actionDisabledTooltip?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
items: () => [],
|
||||||
|
dependents: () => [],
|
||||||
|
variant: 'instance',
|
||||||
|
backupTip: undefined,
|
||||||
|
actionDisabled: false,
|
||||||
|
actionDisabledTooltip: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'delete', disableDependentsAfterDeleting: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
header: {
|
||||||
|
id: 'content.dependency-warning.header',
|
||||||
|
defaultMessage: 'Dependency warning',
|
||||||
|
},
|
||||||
|
admonitionHeader: {
|
||||||
|
id: 'content.dependency-warning.admonition-header',
|
||||||
|
defaultMessage: 'This content is required by other content',
|
||||||
|
},
|
||||||
|
singleAdmonitionBody: {
|
||||||
|
id: 'content.dependency-warning.single-admonition-body',
|
||||||
|
defaultMessage:
|
||||||
|
'{project} is installed as a dependency. Deleting it may break your {context} or stop dependent content from loading correctly.',
|
||||||
|
},
|
||||||
|
bulkAdmonitionBody: {
|
||||||
|
id: 'content.dependency-warning.bulk-admonition-body',
|
||||||
|
defaultMessage:
|
||||||
|
'Some selected projects are installed as dependencies. Deleting them may break your {context} or stop dependent content from loading correctly.',
|
||||||
|
},
|
||||||
|
deletingLabel: {
|
||||||
|
id: 'content.dependency-warning.deleting-label',
|
||||||
|
defaultMessage: 'Deleting',
|
||||||
|
},
|
||||||
|
affectedDependentsLabel: {
|
||||||
|
id: 'content.dependency-warning.affected-dependents-label',
|
||||||
|
defaultMessage: 'Affected {count, plural, one {project} other {projects}}',
|
||||||
|
},
|
||||||
|
whatHappensLabel: {
|
||||||
|
id: 'content.dependency-warning.what-happens-label',
|
||||||
|
defaultMessage: 'What happens?',
|
||||||
|
},
|
||||||
|
effectDependentContent: {
|
||||||
|
id: 'content.dependency-warning.effect-dependent-content',
|
||||||
|
defaultMessage: 'Dependent content may fail to load or may disable itself',
|
||||||
|
},
|
||||||
|
effectInstance: {
|
||||||
|
id: 'content.dependency-warning.effect-instance',
|
||||||
|
defaultMessage: 'Your {context} may crash, refuse to start, or behave unexpectedly',
|
||||||
|
},
|
||||||
|
deleteAnywayButton: {
|
||||||
|
id: 'content.dependency-warning.delete-anyway-button',
|
||||||
|
defaultMessage: 'Delete anyway',
|
||||||
|
},
|
||||||
|
deleteManyAnywayButton: {
|
||||||
|
id: 'content.dependency-warning.delete-many-anyway-button',
|
||||||
|
defaultMessage: 'Delete {count, number} {itemType} anyway',
|
||||||
|
},
|
||||||
|
disableDependentsLabel: {
|
||||||
|
id: 'content.dependency-warning.disable-dependents-label',
|
||||||
|
defaultMessage: 'Disable dependents after deleting',
|
||||||
|
},
|
||||||
|
instanceContext: {
|
||||||
|
id: 'content.dependency-warning.context.instance',
|
||||||
|
defaultMessage: 'instance',
|
||||||
|
},
|
||||||
|
serverContext: {
|
||||||
|
id: 'content.dependency-warning.context.server',
|
||||||
|
defaultMessage: 'server',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const backupCreator = ref<InstanceType<typeof InlineBackupCreator>>()
|
||||||
|
const deletingListRef = ref<HTMLElement | null>(null)
|
||||||
|
const dependentListRef = ref<HTMLElement | null>(null)
|
||||||
|
const visibleItems = ref<ContentCardTableItem[]>(props.items)
|
||||||
|
const visibleDependents = ref<ContentDependencyWarningDependent[]>(props.dependents)
|
||||||
|
const visibleItemType = ref(props.itemType)
|
||||||
|
const disableDependentsAfterDeleting = ref(false)
|
||||||
|
const buttonsDisabled = ref(false)
|
||||||
|
const modalContentCardClasses = 'rounded-xl border border-solid border-surface-5 p-4 !bg-surface-2'
|
||||||
|
const {
|
||||||
|
showTopFade: showDeletingTopFade,
|
||||||
|
showBottomFade: showDeletingBottomFade,
|
||||||
|
checkScrollState: checkDeletingScrollState,
|
||||||
|
forceCheck: forceCheckDeletingScroll,
|
||||||
|
} = useScrollIndicator(deletingListRef)
|
||||||
|
const {
|
||||||
|
showTopFade: showDependentTopFade,
|
||||||
|
showBottomFade: showDependentBottomFade,
|
||||||
|
checkScrollState: checkDependentScrollState,
|
||||||
|
forceCheck: forceCheckDependentScroll,
|
||||||
|
} = useScrollIndicator(dependentListRef)
|
||||||
|
|
||||||
|
const contextLabel = computed(() =>
|
||||||
|
formatMessage(props.variant === 'server' ? messages.serverContext : messages.instanceContext),
|
||||||
|
)
|
||||||
|
|
||||||
|
const backupName = computed(() =>
|
||||||
|
props.backupTip ? `Before deletion (${props.backupTip})` : 'Before deletion',
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteButtonLabel = computed(() => {
|
||||||
|
if (visibleItems.value.length <= 1) return formatMessage(messages.deleteAnywayButton)
|
||||||
|
|
||||||
|
return formatMessage(messages.deleteManyAnywayButton, {
|
||||||
|
count: visibleItems.value.length,
|
||||||
|
itemType: formatContentTypeSentence(
|
||||||
|
formatMessage,
|
||||||
|
visibleItemType.value,
|
||||||
|
visibleItems.value.length,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function show() {
|
||||||
|
await nextTick()
|
||||||
|
visibleItems.value = props.items
|
||||||
|
visibleDependents.value = props.dependents
|
||||||
|
visibleItemType.value = props.itemType
|
||||||
|
disableDependentsAfterDeleting.value = false
|
||||||
|
buttonsDisabled.value = false
|
||||||
|
modal.value?.show()
|
||||||
|
await nextTick()
|
||||||
|
forceCheckDeletingScroll()
|
||||||
|
forceCheckDependentScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
if (props.actionDisabled || buttonsDisabled.value) return
|
||||||
|
modal.value?.hide()
|
||||||
|
emit('delete', disableDependentsAfterDeleting.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -8,6 +8,7 @@ export { default as ConfirmModpackUpdateModal } from './components/modals/Confir
|
|||||||
export { default as ConfirmReinstallModal } from './components/modals/ConfirmReinstallModal.vue'
|
export { default as ConfirmReinstallModal } from './components/modals/ConfirmReinstallModal.vue'
|
||||||
export { default as ConfirmRepairModal } from './components/modals/ConfirmRepairModal.vue'
|
export { default as ConfirmRepairModal } from './components/modals/ConfirmRepairModal.vue'
|
||||||
export { default as ConfirmUnlinkModal } from './components/modals/ConfirmUnlinkModal.vue'
|
export { default as ConfirmUnlinkModal } from './components/modals/ConfirmUnlinkModal.vue'
|
||||||
|
export { default as ContentDependencyWarningModal } from './components/modals/ContentDependencyWarningModal.vue'
|
||||||
export type {
|
export type {
|
||||||
ContentInstallInstance,
|
ContentInstallInstance,
|
||||||
ContentInstallProjectInfo,
|
ContentInstallProjectInfo,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import ContentSelectionBar from './components/ContentSelectionBar.vue'
|
|||||||
import ConfirmBulkUpdateModal from './components/modals/ConfirmBulkUpdateModal.vue'
|
import ConfirmBulkUpdateModal from './components/modals/ConfirmBulkUpdateModal.vue'
|
||||||
import ConfirmDeletionModal from './components/modals/ConfirmDeletionModal.vue'
|
import ConfirmDeletionModal from './components/modals/ConfirmDeletionModal.vue'
|
||||||
import ConfirmUnlinkModal from './components/modals/ConfirmUnlinkModal.vue'
|
import ConfirmUnlinkModal from './components/modals/ConfirmUnlinkModal.vue'
|
||||||
|
import ContentDependencyWarningModal from './components/modals/ContentDependencyWarningModal.vue'
|
||||||
import {
|
import {
|
||||||
getClientWarningType,
|
getClientWarningType,
|
||||||
isClientOnlyEnvironment,
|
isClientOnlyEnvironment,
|
||||||
@@ -319,21 +320,73 @@ const hasOutdatedProjects = computed(() => {
|
|||||||
// Deletion
|
// Deletion
|
||||||
const pendingDeletionItems = ref<ContentItem[]>([])
|
const pendingDeletionItems = ref<ContentItem[]>([])
|
||||||
const confirmDeletionModal = ref<InstanceType<typeof ConfirmDeletionModal>>()
|
const confirmDeletionModal = ref<InstanceType<typeof ConfirmDeletionModal>>()
|
||||||
|
const contentDependencyWarningModal = ref<InstanceType<typeof ContentDependencyWarningModal>>()
|
||||||
|
const pendingDependencyWarningItems = ref<ContentCardTableItem[]>([])
|
||||||
|
const pendingDependencyWarningDependents = ref<
|
||||||
|
Array<{
|
||||||
|
item: ContentCardTableItem
|
||||||
|
dependencies: ContentCardTableItem[]
|
||||||
|
}>
|
||||||
|
>([])
|
||||||
|
const pendingDependencyWarningDisableTargets = ref<ContentItem[]>([])
|
||||||
|
|
||||||
function handleDeleteById(id: string, event?: MouseEvent) {
|
function mapToDisplayItem(item: ContentItem) {
|
||||||
const item = ctx.items.value.find((i) => getItemId(i) === id)
|
return {
|
||||||
if (item) {
|
...ctx.mapToTableItem(item),
|
||||||
pendingDeletionItems.value = [item]
|
id: getItemId(item),
|
||||||
if (event?.shiftKey && !ctx.isBusy.value) {
|
|
||||||
confirmDelete()
|
|
||||||
} else {
|
|
||||||
confirmDeletionModal.value?.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showBulkDeleteModal(event?: MouseEvent) {
|
async function promptDeleteItems(items: ContentItem[], event?: MouseEvent) {
|
||||||
pendingDeletionItems.value = [...selectedItems.value]
|
if (items.length === 0) return
|
||||||
|
pendingDeletionItems.value = items
|
||||||
|
pendingDependencyWarningItems.value = []
|
||||||
|
pendingDependencyWarningDependents.value = []
|
||||||
|
pendingDependencyWarningDisableTargets.value = []
|
||||||
|
const deletingIds = new Set(items.map(getItemId))
|
||||||
|
|
||||||
|
const warning = ctx.getDeleteDependencyWarning
|
||||||
|
? await Promise.resolve()
|
||||||
|
.then(() => ctx.getDeleteDependencyWarning!(items))
|
||||||
|
.catch(() => null)
|
||||||
|
: null
|
||||||
|
if (warning) {
|
||||||
|
const remainingDependents = warning.dependents.filter(
|
||||||
|
(dependent) => !deletingIds.has(getItemId(dependent.item)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (remainingDependents.length === 0) {
|
||||||
|
showDeletionConfirmation(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const relevantDependencyIds = new Set(
|
||||||
|
remainingDependents.flatMap((dependent) => dependent.dependencies.map(getItemId)),
|
||||||
|
)
|
||||||
|
const warningItems = items.filter((item) => relevantDependencyIds.has(getItemId(item)))
|
||||||
|
if (warningItems.length === 0) {
|
||||||
|
showDeletionConfirmation(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDependencyWarningItems.value = warningItems.map(mapToDisplayItem)
|
||||||
|
pendingDependencyWarningDependents.value = remainingDependents.map((dependent) => ({
|
||||||
|
item: mapToDisplayItem(dependent.item),
|
||||||
|
dependencies: dependent.dependencies
|
||||||
|
.filter((dependency) => relevantDependencyIds.has(getItemId(dependency)))
|
||||||
|
.map(mapToDisplayItem),
|
||||||
|
}))
|
||||||
|
pendingDependencyWarningDisableTargets.value = remainingDependents.map(
|
||||||
|
(dependent) => dependent.item,
|
||||||
|
)
|
||||||
|
contentDependencyWarningModal.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showDeletionConfirmation(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDeletionConfirmation(event?: MouseEvent) {
|
||||||
if (event?.shiftKey && !ctx.isBusy.value) {
|
if (event?.shiftKey && !ctx.isBusy.value) {
|
||||||
confirmDelete()
|
confirmDelete()
|
||||||
} else {
|
} else {
|
||||||
@@ -341,6 +394,51 @@ function showBulkDeleteModal(event?: MouseEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteById(id: string, event?: MouseEvent) {
|
||||||
|
const item = ctx.items.value.find((i) => getItemId(i) === id)
|
||||||
|
if (item) {
|
||||||
|
await promptDeleteItems([item], event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showBulkDeleteModal(event?: MouseEvent) {
|
||||||
|
await promptDeleteItems([...selectedItems.value], event)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDependencyWarningDelete(disableDependentsAfterDeleting: boolean) {
|
||||||
|
if (disableDependentsAfterDeleting) {
|
||||||
|
pendingDependencyWarningDisableTargets.value =
|
||||||
|
pendingDependencyWarningDisableTargets.value.filter((item) => item.enabled)
|
||||||
|
} else {
|
||||||
|
pendingDependencyWarningDisableTargets.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDependencyWarningItems.value = []
|
||||||
|
pendingDependencyWarningDependents.value = []
|
||||||
|
await confirmDelete()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disablePendingDependencyWarningDependents() {
|
||||||
|
const items = pendingDependencyWarningDisableTargets.value.filter((item) => item.enabled)
|
||||||
|
pendingDependencyWarningDisableTargets.value = []
|
||||||
|
if (items.length === 0) return
|
||||||
|
|
||||||
|
if (ctx.bulkDisableItems) {
|
||||||
|
await ctx.bulkDisableItems(items)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const id = getItemId(item)
|
||||||
|
markChanging(id)
|
||||||
|
try {
|
||||||
|
await ctx.toggleEnabled(item)
|
||||||
|
} finally {
|
||||||
|
unmarkChanging(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmDelete() {
|
async function confirmDelete() {
|
||||||
if (ctx.isBusy.value) return
|
if (ctx.isBusy.value) return
|
||||||
const itemsToDelete = [...pendingDeletionItems.value]
|
const itemsToDelete = [...pendingDeletionItems.value]
|
||||||
@@ -353,6 +451,7 @@ async function confirmDelete() {
|
|||||||
bulkWaiting.value = true
|
bulkWaiting.value = true
|
||||||
try {
|
try {
|
||||||
await ctx.bulkDeleteItems(itemsToDelete)
|
await ctx.bulkDeleteItems(itemsToDelete)
|
||||||
|
await disablePendingDependencyWarningDependents()
|
||||||
} finally {
|
} finally {
|
||||||
clearSelection()
|
clearSelection()
|
||||||
isBulkOperating.value = false
|
isBulkOperating.value = false
|
||||||
@@ -369,6 +468,7 @@ async function confirmDelete() {
|
|||||||
try {
|
try {
|
||||||
await ctx.deleteItem(item)
|
await ctx.deleteItem(item)
|
||||||
removeFromSelection(id)
|
removeFromSelection(id)
|
||||||
|
await disablePendingDependencyWarningDependents()
|
||||||
} finally {
|
} finally {
|
||||||
unmarkChanging(id)
|
unmarkChanging(id)
|
||||||
}
|
}
|
||||||
@@ -384,6 +484,7 @@ async function confirmDelete() {
|
|||||||
},
|
},
|
||||||
{ onComplete: clearSelection },
|
{ onComplete: clearSelection },
|
||||||
)
|
)
|
||||||
|
await disablePendingDependencyWarningDependents()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggleEnabledById(id: string, _value: boolean) {
|
async function handleToggleEnabledById(id: string, _value: boolean) {
|
||||||
@@ -879,6 +980,17 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
|||||||
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
|
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
|
||||||
@delete="confirmDelete"
|
@delete="confirmDelete"
|
||||||
/>
|
/>
|
||||||
|
<ContentDependencyWarningModal
|
||||||
|
ref="contentDependencyWarningModal"
|
||||||
|
:items="pendingDependencyWarningItems"
|
||||||
|
:dependents="pendingDependencyWarningDependents"
|
||||||
|
:item-type="ctx.contentTypeLabel.value"
|
||||||
|
:variant="ctx.deletionContext ?? 'instance'"
|
||||||
|
:backup-tip="pendingDeletionItems.map((i) => i.project?.title ?? i.file_name).join(', ')"
|
||||||
|
:action-disabled="ctx.isBusy.value"
|
||||||
|
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
|
||||||
|
@delete="confirmDependencyWarningDelete"
|
||||||
|
/>
|
||||||
<ConfirmBulkUpdateModal
|
<ConfirmBulkUpdateModal
|
||||||
v-if="hasBulkUpdateSupport"
|
v-if="hasBulkUpdateSupport"
|
||||||
ref="confirmBulkUpdateModal"
|
ref="confirmBulkUpdateModal"
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ export interface ContentModpackData {
|
|||||||
disabledText?: string
|
disabledText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContentDependencyWarning {
|
||||||
|
items: ContentItem[]
|
||||||
|
dependents: Array<{
|
||||||
|
item: ContentItem
|
||||||
|
dependencies: ContentItem[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContentManagerContext {
|
export interface ContentManagerContext {
|
||||||
// Data
|
// Data
|
||||||
items: Ref<ContentItem[]> | ComputedRef<ContentItem[]>
|
items: Ref<ContentItem[]> | ComputedRef<ContentItem[]>
|
||||||
@@ -55,6 +63,9 @@ export interface ContentManagerContext {
|
|||||||
bulkDeleteItems?: (items: ContentItem[]) => Promise<void>
|
bulkDeleteItems?: (items: ContentItem[]) => Promise<void>
|
||||||
bulkEnableItems?: (items: ContentItem[]) => Promise<void>
|
bulkEnableItems?: (items: ContentItem[]) => Promise<void>
|
||||||
bulkDisableItems?: (items: ContentItem[]) => Promise<void>
|
bulkDisableItems?: (items: ContentItem[]) => Promise<void>
|
||||||
|
getDeleteDependencyWarning?: (
|
||||||
|
items: ContentItem[],
|
||||||
|
) => ContentDependencyWarning | null | Promise<ContentDependencyWarning | null>
|
||||||
|
|
||||||
// Update support (optional per-platform)
|
// Update support (optional per-platform)
|
||||||
hasUpdateSupport: boolean
|
hasUpdateSupport: boolean
|
||||||
|
|||||||
@@ -377,6 +377,48 @@
|
|||||||
"content.confirm-unlink.unlink-button": {
|
"content.confirm-unlink.unlink-button": {
|
||||||
"defaultMessage": "Unlink"
|
"defaultMessage": "Unlink"
|
||||||
},
|
},
|
||||||
|
"content.dependency-warning.admonition-header": {
|
||||||
|
"defaultMessage": "This content is required by other content"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.affected-dependents-label": {
|
||||||
|
"defaultMessage": "Affected {count, plural, one {project} other {projects}}"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.bulk-admonition-body": {
|
||||||
|
"defaultMessage": "Some selected projects are installed as dependencies. Deleting them may break your {context} or stop dependent content from loading correctly."
|
||||||
|
},
|
||||||
|
"content.dependency-warning.context.instance": {
|
||||||
|
"defaultMessage": "instance"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.context.server": {
|
||||||
|
"defaultMessage": "server"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.delete-anyway-button": {
|
||||||
|
"defaultMessage": "Delete anyway"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.delete-many-anyway-button": {
|
||||||
|
"defaultMessage": "Delete {count, number} {itemType} anyway"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.deleting-label": {
|
||||||
|
"defaultMessage": "Deleting"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.disable-dependents-label": {
|
||||||
|
"defaultMessage": "Disable dependents after deleting"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.effect-dependent-content": {
|
||||||
|
"defaultMessage": "Dependent content may fail to load or may disable itself"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.effect-instance": {
|
||||||
|
"defaultMessage": "Your {context} may crash, refuse to start, or behave unexpectedly"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.header": {
|
||||||
|
"defaultMessage": "Dependency warning"
|
||||||
|
},
|
||||||
|
"content.dependency-warning.single-admonition-body": {
|
||||||
|
"defaultMessage": "{project} is installed as a dependency. Deleting it may break your {context} or stop dependent content from loading correctly."
|
||||||
|
},
|
||||||
|
"content.dependency-warning.what-happens-label": {
|
||||||
|
"defaultMessage": "What happens?"
|
||||||
|
},
|
||||||
"content.diff-modal.added-count": {
|
"content.diff-modal.added-count": {
|
||||||
"defaultMessage": "{count} added"
|
"defaultMessage": "{count} added"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||||
|
import ContentDependencyWarningModal from '../../layouts/shared/content-tab/components/modals/ContentDependencyWarningModal.vue'
|
||||||
|
import type { ContentCardTableItem } from '../../layouts/shared/content-tab/types'
|
||||||
|
|
||||||
|
const fabricApiItem: ContentCardTableItem = {
|
||||||
|
id: 'fabric-api',
|
||||||
|
project: {
|
||||||
|
id: 'P7dR8mSH',
|
||||||
|
slug: 'fabric-api',
|
||||||
|
title: 'Fabric API',
|
||||||
|
icon_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
|
||||||
|
},
|
||||||
|
projectLink: '/project/fabric-api',
|
||||||
|
version: {
|
||||||
|
id: 'Lwa1Q6e4',
|
||||||
|
version_number: '0.141.3+1.21.6',
|
||||||
|
file_name: 'fabric-api-0.141.3+1.21.6.jar',
|
||||||
|
},
|
||||||
|
versionLink: '/project/fabric-api/version/Lwa1Q6e4',
|
||||||
|
owner: {
|
||||||
|
id: 'fabricmc',
|
||||||
|
name: 'FabricMC',
|
||||||
|
avatar_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
|
||||||
|
type: 'organization',
|
||||||
|
link: '/organization/fabricmc',
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const sodiumItem: ContentCardTableItem = {
|
||||||
|
id: 'sodium',
|
||||||
|
project: {
|
||||||
|
id: 'AANobbMI',
|
||||||
|
slug: 'sodium',
|
||||||
|
title: 'Sodium',
|
||||||
|
icon_url:
|
||||||
|
'https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp',
|
||||||
|
},
|
||||||
|
projectLink: '/project/sodium',
|
||||||
|
version: {
|
||||||
|
id: 'sodium-version',
|
||||||
|
version_number: 'mc1.21.6-0.6.13-fabric',
|
||||||
|
file_name: 'sodium-fabric-0.6.13+mc1.21.6.jar',
|
||||||
|
},
|
||||||
|
versionLink: '/project/sodium/version/sodium-version',
|
||||||
|
owner: {
|
||||||
|
id: 'jellysquid3',
|
||||||
|
name: 'jellysquid3',
|
||||||
|
type: 'user',
|
||||||
|
link: '/user/jellysquid3',
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const irisItem: ContentCardTableItem = {
|
||||||
|
id: 'iris',
|
||||||
|
project: {
|
||||||
|
id: 'YL57xq9U',
|
||||||
|
slug: 'iris',
|
||||||
|
title: 'Iris Shaders',
|
||||||
|
icon_url: 'https://cdn.modrinth.com/data/YL57xq9U/icon.png',
|
||||||
|
},
|
||||||
|
projectLink: '/project/iris',
|
||||||
|
version: {
|
||||||
|
id: 'iris-version',
|
||||||
|
version_number: '1.8.12+1.21.6-fabric',
|
||||||
|
file_name: 'iris-fabric-1.8.12+mc1.21.6.jar',
|
||||||
|
},
|
||||||
|
versionLink: '/project/iris/version/iris-version',
|
||||||
|
owner: {
|
||||||
|
id: 'coderbot',
|
||||||
|
name: 'coderbot',
|
||||||
|
type: 'user',
|
||||||
|
link: '/user/coderbot',
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const lithiumItem: ContentCardTableItem = {
|
||||||
|
id: 'lithium',
|
||||||
|
project: {
|
||||||
|
id: 'gvQqBUqZ',
|
||||||
|
slug: 'lithium',
|
||||||
|
title: 'Lithium',
|
||||||
|
icon_url:
|
||||||
|
'https://cdn.modrinth.com/data/gvQqBUqZ/d6a1873d52b7d1c82b9a8d9b1889c9c1a29ae92d_96.webp',
|
||||||
|
},
|
||||||
|
projectLink: '/project/lithium',
|
||||||
|
version: {
|
||||||
|
id: 'lithium-version',
|
||||||
|
version_number: 'mc1.21.6-0.16.2-fabric',
|
||||||
|
file_name: 'lithium-fabric-0.16.2+mc1.21.6.jar',
|
||||||
|
},
|
||||||
|
versionLink: '/project/lithium/version/lithium-version',
|
||||||
|
owner: {
|
||||||
|
id: 'caffeinemc',
|
||||||
|
name: 'CaffeineMC',
|
||||||
|
type: 'organization',
|
||||||
|
link: '/organization/caffeinemc',
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const continuityItem: ContentCardTableItem = {
|
||||||
|
id: 'continuity',
|
||||||
|
project: {
|
||||||
|
id: '1IjD5062',
|
||||||
|
slug: 'continuity',
|
||||||
|
title: 'Continuity',
|
||||||
|
icon_url: 'https://cdn.modrinth.com/data/1IjD5062/icon.png',
|
||||||
|
},
|
||||||
|
projectLink: '/project/continuity',
|
||||||
|
version: {
|
||||||
|
id: 'continuity-version',
|
||||||
|
version_number: '3.0.1-beta.2+1.21.6',
|
||||||
|
file_name: 'continuity-3.0.1-beta.2+1.21.6.jar',
|
||||||
|
},
|
||||||
|
versionLink: '/project/continuity/version/continuity-version',
|
||||||
|
owner: {
|
||||||
|
id: 'pepper-bell',
|
||||||
|
name: 'Pepper_Bell',
|
||||||
|
type: 'user',
|
||||||
|
link: '/user/pepper-bell',
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const capeProviderItem: ContentCardTableItem = {
|
||||||
|
id: 'cape-provider',
|
||||||
|
project: {
|
||||||
|
id: 'cape-provider',
|
||||||
|
slug: 'cape-provider',
|
||||||
|
title: 'Cape Provider',
|
||||||
|
icon_url: null,
|
||||||
|
},
|
||||||
|
projectLink: '/project/cape-provider',
|
||||||
|
version: {
|
||||||
|
id: 'cape-provider-version',
|
||||||
|
version_number: '5.4.2',
|
||||||
|
file_name: 'cape-provider-5.4.2.jar',
|
||||||
|
},
|
||||||
|
versionLink: '/project/cape-provider/version/cape-provider-version',
|
||||||
|
owner: {
|
||||||
|
id: 'litetex',
|
||||||
|
name: 'litetex',
|
||||||
|
type: 'user',
|
||||||
|
link: '/user/litetex',
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Instances/ContentDependencyWarningModal',
|
||||||
|
component: ContentDependencyWarningModal,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof ContentDependencyWarningModal>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const InstanceDependency: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { ButtonStyled, ContentDependencyWarningModal },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof ContentDependencyWarningModal> | null>(null)
|
||||||
|
const deleted = ref(false)
|
||||||
|
function handleDelete() {
|
||||||
|
deleted.value = true
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modalRef,
|
||||||
|
deleted,
|
||||||
|
handleDelete,
|
||||||
|
fabricApiItem,
|
||||||
|
sodiumItem,
|
||||||
|
irisItem,
|
||||||
|
continuityItem,
|
||||||
|
lithiumItem,
|
||||||
|
capeProviderItem,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
<ButtonStyled color="orange">
|
||||||
|
<button @click="modalRef?.show()">Delete dependency</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<p v-if="deleted" class="m-0 text-sm text-secondary">Dependency deletion confirmed</p>
|
||||||
|
<ContentDependencyWarningModal
|
||||||
|
ref="modalRef"
|
||||||
|
:items="[fabricApiItem]"
|
||||||
|
item-type="project"
|
||||||
|
:dependents="[
|
||||||
|
{ item: sodiumItem, dependencies: [fabricApiItem] },
|
||||||
|
{ item: irisItem, dependencies: [fabricApiItem] },
|
||||||
|
{ item: continuityItem, dependencies: [fabricApiItem] },
|
||||||
|
{ item: lithiumItem, dependencies: [fabricApiItem] },
|
||||||
|
{ item: capeProviderItem, dependencies: [fabricApiItem] },
|
||||||
|
]"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerDependency: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { ButtonStyled, ContentDependencyWarningModal },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof ContentDependencyWarningModal> | null>(null)
|
||||||
|
const deleted = ref(false)
|
||||||
|
function handleDelete() {
|
||||||
|
deleted.value = true
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modalRef,
|
||||||
|
deleted,
|
||||||
|
handleDelete,
|
||||||
|
lithiumItem,
|
||||||
|
sodiumItem,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
<ButtonStyled color="orange">
|
||||||
|
<button @click="modalRef?.show()">Delete server dependency</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<p v-if="deleted" class="m-0 text-sm text-secondary">Server dependency deletion confirmed</p>
|
||||||
|
<ContentDependencyWarningModal
|
||||||
|
ref="modalRef"
|
||||||
|
:items="[lithiumItem]"
|
||||||
|
item-type="project"
|
||||||
|
:dependents="[{ item: sodiumItem, dependencies: [lithiumItem] }]"
|
||||||
|
variant="server"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BulkDependencies: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { ButtonStyled, ContentDependencyWarningModal },
|
||||||
|
setup() {
|
||||||
|
const modalRef = ref<InstanceType<typeof ContentDependencyWarningModal> | null>(null)
|
||||||
|
const deleted = ref(false)
|
||||||
|
function handleDelete() {
|
||||||
|
deleted.value = true
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modalRef,
|
||||||
|
deleted,
|
||||||
|
handleDelete,
|
||||||
|
fabricApiItem,
|
||||||
|
lithiumItem,
|
||||||
|
sodiumItem,
|
||||||
|
irisItem,
|
||||||
|
continuityItem,
|
||||||
|
capeProviderItem,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: /* html */ `
|
||||||
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
<ButtonStyled color="orange">
|
||||||
|
<button @click="modalRef?.show()">Delete selected dependencies</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<p v-if="deleted" class="m-0 text-sm text-secondary">Bulk dependency deletion confirmed</p>
|
||||||
|
<ContentDependencyWarningModal
|
||||||
|
ref="modalRef"
|
||||||
|
:items="[fabricApiItem, lithiumItem]"
|
||||||
|
item-type="project"
|
||||||
|
:dependents="[
|
||||||
|
{ item: sodiumItem, dependencies: [fabricApiItem, lithiumItem] },
|
||||||
|
{ item: irisItem, dependencies: [fabricApiItem] },
|
||||||
|
{ item: continuityItem, dependencies: [fabricApiItem] },
|
||||||
|
{ item: capeProviderItem, dependencies: [lithiumItem] },
|
||||||
|
]"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -2,12 +2,7 @@ import preset from '@modrinth/tooling-config/tailwind/tailwind-preset.ts'
|
|||||||
import type { Config } from 'tailwindcss'
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
content: [
|
content: ['./src/**/*.{js,vue,ts,mdx}', './.storybook/**/*.{ts,js}'],
|
||||||
'./src/components/**/*.{js,vue,ts}',
|
|
||||||
'./src/pages/**/*.{js,vue,ts}',
|
|
||||||
'./src/stories/**/*.{js,vue,ts,mdx}',
|
|
||||||
'./.storybook/**/*.{ts,js}',
|
|
||||||
],
|
|
||||||
presets: [preset],
|
presets: [preset],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user