mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
[refactor] Simplify conflict acknowledgment system and enhance UX (#4599)
This commit is contained in:
@@ -29,10 +29,10 @@
|
||||
<!-- Conflict Warning Banner -->
|
||||
<div
|
||||
v-if="shouldShowManagerBanner"
|
||||
class="bg-yellow-600 bg-opacity-20 border border-yellow-400 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6"
|
||||
class="bg-yellow-600 bg-opacity-20 border border-yellow-400 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6 relative"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-600 text-lg"></i>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<p class="text-sm font-bold m-0">
|
||||
{{ $t('manager.conflicts.warningBanner.title') }}
|
||||
</p>
|
||||
@@ -46,6 +46,14 @@
|
||||
{{ $t('manager.conflicts.warningBanner.button') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-2 right-2 w-6 h-6 border-none outline-none bg-transparent flex items-center justify-center text-yellow-600 rounded transition-colors"
|
||||
:aria-label="$t('g.close')"
|
||||
@click="dismissWarningBanner"
|
||||
>
|
||||
<i class="pi pi-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:searchQuery="searchQuery"
|
||||
@@ -144,7 +152,7 @@ import { useManagerStatePersistence } from '@/composables/manager/useManagerStat
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
import { useConflictBannerState } from '@/composables/useConflictBannerState'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
@@ -159,7 +167,7 @@ const { initialTab } = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { getPackById } = useComfyRegistryStore()
|
||||
const conflictBannerState = useConflictBannerState()
|
||||
const conflictAcknowledgment = useConflictAcknowledgment()
|
||||
const persistedState = useManagerStatePersistence()
|
||||
const initialState = persistedState.loadStoredState()
|
||||
|
||||
@@ -176,8 +184,12 @@ const {
|
||||
toggle: toggleSideNav
|
||||
} = useResponsiveCollapse()
|
||||
|
||||
// Use conflict banner state from composable
|
||||
const { shouldShowManagerBanner, markConflictsAsSeen } = conflictBannerState
|
||||
// Use conflict acknowledgment state from composable
|
||||
const {
|
||||
shouldShowManagerBanner,
|
||||
dismissWarningBanner,
|
||||
dismissRedDotNotification
|
||||
} = conflictAcknowledgment
|
||||
|
||||
const tabs = ref<TabItem[]>([
|
||||
{ id: ManagerTab.All, label: t('g.all'), icon: 'pi-list' },
|
||||
@@ -510,12 +522,8 @@ watch([searchQuery, selectedTab], () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Automatically mark conflicts as seen when banner is displayed
|
||||
// This ensures red dots disappear and banner is dismissed once user sees it
|
||||
watchEffect(() => {
|
||||
if (shouldShowManagerBanner.value) {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
dismissRedDotNotification()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -129,7 +129,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
conflictedPackages: () => []
|
||||
})
|
||||
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const conflictsExpanded = ref<boolean>(false)
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
<i class="pi pi-exclamation-triangle text-yellow-500 text-xl"></i>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
v-if="!canToggleDirectly"
|
||||
:model-value="isEnabled"
|
||||
:disabled="isLoading"
|
||||
:readonly="!canToggleDirectly"
|
||||
aria-label="Enable or disable pack"
|
||||
@focus="handleToggleInteraction"
|
||||
/>
|
||||
<ToggleSwitch
|
||||
v-else
|
||||
:model-value="isEnabled"
|
||||
:disabled="isLoading"
|
||||
aria-label="Enable or disable pack"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': isLoading
|
||||
}"
|
||||
:pt="{
|
||||
handle: {
|
||||
class: 'bg-white'
|
||||
}
|
||||
}"
|
||||
@update:model-value="handleToggleClick"
|
||||
@update:model-value="onToggle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,12 +53,43 @@ const { t } = useI18n()
|
||||
const { isPackEnabled, enablePack, disablePack } = useComfyManagerStore()
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const { acknowledgeConflict, isConflictAcknowledged } =
|
||||
useConflictAcknowledgment()
|
||||
const { acknowledgmentState, markConflictsAsSeen } = useConflictAcknowledgment()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const isEnabled = computed(() => isPackEnabled(nodePack.id))
|
||||
const packageConflict = computed(() =>
|
||||
getConflictsForPackageByID(nodePack.id || '')
|
||||
)
|
||||
|
||||
const canToggleDirectly = computed(() => {
|
||||
return !(
|
||||
hasConflict &&
|
||||
!acknowledgmentState.value.modal_dismissed &&
|
||||
packageConflict.value
|
||||
)
|
||||
})
|
||||
|
||||
const showConflictModal = () => {
|
||||
if (packageConflict.value && !acknowledgmentState.value.modal_dismissed) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [packageConflict.value],
|
||||
buttonText: !isEnabled.value
|
||||
? t('manager.conflicts.enableAnyway')
|
||||
: t('manager.conflicts.understood'),
|
||||
onButtonClick: async () => {
|
||||
if (!isEnabled.value) {
|
||||
await handleEnable()
|
||||
}
|
||||
},
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnable = () => {
|
||||
if (!nodePack.id) {
|
||||
@@ -90,73 +122,30 @@ const handleDisable = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const performToggle = async (enable: boolean) => {
|
||||
const handleToggle = async (enable: boolean) => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
if (enable) {
|
||||
await handleEnable()
|
||||
} else {
|
||||
await handleDisable()
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
if (enable) {
|
||||
await handleEnable()
|
||||
} else {
|
||||
await handleDisable()
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const onToggle = debounce(
|
||||
(enable: boolean) => {
|
||||
void performToggle(enable)
|
||||
void handleToggle(enable)
|
||||
},
|
||||
TOGGLE_DEBOUNCE_MS,
|
||||
{ trailing: true }
|
||||
)
|
||||
|
||||
const handleToggleClick = async (enable: boolean) => {
|
||||
if (isLoading.value) return
|
||||
|
||||
if (enable && hasConflict) {
|
||||
const conflicts = getConflictsForPackageByID(nodePack.id || '')
|
||||
if (conflicts) {
|
||||
const hasUnacknowledgedConflicts = conflicts.conflicts.some(
|
||||
(conflict) => !isConflictAcknowledged(nodePack.id || '', conflict.type)
|
||||
)
|
||||
|
||||
if (hasUnacknowledgedConflicts) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [conflicts],
|
||||
buttonText: t('manager.conflicts.enableAnyway'),
|
||||
onButtonClick: async () => {
|
||||
for (const conflict of conflicts.conflicts) {
|
||||
acknowledgeConflict(nodePack.id || '', conflict.type, '0.1.0')
|
||||
}
|
||||
await performToggle(true)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
await performToggle(enable)
|
||||
}
|
||||
|
||||
const showConflictModal = () => {
|
||||
const conflicts = getConflictsForPackageByID(nodePack.id || '')
|
||||
if (conflicts) {
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages: [conflicts],
|
||||
buttonText: isEnabled.value
|
||||
? t('manager.conflicts.understood')
|
||||
: t('manager.conflicts.enableAnyway'),
|
||||
onButtonClick: async () => {
|
||||
for (const conflict of conflicts.conflicts) {
|
||||
acknowledgeConflict(nodePack.id || '', conflict.type, '0.1.0')
|
||||
}
|
||||
if (!isEnabled.value) {
|
||||
onToggle(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
const handleToggleInteraction = async (event: Event) => {
|
||||
if (!canToggleDirectly.value) {
|
||||
event.preventDefault()
|
||||
showConflictModal()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
:loading-message="$t('g.installing')"
|
||||
:has-warning="hasConflict"
|
||||
@action="installAllPacks"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -19,26 +18,32 @@
|
||||
import { inject, ref } from 'vue'
|
||||
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import {
|
||||
type ConflictDetail,
|
||||
type ConflictDetectionResult
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks, variant, label, hasConflict } = defineProps<{
|
||||
const { nodePacks, variant, label, hasConflict, conflictInfo } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
variant?: 'default' | 'black'
|
||||
label?: string
|
||||
hasConflict?: boolean
|
||||
conflictInfo?: ConflictDetail[]
|
||||
}>()
|
||||
|
||||
const isInstalling = inject(IsInstallingKey, ref(false))
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const onClick = (): void => {
|
||||
isInstalling.value = true
|
||||
}
|
||||
const { acknowledgmentState, markConflictsAsSeen } = useConflictAcknowledgment()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
|
||||
const createPayload = (
|
||||
installItem: NodePack
|
||||
@@ -69,36 +74,40 @@ const installPack = (item: NodePack) =>
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
// TBD Install Anyway modal
|
||||
// if (hasConflict && !isConflictAcknowledged) {
|
||||
// showNodeConflictDialog({
|
||||
// conflictedPackages: nodePacks,
|
||||
// buttonText: t('manager.conflicts.installAnyway'),
|
||||
// onButtonClick: async () => {
|
||||
// // User chose "Install Anyway" - acknowledge all conflicts and proceed
|
||||
// for (const conflictedPack of packsWithConflicts) {
|
||||
// for (const conflict of conflictedPack.conflicts) {
|
||||
// acknowledgeConflict(
|
||||
// conflictedPack.package_id,
|
||||
// conflict.type,
|
||||
// '0.1.0'
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// // Proceed with installation
|
||||
// await performInstallation(uninstalledPacks)
|
||||
// }
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
if (!uninstalledPacks.length) return
|
||||
if (
|
||||
hasConflict &&
|
||||
conflictInfo &&
|
||||
!acknowledgmentState.value.modal_dismissed
|
||||
) {
|
||||
const conflictedPackages: ConflictDetectionResult[] = nodePacks.map(
|
||||
(pack) => ({
|
||||
package_id: pack.id || '',
|
||||
package_name: pack.name || '',
|
||||
has_conflict: true,
|
||||
conflicts: conflictInfo || [],
|
||||
is_compatible: false
|
||||
})
|
||||
)
|
||||
|
||||
showNodeConflictDialog({
|
||||
conflictedPackages,
|
||||
buttonText: t('manager.conflicts.installAnyway'),
|
||||
onButtonClick: async () => {
|
||||
// Proceed with installation
|
||||
isInstalling.value = true
|
||||
await performInstallation(nodePacks)
|
||||
},
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
// No conflicts or conflicts acknowledged - proceed with installation
|
||||
await performInstallation(uninstalledPacks)
|
||||
isInstalling.value = true
|
||||
await performInstallation(nodePacks)
|
||||
}
|
||||
|
||||
const performInstallation = async (packs: NodePack[]) => {
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
class="flex"
|
||||
style="align-items: center"
|
||||
>
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
<PackEnableToggle
|
||||
:node-pack="nodePack"
|
||||
:has-conflict="hasCompatibilityIssues"
|
||||
/>
|
||||
</MetadataRow>
|
||||
<MetadataRow
|
||||
v-for="item in infoItems"
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
<template v-if="!isInstalled">
|
||||
<PackInstallButton
|
||||
:node-packs="[nodePack]"
|
||||
:has-conflict="hasConflict"
|
||||
:has-conflict="uninstalledPackConflict.hasConflict"
|
||||
:conflict-info="uninstalledPackConflict.conflicts"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackEnableToggle :node-pack="nodePack" :has-conflict="hasConflict" />
|
||||
<PackEnableToggle
|
||||
:node-pack="nodePack"
|
||||
:has-conflict="installedPackHasConflict"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,20 +51,17 @@ const formattedDownloads = computed(() =>
|
||||
const { getConflictsForPackageByID } = useConflictDetectionStore()
|
||||
const { checkNodeCompatibility } = useConflictDetection()
|
||||
|
||||
const hasConflict = computed(() => {
|
||||
const installedPackHasConflict = computed(() => {
|
||||
if (!nodePack.id) return false
|
||||
|
||||
// For installed packages, check conflicts from store
|
||||
if (isInstalled.value) {
|
||||
// Try exact match first
|
||||
let conflicts = getConflictsForPackageByID(nodePack.id)
|
||||
if (conflicts) return true
|
||||
// Try exact match first
|
||||
let conflicts = getConflictsForPackageByID(nodePack.id)
|
||||
if (conflicts) return true
|
||||
|
||||
return false
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// For uninstalled packages, check compatibility directly
|
||||
const compatibility = checkNodeCompatibility(nodePack)
|
||||
return compatibility.hasConflict
|
||||
const uninstalledPackConflict = computed(() => {
|
||||
return checkNodeCompatibility(nodePack)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -142,7 +142,7 @@ import {
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
|
||||
import { useConflictBannerState } from '@/composables/useConflictBannerState'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { type ReleaseNote } from '@/services/releaseService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
@@ -215,10 +215,9 @@ const moreMenuItem = computed(() =>
|
||||
menuItems.value.find((item) => item.key === 'more')
|
||||
)
|
||||
|
||||
// Use conflict banner state from composable
|
||||
const conflictBannerState = useConflictBannerState()
|
||||
const { shouldShowConflictRedDot: shouldShowManagerRedDot } =
|
||||
conflictBannerState
|
||||
// Use conflict acknowledgment state from composable
|
||||
const { shouldShowRedDot: shouldShowManagerRedDot } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const menuItems = computed<MenuItem[]>(() => {
|
||||
const moreItems: MenuItem[] = [
|
||||
|
||||
@@ -63,7 +63,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import ReleaseNotificationToast from '@/components/helpcenter/ReleaseNotificationToast.vue'
|
||||
import WhatsNewPopup from '@/components/helpcenter/WhatsNewPopup.vue'
|
||||
import { useConflictBannerState } from '@/composables/useConflictBannerState'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
@@ -74,12 +74,13 @@ import SidebarIcon from './SidebarIcon.vue'
|
||||
const settingStore = useSettingStore()
|
||||
const releaseStore = useReleaseStore()
|
||||
const conflictDetection = useConflictDetection()
|
||||
const conflictBannerState = useConflictBannerState()
|
||||
const dialogService = useDialogService()
|
||||
const conflictAcknowledgment = useConflictAcknowledgment()
|
||||
const { showNodeConflictDialog } = useDialogService()
|
||||
const isHelpCenterVisible = ref(false)
|
||||
|
||||
// Use conflict banner state from composable
|
||||
const { shouldShowConflictRedDot } = conflictBannerState
|
||||
// Use conflict acknowledgment state from composable
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
||||
conflictAcknowledgment
|
||||
|
||||
// Use either release red dot or conflict red dot
|
||||
const shouldShowRedDot = computed(() => {
|
||||
@@ -123,17 +124,14 @@ const handleWhatsNewDismissed = async () => {
|
||||
* Show the node conflict dialog with current conflict data
|
||||
*/
|
||||
const showConflictModal = () => {
|
||||
// Pass conflict data to the dialog, including onClose callback
|
||||
const conflictData = {
|
||||
conflictedPackages: conflictDetection.conflictedPackages.value
|
||||
}
|
||||
|
||||
// Show dialog with onClose callback in dialogComponentProps
|
||||
dialogService.showNodeConflictDialog({
|
||||
showNodeConflictDialog({
|
||||
...conflictData,
|
||||
dialogComponentProps: {
|
||||
onClose: () => {
|
||||
conflictDetection.dismissConflictModal()
|
||||
markConflictsAsSeen()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
|
||||
/**
|
||||
* LocalStorage keys for conflict acknowledgment tracking
|
||||
*/
|
||||
const STORAGE_KEYS = {
|
||||
CONFLICT_MODAL_DISMISSED: 'comfy_manager_conflict_banner_dismissed',
|
||||
CONFLICT_RED_DOT_DISMISSED: 'comfy_help_center_conflict_seen',
|
||||
ACKNOWLEDGED_CONFLICTS: 'comfy_conflict_acknowledged',
|
||||
LAST_COMFYUI_VERSION: 'comfyui.last_version'
|
||||
CONFLICT_MODAL_DISMISSED: 'Comfy.ConflictModalDismissed',
|
||||
CONFLICT_RED_DOT_DISMISSED: 'Comfy.ConflictRedDotDismissed',
|
||||
CONFLICT_WARNING_BANNER_DISMISSED: 'Comfy.ConflictWarningBannerDismissed'
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Interface for tracking individual conflict acknowledgments
|
||||
*/
|
||||
interface AcknowledgedConflict {
|
||||
package_id: string
|
||||
conflict_type: string
|
||||
timestamp: string
|
||||
comfyui_version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for conflict acknowledgment state
|
||||
*/
|
||||
interface ConflictAcknowledgmentState {
|
||||
modal_dismissed: boolean
|
||||
red_dot_dismissed: boolean
|
||||
acknowledged_conflicts: AcknowledgedConflict[]
|
||||
last_comfyui_version: string
|
||||
warning_banner_dismissed: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,6 +31,8 @@ interface ConflictAcknowledgmentState {
|
||||
* - Detecting ComfyUI version changes to reset acknowledgment state
|
||||
*/
|
||||
export function useConflictAcknowledgment() {
|
||||
const conflictDetectionStore = useConflictDetectionStore()
|
||||
|
||||
// Reactive state using VueUse's useStorage for automatic persistence
|
||||
const modalDismissed = useStorage(
|
||||
STORAGE_KEYS.CONFLICT_MODAL_DISMISSED,
|
||||
@@ -50,156 +42,50 @@ export function useConflictAcknowledgment() {
|
||||
STORAGE_KEYS.CONFLICT_RED_DOT_DISMISSED,
|
||||
false
|
||||
)
|
||||
const acknowledgedConflicts = useStorage<AcknowledgedConflict[]>(
|
||||
STORAGE_KEYS.ACKNOWLEDGED_CONFLICTS,
|
||||
[]
|
||||
const warningBannerDismissed = useStorage(
|
||||
STORAGE_KEYS.CONFLICT_WARNING_BANNER_DISMISSED,
|
||||
false
|
||||
)
|
||||
const lastComfyUIVersion = useStorage(STORAGE_KEYS.LAST_COMFYUI_VERSION, '')
|
||||
|
||||
// Create computed state object for backward compatibility
|
||||
const state = computed<ConflictAcknowledgmentState>(() => ({
|
||||
modal_dismissed: modalDismissed.value,
|
||||
red_dot_dismissed: redDotDismissed.value,
|
||||
acknowledged_conflicts: acknowledgedConflicts.value,
|
||||
last_comfyui_version: lastComfyUIVersion.value
|
||||
warning_banner_dismissed: warningBannerDismissed.value
|
||||
}))
|
||||
|
||||
/**
|
||||
* Check if ComfyUI version has changed since last run
|
||||
* If version changed, reset acknowledgment state
|
||||
*/
|
||||
function checkComfyUIVersionChange(currentVersion: string): boolean {
|
||||
const lastVersion = lastComfyUIVersion.value
|
||||
const versionChanged = lastVersion !== '' && lastVersion !== currentVersion
|
||||
|
||||
if (versionChanged) {
|
||||
console.log(
|
||||
`[ConflictAcknowledgment] ComfyUI version changed from ${lastVersion} to ${currentVersion}, resetting acknowledgment state`
|
||||
)
|
||||
resetAcknowledgmentState()
|
||||
}
|
||||
|
||||
// Update last known version
|
||||
lastComfyUIVersion.value = currentVersion
|
||||
|
||||
return versionChanged
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all acknowledgment state (called when ComfyUI version changes)
|
||||
*/
|
||||
function resetAcknowledgmentState(): void {
|
||||
modalDismissed.value = false
|
||||
redDotDismissed.value = false
|
||||
acknowledgedConflicts.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark conflict modal as dismissed
|
||||
*/
|
||||
function dismissConflictModal(): void {
|
||||
modalDismissed.value = true
|
||||
console.log('[ConflictAcknowledgment] Conflict modal dismissed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark red dot notification as dismissed
|
||||
*/
|
||||
function dismissRedDotNotification(): void {
|
||||
redDotDismissed.value = true
|
||||
console.log('[ConflictAcknowledgment] Red dot notification dismissed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge a specific conflict for a package
|
||||
* Mark manager warning banner as dismissed
|
||||
*/
|
||||
function acknowledgeConflict(
|
||||
packageId: string,
|
||||
conflictType: string,
|
||||
comfyuiVersion: string
|
||||
): void {
|
||||
const acknowledgment: AcknowledgedConflict = {
|
||||
package_id: packageId,
|
||||
conflict_type: conflictType,
|
||||
timestamp: new Date().toISOString(),
|
||||
comfyui_version: comfyuiVersion
|
||||
}
|
||||
|
||||
// Remove any existing acknowledgment for the same package and conflict type
|
||||
acknowledgedConflicts.value = acknowledgedConflicts.value.filter(
|
||||
(ack) =>
|
||||
!(ack.package_id === packageId && ack.conflict_type === conflictType)
|
||||
)
|
||||
|
||||
// Add new acknowledgment
|
||||
acknowledgedConflicts.value.push(acknowledgment)
|
||||
|
||||
console.log(
|
||||
`[ConflictAcknowledgment] Acknowledged conflict for ${packageId}:${conflictType}`
|
||||
)
|
||||
function dismissWarningBanner(): void {
|
||||
warningBannerDismissed.value = true
|
||||
redDotDismissed.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific conflict has been acknowledged
|
||||
* Mark conflicts as seen (unified function for help center and manager)
|
||||
*/
|
||||
function isConflictAcknowledged(
|
||||
packageId: string,
|
||||
conflictType: string
|
||||
): boolean {
|
||||
return acknowledgedConflicts.value.some(
|
||||
(ack) =>
|
||||
ack.package_id === packageId && ack.conflict_type === conflictType
|
||||
)
|
||||
function markConflictsAsSeen(): void {
|
||||
redDotDismissed.value = true
|
||||
modalDismissed.value = true
|
||||
warningBannerDismissed.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove acknowledgment for a specific conflict
|
||||
*/
|
||||
function removeConflictAcknowledgment(
|
||||
packageId: string,
|
||||
conflictType: string
|
||||
): void {
|
||||
acknowledgedConflicts.value = acknowledgedConflicts.value.filter(
|
||||
(ack) =>
|
||||
!(ack.package_id === packageId && ack.conflict_type === conflictType)
|
||||
)
|
||||
console.log(
|
||||
`[ConflictAcknowledgment] Removed acknowledgment for ${packageId}:${conflictType}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all acknowledgments (for debugging/admin purposes)
|
||||
*/
|
||||
function clearAllAcknowledgments(): void {
|
||||
resetAcknowledgmentState()
|
||||
console.log('[ConflictAcknowledgment] Cleared all acknowledgments')
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const hasConflicts = computed(() => conflictDetectionStore.hasConflicts)
|
||||
const shouldShowConflictModal = computed(() => !modalDismissed.value)
|
||||
const shouldShowRedDot = computed(() => !redDotDismissed.value)
|
||||
|
||||
/**
|
||||
* Get all acknowledged package IDs
|
||||
*/
|
||||
const acknowledgedPackageIds = computed(() => {
|
||||
return Array.from(
|
||||
new Set(acknowledgedConflicts.value.map((ack) => ack.package_id))
|
||||
)
|
||||
const shouldShowRedDot = computed(() => {
|
||||
if (!hasConflicts.value) return false
|
||||
return !redDotDismissed.value
|
||||
})
|
||||
|
||||
/**
|
||||
* Get acknowledgment statistics
|
||||
*/
|
||||
const acknowledgmentStats = computed(() => {
|
||||
return {
|
||||
total_acknowledged: acknowledgedConflicts.value.length,
|
||||
unique_packages: acknowledgedPackageIds.value.length,
|
||||
modal_dismissed: modalDismissed.value,
|
||||
red_dot_dismissed: redDotDismissed.value,
|
||||
last_comfyui_version: lastComfyUIVersion.value
|
||||
}
|
||||
const shouldShowManagerBanner = computed(() => {
|
||||
return hasConflicts.value && !warningBannerDismissed.value
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -207,17 +93,11 @@ export function useConflictAcknowledgment() {
|
||||
acknowledgmentState: state,
|
||||
shouldShowConflictModal,
|
||||
shouldShowRedDot,
|
||||
acknowledgedPackageIds,
|
||||
acknowledgmentStats,
|
||||
shouldShowManagerBanner,
|
||||
|
||||
// Methods
|
||||
checkComfyUIVersionChange,
|
||||
dismissConflictModal,
|
||||
dismissRedDotNotification,
|
||||
acknowledgeConflict,
|
||||
isConflictAcknowledged,
|
||||
removeConflictAcknowledgment,
|
||||
clearAllAcknowledgments,
|
||||
resetAcknowledgmentState
|
||||
dismissWarningBanner,
|
||||
markConflictsAsSeen
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
||||
|
||||
/**
|
||||
* Composable for managing conflict banner state across components
|
||||
* Provides centralized logic for conflict visibility and dismissal
|
||||
*/
|
||||
export function useConflictBannerState() {
|
||||
const conflictDetectionStore = useConflictDetectionStore()
|
||||
|
||||
// Storage keys
|
||||
const HELP_CENTER_CONFLICT_SEEN_KEY = 'comfy_help_center_conflict_seen'
|
||||
const MANAGER_CONFLICT_BANNER_DISMISSED_KEY =
|
||||
'comfy_manager_conflict_banner_dismissed'
|
||||
|
||||
// Reactive storage state
|
||||
const hasSeenConflicts = useStorage(HELP_CENTER_CONFLICT_SEEN_KEY, false)
|
||||
const isConflictBannerDismissed = useStorage(
|
||||
MANAGER_CONFLICT_BANNER_DISMISSED_KEY,
|
||||
false
|
||||
)
|
||||
|
||||
// Computed states
|
||||
const hasConflicts = computed(() => conflictDetectionStore.hasConflicts)
|
||||
|
||||
/**
|
||||
* Check if the help center should show a red dot for conflicts
|
||||
*/
|
||||
const shouldShowConflictRedDot = computed(() => {
|
||||
if (!hasConflicts.value) return false
|
||||
return !hasSeenConflicts.value
|
||||
})
|
||||
|
||||
/**
|
||||
* Check if the manager conflict banner should be visible
|
||||
*/
|
||||
const shouldShowManagerBanner = computed(() => {
|
||||
return hasConflicts.value && !isConflictBannerDismissed.value
|
||||
})
|
||||
|
||||
/**
|
||||
* Mark conflicts as seen (used when user opens manager dialog or help center)
|
||||
*/
|
||||
const markConflictsAsSeen = () => {
|
||||
if (hasConflicts.value) {
|
||||
hasSeenConflicts.value = true
|
||||
isConflictBannerDismissed.value = true
|
||||
|
||||
// Force localStorage update as backup due to useStorage sync timing issue
|
||||
// useStorage updates localStorage asynchronously, but we need immediate persistence
|
||||
localStorage.setItem(HELP_CENTER_CONFLICT_SEEN_KEY, 'true')
|
||||
localStorage.setItem(MANAGER_CONFLICT_BANNER_DISMISSED_KEY, 'true')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
hasConflicts,
|
||||
hasSeenConflicts,
|
||||
isConflictBannerDismissed,
|
||||
|
||||
// Computed
|
||||
shouldShowConflictRedDot,
|
||||
shouldShowManagerBanner,
|
||||
|
||||
// Actions
|
||||
markConflictsAsSeen
|
||||
}
|
||||
}
|
||||
@@ -261,8 +261,8 @@ export function useConflictDetection() {
|
||||
// Combine local installation data with version-specific Registry data
|
||||
const requirement: NodePackRequirements = {
|
||||
// Basic package info
|
||||
package_id: packageId,
|
||||
package_name: pack.name || packageId,
|
||||
id: pack.id,
|
||||
name: pack.name,
|
||||
installed_version: installedVersion,
|
||||
is_enabled: isEnabled,
|
||||
|
||||
@@ -287,8 +287,8 @@ export function useConflictDetection() {
|
||||
|
||||
// Create fallback requirement without Registry data
|
||||
const fallbackRequirement: NodePackRequirements = {
|
||||
package_id: packageId,
|
||||
package_name: pack.name || packageId,
|
||||
id: pack.id,
|
||||
name: pack.name,
|
||||
installed_version: installedVersion,
|
||||
is_enabled: isEnabled,
|
||||
is_banned: false,
|
||||
@@ -381,8 +381,8 @@ export function useConflictDetection() {
|
||||
const hasConflict = conflicts.length > 0
|
||||
|
||||
return {
|
||||
package_id: packageReq.package_id,
|
||||
package_name: packageReq.package_name,
|
||||
package_id: packageReq.id ?? '',
|
||||
package_name: packageReq.name ?? '',
|
||||
has_conflict: hasConflict,
|
||||
conflicts,
|
||||
is_compatible: !hasConflict
|
||||
@@ -528,7 +528,7 @@ export function useConflictDetection() {
|
||||
return detectPackageConflicts(packageReq, sysEnv)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[ConflictDetection] Failed to detect conflicts for package ${packageReq.package_name}:`,
|
||||
`[ConflictDetection] Failed to detect conflicts for package ${packageReq.name}:`,
|
||||
error
|
||||
)
|
||||
// Return null for failed packages, will be filtered out
|
||||
@@ -589,14 +589,6 @@ export function useConflictDetection() {
|
||||
)
|
||||
storedMergedConflicts.value = [...mergedConflicts]
|
||||
|
||||
// Check for ComfyUI version change to reset acknowledgments
|
||||
if (sysEnv.comfyui_version !== 'unknown') {
|
||||
acknowledgment.checkComfyUIVersionChange(sysEnv.comfyui_version)
|
||||
}
|
||||
|
||||
// TODO: Show red dot on Help Center based on acknowledgment.shouldShowRedDot
|
||||
// TODO: Store conflict state for event-based dialog triggers
|
||||
|
||||
// Use merged conflicts in response as well
|
||||
const response: ConflictDetectionResponse = {
|
||||
success: true,
|
||||
@@ -708,31 +700,6 @@ export function useConflictDetection() {
|
||||
return hasActualConflicts && canShowModal
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark conflict modal as dismissed
|
||||
*/
|
||||
function dismissConflictModal(): void {
|
||||
acknowledgment.dismissConflictModal()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark red dot notification as dismissed
|
||||
*/
|
||||
function dismissRedDotNotification(): void {
|
||||
acknowledgment.dismissRedDotNotification()
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge a specific conflict
|
||||
*/
|
||||
function acknowledgePackageConflict(
|
||||
packageId: string,
|
||||
conflictType: string
|
||||
): void {
|
||||
const currentVersion = systemEnvironment.value?.comfyui_version || 'unknown'
|
||||
acknowledgment.acknowledgeConflict(packageId, conflictType, currentVersion)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check compatibility for a node.
|
||||
* Used by components like PackVersionSelectorPopover.
|
||||
@@ -844,20 +811,12 @@ export function useConflictDetection() {
|
||||
bannedPackages,
|
||||
securityPendingPackages,
|
||||
|
||||
// Acknowledgment state
|
||||
shouldShowConflictModal: acknowledgment.shouldShowConflictModal,
|
||||
shouldShowRedDot: acknowledgment.shouldShowRedDot,
|
||||
acknowledgedPackageIds: acknowledgment.acknowledgedPackageIds,
|
||||
|
||||
// Methods
|
||||
performConflictDetection,
|
||||
detectSystemEnvironment,
|
||||
initializeConflictDetection,
|
||||
cancelRequests,
|
||||
shouldShowConflictModalAfterUpdate,
|
||||
dismissConflictModal,
|
||||
dismissRedDotNotification,
|
||||
acknowledgePackageConflict,
|
||||
|
||||
// Helper functions for other components
|
||||
checkNodeCompatibility
|
||||
|
||||
@@ -53,8 +53,6 @@ export interface NodePackRequirements extends Node {
|
||||
is_banned: boolean
|
||||
is_pending: boolean
|
||||
// Aliases for backwards compatibility with existing code
|
||||
package_id: string
|
||||
package_name: string
|
||||
version_status?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
|
||||
describe('useConflictAcknowledgment with useStorage refactor', () => {
|
||||
beforeEach(() => {
|
||||
// Set up Pinia for each test
|
||||
setActivePinia(createPinia())
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear()
|
||||
// Reset modules to ensure fresh state
|
||||
@@ -18,20 +21,20 @@ describe('useConflictAcknowledgment with useStorage refactor', () => {
|
||||
const {
|
||||
shouldShowConflictModal,
|
||||
shouldShowRedDot,
|
||||
acknowledgedPackageIds
|
||||
shouldShowManagerBanner
|
||||
} = useConflictAcknowledgment()
|
||||
|
||||
expect(shouldShowConflictModal.value).toBe(true)
|
||||
expect(shouldShowRedDot.value).toBe(true)
|
||||
expect(acknowledgedPackageIds.value).toEqual([])
|
||||
expect(shouldShowRedDot.value).toBe(false) // No conflicts initially
|
||||
expect(shouldShowManagerBanner.value).toBe(false) // No conflicts initially
|
||||
})
|
||||
|
||||
it('should dismiss modal state correctly', () => {
|
||||
const { dismissConflictModal, shouldShowConflictModal } =
|
||||
const { markConflictsAsSeen, shouldShowConflictModal } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
expect(shouldShowConflictModal.value).toBe(true)
|
||||
dismissConflictModal()
|
||||
markConflictsAsSeen()
|
||||
expect(shouldShowConflictModal.value).toBe(false)
|
||||
})
|
||||
|
||||
@@ -39,92 +42,78 @@ describe('useConflictAcknowledgment with useStorage refactor', () => {
|
||||
const { dismissRedDotNotification, shouldShowRedDot } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
expect(shouldShowRedDot.value).toBe(true)
|
||||
expect(shouldShowRedDot.value).toBe(false) // No conflicts initially
|
||||
dismissRedDotNotification()
|
||||
expect(shouldShowRedDot.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should acknowledge conflicts correctly', () => {
|
||||
const {
|
||||
acknowledgeConflict,
|
||||
isConflictAcknowledged,
|
||||
acknowledgedPackageIds
|
||||
} = useConflictAcknowledgment()
|
||||
|
||||
expect(acknowledgedPackageIds.value).toEqual([])
|
||||
|
||||
acknowledgeConflict('package1', 'version_conflict', '1.0.0')
|
||||
|
||||
expect(isConflictAcknowledged('package1', 'version_conflict')).toBe(true)
|
||||
expect(isConflictAcknowledged('package1', 'other_conflict')).toBe(false)
|
||||
expect(acknowledgedPackageIds.value).toContain('package1')
|
||||
})
|
||||
|
||||
it('should reset state when ComfyUI version changes', () => {
|
||||
const {
|
||||
dismissConflictModal,
|
||||
acknowledgeConflict,
|
||||
checkComfyUIVersionChange,
|
||||
shouldShowConflictModal,
|
||||
acknowledgedPackageIds
|
||||
} = useConflictAcknowledgment()
|
||||
|
||||
// Set up some state
|
||||
dismissConflictModal()
|
||||
acknowledgeConflict('package1', 'conflict1', '1.0.0')
|
||||
|
||||
expect(shouldShowConflictModal.value).toBe(false)
|
||||
expect(acknowledgedPackageIds.value).toContain('package1')
|
||||
|
||||
// First check sets the initial version, no change yet
|
||||
const changed1 = checkComfyUIVersionChange('1.0.0')
|
||||
expect(changed1).toBe(false)
|
||||
|
||||
// Now check with different version should reset
|
||||
const changed2 = checkComfyUIVersionChange('2.0.0')
|
||||
expect(changed2).toBe(true)
|
||||
expect(shouldShowConflictModal.value).toBe(true)
|
||||
expect(acknowledgedPackageIds.value).toEqual([])
|
||||
})
|
||||
|
||||
it('should track acknowledgment statistics correctly', () => {
|
||||
const { acknowledgmentStats, dismissConflictModal, acknowledgeConflict } =
|
||||
it('should dismiss warning banner correctly', () => {
|
||||
const { dismissWarningBanner, shouldShowManagerBanner } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
// Initial stats
|
||||
expect(acknowledgmentStats.value).toEqual({
|
||||
total_acknowledged: 0,
|
||||
unique_packages: 0,
|
||||
modal_dismissed: false,
|
||||
red_dot_dismissed: false,
|
||||
last_comfyui_version: ''
|
||||
})
|
||||
// Initially should not show banner (no conflicts)
|
||||
expect(shouldShowManagerBanner.value).toBe(false)
|
||||
|
||||
// Update state
|
||||
dismissConflictModal()
|
||||
acknowledgeConflict('package1', 'conflict1', '1.0.0')
|
||||
acknowledgeConflict('package2', 'conflict2', '1.0.0')
|
||||
// Test dismissWarningBanner function exists and works
|
||||
dismissWarningBanner()
|
||||
expect(shouldShowManagerBanner.value).toBe(false)
|
||||
})
|
||||
|
||||
// Check updated stats
|
||||
expect(acknowledgmentStats.value.total_acknowledged).toBe(2)
|
||||
expect(acknowledgmentStats.value.unique_packages).toBe(2)
|
||||
expect(acknowledgmentStats.value.modal_dismissed).toBe(true)
|
||||
it('should mark conflicts as seen', () => {
|
||||
const {
|
||||
markConflictsAsSeen,
|
||||
shouldShowConflictModal,
|
||||
shouldShowRedDot,
|
||||
shouldShowManagerBanner
|
||||
} = useConflictAcknowledgment()
|
||||
|
||||
// Mark conflicts as seen
|
||||
markConflictsAsSeen()
|
||||
|
||||
// All UI elements should be dismissed
|
||||
expect(shouldShowConflictModal.value).toBe(false)
|
||||
expect(shouldShowRedDot.value).toBe(false)
|
||||
expect(shouldShowManagerBanner.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should manage acknowledgment state correctly', () => {
|
||||
const {
|
||||
acknowledgmentState,
|
||||
markConflictsAsSeen,
|
||||
dismissRedDotNotification,
|
||||
dismissWarningBanner
|
||||
} = useConflictAcknowledgment()
|
||||
|
||||
// Initial state
|
||||
expect(acknowledgmentState.value.modal_dismissed).toBe(false)
|
||||
expect(acknowledgmentState.value.red_dot_dismissed).toBe(false)
|
||||
expect(acknowledgmentState.value.warning_banner_dismissed).toBe(false)
|
||||
|
||||
// Update states
|
||||
markConflictsAsSeen()
|
||||
dismissRedDotNotification()
|
||||
dismissWarningBanner()
|
||||
|
||||
// Check updated state
|
||||
expect(acknowledgmentState.value.modal_dismissed).toBe(true)
|
||||
expect(acknowledgmentState.value.red_dot_dismissed).toBe(true)
|
||||
expect(acknowledgmentState.value.warning_banner_dismissed).toBe(true)
|
||||
})
|
||||
|
||||
it('should use VueUse useStorage for persistence', () => {
|
||||
// This test verifies that useStorage is being used by checking
|
||||
// that values are automatically synced to localStorage
|
||||
const { dismissConflictModal, acknowledgeConflict } =
|
||||
const { markConflictsAsSeen, dismissWarningBanner } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
dismissConflictModal()
|
||||
acknowledgeConflict('test-pkg', 'test-conflict', '1.0.0')
|
||||
markConflictsAsSeen()
|
||||
dismissWarningBanner()
|
||||
|
||||
// VueUse useStorage should automatically persist to localStorage
|
||||
// We can verify the keys exist (values will be stringified by VueUse)
|
||||
expect(localStorage.getItem('Comfy.ConflictModalDismissed')).not.toBeNull()
|
||||
expect(
|
||||
localStorage.getItem('comfy_manager_conflict_banner_dismissed')
|
||||
localStorage.getItem('Comfy.ConflictWarningBannerDismissed')
|
||||
).not.toBeNull()
|
||||
expect(localStorage.getItem('comfy_conflict_acknowledged')).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,426 +1,138 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
|
||||
describe('useConflictAcknowledgment', () => {
|
||||
// Mock localStorage
|
||||
const mockLocalStorage = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset localStorage mock
|
||||
mockLocalStorage.getItem.mockClear()
|
||||
mockLocalStorage.setItem.mockClear()
|
||||
mockLocalStorage.removeItem.mockClear()
|
||||
mockLocalStorage.clear.mockClear()
|
||||
|
||||
// Mock localStorage globally
|
||||
Object.defineProperty(global, 'localStorage', {
|
||||
value: mockLocalStorage,
|
||||
writable: true
|
||||
})
|
||||
|
||||
// Default mock returns
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
// Set up Pinia for each test
|
||||
setActivePinia(createPinia())
|
||||
// Clear localStorage before each test
|
||||
localStorage.clear()
|
||||
// Reset modules to ensure fresh state
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('initial state loading', () => {
|
||||
it('should load empty state when localStorage is empty', () => {
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
const { acknowledgmentState } = useConflictAcknowledgment()
|
||||
|
||||
expect(acknowledgmentState.value).toEqual({
|
||||
modal_dismissed: false,
|
||||
red_dot_dismissed: false,
|
||||
acknowledged_conflicts: [],
|
||||
last_comfyui_version: ''
|
||||
warning_banner_dismissed: false
|
||||
})
|
||||
})
|
||||
|
||||
it('should load existing state from localStorage', () => {
|
||||
mockLocalStorage.getItem.mockImplementation((key) => {
|
||||
switch (key) {
|
||||
case 'comfy_manager_conflict_banner_dismissed':
|
||||
return 'true'
|
||||
case 'comfy_help_center_conflict_seen':
|
||||
return 'true'
|
||||
case 'comfy_conflict_acknowledged':
|
||||
return JSON.stringify([
|
||||
{
|
||||
package_id: 'TestPackage',
|
||||
conflict_type: 'os',
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
comfyui_version: '0.3.41'
|
||||
}
|
||||
])
|
||||
case 'comfyui.last_version':
|
||||
return '0.3.41'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
// Pre-populate localStorage
|
||||
localStorage.setItem('Comfy.ConflictModalDismissed', 'true')
|
||||
localStorage.setItem('Comfy.ConflictRedDotDismissed', 'true')
|
||||
localStorage.setItem('Comfy.ConflictWarningBannerDismissed', 'true')
|
||||
|
||||
const { acknowledgmentState } = useConflictAcknowledgment()
|
||||
|
||||
expect(acknowledgmentState.value).toEqual({
|
||||
modal_dismissed: true,
|
||||
red_dot_dismissed: true,
|
||||
acknowledged_conflicts: [
|
||||
{
|
||||
package_id: 'TestPackage',
|
||||
conflict_type: 'os',
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
comfyui_version: '0.3.41'
|
||||
}
|
||||
],
|
||||
last_comfyui_version: '0.3.41'
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('should handle corrupted localStorage data gracefully', () => {
|
||||
mockLocalStorage.getItem.mockImplementation((key) => {
|
||||
if (key === 'comfy_conflict_acknowledged') {
|
||||
return 'invalid-json'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// VueUse's useStorage should handle corrupted data gracefully
|
||||
const { acknowledgmentState } = useConflictAcknowledgment()
|
||||
|
||||
// Should fall back to default values when localStorage contains invalid JSON
|
||||
expect(acknowledgmentState.value).toEqual({
|
||||
modal_dismissed: false,
|
||||
red_dot_dismissed: false,
|
||||
acknowledged_conflicts: [],
|
||||
last_comfyui_version: ''
|
||||
warning_banner_dismissed: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ComfyUI version change detection', () => {
|
||||
it('should detect version change and reset state', () => {
|
||||
// Setup existing state
|
||||
mockLocalStorage.getItem.mockImplementation((key) => {
|
||||
switch (key) {
|
||||
case 'comfyui.conflict.modal.dismissed':
|
||||
return 'true'
|
||||
case 'comfyui.conflict.red_dot.dismissed':
|
||||
return 'true'
|
||||
case 'comfyui.last_version':
|
||||
return '0.3.40'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const { checkComfyUIVersionChange, acknowledgmentState } =
|
||||
describe('dismissal functions', () => {
|
||||
it('should mark conflicts as seen with unified function', () => {
|
||||
const { markConflictsAsSeen, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const versionChanged = checkComfyUIVersionChange('0.3.41')
|
||||
|
||||
expect(versionChanged).toBe(true)
|
||||
expect(acknowledgmentState.value.modal_dismissed).toBe(false)
|
||||
expect(acknowledgmentState.value.red_dot_dismissed).toBe(false)
|
||||
expect(acknowledgmentState.value.last_comfyui_version).toBe('0.3.41')
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('ComfyUI version changed from 0.3.40 to 0.3.41')
|
||||
)
|
||||
|
||||
consoleLogSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should not detect version change for same version', () => {
|
||||
mockLocalStorage.getItem.mockImplementation((key) => {
|
||||
if (key === 'comfyui.last_version') {
|
||||
return '0.3.41'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const { checkComfyUIVersionChange } = useConflictAcknowledgment()
|
||||
|
||||
const versionChanged = checkComfyUIVersionChange('0.3.41')
|
||||
|
||||
expect(versionChanged).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle first run (no previous version)', () => {
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
const { checkComfyUIVersionChange } = useConflictAcknowledgment()
|
||||
|
||||
const versionChanged = checkComfyUIVersionChange('0.3.41')
|
||||
|
||||
expect(versionChanged).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('modal dismissal', () => {
|
||||
it('should dismiss conflict modal and save to localStorage', () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const { dismissConflictModal, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
dismissConflictModal()
|
||||
markConflictsAsSeen()
|
||||
|
||||
expect(acknowledgmentState.value.modal_dismissed).toBe(true)
|
||||
// useStorage handles localStorage synchronization internally
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[ConflictAcknowledgment] Conflict modal dismissed'
|
||||
)
|
||||
|
||||
consoleLogSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should dismiss red dot notification and save to localStorage', () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
it('should dismiss red dot notification', () => {
|
||||
const { dismissRedDotNotification, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
dismissRedDotNotification()
|
||||
|
||||
expect(acknowledgmentState.value.red_dot_dismissed).toBe(true)
|
||||
// useStorage handles localStorage synchronization internally
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[ConflictAcknowledgment] Red dot notification dismissed'
|
||||
)
|
||||
|
||||
consoleLogSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('conflict acknowledgment', () => {
|
||||
it('should acknowledge a conflict and save to localStorage', () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {})
|
||||
const dateSpy = vi
|
||||
.spyOn(Date.prototype, 'toISOString')
|
||||
.mockReturnValue('2023-01-01T00:00:00.000Z')
|
||||
|
||||
const { acknowledgeConflict, acknowledgmentState } =
|
||||
it('should dismiss warning banner', () => {
|
||||
const { dismissWarningBanner, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
acknowledgeConflict('TestPackage', 'os', '0.3.41')
|
||||
dismissWarningBanner()
|
||||
|
||||
expect(acknowledgmentState.value.acknowledged_conflicts).toHaveLength(1)
|
||||
expect(acknowledgmentState.value.acknowledged_conflicts[0]).toEqual({
|
||||
package_id: 'TestPackage',
|
||||
conflict_type: 'os',
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
comfyui_version: '0.3.41'
|
||||
})
|
||||
|
||||
// useStorage handles localStorage synchronization internally
|
||||
expect(acknowledgmentState.value.acknowledged_conflicts).toHaveLength(1)
|
||||
expect(acknowledgmentState.value.acknowledged_conflicts[0]).toEqual({
|
||||
package_id: 'TestPackage',
|
||||
conflict_type: 'os',
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
comfyui_version: '0.3.41'
|
||||
})
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[ConflictAcknowledgment] Acknowledged conflict for TestPackage:os'
|
||||
)
|
||||
|
||||
dateSpy.mockRestore()
|
||||
consoleLogSpy.mockRestore()
|
||||
expect(acknowledgmentState.value.warning_banner_dismissed).toBe(true)
|
||||
})
|
||||
|
||||
it('should replace existing acknowledgment for same package and conflict type', () => {
|
||||
const { acknowledgeConflict, acknowledgmentState } =
|
||||
it('should mark all conflicts as seen', () => {
|
||||
const { markConflictsAsSeen, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
// First acknowledgment
|
||||
acknowledgeConflict('TestPackage', 'os', '0.3.41')
|
||||
expect(acknowledgmentState.value.acknowledged_conflicts).toHaveLength(1)
|
||||
markConflictsAsSeen()
|
||||
|
||||
// Second acknowledgment for same package and conflict type
|
||||
acknowledgeConflict('TestPackage', 'os', '0.3.42')
|
||||
expect(acknowledgmentState.value.acknowledged_conflicts).toHaveLength(1)
|
||||
expect(
|
||||
acknowledgmentState.value.acknowledged_conflicts[0].comfyui_version
|
||||
).toBe('0.3.42')
|
||||
})
|
||||
|
||||
it('should allow multiple acknowledgments for different conflict types', () => {
|
||||
const { acknowledgeConflict, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
acknowledgeConflict('TestPackage', 'os', '0.3.41')
|
||||
acknowledgeConflict('TestPackage', 'accelerator', '0.3.41')
|
||||
|
||||
expect(acknowledgmentState.value.acknowledged_conflicts).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('conflict checking', () => {
|
||||
it('should check if conflict is acknowledged', () => {
|
||||
const { acknowledgeConflict, isConflictAcknowledged } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
// Initially not acknowledged
|
||||
expect(isConflictAcknowledged('TestPackage', 'os')).toBe(false)
|
||||
|
||||
// After acknowledgment
|
||||
acknowledgeConflict('TestPackage', 'os', '0.3.41')
|
||||
expect(isConflictAcknowledged('TestPackage', 'os')).toBe(true)
|
||||
|
||||
// Different conflict type should not be acknowledged
|
||||
expect(isConflictAcknowledged('TestPackage', 'accelerator')).toBe(false)
|
||||
})
|
||||
|
||||
it('should remove conflict acknowledgment', () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const {
|
||||
acknowledgeConflict,
|
||||
removeConflictAcknowledgment,
|
||||
isConflictAcknowledged,
|
||||
acknowledgmentState
|
||||
} = useConflictAcknowledgment()
|
||||
|
||||
// Add acknowledgment
|
||||
acknowledgeConflict('TestPackage', 'os', '0.3.41')
|
||||
expect(isConflictAcknowledged('TestPackage', 'os')).toBe(true)
|
||||
|
||||
// Remove acknowledgment
|
||||
removeConflictAcknowledgment('TestPackage', 'os')
|
||||
expect(isConflictAcknowledged('TestPackage', 'os')).toBe(false)
|
||||
expect(acknowledgmentState.value.acknowledged_conflicts).toHaveLength(0)
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[ConflictAcknowledgment] Removed acknowledgment for TestPackage:os'
|
||||
)
|
||||
|
||||
consoleLogSpy.mockRestore()
|
||||
expect(acknowledgmentState.value.modal_dismissed).toBe(true)
|
||||
expect(acknowledgmentState.value.red_dot_dismissed).toBe(true)
|
||||
expect(acknowledgmentState.value.warning_banner_dismissed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('computed properties', () => {
|
||||
it('should calculate shouldShowConflictModal correctly', () => {
|
||||
const { shouldShowConflictModal, dismissConflictModal } =
|
||||
const { shouldShowConflictModal, markConflictsAsSeen } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
expect(shouldShowConflictModal.value).toBe(true)
|
||||
|
||||
dismissConflictModal()
|
||||
markConflictsAsSeen()
|
||||
expect(shouldShowConflictModal.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should calculate shouldShowRedDot correctly', () => {
|
||||
it('should calculate shouldShowRedDot correctly based on conflicts', () => {
|
||||
const { shouldShowRedDot, dismissRedDotNotification } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
expect(shouldShowRedDot.value).toBe(true)
|
||||
// Initially false because no conflicts exist
|
||||
expect(shouldShowRedDot.value).toBe(false)
|
||||
|
||||
dismissRedDotNotification()
|
||||
expect(shouldShowRedDot.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should calculate acknowledgedPackageIds correctly', () => {
|
||||
const { acknowledgeConflict, acknowledgedPackageIds } =
|
||||
it('should calculate shouldShowManagerBanner correctly', () => {
|
||||
const { shouldShowManagerBanner, dismissWarningBanner } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
expect(acknowledgedPackageIds.value).toEqual([])
|
||||
// Initially false because no conflicts exist
|
||||
expect(shouldShowManagerBanner.value).toBe(false)
|
||||
|
||||
acknowledgeConflict('Package1', 'os', '0.3.41')
|
||||
acknowledgeConflict('Package2', 'accelerator', '0.3.41')
|
||||
acknowledgeConflict('Package1', 'accelerator', '0.3.41') // Same package, different conflict
|
||||
|
||||
expect(acknowledgedPackageIds.value).toEqual(['Package1', 'Package2'])
|
||||
})
|
||||
|
||||
it('should calculate acknowledgmentStats correctly', () => {
|
||||
const { acknowledgeConflict, dismissConflictModal, acknowledgmentStats } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
acknowledgeConflict('Package1', 'os', '0.3.41')
|
||||
acknowledgeConflict('Package2', 'accelerator', '0.3.41')
|
||||
dismissConflictModal()
|
||||
|
||||
expect(acknowledgmentStats.value).toEqual({
|
||||
total_acknowledged: 2,
|
||||
unique_packages: 2,
|
||||
modal_dismissed: true,
|
||||
red_dot_dismissed: false,
|
||||
last_comfyui_version: ''
|
||||
})
|
||||
dismissWarningBanner()
|
||||
expect(shouldShowManagerBanner.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear functionality', () => {
|
||||
it('should clear all acknowledgments', () => {
|
||||
const consoleLogSpy = vi
|
||||
.spyOn(console, 'log')
|
||||
.mockImplementation(() => {})
|
||||
describe('localStorage persistence', () => {
|
||||
it('should persist to localStorage automatically', () => {
|
||||
const { markConflictsAsSeen, dismissWarningBanner } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const {
|
||||
acknowledgeConflict,
|
||||
dismissConflictModal,
|
||||
clearAllAcknowledgments,
|
||||
acknowledgmentState
|
||||
} = useConflictAcknowledgment()
|
||||
markConflictsAsSeen()
|
||||
dismissWarningBanner()
|
||||
|
||||
// Add some data
|
||||
acknowledgeConflict('Package1', 'os', '0.3.41')
|
||||
dismissConflictModal()
|
||||
|
||||
// Clear all
|
||||
clearAllAcknowledgments()
|
||||
|
||||
expect(acknowledgmentState.value).toEqual({
|
||||
modal_dismissed: false,
|
||||
red_dot_dismissed: false,
|
||||
acknowledged_conflicts: [],
|
||||
last_comfyui_version: ''
|
||||
})
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[ConflictAcknowledgment] Cleared all acknowledgments'
|
||||
)
|
||||
|
||||
consoleLogSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('localStorage error handling', () => {
|
||||
it('should handle localStorage setItem errors gracefully', () => {
|
||||
mockLocalStorage.setItem.mockImplementation(() => {
|
||||
throw new Error('localStorage full')
|
||||
})
|
||||
|
||||
const { dismissConflictModal, acknowledgmentState } = useConflictAcknowledgment()
|
||||
|
||||
// VueUse's useStorage should handle localStorage errors gracefully
|
||||
expect(() => dismissConflictModal()).not.toThrow()
|
||||
|
||||
// State should still be updated in memory even if localStorage fails
|
||||
expect(acknowledgmentState.value.modal_dismissed).toBe(true)
|
||||
// VueUse useStorage should automatically persist to localStorage
|
||||
expect(
|
||||
localStorage.getItem('Comfy.ConflictModalDismissed')
|
||||
).not.toBeNull()
|
||||
expect(
|
||||
localStorage.getItem('Comfy.ConflictWarningBannerDismissed')
|
||||
).not.toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -906,23 +906,11 @@ describe.skip('useConflictDetection with Registry Store', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should expose acknowledgment state and methods', () => {
|
||||
it('should expose conflict modal display method', () => {
|
||||
const {
|
||||
shouldShowConflictModal,
|
||||
shouldShowRedDot,
|
||||
acknowledgedPackageIds,
|
||||
dismissConflictModal,
|
||||
dismissRedDotNotification,
|
||||
acknowledgePackageConflict,
|
||||
shouldShowConflictModalAfterUpdate
|
||||
} = useConflictDetection()
|
||||
|
||||
expect(shouldShowConflictModal).toBeDefined()
|
||||
expect(shouldShowRedDot).toBeDefined()
|
||||
expect(acknowledgedPackageIds).toBeDefined()
|
||||
expect(dismissConflictModal).toBeDefined()
|
||||
expect(dismissRedDotNotification).toBeDefined()
|
||||
expect(acknowledgePackageConflict).toBeDefined()
|
||||
expect(shouldShowConflictModalAfterUpdate).toBeDefined()
|
||||
})
|
||||
|
||||
@@ -981,18 +969,8 @@ describe.skip('useConflictDetection with Registry Store', () => {
|
||||
expect(result).toBe(true) // Should show modal when conflicts exist and not dismissed
|
||||
})
|
||||
|
||||
it('should call acknowledgment methods when dismissing', () => {
|
||||
const { dismissConflictModal, dismissRedDotNotification } =
|
||||
useConflictDetection()
|
||||
|
||||
dismissConflictModal()
|
||||
expect(mockAcknowledgment.dismissConflictModal).toHaveBeenCalled()
|
||||
|
||||
dismissRedDotNotification()
|
||||
expect(mockAcknowledgment.dismissRedDotNotification).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should acknowledge package conflicts with system version', async () => {
|
||||
it('should detect system environment correctly', async () => {
|
||||
// Mock system environment
|
||||
mockSystemStatsStore.systemStats = {
|
||||
system: {
|
||||
@@ -1002,20 +980,12 @@ describe.skip('useConflictDetection with Registry Store', () => {
|
||||
devices: []
|
||||
}
|
||||
|
||||
const { acknowledgePackageConflict, detectSystemEnvironment } =
|
||||
useConflictDetection()
|
||||
const { detectSystemEnvironment } = useConflictDetection()
|
||||
|
||||
// First detect system environment
|
||||
await detectSystemEnvironment()
|
||||
// Detect system environment
|
||||
const environment = await detectSystemEnvironment()
|
||||
|
||||
// Then acknowledge conflict
|
||||
acknowledgePackageConflict('TestPackage', 'os')
|
||||
|
||||
expect(mockAcknowledgment.acknowledgeConflict).toHaveBeenCalledWith(
|
||||
'TestPackage',
|
||||
'os',
|
||||
'0.3.41' // System version from mock data
|
||||
)
|
||||
expect(environment.comfyui_version).toBe('0.3.41')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user