feat:移除了弹窗,服务器添加sls
This commit is contained in:
commit
6a295f9a7a
4082 files changed
+1322534
No files matched your search
@@ -0,0 +1,725 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon, XIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, Checkbox, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { getActiveDependencyConflictIdentities } from '@/providers/content-selection-logic'
|
||||
|
||||
export interface ContentInstallPreviewDependency {
|
||||
id: string
|
||||
title: string
|
||||
iconUrl?: string | null
|
||||
versionNumber?: string
|
||||
fileName?: string
|
||||
description?: string
|
||||
projectUrl?: string
|
||||
requiredBy: string[]
|
||||
alreadyInstalled: boolean
|
||||
status?: 'installed' | 'included'
|
||||
versionMismatch?: boolean
|
||||
selectionReason?: string
|
||||
required?: boolean
|
||||
requiredByKeys?: string[]
|
||||
}
|
||||
|
||||
export interface ContentInstallPreviewSkipped {
|
||||
id: string
|
||||
title: string
|
||||
reason: string
|
||||
requiredByKeys?: string[]
|
||||
}
|
||||
|
||||
export interface ContentInstallPreviewData {
|
||||
primary?: ContentInstallPreviewPrimary
|
||||
primaries?: ContentInstallPreviewPrimary[]
|
||||
instanceName: string
|
||||
installDependencies: boolean
|
||||
dependencies: ContentInstallPreviewDependency[]
|
||||
skipped: ContentInstallPreviewSkipped[]
|
||||
}
|
||||
|
||||
export interface ContentInstallPreviewPrimary {
|
||||
key?: string
|
||||
title: string
|
||||
iconUrl?: string | null
|
||||
versionNumber?: string
|
||||
provider?: string
|
||||
contentType?: string
|
||||
error?: string
|
||||
conflictIdentities?: string[]
|
||||
removable?: boolean
|
||||
}
|
||||
|
||||
export interface ContentInstallBatchPreviewResult {
|
||||
approvedIds: string[]
|
||||
primaryKeys: string[]
|
||||
}
|
||||
|
||||
export interface ContentInstallConflictPrompt {
|
||||
candidate: {
|
||||
title: string
|
||||
provider: string
|
||||
contentType: string
|
||||
iconUrl?: string | null
|
||||
}
|
||||
existing: Array<{
|
||||
title: string
|
||||
provider: string
|
||||
fileName?: string
|
||||
}>
|
||||
source: 'heuristic'
|
||||
confidence: 'high' | 'possible'
|
||||
}
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
header: {
|
||||
id: 'app.content-install.preview.header',
|
||||
defaultMessage: 'Confirm installation',
|
||||
},
|
||||
description: {
|
||||
id: 'app.content-install.preview.description',
|
||||
defaultMessage:
|
||||
'{count, plural, one {# dependency will be installed automatically} other {# dependencies will be installed automatically}} for {project} in {instance}.',
|
||||
},
|
||||
batchDescription: {
|
||||
id: 'app.content-install.preview.batch-description',
|
||||
defaultMessage:
|
||||
'Review {projectCount, plural, one {# project} other {# projects}} and {dependencyCount, plural, one {# dependency} other {# dependencies}} for {instance}.',
|
||||
},
|
||||
selectedContentHeader: {
|
||||
id: 'app.content-install.preview.selected-content-header',
|
||||
defaultMessage: 'Selected content',
|
||||
},
|
||||
removeProject: {
|
||||
id: 'app.content-install.preview.remove-project',
|
||||
defaultMessage: 'Remove {project} from this installation',
|
||||
},
|
||||
dependenciesHeader: {
|
||||
id: 'app.content-install.preview.dependencies-header',
|
||||
defaultMessage: 'Dependencies',
|
||||
},
|
||||
dependenciesCount: {
|
||||
id: 'app.content-install.preview.dependencies-count',
|
||||
defaultMessage: '{count, plural, one {# dependency} other {# dependencies}}',
|
||||
},
|
||||
requiredDependenciesHeader: {
|
||||
id: 'app.content-install.preview.required-dependencies-header',
|
||||
defaultMessage: 'Required dependencies',
|
||||
},
|
||||
optionalDependenciesHeader: {
|
||||
id: 'app.content-install.preview.optional-dependencies-header',
|
||||
defaultMessage: 'Optional dependencies',
|
||||
},
|
||||
requiredBy: {
|
||||
id: 'app.content-install.preview.required-by',
|
||||
defaultMessage: 'Required by {projects}',
|
||||
},
|
||||
alreadyInstalled: {
|
||||
id: 'app.content-install.preview.already-installed',
|
||||
defaultMessage: 'Already installed',
|
||||
},
|
||||
alreadyIncluded: {
|
||||
id: 'app.content-install.preview.already-included',
|
||||
defaultMessage: 'Already included',
|
||||
},
|
||||
versionMismatch: {
|
||||
id: 'app.content-install.preview.version-mismatch',
|
||||
defaultMessage: 'Version may not match this instance',
|
||||
},
|
||||
skippedHeader: {
|
||||
id: 'app.content-install.preview.skipped-header',
|
||||
defaultMessage: 'Skipped',
|
||||
},
|
||||
onlyChecked: {
|
||||
id: 'app.content-install.preview.only-checked',
|
||||
defaultMessage: 'Only checked dependencies will be installed.',
|
||||
},
|
||||
selectAll: {
|
||||
id: 'app.content-install.preview.select-all',
|
||||
defaultMessage: 'Select all',
|
||||
},
|
||||
clearAll: {
|
||||
id: 'app.content-install.preview.clear-all',
|
||||
defaultMessage: 'Clear all',
|
||||
},
|
||||
cancel: {
|
||||
id: 'app.content-install.preview.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
viewDetails: {
|
||||
id: 'app.content-install.preview.view-details',
|
||||
defaultMessage: 'View details',
|
||||
},
|
||||
openProjectPage: {
|
||||
id: 'app.content-install.preview.open-project-page',
|
||||
defaultMessage: 'Open project page',
|
||||
},
|
||||
descriptionUnavailable: {
|
||||
id: 'app.content-install.preview.description-unavailable',
|
||||
defaultMessage: 'No description available.',
|
||||
},
|
||||
install: {
|
||||
id: 'app.content-install.preview.install',
|
||||
defaultMessage: 'Install',
|
||||
},
|
||||
installResolved: {
|
||||
id: 'app.content-install.preview.install-resolved',
|
||||
defaultMessage: 'Install resolved content',
|
||||
},
|
||||
conflictHeader: {
|
||||
id: 'app.content-install.preview.conflict-header',
|
||||
defaultMessage: 'Possible duplicate content',
|
||||
},
|
||||
conflictDescription: {
|
||||
id: 'app.content-install.preview.conflict-description',
|
||||
defaultMessage:
|
||||
'{candidate} may be the same content as an installed or selected project. Continue anyway?',
|
||||
},
|
||||
continueAnyway: {
|
||||
id: 'app.content-install.preview.continue-anyway',
|
||||
defaultMessage: 'Install anyway',
|
||||
},
|
||||
existingContent: {
|
||||
id: 'app.content-install.preview.existing-content',
|
||||
defaultMessage: 'Existing content',
|
||||
},
|
||||
})
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
const data = ref<ContentInstallPreviewData | null>(null)
|
||||
const selectedIds = ref<Set<string>>(new Set())
|
||||
const expandedDependencyIds = ref<Set<string>>(new Set())
|
||||
const removedPrimaryKeys = ref<Set<string>>(new Set())
|
||||
let settled = false
|
||||
let batchMode = false
|
||||
let conflictMode = false
|
||||
const conflictPrompt = ref<ContentInstallConflictPrompt | null>(null)
|
||||
let resolveShow:
|
||||
| ((result: string[] | ContentInstallBatchPreviewResult | boolean | null) => void)
|
||||
| null = null
|
||||
|
||||
const primaries = computed(() => {
|
||||
if (!data.value) return []
|
||||
if (data.value.primaries?.length) return data.value.primaries
|
||||
return data.value.primary ? [data.value.primary] : []
|
||||
})
|
||||
const visiblePrimaries = computed(() =>
|
||||
primaries.value.filter((primary) => !primary.key || !removedPrimaryKeys.value.has(primary.key)),
|
||||
)
|
||||
const visiblePrimaryKeys = computed(() =>
|
||||
visiblePrimaries.value.map((primary) => primary.key).filter((key): key is string => !!key),
|
||||
)
|
||||
const visiblePrimaryKeySet = computed(() => new Set(visiblePrimaryKeys.value))
|
||||
const visibleDependencies = computed(
|
||||
() =>
|
||||
data.value?.dependencies.filter(
|
||||
(dependency) =>
|
||||
!dependency.requiredByKeys?.length ||
|
||||
dependency.requiredByKeys.some((key) => visiblePrimaryKeySet.value.has(key)),
|
||||
) ?? [],
|
||||
)
|
||||
const activeConflictIdentities = computed(() =>
|
||||
getActiveDependencyConflictIdentities(data.value?.dependencies ?? [], visiblePrimaryKeySet.value),
|
||||
)
|
||||
function primaryError(primary: ContentInstallPreviewPrimary) {
|
||||
if (!primary.error) return null
|
||||
if (!primary.conflictIdentities?.length) return primary.error
|
||||
return primary.conflictIdentities.some((identity) => activeConflictIdentities.value.has(identity))
|
||||
? primary.error
|
||||
: null
|
||||
}
|
||||
const visibleSkipped = computed(
|
||||
() =>
|
||||
data.value?.skipped.filter(
|
||||
(skipped) =>
|
||||
!skipped.requiredByKeys?.length ||
|
||||
skipped.requiredByKeys.some((key) => visiblePrimaryKeySet.value.has(key)),
|
||||
) ?? [],
|
||||
)
|
||||
const hasBlockingPrimary = computed(() =>
|
||||
visiblePrimaries.value.some((primary) => !!primaryError(primary)),
|
||||
)
|
||||
|
||||
const installableDependencies = computed(() => visibleDependencies.value)
|
||||
const dependencyGroups = computed(() =>
|
||||
[
|
||||
{
|
||||
id: 'required',
|
||||
header: messages.requiredDependenciesHeader,
|
||||
dependencies: visibleDependencies.value.filter((dependency) => dependency.required !== false),
|
||||
},
|
||||
{
|
||||
id: 'optional',
|
||||
header: messages.optionalDependenciesHeader,
|
||||
dependencies: visibleDependencies.value.filter((dependency) => dependency.required === false),
|
||||
},
|
||||
].filter((group) => group.dependencies.length > 0),
|
||||
)
|
||||
const selectedInstallableCount = computed(
|
||||
() =>
|
||||
installableDependencies.value.filter((dependency) => selectedIds.value.has(dependency.id))
|
||||
.length,
|
||||
)
|
||||
const hasUnresolvedDependencies = computed(() => visibleSkipped.value.length > 0)
|
||||
|
||||
function toggleDependency(id: string, value: boolean) {
|
||||
const next = new Set(selectedIds.value)
|
||||
if (value) next.add(id)
|
||||
else next.delete(id)
|
||||
selectedIds.value = next
|
||||
}
|
||||
|
||||
function toggleAll(value: boolean) {
|
||||
const next = new Set(selectedIds.value)
|
||||
for (const dependency of installableDependencies.value) {
|
||||
if (value) next.add(dependency.id)
|
||||
else next.delete(dependency.id)
|
||||
}
|
||||
selectedIds.value = next
|
||||
}
|
||||
|
||||
function hasDependencyDetails(dependency: ContentInstallPreviewDependency) {
|
||||
return !!dependency.description || !!dependency.projectUrl
|
||||
}
|
||||
|
||||
function toggleDependencyDetails(id: string) {
|
||||
const next = new Set(expandedDependencyIds.value)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
expandedDependencyIds.value = next
|
||||
}
|
||||
|
||||
async function openDependencyPage(dependency: ContentInstallPreviewDependency) {
|
||||
if (!dependency.projectUrl) return
|
||||
await openUrl(dependency.projectUrl)
|
||||
}
|
||||
|
||||
function initialSelectedIds(value: ContentInstallPreviewData) {
|
||||
if (!value.installDependencies) return new Set<string>()
|
||||
return new Set(
|
||||
value.dependencies
|
||||
.filter((dependency) => !dependency.alreadyInstalled && dependency.required !== false)
|
||||
.map((dependency) => dependency.id),
|
||||
)
|
||||
}
|
||||
|
||||
function finish(result: string[] | ContentInstallBatchPreviewResult | boolean | null) {
|
||||
if (settled) return
|
||||
settled = true
|
||||
const resolve = resolveShow
|
||||
resolveShow = null
|
||||
if (resolve) resolve(result)
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (conflictMode) {
|
||||
finish(true)
|
||||
return
|
||||
}
|
||||
if (hasBlockingPrimary.value || visiblePrimaries.value.length === 0) return
|
||||
const approvedIds = visibleDependencies.value
|
||||
.filter((dependency) => selectedIds.value.has(dependency.id))
|
||||
.map((dependency) => dependency.id)
|
||||
finish(batchMode ? { approvedIds, primaryKeys: visiblePrimaryKeys.value } : approvedIds)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
finish(null)
|
||||
}
|
||||
|
||||
function show(value: ContentInstallPreviewData): Promise<string[] | null> {
|
||||
resolveShow?.(null)
|
||||
resolveShow = null
|
||||
data.value = value
|
||||
batchMode = false
|
||||
conflictMode = false
|
||||
conflictPrompt.value = null
|
||||
removedPrimaryKeys.value = new Set()
|
||||
selectedIds.value = initialSelectedIds(value)
|
||||
expandedDependencyIds.value = new Set()
|
||||
settled = false
|
||||
modal.value?.show()
|
||||
return new Promise<string[] | null>((resolve) => {
|
||||
resolveShow = (result) => resolve(Array.isArray(result) ? result : null)
|
||||
})
|
||||
}
|
||||
|
||||
function showBatch(
|
||||
value: ContentInstallPreviewData,
|
||||
): Promise<ContentInstallBatchPreviewResult | null> {
|
||||
resolveShow?.(null)
|
||||
resolveShow = null
|
||||
data.value = value
|
||||
batchMode = true
|
||||
conflictMode = false
|
||||
conflictPrompt.value = null
|
||||
removedPrimaryKeys.value = new Set()
|
||||
selectedIds.value = initialSelectedIds(value)
|
||||
expandedDependencyIds.value = new Set()
|
||||
settled = false
|
||||
modal.value?.show()
|
||||
return new Promise<ContentInstallBatchPreviewResult | null>((resolve) => {
|
||||
resolveShow = (result) =>
|
||||
resolve(result && !Array.isArray(result) && typeof result !== 'boolean' ? result : null)
|
||||
})
|
||||
}
|
||||
|
||||
function showConflict(value: ContentInstallConflictPrompt): Promise<boolean> {
|
||||
resolveShow?.(null)
|
||||
resolveShow = null
|
||||
data.value = null
|
||||
batchMode = false
|
||||
conflictMode = true
|
||||
conflictPrompt.value = value
|
||||
settled = false
|
||||
modal.value?.show()
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolveShow = (result) => resolve(result === true)
|
||||
})
|
||||
}
|
||||
|
||||
function removePrimary(key: string) {
|
||||
removedPrimaryKeys.value = new Set([...removedPrimaryKeys.value, key])
|
||||
}
|
||||
|
||||
defineExpose({ show, showBatch, showConflict })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal
|
||||
ref="modal"
|
||||
:header="formatMessage(conflictMode ? messages.conflictHeader : messages.header)"
|
||||
scrollable
|
||||
max-content-height="70vh"
|
||||
width="40rem"
|
||||
max-width="40rem"
|
||||
:on-hide="hide"
|
||||
>
|
||||
<div v-if="conflictPrompt" class="flex flex-col gap-4">
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-lg border border-solid border-warning bg-warning-bg p-3"
|
||||
>
|
||||
<Avatar
|
||||
:src="conflictPrompt.candidate.iconUrl"
|
||||
:alt="conflictPrompt.candidate.title"
|
||||
size="2.5rem"
|
||||
:tint-by="conflictPrompt.candidate.title"
|
||||
no-shadow
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="block truncate font-semibold text-contrast">{{
|
||||
conflictPrompt.candidate.title
|
||||
}}</span>
|
||||
<span class="block truncate text-sm text-secondary">
|
||||
{{
|
||||
[conflictPrompt.candidate.provider, conflictPrompt.candidate.contentType].join(' · ')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 text-primary">
|
||||
{{
|
||||
formatMessage(messages.conflictDescription, {
|
||||
candidate: conflictPrompt.candidate.title,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.existingContent)
|
||||
}}</span>
|
||||
<div
|
||||
v-for="item in conflictPrompt.existing"
|
||||
:key="`${item.provider}:${item.title}:${item.fileName ?? ''}`"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-solid border-surface-4 bg-surface-2 px-3 py-2"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium text-contrast">{{ item.title }}</span>
|
||||
<span class="shrink-0 text-sm text-secondary">{{ item.provider }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="data" class="flex min-w-0 flex-col gap-4">
|
||||
<div v-if="batchMode" class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.selectedContentHeader)
|
||||
}}</span>
|
||||
<div
|
||||
v-for="primary in visiblePrimaries"
|
||||
:key="primary.key ?? primary.title"
|
||||
class="flex items-center gap-3 rounded-lg border border-solid border-surface-4 bg-surface-2 p-3"
|
||||
>
|
||||
<Avatar
|
||||
:src="primary.iconUrl"
|
||||
:alt="primary.title"
|
||||
size="2.5rem"
|
||||
:tint-by="primary.title"
|
||||
no-shadow
|
||||
/>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span class="truncate font-semibold text-contrast">{{ primary.title }}</span>
|
||||
<span class="truncate text-sm text-secondary">
|
||||
{{
|
||||
[primary.versionNumber, primary.provider, primary.contentType]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
}}
|
||||
</span>
|
||||
<span v-if="primaryError(primary)" class="text-sm text-red">
|
||||
{{ primaryError(primary) }}
|
||||
</span>
|
||||
</div>
|
||||
<ButtonStyled v-if="primary.removable && primary.key" circular type="transparent">
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="formatMessage(messages.removeProject, { project: primary.title })"
|
||||
@click="removePrimary(primary.key)"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="visiblePrimaries[0]"
|
||||
class="flex items-center gap-3 rounded-lg border border-solid border-surface-4 bg-surface-2 p-3"
|
||||
>
|
||||
<Avatar
|
||||
:src="visiblePrimaries[0].iconUrl"
|
||||
:alt="visiblePrimaries[0].title"
|
||||
size="2.5rem"
|
||||
:tint-by="visiblePrimaries[0].title"
|
||||
no-shadow
|
||||
/>
|
||||
<div class="flex min-w-0 flex-col gap-0.5">
|
||||
<span class="truncate font-semibold text-contrast">{{ visiblePrimaries[0].title }}</span>
|
||||
<span v-if="visiblePrimaries[0].versionNumber" class="truncate text-sm text-secondary">
|
||||
{{ visiblePrimaries[0].versionNumber }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="m-0 text-primary">
|
||||
{{
|
||||
batchMode
|
||||
? formatMessage(messages.batchDescription, {
|
||||
projectCount: visiblePrimaries.length,
|
||||
dependencyCount: visibleDependencies.length,
|
||||
instance: data.instanceName,
|
||||
})
|
||||
: formatMessage(messages.description, {
|
||||
count: selectedInstallableCount,
|
||||
project: visiblePrimaries[0]?.title ?? '',
|
||||
instance: data.instanceName,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
|
||||
<div v-if="visibleDependencies.length > 0" class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center gap-2 font-semibold text-contrast">
|
||||
{{ formatMessage(messages.dependenciesHeader) }}
|
||||
<span
|
||||
class="rounded-full bg-surface-4 px-2 py-0.5 text-xs font-medium tabular-nums text-secondary"
|
||||
>
|
||||
{{ formatMessage(messages.dependenciesCount, { count: visibleDependencies.length }) }}
|
||||
</span>
|
||||
</span>
|
||||
<ButtonStyled v-if="installableDependencies.length > 1" size="small" type="transparent">
|
||||
<button @click="toggleAll(selectedInstallableCount !== installableDependencies.length)">
|
||||
{{
|
||||
selectedInstallableCount === installableDependencies.length
|
||||
? formatMessage(messages.clearAll)
|
||||
: formatMessage(messages.selectAll)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-3"
|
||||
:class="{ 'sm:grid-cols-2': dependencyGroups.length > 1 }"
|
||||
>
|
||||
<div
|
||||
v-for="group in dependencyGroups"
|
||||
:key="group.id"
|
||||
class="flex min-w-0 flex-col gap-2"
|
||||
>
|
||||
<span class="flex items-center gap-2 font-semibold text-contrast">
|
||||
{{ formatMessage(group.header) }}
|
||||
<span
|
||||
class="rounded-full bg-surface-4 px-2 py-0.5 text-xs font-medium tabular-nums text-secondary"
|
||||
>
|
||||
{{ group.dependencies.length }}
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
v-for="dependency in group.dependencies"
|
||||
:key="dependency.id"
|
||||
class="flex w-full min-w-0 flex-col overflow-hidden rounded-xl border border-solid border-surface-4 bg-surface-2"
|
||||
:class="{ 'opacity-60': dependency.alreadyInstalled }"
|
||||
>
|
||||
<div class="flex items-start gap-3 p-3">
|
||||
<Checkbox
|
||||
:model-value="selectedIds.has(dependency.id)"
|
||||
class="mt-2 shrink-0"
|
||||
@update:model-value="(value) => toggleDependency(dependency.id, value)"
|
||||
/>
|
||||
<Avatar
|
||||
:src="dependency.iconUrl"
|
||||
:alt="dependency.title"
|
||||
size="2.5rem"
|
||||
:tint-by="dependency.title"
|
||||
no-shadow
|
||||
/>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<button
|
||||
v-if="hasDependencyDetails(dependency)"
|
||||
type="button"
|
||||
class="group flex w-full min-w-0 cursor-pointer items-center gap-1 border-none bg-transparent p-0 text-left"
|
||||
:aria-expanded="expandedDependencyIds.has(dependency.id)"
|
||||
:aria-label="formatMessage(messages.viewDetails, { project: dependency.title })"
|
||||
@click="toggleDependencyDetails(dependency.id)"
|
||||
>
|
||||
<span
|
||||
v-tooltip="
|
||||
dependency.description
|
||||
? {
|
||||
content: dependency.description,
|
||||
placement: 'top',
|
||||
popperClass: 'preview-dependency-tooltip',
|
||||
}
|
||||
: null
|
||||
"
|
||||
class="min-w-0 truncate font-semibold text-contrast group-hover:underline"
|
||||
>
|
||||
{{ dependency.title }}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
aria-hidden="true"
|
||||
class="shrink-0 text-secondary transition-transform duration-150"
|
||||
:class="{
|
||||
'rotate-180': expandedDependencyIds.has(dependency.id),
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
<span v-else class="truncate font-semibold text-contrast">
|
||||
{{ dependency.title }}
|
||||
</span>
|
||||
<span
|
||||
v-if="dependency.versionNumber"
|
||||
class="min-w-0 truncate text-sm text-secondary"
|
||||
>
|
||||
{{ dependency.versionNumber }}
|
||||
</span>
|
||||
<span
|
||||
v-if="dependency.requiredBy.length > 0"
|
||||
class="min-w-0 truncate text-sm text-secondary"
|
||||
>
|
||||
{{
|
||||
formatMessage(messages.requiredBy, {
|
||||
projects: dependency.requiredBy.join(', '),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-1 pt-1">
|
||||
<span
|
||||
v-if="dependency.versionMismatch"
|
||||
class="rounded-full bg-warning-bg px-2 py-0.5 text-xs font-medium text-warning-text"
|
||||
>
|
||||
{{ formatMessage(messages.versionMismatch) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="dependency.selectionReason"
|
||||
class="rounded-full bg-surface-4 px-2 py-0.5 text-xs font-medium text-secondary"
|
||||
>
|
||||
{{ dependency.selectionReason }}
|
||||
</span>
|
||||
<span
|
||||
v-if="dependency.alreadyInstalled"
|
||||
class="rounded-full bg-surface-4 px-2 py-0.5 text-xs font-medium text-secondary"
|
||||
>
|
||||
{{
|
||||
formatMessage(
|
||||
dependency.status === 'included'
|
||||
? messages.alreadyIncluded
|
||||
: messages.alreadyInstalled,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedDependencyIds.has(dependency.id)"
|
||||
class="mb-3 flex w-auto min-w-0 flex-col gap-2.5 rounded-lg bg-surface-1 px-3 py-2.5 mx-3"
|
||||
>
|
||||
<p
|
||||
v-if="dependency.description"
|
||||
class="m-0 w-full min-w-0 text-sm leading-relaxed text-secondary [overflow-wrap:anywhere]"
|
||||
>
|
||||
{{ dependency.description }}
|
||||
</p>
|
||||
<p v-else class="m-0 w-full min-w-0 text-sm text-secondary">
|
||||
{{ formatMessage(messages.descriptionUnavailable) }}
|
||||
</p>
|
||||
<ButtonStyled v-if="dependency.projectUrl" class="self-start" type="outlined">
|
||||
<button type="button" @click="openDependencyPage(dependency)">
|
||||
{{ formatMessage(messages.openProjectPage) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-secondary">{{ formatMessage(messages.onlyChecked) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="visibleSkipped.length > 0" class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.skippedHeader) }}</span>
|
||||
<div
|
||||
v-for="skipped in visibleSkipped"
|
||||
:key="skipped.id"
|
||||
class="flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-solid border-surface-4 bg-surface-2 px-3 py-2"
|
||||
>
|
||||
<span class="min-w-0 flex-1 truncate font-medium text-contrast">
|
||||
{{ skipped.title }}
|
||||
</span>
|
||||
<span class="shrink-0 text-sm text-secondary">{{ skipped.reason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide">{{ formatMessage(messages.cancel) }}</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:disabled="!conflictMode && (hasBlockingPrimary || visiblePrimaries.length === 0)"
|
||||
@click="confirm"
|
||||
>
|
||||
{{
|
||||
conflictMode
|
||||
? formatMessage(messages.continueAnyway)
|
||||
: hasUnresolvedDependencies
|
||||
? formatMessage(messages.installResolved)
|
||||
: formatMessage(messages.install)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.preview-dependency-tooltip.v-popper--theme-tooltip .v-popper__inner {
|
||||
max-width: 22rem;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user