You've already forked AstralRinth
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 ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
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 { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
|
||||
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) {
|
||||
if (!mod.file_path) return
|
||||
const operation = beginContentOperation(mod)
|
||||
@@ -1081,6 +1138,7 @@ provideContentManager({
|
||||
deleteItem: removeMod,
|
||||
bulkDeleteItems: (items: ContentItem[]) =>
|
||||
Promise.all(items.map((item) => removeMod(item))).then(() => {}),
|
||||
getDeleteDependencyWarning,
|
||||
refresh: () => initProjects('must_revalidate'),
|
||||
browse: handleBrowseContent,
|
||||
uploadFiles: handleUploadFiles,
|
||||
|
||||
@@ -61,6 +61,7 @@ interface Props {
|
||||
showCheckbox?: boolean
|
||||
hideDelete?: boolean
|
||||
hideActions?: boolean
|
||||
inline?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -82,12 +83,14 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
showCheckbox: false,
|
||||
hideDelete: false,
|
||||
hideActions: false,
|
||||
inline: false,
|
||||
})
|
||||
|
||||
const selected = defineModel<boolean>('selected')
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:enabled': [value: boolean]
|
||||
select: [value: boolean, event?: MouseEvent]
|
||||
delete: [event: MouseEvent]
|
||||
update: []
|
||||
switchVersion: []
|
||||
@@ -124,8 +127,10 @@ const deleteHovered = ref(false)
|
||||
<template>
|
||||
<div
|
||||
role="row"
|
||||
class="flex h-[74px] items-center justify-between gap-4 px-3"
|
||||
class="flex items-center justify-between"
|
||||
:class="{
|
||||
'h-[74px] gap-4 px-3': !inline,
|
||||
'gap-3': inline,
|
||||
'opacity-50 grayscale': disabled && !installing,
|
||||
'opacity-50': installing,
|
||||
}"
|
||||
@@ -141,7 +146,7 @@ const deleteHovered = ref(false)
|
||||
:model-value="selected ?? false"
|
||||
:aria-label="formatMessage(messages.selectProject, { project: project.title })"
|
||||
class="shrink-0"
|
||||
@update:model-value="selected = $event"
|
||||
@update:model-value="(value, event) => emit('select', value, event)"
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -150,7 +155,7 @@ const deleteHovered = ref(false)
|
||||
>
|
||||
<div
|
||||
v-tooltip="installing ? formatMessage(commonMessages.installingLabel) : undefined"
|
||||
class="relative shrink-0"
|
||||
class="relative flex shrink-0 items-center"
|
||||
>
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
@@ -180,6 +185,7 @@ const deleteHovered = ref(false)
|
||||
>
|
||||
{{ project.title }}
|
||||
</AutoLink>
|
||||
<slot name="title-badges" />
|
||||
<Tooltip
|
||||
v-if="isClientOnly"
|
||||
theme="dismissable-prompt"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
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 { useVIntl } from '#ui/composables/i18n'
|
||||
@@ -119,26 +119,14 @@ function toggleSelectAll() {
|
||||
}
|
||||
|
||||
const lastSelectedIndex = ref<number | null>(null)
|
||||
const shiftHeld = ref(false)
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Shift') shiftHeld.value = true
|
||||
}
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
if (e.key === 'Shift') shiftHeld.value = false
|
||||
}
|
||||
|
||||
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) {
|
||||
function toggleItemSelection(
|
||||
itemId: string,
|
||||
selected: boolean,
|
||||
index?: number,
|
||||
event?: MouseEvent,
|
||||
) {
|
||||
if (selected && event?.shiftKey && lastSelectedIndex.value !== null && index !== undefined) {
|
||||
const start = Math.min(lastSelectedIndex.value, index)
|
||||
const end = Math.max(lastSelectedIndex.value, index)
|
||||
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',
|
||||
visibleRange.start + idx === items.length - 1 && !flat ? 'rounded-b-[20px]' : '',
|
||||
]"
|
||||
@update:selected="
|
||||
(val) => toggleItemSelection(item.id, val ?? false, visibleRange.start + idx)
|
||||
@select="
|
||||
(val, event) =>
|
||||
toggleItemSelection(item.id, val ?? false, visibleRange.start + idx, event)
|
||||
"
|
||||
@update:enabled="(val) => emit('update:enabled', item.id, val)"
|
||||
@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',
|
||||
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)"
|
||||
@delete="(e: MouseEvent) => emit('delete', item.id, e)"
|
||||
@update="emit('update', item.id)"
|
||||
|
||||
+3
-6
@@ -6,15 +6,12 @@
|
||||
itemType: formatContentTypeSentence(formatMessage, visibleItemType, visibleCount),
|
||||
})
|
||||
"
|
||||
:fade="props.variant === 'server' ? 'warning' : 'danger'"
|
||||
fade="warning"
|
||||
max-width="500px"
|
||||
:on-hide="() => backupCreator?.cancelBackup()"
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition
|
||||
:type="props.variant === 'server' ? 'warning' : 'critical'"
|
||||
:header="formatMessage(messages.admonitionHeader)"
|
||||
>
|
||||
<Admonition type="warning" :header="formatMessage(messages.admonitionHeader)">
|
||||
{{ formatMessage(messages.admonitionBody) }}
|
||||
</Admonition>
|
||||
<InlineBackupCreator
|
||||
@@ -32,7 +29,7 @@
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled :color="props.variant === 'server' ? 'orange' : 'red'">
|
||||
<ButtonStyled color="orange">
|
||||
<button
|
||||
v-tooltip="props.actionDisabled ? props.actionDisabledTooltip : undefined"
|
||||
: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 ConfirmRepairModal } from './components/modals/ConfirmRepairModal.vue'
|
||||
export { default as ConfirmUnlinkModal } from './components/modals/ConfirmUnlinkModal.vue'
|
||||
export { default as ContentDependencyWarningModal } from './components/modals/ContentDependencyWarningModal.vue'
|
||||
export type {
|
||||
ContentInstallInstance,
|
||||
ContentInstallProjectInfo,
|
||||
|
||||
@@ -34,6 +34,7 @@ import ContentSelectionBar from './components/ContentSelectionBar.vue'
|
||||
import ConfirmBulkUpdateModal from './components/modals/ConfirmBulkUpdateModal.vue'
|
||||
import ConfirmDeletionModal from './components/modals/ConfirmDeletionModal.vue'
|
||||
import ConfirmUnlinkModal from './components/modals/ConfirmUnlinkModal.vue'
|
||||
import ContentDependencyWarningModal from './components/modals/ContentDependencyWarningModal.vue'
|
||||
import {
|
||||
getClientWarningType,
|
||||
isClientOnlyEnvironment,
|
||||
@@ -319,21 +320,73 @@ const hasOutdatedProjects = computed(() => {
|
||||
// Deletion
|
||||
const pendingDeletionItems = ref<ContentItem[]>([])
|
||||
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) {
|
||||
const item = ctx.items.value.find((i) => getItemId(i) === id)
|
||||
if (item) {
|
||||
pendingDeletionItems.value = [item]
|
||||
if (event?.shiftKey && !ctx.isBusy.value) {
|
||||
confirmDelete()
|
||||
} else {
|
||||
confirmDeletionModal.value?.show()
|
||||
}
|
||||
function mapToDisplayItem(item: ContentItem) {
|
||||
return {
|
||||
...ctx.mapToTableItem(item),
|
||||
id: getItemId(item),
|
||||
}
|
||||
}
|
||||
|
||||
function showBulkDeleteModal(event?: MouseEvent) {
|
||||
pendingDeletionItems.value = [...selectedItems.value]
|
||||
async function promptDeleteItems(items: ContentItem[], event?: MouseEvent) {
|
||||
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) {
|
||||
confirmDelete()
|
||||
} 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() {
|
||||
if (ctx.isBusy.value) return
|
||||
const itemsToDelete = [...pendingDeletionItems.value]
|
||||
@@ -353,6 +451,7 @@ async function confirmDelete() {
|
||||
bulkWaiting.value = true
|
||||
try {
|
||||
await ctx.bulkDeleteItems(itemsToDelete)
|
||||
await disablePendingDependencyWarningDependents()
|
||||
} finally {
|
||||
clearSelection()
|
||||
isBulkOperating.value = false
|
||||
@@ -369,6 +468,7 @@ async function confirmDelete() {
|
||||
try {
|
||||
await ctx.deleteItem(item)
|
||||
removeFromSelection(id)
|
||||
await disablePendingDependencyWarningDependents()
|
||||
} finally {
|
||||
unmarkChanging(id)
|
||||
}
|
||||
@@ -384,6 +484,7 @@ async function confirmDelete() {
|
||||
},
|
||||
{ onComplete: clearSelection },
|
||||
)
|
||||
await disablePendingDependencyWarningDependents()
|
||||
}
|
||||
|
||||
async function handleToggleEnabledById(id: string, _value: boolean) {
|
||||
@@ -879,6 +980,17 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
|
||||
@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
|
||||
v-if="hasBulkUpdateSupport"
|
||||
ref="confirmBulkUpdateModal"
|
||||
|
||||
@@ -25,6 +25,14 @@ export interface ContentModpackData {
|
||||
disabledText?: string
|
||||
}
|
||||
|
||||
export interface ContentDependencyWarning {
|
||||
items: ContentItem[]
|
||||
dependents: Array<{
|
||||
item: ContentItem
|
||||
dependencies: ContentItem[]
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ContentManagerContext {
|
||||
// Data
|
||||
items: Ref<ContentItem[]> | ComputedRef<ContentItem[]>
|
||||
@@ -55,6 +63,9 @@ export interface ContentManagerContext {
|
||||
bulkDeleteItems?: (items: ContentItem[]) => Promise<void>
|
||||
bulkEnableItems?: (items: ContentItem[]) => Promise<void>
|
||||
bulkDisableItems?: (items: ContentItem[]) => Promise<void>
|
||||
getDeleteDependencyWarning?: (
|
||||
items: ContentItem[],
|
||||
) => ContentDependencyWarning | null | Promise<ContentDependencyWarning | null>
|
||||
|
||||
// Update support (optional per-platform)
|
||||
hasUpdateSupport: boolean
|
||||
|
||||
@@ -377,6 +377,48 @@
|
||||
"content.confirm-unlink.unlink-button": {
|
||||
"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": {
|
||||
"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'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./src/components/**/*.{js,vue,ts}',
|
||||
'./src/pages/**/*.{js,vue,ts}',
|
||||
'./src/stories/**/*.{js,vue,ts,mdx}',
|
||||
'./.storybook/**/*.{ts,js}',
|
||||
],
|
||||
content: ['./src/**/*.{js,vue,ts,mdx}', './.storybook/**/*.{ts,js}'],
|
||||
presets: [preset],
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user