item handling

This commit is contained in:
2025-06-16 22:06:15 +02:00
parent 0b8f2c5532
commit 2972bb9e91
63 changed files with 3975 additions and 979 deletions

View File

@ -1,5 +1,5 @@
<template>
<div class="p-4">
<div class="p-2 h-full">
<NuxtPage />
</div>
</template>
@ -8,6 +8,4 @@
definePageMeta({
name: 'passwords',
})
const { currentGroupId } = storeToRefs(usePasswordGroupStore())
</script>

View File

@ -1,78 +1,32 @@
<template>
<div>
<VaultCard
:title="t('title')"
icon="mdi:folder-plus-outline"
<HaexPassGroup
v-model="group"
mode="create"
@close="onClose"
>
<div
class="flex flex-col gap-4 w-full p-4"
@keyup.enter="onCreate"
>
<UiInput
:check-input="check"
:label="t('name.label')"
:placeholder="t('name.label')"
autofocus
v-model:errors="errors.name"
v-model="vaultGroup.name"
/>
<UiInput
v-model="vaultGroup.description"
:check-input="check"
:label="t('description.label')"
:placeholder="t('description.label')"
/>
<UiSelectColor v-model="vaultGroup.color" />
{{ vaultGroup.icon }}
<UiSelectIcon v-model="vaultGroup.icon" />
<div class="flex flex-wrap justify-end gap-4">
<button
class="btn btn-error btn-outline flex-1 flex-nowrap"
@click="onClose"
type="button"
>
{{ t('abort') }}
<Icon name="mdi:close" />
</button>
<button
class="btn btn-primary flex-1 flex-nowrap"
type="button"
@click="onCreate"
>
{{ t('create') }}
<Icon name="mdi:check" />
</button>
</div>
</div>
</VaultCard>
@submit="createAsync"
/>
</div>
</template>
<script setup lang="ts">
import type { InsertHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
definePageMeta({
name: 'passwordGroupCreate',
})
const { t } = useI18n()
const check = ref(false)
const { currentGroupId } = storeToRefs(usePasswordGroupStore())
const vaultGroup = ref<InsertHaexPasswordsGroups>({
const group = ref<SelectHaexPasswordsGroups>({
name: '',
description: '',
id: '',
color: null,
icon: null,
order: null,
parentId: currentGroupId.value,
parentId: currentGroupId.value || null,
createdAt: null,
updateAt: null,
})
const errors = ref({
@ -84,23 +38,18 @@ const onClose = () => {
useRouter().back()
}
const onCreate = async () => {
const { addGroupAsync } = usePasswordGroupStore()
const createAsync = async () => {
try {
check.value = true
if (errors.value.name.length || errors.value.description.length) return
const { addGroupAsync } = usePasswordGroupStore()
const newGroup = await addGroupAsync(vaultGroup.value)
const newGroup = await addGroupAsync(group.value)
console.log('newGroup', newGroup)
if (!newGroup.id) {
return
}
//console.log('created group with id', newGroup?.id)
//currentGroupId.value = newGroup?.id
await navigateTo(
useLocalePath()({
name: 'passwordGroupItems',
@ -117,31 +66,3 @@ const onCreate = async () => {
}
}
</script>
<i18n lang="json">
{
"de": {
"title": "Neue Gruppe anlegen",
"abort": "Abbrechen",
"create": "Anlegen",
"name": {
"label": "Name"
},
"description": {
"label": "Beschreibung"
}
},
"en": {
"title": "Create new Group",
"abort": "Abort",
"create": "Create",
"name": {
"label": "Name"
},
"description": {
"label": "Description"
}
}
}
</i18n>

View File

@ -1,150 +1,85 @@
<template>
<div>
<VaultGroup
<HaexPassGroup
v-model="currentGroup"
mode="edit"
@close="onClose"
@submit="onSaveAsync"
@back="onBackAsync"
@reject="onRejectAsync"
@close="onCloseAsync"
v-model="vaultGroup"
v-model:read_only="read_only"
:originally
>
<!-- <template #bottom="{ onSubmit, onClose }">
<button
class="btn btn-error flex-1 flex-nowrap"
@click="onClose"
type="button"
>
{{ t('abort') }}
<Icon name="mdi:close" />
</button>
<button
class="btn btn-primary flex-1 flex-nowrap"
type="button"
@click="onSubmit"
>
{{ t('save') }}
<Icon name="mdi:check" />
</button>
</template> -->
</VaultGroup>
/>
</div>
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
definePageMeta({
name: 'passwordGroupEdit',
})
const { read_only } = storeToRefs(useVaultStore())
const { t } = useI18n()
const vaultGroup = ref<SelectHaexPasswordsGroups>({
color: '',
description: '',
icon: '',
id: '',
name: '',
order: null,
parentId: '',
createdAt: null,
updateAt: null,
const check = ref(false)
const { currentGroup } = storeToRefs(usePasswordGroupStore())
//const group = computed(() => currentGroup.value)
const errors = ref({
name: [],
description: [],
})
const originally = ref<SelectHaexPasswordsGroups>()
const onCloseAsync = async () => {
if (read_only.value) return navigateToGroupItemsAsync(vaultGroup.value.id)
else read_only.value = true
const onClose = () => {
useRouter().back()
}
/* {
await navigateTo(
useLocaleRoute()({
name: 'vaultGroupEntries',
params: {
...useRouter().currentRoute.value.params,
},
query: {
...useRouter().currentRoute.value.query,
},
})
);
}; */
const { currentGroupId } = storeToRefs(usePasswordGroupStore())
const { readGroupAsync, navigateToGroupItemsAsync } = usePasswordGroupStore()
const getGroupAsync = async () => {
if (!currentGroupId.value) return
const group = await readGroupAsync(currentGroupId.value)
console.log('found group', group)
if (group) {
vaultGroup.value = group
originally.value = { ...group }
}
}
watch(currentGroupId, async () => getGroupAsync(), { immediate: true })
const { add } = useSnackbar()
const onSaveAsync = async (to?: RouteLocationNormalizedLoadedGeneric) => {
const onSaveAsync = async () => {
try {
check.value = true
if (!currentGroup.value) return
console.log('onSave', errors.value)
if (errors.value.name.length || errors.value.description.length) return
const { updateAsync } = usePasswordGroupStore()
await updateAsync(vaultGroup.value)
await getGroupAsync()
read_only.value = true
if (to) {
return navigateTo(to)
}
await updateAsync(currentGroup.value)
add({ type: 'success', text: t('change.success') })
onClose()
} catch (error) {
add({
type: 'error',
text: JSON.stringify(error),
})
add({ type: 'error', text: t('change.error') })
console.log(error)
}
}
const onBackAsync = async () => {
if (originally.value) vaultGroup.value = { ...originally.value }
await navigateToGroupItemsAsync(vaultGroup.value.id)
}
const onRejectAsync = async (to?: RouteLocationNormalizedLoadedGeneric) => {
if (originally.value) vaultGroup.value = { ...originally.value }
if (to) return navigateTo(to)
else return onBackAsync
}
</script>
<i18n lang="json">
{
"de": {
"title": "Gruppe anpassen",
"abort": "Abbrechen",
"save": "Speichern",
"name": {
"label": "Name"
},
"description": {
"label": "Beschreibung"
}
},
<i18n lang="yaml">
de:
title: Gruppe ändern
abort: Abbrechen
save: Speichern
name:
label: Name
"en": {
"title": "Edit Group",
"abort": "Abort",
"save": "Save",
"name": {
"label": "Name"
},
"description": {
"label": "Description"
}
}
}
description:
label: Beschreibung
change:
success: Änderung erfolgreich gespeichert
error: Änderung konnte nicht gespeichert werden
en:
title: Edit Group
abort: Abort
save: Save
name:
label: Name
description:
label: Description
change:
success: Change successfully saved
error: Change could not be saved
</i18n>

View File

@ -1,40 +1,228 @@
<template>
<div class="relative h-full">
<div>
<HaexPassMobileMenu :group-items="currentGroupItems" />
<div class="h-full">
<div class="h-full overflow-auto p-1 flex flex-col">
<HaexPassGroupBreadcrumbs
:items="breadCrumbs"
v-show="breadCrumbs.length"
/>
<HaexPassMobileMenu
ref="listRef"
:menu-items="groupItems"
v-model:selected-items="selectedItems"
sel
/>
</div>
<div
class="fixed bottom-4 flex justify-center w-full transition-all pointer-events-none"
:class="[isVisible ? 'left-15' : 'left-0']"
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']"
>
<!-- <UiButton class="btn btn-primary btn-lg btn-square rotate-45">
<Icon name="mdi:plus" />
</UiButton> -->
<div class="w-full pl-8"></div>
<UiButtonAction
class="pointer-events-auto"
:menu
v-if="!inTrashGroup"
/>
<div
class="flex flex-col sm:flex-row gap-4 w-full justify-end items-end pr-8"
>
<UiButton
v-show="selectedItems.size === 1"
class="btn-square btn-accent"
@click="onEditAsync"
:tooltip="t('edit')"
>
<Icon name="mdi:pencil" />
</UiButton>
<UiButton
class="btn-square btn-accent"
v-show="selectedItems.size"
@click="onCut"
:tooltip="t('cut')"
>
<Icon name="mdi:scissors" />
</UiButton>
<UiButton
class="btn-square btn-accent"
v-show="selectedGroupItems?.length"
@click="onPasteAsync"
:tooltip="t('paste')"
>
<Icon name="proicons:clipboard-paste" />
</UiButton>
<UiButton
v-show="selectedItems.size"
class="btn-square btn-accent"
@click="onDeleteAsync"
:tooltip="t('delete')"
>
<Icon name="mdi:trash" />
</UiButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsItems } from '~~/src-tauri/database/schemas/vault'
import type { IPasswordMenuItem } from '~/components/haex/pass/mobile/menu/types'
import { useMagicKeys } from '@vueuse/core'
definePageMeta({
name: 'passwordGroupItems',
})
const selectedItems = ref<Set<IPasswordMenuItem>>(new Set())
const { menu } = storeToRefs(usePasswordsActionMenuStore())
const items = ref<SelectHaexPasswordsItems[]>([])
const { readGroupItemsAsync } = usePasswordGroupStore()
const { currentGroupItems } = storeToRefs(usePasswordGroupStore())
const {
currentGroupItems,
breadCrumbs,
selectedGroupItems,
currentGroupId,
inTrashGroup,
} = storeToRefs(usePasswordGroupStore())
const { insertGroupItemsAsync } = usePasswordGroupStore()
const groupItems = computed<IPasswordMenuItem[]>(() => {
const items: IPasswordMenuItem[] = []
items.push(
...currentGroupItems.value.groups.map<IPasswordMenuItem>((group) => ({
name: group.name,
id: group.id,
icon: group.icon,
type: 'group',
color: group.color,
})),
)
items.push(
...currentGroupItems.value.items.map<IPasswordMenuItem>((item) => ({
name: item.title,
id: item.id,
icon: item.icon,
type: 'item',
})),
)
return items
})
const { isVisible } = storeToRefs(useSidebarStore())
console.log('currentGroupItems', currentGroupItems.value)
const test = () => console.log('currentGroupItems', currentGroupItems.value)
const onEditAsync = async () => {
const item = selectedItems.value.values().next().value
console.log('onEditAsync', item)
if (item?.type === 'group')
await navigateTo(
useLocalePath()({
name: 'passwordGroupEdit',
params: { groupId: item.id },
}),
)
else if (item?.type === 'item') {
await navigateTo(
useLocalePath()({
name: 'passwordItemEdit',
params: { itemId: item.id },
}),
)
}
}
onKeyStroke('e', async (e) => {
if (e.ctrlKey) {
await onEditAsync()
}
})
onMounted(async () => {})
const onCut = () => {
selectedGroupItems.value = [...selectedItems.value]
selectedItems.value.clear()
}
onKeyStroke('x', (event) => {
if (event.ctrlKey && selectedItems.value.size) {
event.preventDefault()
onCut()
}
})
const { t } = useI18n()
const { add } = useSnackbar()
const onPasteAsync = async () => {
if (!selectedGroupItems.value?.length) return
try {
await insertGroupItemsAsync(
[...selectedGroupItems.value],
currentGroupId.value,
)
await syncGroupItemsAsync(currentGroupId.value)
selectedGroupItems.value = []
selectedItems.value.clear()
} catch (error) {
console.error(error)
selectedGroupItems.value = []
add({ type: 'error', text: t('error.paste') })
}
}
onKeyStroke('v', async (event) => {
if (event.ctrlKey) {
event.preventDefault()
await onPasteAsync()
}
})
const { escape } = useMagicKeys()
watch(escape, () => {
selectedItems.value.clear()
})
onKeyStroke('a', (event) => {
if (event.ctrlKey) {
event.preventDefault()
selectedItems.value = new Set(groupItems.value)
}
})
const { deleteAsync } = usePasswordItemStore()
const { deleteGroupAsync, syncGroupItemsAsync } = usePasswordGroupStore()
const onDeleteAsync = async () => {
for (const item of selectedItems.value) {
if (item.type === 'group') {
await deleteGroupAsync(item.id, inTrashGroup.value)
}
if (item.type === 'item') {
await deleteAsync(item.id, inTrashGroup.value)
}
}
selectedItems.value.clear()
await syncGroupItemsAsync(currentGroupId.value)
}
const keys = useMagicKeys()
watch(keys.delete, async () => {
await onDeleteAsync()
})
const listRef = useTemplateRef<HTMLElement>('listRef')
onClickOutside(listRef, () => setTimeout(() => selectedItems.value.clear(), 50))
</script>
<i18n lang="yaml">
de:
cut: Ausschneiden
paste: Einfügen
delete: Löschen
edit: Bearbeiten
en:
cut: Cut
paste: Paste
delete: Delete
edit: Edit
</i18n>

View File

@ -1,3 +1,297 @@
<template>
<div>item</div>
<div>
<HaexPassItem
:history="item.history"
:read_only
@close="onClose"
v-model:details="item.details"
v-model:key-values-add="item.keyValuesAdd"
v-model:key-values-delete="item.keyValuesDelete"
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 w-full">
<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 w-full">
<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>
<UiDialogConfirm
v-model:open="showConfirmDeleteDialog"
:confirm-label="t('dialog.delete.label')"
:title="t('dialog.delete.title')"
@abort="showConfirmDeleteDialog = false"
@confirm="deleteItemAsync"
>
{{ t('dialog.delete.question') }}
</UiDialogConfirm>
<UiDialogConfirm
v-model:open="showUnsavedChangesDialog"
:confirm-label="t('dialog.unsavedChanges.label')"
:title="t('dialog.unsavedChanges.title')"
@abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges"
>
{{ t('dialog.unsavedChanges.question') }}
</UiDialogConfirm>
</div>
</template>
<script setup lang="ts">
import type {
SelectHaexPasswordsItemDetails,
SelectHaexPasswordsItemHistory,
SelectHaexPasswordsItemKeyValues,
} from '~~/src-tauri/database/schemas/vault'
definePageMeta({
name: 'passwordItemEdit',
})
defineProps({
icon: String,
title: String,
withCopyButton: Boolean,
})
const { isVisible } = storeToRefs(useSidebarStore())
const read_only = ref(true)
const showConfirmDeleteDialog = ref(false)
const { t } = useI18n()
const item = reactive<{
details: SelectHaexPasswordsItemDetails
history: SelectHaexPasswordsItemHistory[]
keyValues: SelectHaexPasswordsItemKeyValues[]
keyValuesAdd: SelectHaexPasswordsItemKeyValues[]
keyValuesDelete: SelectHaexPasswordsItemKeyValues[]
originalDetails: string | null
originalKeyValues: string | null
}>({
details: {
id: '',
createdAt: null,
icon: null,
note: null,
password: null,
tags: null,
title: null,
updateAt: null,
url: null,
username: null,
},
keyValues: [],
history: [],
keyValuesAdd: [],
keyValuesDelete: [],
originalDetails: null,
originalKeyValues: null,
})
const { currentItem } = storeToRefs(usePasswordItemStore())
watch(currentItem, () => {
item.details = JSON.parse(JSON.stringify(currentItem.value?.details))
item.keyValues = JSON.parse(JSON.stringify(currentItem.value?.keyValues))
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)
ignoreChanges.value = false
})
const { add } = useSnackbar()
const { deleteAsync, updateAsync } = usePasswordItemStore()
const { syncGroupItemsAsync, trashId } = usePasswordGroupStore()
const { currentGroupId } = storeToRefs(usePasswordGroupStore())
const onUpdateAsync = async () => {
try {
const newId = await updateAsync({
details: item.details,
groupId: currentGroupId.value || null,
keyValues: item.keyValues,
keyValuesAdd: item.keyValuesAdd,
keyValuesDelete: item.keyValuesDelete,
})
if (newId) add({ type: 'success', text: t('success.update') })
syncGroupItemsAsync(currentGroupId.value)
ignoreChanges.value = true
onClose()
} catch (error) {
add({ type: 'error', text: t('error.update') })
}
}
const onClose = () => {
if (showConfirmDeleteDialog.value || showUnsavedChangesDialog.value) return
if (hasChanges.value && !ignoreChanges.value)
return (showUnsavedChangesDialog.value = true)
ignoreChanges.value = false
read_only.value = true
useRouter().back()
}
const deleteItemAsync = async () => {
try {
await deleteAsync(item.details.id, currentGroupId.value === trashId)
showConfirmDeleteDialog.value = false
add({ type: 'success', text: t('success.delete') })
syncGroupItemsAsync(currentGroupId.value)
onClose()
} catch (errro) {
add({
type: 'error',
text: t('error.delete'),
})
}
}
const hasChanges = computed(
() =>
!!(
item.originalDetails !== JSON.stringify(item.details) ||
item.originalKeyValues !== JSON.stringify(item.keyValues) ||
item.keyValuesAdd.length ||
item.keyValuesDelete.length
),
)
const showUnsavedChangesDialog = ref(false)
const ignoreChanges = ref(false)
const onConfirmIgnoreChanges = () => {
ignoreChanges.value = true
showUnsavedChangesDialog.value = false
onClose()
}
</script>
<i18n lang="yaml">
de:
save: Speichern
abort: Abbrechen
edit: Bearbeiten
noEdit: Lesemodus
delete: Löschen
success:
update: Eintrag erfolgreich aktualisiert
delete: Eintrag wurde gelöscht
error:
update: Eintrag konnte nicht aktualisiert werden
delete: Eintrag konnte nicht gelöscht werden
tab:
details: Details
keyValue: Extra
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:
save: Save
abort: Abort
edit: Edit
noEdit: Read Mode
delete: Delete
success:
update: Entry successfully updated
delete: Entry successfully removed
error:
update: Entry could not be updated
delete: Entry could not be deleted
tab:
details: Details
keyValue: Extra
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>

View File

@ -1 +1,133 @@
<template><div>create item</div></template>
<template>
<div>
<HaexPassItem
:default-icon="currentGroup?.icon"
:history="item.history"
@close="onClose"
v-model:details="item.details"
v-model:key-values-add="item.keyValuesAdd"
/>
<div
class="fixed bottom-4 flex justify-between transition-all pointer-events-none right-15 sm:items-center items-end"
:class="[isVisible ? 'left-15 ' : 'left-0']"
>
<div class="flex items-center justify-center w-full">
<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 w-full"></div>
</div>
</div>
</template>
<script setup lang="ts">
import type {
SelectHaexPasswordsItemDetails,
SelectHaexPasswordsItemHistory,
SelectHaexPasswordsItemKeyValues,
} from '~~/src-tauri/database/schemas/vault'
definePageMeta({
name: 'passwordItemCreate',
})
defineProps({
icon: String,
title: String,
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
}>({
details: {
id: '',
createdAt: null,
icon: null,
note: null,
password: null,
tags: null,
title: null,
updateAt: null,
url: null,
username: null,
},
history: [],
keyValuesAdd: [],
keyValuesDelete: [],
originalDetails: null,
originalKeyValues: null,
})
const { add } = useSnackbar()
const { currentGroup } = storeToRefs(usePasswordGroupStore())
const { syncGroupItemsAsync } = usePasswordGroupStore()
const { addAsync } = usePasswordItemStore()
const onCreateAsync = async () => {
try {
const newId = await addAsync(
item.details,
item.keyValuesAdd,
currentGroup.value,
)
if (newId) {
add({ type: 'success', text: t('success') })
syncGroupItemsAsync(currentGroup.value?.id)
onClose()
}
} catch (error) {
add({ type: 'error', text: t('error') })
}
}
const onClose = () => useRouter().back()
</script>
<i18n lang="yaml">
de:
create: Anlegen
abort: Abbrechen
success: Eintrag erfolgreich erstellt
error: Eintrag konnte nicht erstellt werden
tab:
details: Details
keyValue: Extra
history: Verlauf
en:
create: Create
abort: Abort
success: Entry successfully created
error: Entry could not be created
tab:
details: Details
keyValue: Extra
history: History
</i18n>