fixed unsaved changes

This commit is contained in:
2025-06-20 11:52:45 +02:00
parent b5114ac6fb
commit 3c954ac715
8 changed files with 223 additions and 167 deletions

View File

@ -0,0 +1,24 @@
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
export const usePasswordGroup = () => {
const areItemsEqual = (
groupA: unknown | unknown[] | null,
groupB: unknown | unknown[] | null,
) => {
if (groupA === null && groupB === null) return true
if (Array.isArray(groupA) && Array.isArray(groupB)) {
console.log('compare object arrays', groupA, groupB)
if (groupA.length === groupB.length) return true
return groupA.some((group, index) => {
return areObjectsEqual(group, groupA[index])
})
}
return areObjectsEqual(groupA, groupB)
}
return {
areItemsEqual,
}
}

View File

@ -52,7 +52,7 @@
class="btn-primary btn-outline flex-1-1 min-w-40"
>
<Icon name="mdi:plus" />
<p class="hidden sm:inline-flex">{{ t('add') }}</p>
<p class="hidden sm:inline-block">{{ t('add') }}</p>
</UiButton>
</div>
</div>

View File

@ -77,7 +77,7 @@ watch(
try {
const foundGroup = await readGroupAsync(currentGroupId.value)
if (foundGroup) {
original.value = JSON.stringify(foundGroup)
original.value = JSON.parse(JSON.stringify(foundGroup))
group.value = foundGroup
}
} catch (error) {
@ -86,22 +86,11 @@ watch(
},
{ immediate: true },
)
/* watch(
currentGroup,
(n, o) => {
console.log('currentGroup', currentGroup.value, n, o)
original.value = JSON.stringify(currentGroup.value)
group.value = JSON.parse(original.value)
ignoreChanges.value = false
},
{ immediate: true },
) */
const read_only = ref(false)
const hasChanges = computed(
() => JSON.stringify(group.value) !== original.value,
)
const { areItemsEqual } = usePasswordGroup()
const hasChanges = computed(() => !!!areItemsEqual(group.value, original.value))
const onClose = () => {
if (showConfirmDeleteDialog.value || showUnsavedChangesDialog.value) return
@ -121,7 +110,7 @@ const onSaveAsync = async () => {
ignoreChanges.value = true
await updateAsync(group.value)
await syncGroupItemsAsync(group.value.id)
await syncGroupItemsAsync()
add({ type: 'success', text: t('change.success') })
onClose()
} catch (error) {
@ -141,7 +130,7 @@ const onDeleteAsync = async () => {
try {
const parentId = group.value.parentId
await deleteGroupAsync(group.value.id, inTrashGroup.value)
await syncGroupItemsAsync(parentId)
await syncGroupItemsAsync()
showConfirmDeleteDialog.value = false
ignoreChanges.value = true
await navigateTo(

View File

@ -194,7 +194,7 @@ const onPasteAsync = async () => {
[...selectedGroupItems.value],
currentGroupId.value,
)
await syncGroupItemsAsync(currentGroupId.value)
await syncGroupItemsAsync()
selectedGroupItems.value = []
selectedItems.value.clear()
} catch (error) {
@ -235,7 +235,7 @@ const onDeleteAsync = async () => {
}
}
selectedItems.value.clear()
await syncGroupItemsAsync(currentGroupId.value)
await syncGroupItemsAsync()
}
const keys = useMagicKeys()
watch(keys.delete, async () => {

View File

@ -1,5 +1,11 @@
<template>
<div>
<div class="flex flex-col">
<p>
{{ item.originalDetails }}
</p>
{{ item.details }}
</div>
<HaexPassItem
:history="item.history"
:read_only
@ -11,83 +17,8 @@
v-model:key-values="item.keyValues"
/>
<!-- <div
class="fixed bottom-4 flex justify-between transition-all pointer-events-none right-0 sm:items-center items-end"
:class="[isVisible ? 'left-15 ' : 'left-0']"
>
<div class="flex items-center justify-center flex-1">
<UiTooltip :tooltip="t('abort')">
<UiButton
class="btn-accent btn-square"
@click="onClose"
>
<Icon name="mdi:close" />
</UiButton>
</UiTooltip>
</div>
<UiTooltip
v-show="read_only && !hasChanges"
:tooltip="t('edit')"
>
<UiButton
class="btn-xl btn-square btn-primary"
@click="read_only = false"
>
<Icon
name="mdi:pencil-outline"
class="size-11 shrink-0"
/>
</UiButton>
</UiTooltip>
<UiTooltip
v-show="!read_only && !hasChanges"
:tooltip="t('noEdit')"
>
<UiButton
class="btn-xl btn-square btn-primary"
@click="read_only = true"
>
<Icon
name="mdi:pencil-off-outline"
class="size-11 shrink-0"
/>
</UiButton>
</UiTooltip>
<UiTooltip
:tooltip="t('save')"
v-show="!read_only && hasChanges"
>
<UiButton
class="btn-xl btn-square btn-primary motion-duration-2000"
:class="{ 'motion-preset-pulse-sm': hasChanges }"
@click="onUpdateAsync"
>
<Icon
name="mdi:content-save-outline"
class="size-11 shrink-0"
/>
</UiButton>
</UiTooltip>
<div class="flex items-center justify-center flex-1">
<UiTooltip :tooltip="t('delete')">
<UiButton
class="btn-square btn-error"
@click="showConfirmDeleteDialog = true"
>
<Icon
name="mdi:trash-outline"
class="shrink-0"
/>
</UiButton>
</UiTooltip>
</div>
</div> -->
<HaexPassMenuBottom
:has-changes
:show-edit-button="read_only && !hasChanges"
:show-readonly-button="!read_only && !hasChanges"
:show-save-button="!read_only && hasChanges"
@ -119,6 +50,7 @@
</template>
<script setup lang="ts">
import { usePasswordGroup } from '~/components/haex/pass/group/composables'
import type {
SelectHaexPasswordsItemDetails,
SelectHaexPasswordsItemHistory,
@ -145,8 +77,8 @@ const item = reactive<{
keyValues: SelectHaexPasswordsItemKeyValues[]
keyValuesAdd: SelectHaexPasswordsItemKeyValues[]
keyValuesDelete: SelectHaexPasswordsItemKeyValues[]
originalDetails: string | null
originalKeyValues: string | null
originalDetails: SelectHaexPasswordsItemDetails | null
originalKeyValues: SelectHaexPasswordsItemKeyValues[] | null
}>({
details: {
id: '',
@ -164,7 +96,18 @@ const item = reactive<{
history: [],
keyValuesAdd: [],
keyValuesDelete: [],
originalDetails: null,
originalDetails: {
id: '',
createdAt: null,
icon: null,
note: null,
password: null,
tags: null,
title: null,
updateAt: null,
url: null,
username: null,
},
originalKeyValues: null,
})
@ -179,8 +122,12 @@ watch(
item.history = JSON.parse(JSON.stringify(currentItem.value?.history))
item.keyValuesAdd = []
item.keyValuesDelete = []
item.originalDetails = JSON.stringify(currentItem.value?.details)
item.originalKeyValues = JSON.stringify(currentItem.value?.keyValues)
item.originalDetails = JSON.parse(
JSON.stringify(currentItem.value?.details),
)
item.originalKeyValues = JSON.parse(
JSON.stringify(currentItem.value?.keyValues),
)
},
{ immediate: true },
)
@ -201,7 +148,7 @@ const onUpdateAsync = async () => {
keyValuesDelete: item.keyValuesDelete,
})
if (newId) add({ type: 'success', text: t('success.update') })
syncGroupItemsAsync(currentGroupId.value)
syncGroupItemsAsync()
ignoreChanges.value = true
onClose()
} catch (error) {
@ -224,7 +171,7 @@ const deleteItemAsync = async () => {
await deleteAsync(item.details.id, inTrashGroup.value)
showConfirmDeleteDialog.value = false
add({ type: 'success', text: t('success.delete') })
await syncGroupItemsAsync(currentGroupId.value)
await syncGroupItemsAsync()
onClose()
} catch (errro) {
add({
@ -234,11 +181,12 @@ const deleteItemAsync = async () => {
}
}
const { areItemsEqual } = usePasswordGroup()
const hasChanges = computed(
() =>
!!(
item.originalDetails !== JSON.stringify(item.details) ||
item.originalKeyValues !== JSON.stringify(item.keyValues) ||
!areItemsEqual(item.originalDetails, item.details) ||
!areItemsEqual(item.originalKeyValues, item.keyValues) ||
item.keyValuesAdd.length ||
item.keyValuesDelete.length
),

View File

@ -1,5 +1,6 @@
<template>
<div>
{{ currentGroup?.id }} {{ currentGroupId }}
<HaexPassItem
:default-icon="currentGroup?.icon"
:history="item.history"
@ -10,6 +11,7 @@
/>
<HaexPassMenuBottom
:has-changes
@close="onClose"
@save="onCreateAsync"
show-close-button
@ -17,33 +19,13 @@
>
</HaexPassMenuBottom>
<!-- <div
class="fixed bottom-4 flex justify-between transition-all pointer-events-none right-0 sm:items-center items-end"
:class="[isVisible ? 'left-15 ' : 'left-0']"
>
<div class="flex items-center justify-center flex-1">
<UiTooltip :tooltip="t('abort')">
<UiButton
class="btn-error btn-square"
@click="onClose"
>
<Icon name="mdi:close" />
</UiButton>
</UiTooltip>
</div>
<UiTooltip :tooltip="t('create')">
<UiButton
class="btn-xl btn-square btn-primary"
@click="onCreateAsync"
>
<Icon
name="mdi:content-save-outline"
class="size-11 shrink-0"
/>
</UiButton>
</UiTooltip>
<div class="flex items-center justify-center flex-1"></div>
</div> -->
<HaexPassDialogUnsavedChanges
:has-changes="hasChanges"
v-model:ignore-changes="ignoreChanges"
@abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges"
v-model:open="showUnsavedChangesDialog"
/>
</div>
</template>
@ -64,17 +46,14 @@ defineProps({
withCopyButton: Boolean,
})
const { isVisible } = storeToRefs(useSidebarStore())
const { t } = useI18n()
const item = reactive<{
details: SelectHaexPasswordsItemDetails
history: SelectHaexPasswordsItemHistory[]
keyValuesAdd: SelectHaexPasswordsItemKeyValues[]
keyValuesDelete: SelectHaexPasswordsItemKeyValues[]
originalDetails: string | null
originalKeyValues: string | null
originalDetails: SelectHaexPasswordsItemDetails
originalKeyValuesAdd: []
}>({
details: {
id: '',
@ -90,13 +69,23 @@ const item = reactive<{
},
history: [],
keyValuesAdd: [],
keyValuesDelete: [],
originalDetails: null,
originalKeyValues: null,
originalDetails: {
id: '',
createdAt: null,
icon: null,
note: null,
password: null,
tags: null,
title: null,
updateAt: null,
url: null,
username: null,
},
originalKeyValuesAdd: [],
})
const { add } = useSnackbar()
const { currentGroup } = storeToRefs(usePasswordGroupStore())
const { currentGroup, currentGroupId } = storeToRefs(usePasswordGroupStore())
const { syncGroupItemsAsync } = usePasswordGroupStore()
const { addAsync } = usePasswordItemStore()
@ -107,9 +96,11 @@ const onCreateAsync = async () => {
item.keyValuesAdd,
currentGroup.value,
)
if (newId) {
ignoreChanges.value = true
add({ type: 'success', text: t('success') })
syncGroupItemsAsync(currentGroup.value?.id)
await syncGroupItemsAsync()
onClose()
}
} catch (error) {
@ -117,7 +108,32 @@ const onCreateAsync = async () => {
}
}
const onClose = () => useRouter().back()
const ignoreChanges = ref(false)
const onClose = () => {
if (showUnsavedChangesDialog.value) return
if (hasChanges.value && !ignoreChanges.value)
return (showUnsavedChangesDialog.value = true)
useRouter().back()
}
const { areItemsEqual } = usePasswordGroup()
const hasChanges = computed(
() =>
!!(
!areItemsEqual(item.originalDetails, item.details) ||
item.keyValuesAdd.length
),
)
const showUnsavedChangesDialog = ref(false)
const onConfirmIgnoreChanges = () => {
showUnsavedChangesDialog.value = false
ignoreChanges.value = true
onClose()
}
</script>
<i18n lang="yaml">

View File

@ -6,7 +6,6 @@ import {
type InsertHaexPasswordsGroups,
type SelectHaexPasswordsGroupItems,
type SelectHaexPasswordsGroups,
type SelectHaexPasswordsItemDetails,
} from '~~/src-tauri/database/schemas/vault'
export const trashId = 'trash'
@ -28,14 +27,6 @@ export const usePasswordGroupStore = defineStore('passwordGroupStore', () => {
currentGroupId.value ? readGroupAsync(currentGroupId.value) : null,
)
/* const currentGroupItems = reactive<{
items: SelectHaexPasswordsItemDetails[]
groups: SelectHaexPasswordsGroups[]
}>({
items: [],
groups: [],
}) */
const selectedGroupItems = ref<IPasswordMenuItem[]>()
const breadCrumbs = computed(() => getParentChain(currentGroupId.value))
@ -55,15 +46,14 @@ export const usePasswordGroupStore = defineStore('passwordGroupStore', () => {
return chain.reverse()
}
const syncGroupItemsAsync = async (currentGroupId?: string | null) => {
const { addNotificationAsync } = useNotificationStore()
const { readByGroupIdAsync, syncItemsAsync } = usePasswordItemStore()
const syncGroupItemsAsync = async () => {
const { syncItemsAsync } = usePasswordItemStore()
groups.value = await readGroupsAsync()
await syncItemsAsync()
currentGroup.value = groups.value?.find(
/* currentGroup.value = groups.value?.find(
(group) => group.id === currentGroupId,
)
) */
/* try {
currentGroupItems.groups =
@ -80,7 +70,7 @@ export const usePasswordGroupStore = defineStore('passwordGroupStore', () => {
} */
}
watch(currentGroupId, () => syncGroupItemsAsync(currentGroupId.value), {
watch(currentGroupId, () => syncGroupItemsAsync(), {
immediate: true,
})
@ -90,6 +80,7 @@ export const usePasswordGroupStore = defineStore('passwordGroupStore', () => {
return {
addGroupAsync,
areGroupsEqual,
breadCrumbs,
createTrashIfNotExistsAsync,
currentGroup,
@ -131,13 +122,11 @@ const addGroupAsync = async (group: Partial<InsertHaexPasswordsGroups>) => {
const readGroupAsync = async (groupId: string) => {
const { currentVault } = useVaultStore()
return (
await currentVault.drizzle
?.select()
.from(haexPasswordsGroups)
.where(eq(haexPasswordsGroups.id, groupId))
).at(0)
const group = await currentVault.drizzle.query.haexPasswordsGroups.findFirst({
where: eq(haexPasswordsGroups.id, groupId),
})
console.log('readGroupAsync', groupId, group)
return group
}
const readGroupsAsync = async (filter?: { parentId?: string | null }) => {
@ -274,8 +263,6 @@ const insertGroupItemsAsync = async (
const targetGroup = groups.find((group) => group.id === groupdId)
console.log('insertGroupItemsAsync', items, targetGroup)
for (const item of items) {
if (item.type === 'group') {
const updateGroup = groups.find((group) => group.id === item.id)
@ -297,7 +284,7 @@ const insertGroupItemsAsync = async (
.where(eq(haexPasswordsGroupItems.itemId, item.id))
}
}
return syncGroupItemsAsync(targetGroup?.id)
return syncGroupItemsAsync()
}
const createTrashIfNotExistsAsync = async () => {
@ -340,3 +327,20 @@ const deleteGroupAsync = async (groupId: string, final: boolean = false) => {
await updateAsync({ id: groupId, parentId: trashId })
}
}
const areGroupsEqual = (
groupA: unknown | unknown[] | null,
groupB: unknown | unknown[] | null,
) => {
if (groupA === null && groupB === null) return true
if (Array.isArray(groupA) && Array.isArray(groupB)) {
console.log('compare object arrays', groupA, groupB)
if (groupA.length === groupB.length) return true
return groupA.some((group, index) => {
return areObjectsEqual(group, groupA[index])
})
}
return areObjectsEqual(groupA, groupB)
}

View File

@ -155,3 +155,78 @@ export const getContrastingTextColor = (
// Ein Wert > 186 wird oft als "hell" genug für schwarzen Text angesehen.
return luminance > 186 ? 'black' : 'white'
}
/**
* Eine "Type Guard"-Funktion, die prüft, ob ein Wert ein Objekt (aber nicht null) ist.
* Wenn sie `true` zurückgibt, weiß TypeScript, dass der Wert sicher als Objekt behandelt werden kann.
* @param value Der zu prüfende Wert vom Typ `unknown`.
* @returns {boolean} `true`, wenn der Wert ein Objekt ist.
*/
export const isObject = (value: unknown): value is Record<string, unknown> => {
return typeof value === 'object' && value !== null
}
/**
* Führt einen typsicheren, tiefen Vergleich (deep comparison) von zwei Werten durch.
* Gibt `true` zurück, wenn die Werte als gleich angesehen werden.
*
* @param valueA Der erste Wert für den Vergleich.
* @param valueB Der zweite Wert für den Vergleich.
* @returns {boolean} `true`, wenn die Werte gleich sind, andernfalls `false`.
*/
export const areObjectsEqual = (valueA: unknown, valueB: unknown): boolean => {
console.log('areObjectsEqual', valueA, valueB)
// 1. Schneller Check für exakt die gleiche Referenz oder primitive Gleichheit
if (valueA === valueB) {
return true
}
// DEINE SONDERREGEL: Behandle `null` und einen leeren String `""` als gleichwertig.
const areNullAndEmptyString =
(valueA === null && valueB === '') || (valueA === '' && valueB === null)
if (areNullAndEmptyString) {
return true
}
// 2. Nutzen der Type Guard: Wenn beide Werte keine Objekte sind,
// und die vorherigen Checks fehlschlugen, sind sie ungleich.
if (!isObject(valueA) || !isObject(valueB)) {
console.log('areObjectsEqual no objects', valueA, valueB)
return false
}
// Ab hier weiß TypeScript dank der Type Guard, dass valueA und valueB Objekte sind.
// 3. Holen der Schlüssel und Vergleich der Anzahl
const keysA = Object.keys(valueA)
const keysB = Object.keys(valueB)
if (keysA.length !== keysB.length) {
console.log('areObjectsEqual length')
return false
}
// 4. Iteration über alle Schlüssel und rekursiver Vergleich der Werte
for (const key of keysA) {
// Prüfen, ob der Schlüssel auch im zweiten Objekt überhaupt existiert
if (!keysB.includes(key)) {
console.log('areObjectsEqual keys')
return false
}
// Die Werte der Schlüssel sind wieder `unknown`, daher nutzen wir Rekursion.
const nestedValueA = valueA[key]
const nestedValueB = valueB[key]
// Wenn der rekursive Aufruf für einen der Werte `false` zurückgibt,
// sind die gesamten Objekte ungleich.
if (!areObjectsEqual(nestedValueA, nestedValueB)) {
console.log('areObjectsEqual nested')
return false
}
}
// 5. Wenn die Schleife durchläuft, sind die Objekte gleich
return true
}