implemented search

This commit is contained in:
2025-06-19 14:47:42 +02:00
parent 62ddc33290
commit 25f63d30be
24 changed files with 604 additions and 175 deletions

View File

@ -12,7 +12,6 @@ pub fn run() {
let protocol_name = "haex-extension"; let protocol_name = "haex-extension";
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
.register_uri_scheme_protocol(protocol_name, move |context, request| { .register_uri_scheme_protocol(protocol_name, move |context, request| {
match extension::core::extension_protocol_handler(&context, &request) { match extension::core::extension_protocol_handler(&context, &request) {
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
@ -45,6 +44,7 @@ pub fn run() {
}) })
.manage(DbConnection(Mutex::new(None))) .manage(DbConnection(Mutex::new(None)))
.manage(ExtensionState::default()) .manage(ExtensionState::default())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
@ -57,10 +57,10 @@ pub fn run() {
database::open_encrypted_database, database::open_encrypted_database,
database::sql_execute, database::sql_execute,
database::sql_select, database::sql_select,
database::test,
extension::copy_directory,
extension::database::extension_sql_execute, extension::database::extension_sql_execute,
extension::database::extension_sql_select, extension::database::extension_sql_select,
extension::copy_directory,
database::test
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@ -0,0 +1,44 @@
<template>
<UiDialogConfirm
v-model:open="showConfirmDeleteDialog"
:confirm-label="final ? t('final.label') : t('label')"
:title="final ? t('final.title') : t('title', { itemName })"
@abort="$emit('abort')"
@confirm="$emit('confirm')"
>
{{
final ? t('final.question', { itemName }) : t('question', { itemName })
}}
</UiDialogConfirm>
</template>
<script setup lang="ts">
const { t } = useI18n()
const showConfirmDeleteDialog = defineModel<boolean>('open')
defineProps<{ final?: boolean; itemName?: string | null }>()
defineEmits(['confirm', 'abort'])
</script>
<i18n lang="yaml">
de:
title: Eintrag löschen
question: Soll der Eintrag "{itemName}" in den Papierkorb verschoben werden?
label: Verschieben
final:
title: Eintrag endgültig löschen
question: Soll der Eintrag "{itemName}" endgültig gelöscht werden?
label: Löschen
en:
title: Delete Entry
question: Should the {itemName} entry be moved to the recycle bin?
label: Move
final:
title: Delete entry permanently
question: Should the entry {itemName} be permanently deleted?
label: Delete
</i18n>

View File

@ -0,0 +1,47 @@
<template>
<UiDialogConfirm
:confirm-label="t('label')"
:title="t('title')"
@abort="$emit('abort')"
@confirm="onConfirm"
v-model:open="showUnsavedChangesDialog"
>
{{ t('question') }}
</UiDialogConfirm>
</template>
<script setup lang="ts">
const { t } = useI18n()
const showUnsavedChangesDialog = defineModel<boolean>('open')
const ignoreChanges = defineModel<boolean>('ignoreChanges')
const { hasChanges } = defineProps<{ hasChanges: boolean }>()
const emit = defineEmits(['confirm', 'abort'])
const onConfirm = () => {
ignoreChanges.value = true
emit('confirm')
}
onBeforeRouteLeave(() => {
if (hasChanges && !ignoreChanges.value) {
showUnsavedChangesDialog.value = true
return false
}
return true
})
</script>
<i18n lang="yaml">
de:
title: Nicht gespeicherte Änderungen
question: Sollen die Änderungen verworfen werden?
label: Verwerfen
en:
title: Unsaved changes
question: Should the changes be discarded?
label: discard
</i18n>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="breadcrumbs sticky top-0"> <div class="breadcrumbs">
<ul> <ul>
<li> <li>
<NuxtLinkLocale :to="{ name: 'passwordGroupItems' }"> <NuxtLinkLocale :to="{ name: 'passwordGroupItems' }">

View File

@ -2,7 +2,7 @@
<div class="p-1"> <div class="p-1">
<UiCard <UiCard
v-if="group" v-if="group"
:title="mode === 'create' ? t('title.create') : t('title.edit')" :title="mode === 'edit' ? t('title.edit') : t('title.create')"
icon="mdi:folder-plus-outline" icon="mdi:folder-plus-outline"
@close="$emit('close')" @close="$emit('close')"
body-class="px-0" body-class="px-0"
@ -12,9 +12,9 @@
@submit.prevent="$emit('submit')" @submit.prevent="$emit('submit')"
> >
<UiInput <UiInput
:check-input="check"
:label="t('name')" :label="t('name')"
:placeholder="t('name')" :placeholder="t('name')"
:read_only
autofocus autofocus
v-model="group.name" v-model="group.name"
ref="nameRef" ref="nameRef"
@ -23,9 +23,9 @@
<UiInput <UiInput
v-model="group.description" v-model="group.description"
:check-input="check"
:label="t('description')" :label="t('description')"
:placeholder="t('description')" :placeholder="t('description')"
:read_only
@keyup.enter="$emit('submit')" @keyup.enter="$emit('submit')"
/> />
@ -33,12 +33,16 @@
<UiSelectIcon <UiSelectIcon
v-model="group.icon" v-model="group.icon"
default-icon="mdi:folder-outline" default-icon="mdi:folder-outline"
:read_only
/> />
<UiSelectColor v-model="group.color" /> <UiSelectColor
v-model="group.color"
:read_only
/>
</div> </div>
<div class="flex flex-wrap justify-end gap-4"> <!-- <div class="flex flex-wrap justify-end gap-4">
<UiButton <UiButton
class="btn-error btn-outline flex-1" class="btn-error btn-outline flex-1"
@click="$emit('close')" @click="$emit('close')"
@ -54,7 +58,7 @@
{{ mode === 'create' ? t('create') : t('save') }} {{ mode === 'create' ? t('create') : t('save') }}
<Icon name="mdi:check" /> <Icon name="mdi:check" />
</UiButton> </UiButton>
</div> </div> -->
</form> </form>
</UiCard> </UiCard>
</div> </div>
@ -64,13 +68,14 @@
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault' import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
const group = defineModel<SelectHaexPasswordsGroups | null>() const group = defineModel<SelectHaexPasswordsGroups | null>()
defineEmits(['close', 'submit', 'back']) const { read_only = false } = defineProps<{
defineProps<{ mode: 'create' | 'edit' }>() read_only?: boolean
mode: 'create' | 'edit'
}>()
defineEmits(['close', 'submit'])
const { t } = useI18n() const { t } = useI18n()
const check = ref<boolean>(false)
const nameRef = useTemplateRef('nameRef') const nameRef = useTemplateRef('nameRef')
onStartTyping(() => { onStartTyping(() => {
nameRef.value?.inputRef?.focus() nameRef.value?.inputRef?.focus()

View File

@ -25,6 +25,7 @@
:with-copy-button :with-copy-button
:read_only :read_only
v-model.trim="itemDetails.username" v-model.trim="itemDetails.username"
@keyup.enter="$emit('submit')"
/> />
<UiInputPassword <UiInputPassword
@ -33,6 +34,7 @@
:read_only :read_only
:with-copy-button :with-copy-button
v-model.trim="itemDetails.password" v-model.trim="itemDetails.password"
@keyup.enter="$emit('submit')"
> >
<template #append> <template #append>
<UiDialogPasswordGenerator <UiDialogPasswordGenerator
@ -51,6 +53,7 @@
:read_only :read_only
:with-copy-button :with-copy-button
v-model="itemDetails.url" v-model="itemDetails.url"
@keyup.enter="$emit('submit')"
/> />
<UiSelectIcon <UiSelectIcon

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="p-1"> <div class="p-1">
<UiCard <UiCard
body-class="rounded overflow-auto px-0 h-full" body-class="rounded overflow-auto p-0 h-full"
@close="onClose" @close="onClose"
> >
<div class=""> <div class="">

View File

@ -0,0 +1,102 @@
<template>
<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">
<UiButton
v-show="showCloseButton"
:tooltip="t('abort')"
@click="$emit('close')"
class="btn-accent btn-square"
>
<Icon name="mdi:close" />
</UiButton>
</div>
<div>
<UiButton
v-show="showEditButton"
:tooltip="t('edit')"
@click="$emit('edit')"
class="btn-xl btn-square btn-primary"
>
<Icon
name="mdi:pencil-outline"
class="size-11 shrink-0"
/>
</UiButton>
<UiButton
v-show="showReadonlyButton"
:tooltip="t('readonly')"
class="btn-xl btn-square btn-primary"
@click="$emit('readonly')"
>
<Icon
name="mdi:pencil-off-outline"
class="size-11 shrink-0"
/>
</UiButton>
<UiButton
v-show="showSaveButton"
:tooltip="t('save')"
class="btn-xl btn-square btn-primary motion-duration-2000"
:class="{ 'motion-preset-pulse-sm': hasChanges }"
@click="$emit('save')"
>
<Icon
name="mdi:content-save-outline"
class="size-11 shrink-0"
/>
</UiButton>
</div>
<div class="flex items-center justify-center flex-1">
<UiButton
v-show="showDeleteButton"
:tooltip="t('delete')"
class="btn-square btn-error"
@click="$emit('delete')"
>
<Icon
name="mdi:trash-outline"
class="shrink-0"
/>
</UiButton>
</div>
</div>
</template>
<script setup lang="ts">
const { isVisible } = storeToRefs(useSidebarStore())
const { t } = useI18n()
defineProps<{
showCloseButton?: boolean
showDeleteButton?: boolean
showEditButton?: boolean
showReadonlyButton?: boolean
showSaveButton?: boolean
hasChanges?: boolean
}>()
defineEmits(['close', 'edit', 'readonly', 'save', 'delete'])
</script>
<i18n lang="yaml">
de:
save: Speichern
abort: Abbrechen
edit: Bearbeiten
readonly: Lesemodus
delete: Löschen
en:
save: Save
abort: Abort
edit: Edit
readonly: Read Mode
delete: Delete
</i18n>

View File

@ -1,5 +1,8 @@
<template> <template>
<div v-if="menuItems?.length"> <div
v-if="menuItems?.length"
class="h-full"
>
<ul <ul
class="flex flex-col w-full h-full gap-y-2 first:rounded-t-md last:rounded-b-md p-1" class="flex flex-col w-full h-full gap-y-2 first:rounded-t-md last:rounded-b-md p-1"
ref="listRef" ref="listRef"
@ -40,7 +43,7 @@
</div> </div>
<div <div
v-else v-else
class="flex justify-center items-center px-20 h-full" class="flex justify-center items-center px-20 flex-1 bg-red-100"
> >
<UiIconNoData class="text-primary size-24 shrink-0" /> <UiIconNoData class="text-primary size-24 shrink-0" />
<!-- <p>{{ t('empty') }}</p> --> <!-- <p>{{ t('empty') }}</p> -->
@ -78,6 +81,7 @@ watch(selectedItems, () => {
const localePath = useLocalePath() const localePath = useLocalePath()
const { ctrl } = useMagicKeys() const { ctrl } = useMagicKeys()
const { search } = storeToRefs(useSearchStore())
const onClickItemAsync = async (item: IPasswordMenuItem) => { const onClickItemAsync = async (item: IPasswordMenuItem) => {
currentSelectedItem.value = null currentSelectedItem.value = null
@ -91,6 +95,7 @@ const onClickItemAsync = async (item: IPasswordMenuItem) => {
if (!selectedItems.value.size) longPressedHook.value = false if (!selectedItems.value.size) longPressedHook.value = false
} else { } else {
search.value = ''
if (item.type === 'group') if (item.type === 'group')
await navigateTo( await navigateTo(
localePath({ localePath({

View File

@ -13,53 +13,55 @@
</slot> </slot>
</button> </button>
<Teleport to="body"> <div class="hidden">
<div <Teleport to="body">
:id
ref="modalRef"
class="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden modal-middle p-0 xs:p-2 --prevent-on-load-init pointer-events-auto max-w-none"
role="dialog"
tabindex="-1"
>
<div <div
class="overlay-animation-target overlay-open:duration-300 overlay-open:opacity-100 transition-all ease-out modal-dialog" :id
ref="modalRef"
class="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden modal-middle p-0 xs:p-2 --prevent-on-load-init pointer-events-auto max-w-none"
role="dialog"
tabindex="-1"
> >
<div class="modal-content justify-between"> <div
<div class="modal-header py-0 sm:py-4"> class="overlay-animation-target overlay-open:duration-300 overlay-open:opacity-100 transition-all ease-out modal-dialog"
<div >
v-if="title || $slots.title" <div class="modal-content justify-between">
class="modal-title py-4 break-all" <div class="modal-header py-0 sm:py-4">
> <div
<slot name="title"> v-if="title || $slots.title"
{{ title }} class="modal-title py-4 break-all"
</slot> >
<slot name="title">
{{ title }}
</slot>
</div>
<button
type="button"
class="btn btn-text btn-circle btn-sm absolute end-3 top-3"
:aria-label="t('close')"
tabindex="1"
@click="open = false"
>
<Icon
name="mdi:close"
size="18"
/>
</button>
</div> </div>
<button <div class="modal-body text-sm sm:text-base grow mt-0 pt-0">
type="button" <slot />
class="btn btn-text btn-circle btn-sm absolute end-3 top-3" </div>
:aria-label="t('close')"
tabindex="1"
@click="open = false"
>
<Icon
name="mdi:close"
size="18"
/>
</button>
</div>
<div class="modal-body text-sm sm:text-base grow mt-0 pt-0"> <div class="modal-footer flex-col sm:flex-row">
<slot /> <slot name="buttons" />
</div> </div>
<div class="modal-footer flex-col sm:flex-row">
<slot name="buttons" />
</div> </div>
</div> </div>
</div> </div>
</div> </Teleport>
</Teleport> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -32,7 +32,7 @@
> >
<li <li
:is="itemIs" :is="itemIs"
@click="$emit('select', item)" @click="read_only ? '' : $emit('select', item)"
class="dropdown-item" class="dropdown-item"
v-for="item in items" v-for="item in items"
> >
@ -59,6 +59,7 @@ const { itemIs = 'li', offset = '[--offset:0]' } = defineProps<{
itemIs?: string itemIs?: string
activatorClass?: string activatorClass?: string
offset?: string offset?: string
read_only?: boolean
}>() }>()
defineEmits<{ select: [T] }>() defineEmits<{ select: [T] }>()

View File

@ -8,6 +8,7 @@
:rules :rules
:type="type" :type="type"
:with-copy-button :with-copy-button
@keyup="(e) => $emit('keyup', e)"
> >
<template #append> <template #append>
<slot name="append" /> <slot name="append" />
@ -38,6 +39,10 @@ defineProps<{
withCopyButton?: boolean withCopyButton?: boolean
}>() }>()
defineEmits<{
keyup: [KeyboardEvent]
}>()
const type = ref<'password' | 'text'>('password') const type = ref<'password' | 'text'>('password')
const tooglePasswordType = () => { const tooglePasswordType = () => {

View File

@ -8,6 +8,7 @@
:rules :rules
:with-copy-button :with-copy-button
v-model.trim="value" v-model.trim="value"
@keyup="(e) => $emit('keyup', e)"
> >
<template #append> <template #append>
<UiButton <UiButton
@ -38,6 +39,10 @@ defineProps({
withCopyButton: Boolean, withCopyButton: Boolean,
read_only: Boolean, read_only: Boolean,
}) })
defineEmits<{
keyup: [KeyboardEvent]
}>()
</script> </script>
<i18n lang="json"> <i18n lang="json">

View File

@ -3,6 +3,7 @@
:items="icons" :items="icons"
class="btn" class="btn"
@select="(newIcon) => (iconName = newIcon)" @select="(newIcon) => (iconName = newIcon)"
:read_only
> >
<template #activator> <template #activator>
<Icon :name="iconName ? iconName : defaultIcon || icons.at(0)" /> <Icon :name="iconName ? iconName : defaultIcon || icons.at(0)" />
@ -13,7 +14,7 @@
<li <li
class="dropdown-item" class="dropdown-item"
v-for="item in items" v-for="item in items"
@click="iconName = item" @click="read_only ? '' : (iconName = item)"
> >
<Icon <Icon
:name="item" :name="item"
@ -73,5 +74,5 @@ const icons = [
const iconName = defineModel<string | undefined | null>() const iconName = defineModel<string | undefined | null>()
defineProps<{ defaultIcon?: string }>() defineProps<{ defaultIcon?: string; read_only?: boolean }>()
</script> </script>

View File

@ -1,16 +1,16 @@
/* import de from '@/stores/sidebar/de.json'; import passwordActionMenuDe from '@/stores/passwords/actionMenu/de.json'
import en from '@/stores/sidebar/en.json'; */ import passwordActionMenuEn from '@/stores/passwords/actionMenu/en.json'
export default defineI18nConfig(() => { export default defineI18nConfig(() => {
return { return {
legacy: false, legacy: false,
messages: { messages: {
de: { de: {
//sidebar: de, passwordActionMenu: passwordActionMenuDe,
}, },
en: { en: {
//sidebar: en, passwordActionMenu: passwordActionMenuEn,
}, },
}, },
}; }
}); })

View File

@ -35,7 +35,25 @@
</NuxtLinkLocale> </NuxtLinkLocale>
</div> </div>
<div></div>
<div class="flex items-center gap-x-4"> <div class="flex items-center gap-x-4">
<div class="flex items-center">
<UiInput
v-model="search"
:label="t('search.label')"
class=""
>
<template #append>
<UiButton class="btn-square btn-primary">
<Icon
name="mdi:magnify"
class="size-full p-1"
/>
</UiButton>
</template>
</UiInput>
</div>
<HaexMenuNotifications /> <HaexMenuNotifications />
<HaexMenuMain /> <HaexMenuMain />
</div> </div>
@ -78,26 +96,34 @@ const { t } = useI18n()
const { currentVaultName } = storeToRefs(useVaultStore()) const { currentVaultName } = storeToRefs(useVaultStore())
const { menu, isVisible } = storeToRefs(useSidebarStore())
const { extensionLinks } = storeToRefs(useExtensionsStore()) const { extensionLinks } = storeToRefs(useExtensionsStore())
const { menu, isVisible } = storeToRefs(useSidebarStore())
const toogleSidebar = () => { const toogleSidebar = () => {
isVisible.value = !isVisible.value isVisible.value = !isVisible.value
} }
const { search } = storeToRefs(useSearchStore())
</script> </script>
<i18n lang="yaml"> <i18n lang="yaml">
de: de:
vault: vault:
close: Vault schließen close: Vault schließen
sidebar: sidebar:
close: Sidebar ausblenden close: Sidebar ausblenden
show: Sidebar anzeigen show: Sidebar anzeigen
search:
label: Suche
en: en:
vault: vault:
close: Close vault close: Close vault
sidebar: sidebar:
close: close sidebar close: close sidebar
show: show sidebar show: show sidebar
search:
label: Search
</i18n> </i18n>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="p-2 min-h-full"> <div class="h-full overflow-auto p-2">
<NuxtPage /> <NuxtPage />
</div> </div>
</template> </template>

View File

@ -1,11 +1,29 @@
<template> <template>
<div> <div>
{{ currentGroupId }}
<HaexPassGroup <HaexPassGroup
v-model="group" v-model="group"
mode="create" mode="create"
@close="onClose" @close="onClose"
@submit="createAsync" @submit="createAsync"
/> />
<HaexPassMenuBottom
@close="onClose"
@save="createAsync"
show-close-button
show-save-button
:has-changes
>
</HaexPassMenuBottom>
<HaexPassDialogUnsavedChanges
:has-changes
@abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges"
v-model:ignore-changes="ignoreChanges"
v-model:open="showUnsavedChangesDialog"
/>
</div> </div>
</template> </template>
@ -34,7 +52,14 @@ const errors = ref({
description: [], description: [],
}) })
const ignoreChanges = ref(false)
const onClose = () => { const onClose = () => {
if (showUnsavedChangesDialog.value) return
if (hasChanges.value && !ignoreChanges.value) {
return (showUnsavedChangesDialog.value = true)
}
useRouter().back() useRouter().back()
} }
@ -45,11 +70,11 @@ const createAsync = async () => {
const newGroup = await addGroupAsync(group.value) const newGroup = await addGroupAsync(group.value)
console.log('newGroup', newGroup)
if (!newGroup.id) { if (!newGroup.id) {
return return
} }
ignoreChanges.value = true
await navigateTo( await navigateTo(
useLocalePath()({ useLocalePath()({
name: 'passwordGroupItems', name: 'passwordGroupItems',
@ -65,4 +90,20 @@ const createAsync = async () => {
console.log(error) console.log(error)
} }
} }
const hasChanges = computed(() => {
return !!(
group.value.color ||
group.value.description ||
group.value.icon ||
group.value.name
)
})
const showUnsavedChangesDialog = ref(false)
const onConfirmIgnoreChanges = () => {
showUnsavedChangesDialog.value = false
ignoreChanges.value = true
onClose()
}
</script> </script>

View File

@ -1,11 +1,43 @@
<template> <template>
<div> <div>
currentGroup{{ currentGroup }}
<HaexPassGroup <HaexPassGroup
v-model="group" :read_only
mode="edit"
@close="onClose" @close="onClose"
@submit="onSaveAsync" @submit="onSaveAsync"
mode="edit"
v-model="group"
/>
<HaexPassMenuBottom
:show-edit-button="read_only && !hasChanges"
:show-readonly-button="!read_only && !hasChanges"
:show-save-button="hasChanges"
:has-changes
@close="onClose()"
@delete="showConfirmDeleteDialog = true"
@edit="read_only = false"
@readonly="read_only = true"
@save="onSaveAsync"
show-close-button
show-delete-button
>
</HaexPassMenuBottom>
<HaexPassDialogDeleteItem
v-model:open="showConfirmDeleteDialog"
@abort="showConfirmDeleteDialog = false"
@confirm="onDeleteAsync"
:item-name="group.name"
:final="inTrashGroup"
>
</HaexPassDialogDeleteItem>
<HaexPassDialogUnsavedChanges
:has-changes="hasChanges"
v-model:ignore-changes="ignoreChanges"
@abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges"
v-model:open="showUnsavedChangesDialog"
/> />
</div> </div>
</template> </template>
@ -19,42 +51,79 @@ definePageMeta({
const { t } = useI18n() const { t } = useI18n()
const check = ref(false) const { currentGroup, inTrashGroup, currentGroupId } = storeToRefs(
usePasswordGroupStore(),
)
const { currentGroup } = storeToRefs(usePasswordGroupStore()) const group = ref<SelectHaexPasswordsGroups>({
color: null,
createdAt: null,
description: null,
icon: null,
id: '',
name: null,
order: null,
parentId: null,
updateAt: null,
})
const group = ref<SelectHaexPasswordsGroups>() const original = ref<string>('')
const ignoreChanges = ref(false)
const { readGroupAsync } = usePasswordGroupStore()
watch( watch(
currentGroup, currentGroupId,
() => { async () => {
group.value = JSON.parse(JSON.stringify(currentGroup.value)) if (!currentGroupId.value) return
ignoreChanges.value = false
try {
const foundGroup = await readGroupAsync(currentGroupId.value)
if (foundGroup) {
original.value = JSON.stringify(foundGroup)
group.value = foundGroup
}
} catch (error) {
console.error(error)
}
}, },
{ immediate: true }, { 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 errors = ref({ const read_only = ref(true)
name: [],
description: [], const hasChanges = computed(
}) () => JSON.stringify(group.value) !== original.value,
)
const onClose = () => { const onClose = () => {
if (showConfirmDeleteDialog.value || showUnsavedChangesDialog.value) return
read_only.value = true
useRouter().back() useRouter().back()
} }
const { add } = useSnackbar() const { add } = useSnackbar()
const { updateAsync, syncGroupItemsAsync } = usePasswordGroupStore() const { updateAsync, syncGroupItemsAsync, deleteGroupAsync } =
usePasswordGroupStore()
const onSaveAsync = async () => { const onSaveAsync = async () => {
try { try {
check.value = true
if (!group.value) return if (!group.value) return
if (errors.value.name.length || errors.value.description.length) return ignoreChanges.value = true
await updateAsync(group.value) await updateAsync(group.value)
syncGroupItemsAsync() await syncGroupItemsAsync(group.value.id)
add({ type: 'success', text: t('change.success') }) add({ type: 'success', text: t('change.success') })
onClose() onClose()
} catch (error) { } catch (error) {
@ -62,6 +131,34 @@ const onSaveAsync = async () => {
console.log(error) console.log(error)
} }
} }
const showConfirmDeleteDialog = ref(false)
const showUnsavedChangesDialog = ref(false)
const onConfirmIgnoreChanges = () => {
showUnsavedChangesDialog.value = false
onClose()
}
const onDeleteAsync = async () => {
try {
const parentId = group.value.parentId
await deleteGroupAsync(group.value.id, inTrashGroup.value)
await syncGroupItemsAsync(parentId)
showConfirmDeleteDialog.value = false
ignoreChanges.value = true
await navigateTo(
useLocalePath()({
name: 'passwordGroupItems',
params: {
...useRouter().currentRoute.value.params,
groupId: parentId,
},
}),
)
} catch (error) {
console.error(error)
}
}
</script> </script>
<i18n lang="yaml"> <i18n lang="yaml">

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="h-full relative"> <div class="h-full relative">
<div class="h-full"> <div class="h-full flex flex-col">
<HaexPassGroupBreadcrumbs <HaexPassGroupBreadcrumbs
:items="breadCrumbs" :items="breadCrumbs"
class="px-2 z-10 bg-base-200" class="px-2"
v-show="breadCrumbs.length" v-show="breadCrumbs.length"
/> />
<div class="h-full overflow-auto flex flex-col"> <div class="flex-1 overflow-auto">
<HaexPassMobileMenu <HaexPassMobileMenu
:menu-items="groupItems" :menu-items="groupItems"
ref="listRef" ref="listRef"
@ -76,40 +76,77 @@ definePageMeta({
name: 'passwordGroupItems', name: 'passwordGroupItems',
}) })
const { t } = useI18n()
const { add } = useSnackbar()
const selectedItems = ref<Set<IPasswordMenuItem>>(new Set()) const selectedItems = ref<Set<IPasswordMenuItem>>(new Set())
const { menu } = storeToRefs(usePasswordsActionMenuStore()) const { menu } = storeToRefs(usePasswordsActionMenuStore())
const { syncItemsAsync } = usePasswordItemStore()
const { syncGroupItemsAsync } = usePasswordGroupStore()
onMounted(async () => {
try {
await Promise.allSettled([syncItemsAsync(), syncGroupItemsAsync()])
} catch (error) {}
})
const { const {
breadCrumbs, breadCrumbs,
currentGroupId, currentGroupId,
currentGroupItems,
inTrashGroup, inTrashGroup,
selectedGroupItems, selectedGroupItems,
groups,
} = storeToRefs(usePasswordGroupStore()) } = storeToRefs(usePasswordGroupStore())
const { insertGroupItemsAsync } = usePasswordGroupStore()
const { items } = storeToRefs(usePasswordItemStore())
const { search } = storeToRefs(useSearchStore())
const groupItems = computed<IPasswordMenuItem[]>(() => { const groupItems = computed<IPasswordMenuItem[]>(() => {
const items: IPasswordMenuItem[] = [] const menuItems: IPasswordMenuItem[] = []
items.push( menuItems.push(
...currentGroupItems.value.groups.map<IPasswordMenuItem>((group) => ({ ...groups.value
color: group.color, .filter((group) => {
icon: group.icon, if (!search.value) return group.parentId == currentGroupId.value
id: group.id,
name: group.name, return (
type: 'group', group.name?.includes(search.value) ||
})), group.description?.includes(search.value)
)
})
.map<IPasswordMenuItem>((group) => ({
color: group.color,
icon: group.icon,
id: group.id,
name: group.name,
type: 'group',
})),
) )
items.push( menuItems.push(
...currentGroupItems.value.items.map<IPasswordMenuItem>((item) => ({ ...items.value
icon: item.icon, .filter((item) => {
id: item.id, if (!search.value)
name: item.title, return item.haex_passwords_group_items.groupId == currentGroupId.value
type: 'item',
})), return (
item.haex_passwords_item_details.title?.includes(search.value) ||
item.haex_passwords_item_details.note?.includes(search.value) ||
item.haex_passwords_item_details.password?.includes(search.value) ||
item.haex_passwords_item_details.tags?.includes(search.value) ||
item.haex_passwords_item_details.url?.includes(search.value) ||
item.haex_passwords_item_details.username?.includes(search.value)
)
})
.map<IPasswordMenuItem>((item) => ({
icon: item.haex_passwords_item_details.icon,
id: item.haex_passwords_item_details.id,
name: item.haex_passwords_item_details.title,
type: 'item',
})),
) )
return items
return menuItems
}) })
const { isVisible } = storeToRefs(useSidebarStore()) const { isVisible } = storeToRefs(useSidebarStore())
@ -150,8 +187,7 @@ onKeyStroke('x', (event) => {
} }
}) })
const { t } = useI18n() const { insertGroupItemsAsync } = usePasswordGroupStore()
const { add } = useSnackbar()
const onPasteAsync = async () => { const onPasteAsync = async () => {
if (!selectedGroupItems.value?.length) return if (!selectedGroupItems.value?.length) return
@ -190,7 +226,8 @@ onKeyStroke('a', (event) => {
}) })
const { deleteAsync } = usePasswordItemStore() const { deleteAsync } = usePasswordItemStore()
const { deleteGroupAsync, syncGroupItemsAsync } = usePasswordGroupStore() const { deleteGroupAsync } = usePasswordGroupStore()
const onDeleteAsync = async () => { const onDeleteAsync = async () => {
for (const item of selectedItems.value) { for (const item of selectedItems.value) {
if (item.type === 'group') { if (item.type === 'group') {

View File

@ -3,7 +3,7 @@
<HaexPassItem <HaexPassItem
:history="item.history" :history="item.history"
:read_only :read_only
@close="onClose" @close="onClose()"
@submit="onUpdateAsync" @submit="onUpdateAsync"
v-model:details="item.details" v-model:details="item.details"
v-model:key-values-add="item.keyValuesAdd" v-model:key-values-add="item.keyValuesAdd"
@ -11,7 +11,7 @@
v-model:key-values="item.keyValues" v-model:key-values="item.keyValues"
/> />
<div <!-- <div
class="fixed bottom-4 flex justify-between transition-all pointer-events-none right-0 sm:items-center items-end" 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']" :class="[isVisible ? 'left-15 ' : 'left-0']"
> >
@ -85,27 +85,35 @@
</UiButton> </UiButton>
</UiTooltip> </UiTooltip>
</div> </div>
</div> </div> -->
<UiDialogConfirm <HaexPassMenuBottom
:show-edit-button="read_only && !hasChanges"
:show-readonly-button="!read_only && !hasChanges"
:show-save-button="!read_only && hasChanges"
@close="onClose"
@delete="showConfirmDeleteDialog = true"
@edit="read_only = false"
@readonly="read_only = true"
@save="onUpdateAsync"
show-close-button
show-delete-button
>
</HaexPassMenuBottom>
<HaexPassDialogDeleteItem
v-model:open="showConfirmDeleteDialog" v-model:open="showConfirmDeleteDialog"
:confirm-label="t('dialog.delete.label')"
:title="t('dialog.delete.title')"
@abort="showConfirmDeleteDialog = false" @abort="showConfirmDeleteDialog = false"
@confirm="deleteItemAsync" @confirm="deleteItemAsync"
> >
{{ t('dialog.delete.question') }} </HaexPassDialogDeleteItem>
</UiDialogConfirm>
<UiDialogConfirm <HaexPassDialogUnsavedChanges
v-model:open="showUnsavedChangesDialog" :has-changes="hasChanges"
:confirm-label="t('dialog.unsavedChanges.label')"
:title="t('dialog.unsavedChanges.title')"
@abort="showUnsavedChangesDialog = false" @abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges" @confirm="onConfirmIgnoreChanges"
> v-model:open="showUnsavedChangesDialog"
{{ t('dialog.unsavedChanges.question') }} />
</UiDialogConfirm>
</div> </div>
</template> </template>
@ -126,7 +134,6 @@ defineProps({
withCopyButton: Boolean, withCopyButton: Boolean,
}) })
const { isVisible } = storeToRefs(useSidebarStore())
const read_only = ref(true) const read_only = ref(true)
const showConfirmDeleteDialog = ref(false) const showConfirmDeleteDialog = ref(false)
const { t } = useI18n() const { t } = useI18n()
@ -165,7 +172,6 @@ const { currentItem } = storeToRefs(usePasswordItemStore())
watch( watch(
currentItem, currentItem,
() => { () => {
console.log('watch currentItem', currentItem.value)
if (!currentItem.value) return if (!currentItem.value) return
item.details = JSON.parse(JSON.stringify(currentItem.value?.details)) item.details = JSON.parse(JSON.stringify(currentItem.value?.details))
item.keyValues = JSON.parse(JSON.stringify(currentItem.value?.keyValues)) item.keyValues = JSON.parse(JSON.stringify(currentItem.value?.keyValues))
@ -174,15 +180,14 @@ watch(
item.keyValuesDelete = [] item.keyValuesDelete = []
item.originalDetails = JSON.stringify(currentItem.value?.details) item.originalDetails = JSON.stringify(currentItem.value?.details)
item.originalKeyValues = JSON.stringify(currentItem.value?.keyValues) item.originalKeyValues = JSON.stringify(currentItem.value?.keyValues)
ignoreChanges.value = false
}, },
{ immediate: true }, { immediate: true },
) )
const { add } = useSnackbar() const { add } = useSnackbar()
const { deleteAsync, updateAsync } = usePasswordItemStore() const { deleteAsync, updateAsync } = usePasswordItemStore()
const { syncGroupItemsAsync, trashId } = usePasswordGroupStore() const { syncGroupItemsAsync } = usePasswordGroupStore()
const { currentGroupId } = storeToRefs(usePasswordGroupStore()) const { currentGroupId, inTrashGroup } = storeToRefs(usePasswordGroupStore())
const onUpdateAsync = async () => { const onUpdateAsync = async () => {
try { try {
@ -195,31 +200,29 @@ const onUpdateAsync = async () => {
}) })
if (newId) add({ type: 'success', text: t('success.update') }) if (newId) add({ type: 'success', text: t('success.update') })
syncGroupItemsAsync(currentGroupId.value) syncGroupItemsAsync(currentGroupId.value)
ignoreChanges.value = true onClose(true)
onClose()
} catch (error) { } catch (error) {
add({ type: 'error', text: t('error.update') }) add({ type: 'error', text: t('error.update') })
} }
} }
const onClose = () => { const onClose = (ignoreChanges?: boolean) => {
if (showConfirmDeleteDialog.value || showUnsavedChangesDialog.value) return if (showConfirmDeleteDialog.value || showUnsavedChangesDialog.value) return
if (hasChanges.value && !ignoreChanges.value) if (hasChanges.value && !ignoreChanges)
return (showUnsavedChangesDialog.value = true) return (showUnsavedChangesDialog.value = true)
ignoreChanges.value = false
read_only.value = true read_only.value = true
useRouter().back() useRouter().back()
} }
const deleteItemAsync = async () => { const deleteItemAsync = async () => {
try { try {
await deleteAsync(item.details.id, currentGroupId.value === trashId) await deleteAsync(item.details.id, inTrashGroup.value)
showConfirmDeleteDialog.value = false showConfirmDeleteDialog.value = false
add({ type: 'success', text: t('success.delete') }) add({ type: 'success', text: t('success.delete') })
syncGroupItemsAsync(currentGroupId.value) await syncGroupItemsAsync(currentGroupId.value)
onClose() onClose(true)
} catch (errro) { } catch (errro) {
add({ add({
type: 'error', type: 'error',
@ -239,22 +242,14 @@ const hasChanges = computed(
) )
const showUnsavedChangesDialog = ref(false) const showUnsavedChangesDialog = ref(false)
const ignoreChanges = ref(false)
const onConfirmIgnoreChanges = () => { const onConfirmIgnoreChanges = () => {
ignoreChanges.value = true
showUnsavedChangesDialog.value = false showUnsavedChangesDialog.value = false
onClose() onClose(true)
} }
</script> </script>
<i18n lang="yaml"> <i18n lang="yaml">
de: de:
save: Speichern
abort: Abbrechen
edit: Bearbeiten
noEdit: Lesemodus
delete: Löschen
success: success:
update: Eintrag erfolgreich aktualisiert update: Eintrag erfolgreich aktualisiert
delete: Eintrag wurde gelöscht delete: Eintrag wurde gelöscht
@ -266,21 +261,7 @@ de:
keyValue: Extra keyValue: Extra
history: Verlauf history: Verlauf
dialog:
delete:
title: Eintrag löschen
question: Soll der Eintrag wirklich gelöscht werden?
label: Löschen
unsavedChanges:
title: Nicht gespeicherte Änderungen
question: Sollen die Änderungen verworfen werden?
label: Verwerfen
en: en:
save: Save
abort: Abort
edit: Edit
noEdit: Read Mode
delete: Delete
success: success:
update: Entry successfully updated update: Entry successfully updated
delete: Entry successfully removed delete: Entry successfully removed
@ -291,14 +272,4 @@ en:
details: Details details: Details
keyValue: Extra keyValue: Extra
history: History history: History
dialog:
delete:
title: Delete Entry
question: Should the entry really be deleted?
label: Delete
unsavedChanges:
title: Unsaved changes
question: Should the changes be discarded?
label: discard
</i18n> </i18n>

View File

@ -9,7 +9,15 @@
v-model:key-values-add="item.keyValuesAdd" v-model:key-values-add="item.keyValuesAdd"
/> />
<div <HaexPassMenuBottom
@close="onClose"
@save="onCreateAsync"
show-close-button
show-save-button
>
</HaexPassMenuBottom>
<!-- <div
class="fixed bottom-4 flex justify-between transition-all pointer-events-none right-0 sm:items-center items-end" 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']" :class="[isVisible ? 'left-15 ' : 'left-0']"
> >
@ -35,7 +43,7 @@
</UiButton> </UiButton>
</UiTooltip> </UiTooltip>
<div class="flex items-center justify-center flex-1"></div> <div class="flex items-center justify-center flex-1"></div>
</div> </div> -->
</div> </div>
</template> </template>

View File

@ -6,6 +6,7 @@ import {
haexPasswordsItemKeyValues, haexPasswordsItemKeyValues,
type InsertHaexPasswordsItemDetails, type InsertHaexPasswordsItemDetails,
type InserthaexPasswordsItemKeyValues, type InserthaexPasswordsItemKeyValues,
type SelectHaexPasswordsGroupItems,
type SelectHaexPasswordsGroups, type SelectHaexPasswordsGroups,
type SelectHaexPasswordsItemDetails, type SelectHaexPasswordsItemDetails,
type SelectHaexPasswordsItemKeyValues, type SelectHaexPasswordsItemKeyValues,
@ -23,6 +24,25 @@ export const usePasswordItemStore = defineStore('passwordItemStore', () => {
const currentItem = computedAsync(() => readAsync(currentItemId.value)) const currentItem = computedAsync(() => readAsync(currentItemId.value))
const items = ref<
{
haex_passwords_item_details: SelectHaexPasswordsItemDetails
haex_passwords_group_items: SelectHaexPasswordsGroupItems
}[]
>([])
const syncItemsAsync = async () => {
const { currentVault } = useVaultStore()
items.value = await currentVault.drizzle
.select()
.from(haexPasswordsItemDetails)
.innerJoin(
haexPasswordsGroupItems,
eq(haexPasswordsItemDetails.id, haexPasswordsGroupItems.itemId),
)
}
return { return {
currentItemId, currentItemId,
currentItem, currentItem,
@ -31,9 +51,11 @@ export const usePasswordItemStore = defineStore('passwordItemStore', () => {
addKeyValuesAsync, addKeyValuesAsync,
deleteAsync, deleteAsync,
deleteKeyValueAsync, deleteKeyValueAsync,
items,
readByGroupIdAsync, readByGroupIdAsync,
readAsync, readAsync,
readKeyValuesAsync, readKeyValuesAsync,
syncItemsAsync,
updateAsync, updateAsync,
} }
}) })

View File

@ -0,0 +1,7 @@
export const useSearchStore = defineStore('searchStore', () => {
const search = ref()
return {
search,
}
})