feat(quick-261022-k9k): Esc, slash and help shortcuts, settings, account and modal save keys
- '?' help dialog (ShortcutHelpDialog) listing the current screen's shortcuts - Esc = Cancel on record forms; '/' focuses the list search - settings forms: Save and close (Ctrl/Cmd+Enter, then /settings) and Ctrl/Cmd+S - My account: Ctrl/Cmd+S - modal forms: Ctrl/Cmd+S and Ctrl/Cmd+Enter submit the innermost dialog only - docs: Keyboard shortcuts section in the admin SPA page
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed, nextTick, ref } from 'vue'
|
||||
import { CircleAlert, ImageUp, Trash2 } from '@lucide/vue'
|
||||
import { api } from '../api/client'
|
||||
import type { AdminAccountRequest } from '../api/types'
|
||||
import { shortcutHint } from '../app/hotkeys'
|
||||
import { t } from '../app/i18n'
|
||||
import FormErrorBanner from '../components/form/FormErrorBanner.vue'
|
||||
import { controlClass } from '../components/form/control'
|
||||
@@ -22,7 +23,7 @@ import { showToast } from '../state/useToasts'
|
||||
// typed. The avatar is uploaded (POST /account/avatar, one file_data part)
|
||||
// and removed (DELETE /account/avatar, after a confirmation) on its own.
|
||||
// Every success passes the returned profile to setCurrentUser, so the top
|
||||
// bar updates without a reload.
|
||||
// bar updates without a reload. Save answers Ctrl/Cmd+S (app/hotkeys.ts).
|
||||
const ID_PREFIX = 'account'
|
||||
|
||||
type ProfileKey = 'first_name' | 'last_name' | 'email' | 'subtitle'
|
||||
@@ -427,7 +428,14 @@ async function onSave(): Promise<void> {
|
||||
<footer
|
||||
class="fixed right-0 bottom-0 left-[var(--shell-offset,0px)] z-30 flex items-center justify-end gap-2.5 border-t border-border bg-surface px-8 py-3.5"
|
||||
>
|
||||
<Button variant="primary" data-action="save" :disabled="busy" @click="onSave">
|
||||
<Button
|
||||
variant="primary"
|
||||
data-action="save"
|
||||
hotkey="mod+s"
|
||||
:title="shortcutHint(t('backend::lang.form.save'), 'mod+s')"
|
||||
:disabled="busy"
|
||||
@click="onSave"
|
||||
>
|
||||
{{ busy ? t('backend::lang.form.saving') : t('backend::lang.form.save') }}
|
||||
</Button>
|
||||
</footer>
|
||||
|
||||
@@ -691,7 +691,7 @@ void load()
|
||||
{{ t('backend::lang.form.delete') }}
|
||||
</Button>
|
||||
<div class="ml-auto flex items-center gap-2.5">
|
||||
<Button variant="ghost" data-action="cancel" @click="onLeave">{{ t('backend::lang.form.cancel') }}</Button>
|
||||
<Button variant="ghost" data-action="cancel" hotkey="escape" @click="onLeave">{{ t('backend::lang.form.cancel') }}</Button>
|
||||
<Button
|
||||
v-if="mode === 'create'"
|
||||
variant="outline"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, provide, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
import { api } from '../api/client'
|
||||
import type { AdminRecord, FormField, FormView } from '../api/types'
|
||||
import { shortcutHint } from '../app/hotkeys'
|
||||
import { t } from '../app/i18n'
|
||||
import FormErrorBanner from '../components/form/FormErrorBanner.vue'
|
||||
import FormGrid from '../components/form/FormGrid.vue'
|
||||
@@ -34,7 +35,9 @@ import { showToast } from '../state/useToasts'
|
||||
// empty state from meta.secrets (F2) and are sent only when the
|
||||
// administrator typed a value (a string) or removed the stored one (null);
|
||||
// richeditor save notices from meta.notices render under their field (F3).
|
||||
// Save and Save and close answer Ctrl/Cmd+S and Ctrl/Cmd+Enter (app/hotkeys.ts).
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const code = String(route.params.code ?? '')
|
||||
const ID_PREFIX = 'setting'
|
||||
|
||||
@@ -132,9 +135,10 @@ function settingsPayload(editable: FormField[], current: AdminRecord): AdminReco
|
||||
return out
|
||||
}
|
||||
|
||||
async function onSave(): Promise<void> {
|
||||
/** Saves the values; true when the PUT succeeded. */
|
||||
async function save(): Promise<boolean> {
|
||||
if (busy.value || !schema.value) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
busy.value = true
|
||||
try {
|
||||
@@ -148,7 +152,7 @@ async function onSave(): Promise<void> {
|
||||
secrets.value = { ...(result.data.data.meta?.secrets ?? {}) }
|
||||
notices.value = noticeLines(result.data.data.meta?.notices)
|
||||
showToast(t('backend::lang.settings.saved'))
|
||||
return
|
||||
return true
|
||||
}
|
||||
if (result.response.status === 422) {
|
||||
errors.value = fieldErrors(result.error?.error.details)
|
||||
@@ -161,16 +165,29 @@ async function onSave(): Promise<void> {
|
||||
if (first) {
|
||||
focusField(ID_PREFIX, first.name)
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
showToast(result.error?.error.message || t('backend::lang.form.error_generic'), 'danger')
|
||||
return false
|
||||
} catch {
|
||||
showToast(t('backend::lang.form.error_generic'), 'danger')
|
||||
return false
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave(): Promise<void> {
|
||||
await save()
|
||||
}
|
||||
|
||||
/** Zapisz i zamknij: saves, then returns to the settings index (the Cancel target). */
|
||||
async function onSaveAndClose(): Promise<void> {
|
||||
if (await save()) {
|
||||
await router.push('/settings')
|
||||
}
|
||||
}
|
||||
|
||||
void load()
|
||||
</script>
|
||||
|
||||
@@ -220,8 +237,27 @@ void load()
|
||||
</template>
|
||||
|
||||
<footer class="fixed right-0 bottom-0 left-[var(--shell-offset,0px)] z-30 flex items-center justify-end gap-2.5 border-t border-border bg-surface px-8 py-3.5">
|
||||
<!-- No Esc on Cancel: a settings form has no unsaved-changes question,
|
||||
so Esc would discard edits silently. -->
|
||||
<Button variant="ghost" to="/settings">{{ t('backend::lang.form.cancel') }}</Button>
|
||||
<Button variant="primary" data-action="save" :disabled="busy || loading || !schema" @click="onSave">
|
||||
<Button
|
||||
variant="outline"
|
||||
data-action="save-close"
|
||||
hotkey="mod+enter"
|
||||
:title="shortcutHint(t('backend::lang.form.save_and_close'), 'mod+enter')"
|
||||
:disabled="busy || loading || !schema"
|
||||
@click="onSaveAndClose"
|
||||
>
|
||||
{{ t('backend::lang.form.save_and_close') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
data-action="save"
|
||||
hotkey="mod+s"
|
||||
:title="shortcutHint(t('backend::lang.form.save'), 'mod+s')"
|
||||
:disabled="busy || loading || !schema"
|
||||
@click="onSave"
|
||||
>
|
||||
{{ busy ? t('backend::lang.form.saving') : t('backend::lang.form.save') }}
|
||||
</Button>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user