feat(quick-261022-k9k): admin save shortcuts and Create and new on record forms

- app/hotkeys.ts: dependency-free data-hotkey core (Ctrl/Cmd+S, Ctrl/Cmd+Enter,
  Ctrl/Cmd+Shift+S, Esc, / and ?), innermost-modal root, capture-phase modifier
  listener, platform labels and the help-dialog state
- Button hotkey prop (data-hotkey, aria-keyshortcuts); shortcuts installed by AppShell
- FormView: save keys and tooltips, Create and new on create forms via remountView
- phrasebook: form.create_and_new and the shortcuts group (en, pl)
This commit is contained in:
Jakub Zych
2026-10-22 21:00:57 +02:00
parent 1aff96f58a
commit 49accf675e
11 changed files with 465 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ import { api } from '../api/client'
import { parentFileRoutes } from '../api/files'
import type { AdminRecord, ErrorEnvelope, FormView, RecordEnvelope, RecordMeta, RelationOption } from '../api/types'
import { controllerIdFromParams, controllerPath } from '../app/controllerRoutes'
import { shortcutHint } from '../app/hotkeys'
import { currentLocale, message, t } from '../app/i18n'
import { loadControllerAssets } from '../app/pluginAssets'
import { SESSION_HEADER, newSessionKey } from '../app/sessionKey'
@@ -58,12 +59,15 @@ import Button from '../components/ui/Button.vue'
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
import { useConfirm } from '../components/ui/confirm'
import { clearRecordCrumb, setRecordCrumb } from '../state/useBreadcrumbs'
import { remountView } from '../state/useRemount'
import { showToast } from '../state/useToasts'
// Schema-driven create/update form (D-05, D-09, D-10, D-18; design screen
// 4). The view is keyed by route path, so one instance serves one record or
// one create. Tabs group fields by `tab`; a sticky footer holds delete,
// cancel, save-and-close and save; a dirty form confirms before leaving.
// cancel, save-and-close and save (a create form also has create-and-new);
// a dirty form confirms before leaving. The save buttons carry the keyboard
// shortcuts (Ctrl/Cmd+S, Ctrl/Cmd+Enter, Ctrl/Cmd+Shift+S; app/hotkeys.ts).
const route = useRoute()
const router = useRouter()
@@ -527,6 +531,18 @@ async function onSaveAndClose(): Promise<void> {
}
}
/**
* Utwórz i nowy (WinterCMS create_and_new): saves, then shows a blank create
* form of the same model in place, with a new session key; both redirects
* are ignored.
*/
async function onSaveAndNew(): Promise<void> {
const record = await save()
if (record) {
remountView()
}
}
async function onDelete(): Promise<void> {
if (recordId === null || busy.value || !schema.value) {
return
@@ -676,10 +692,35 @@ void load()
</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="outline" data-action="save-close" :disabled="busy || loading || !schema || activeUploads > 0" @click="onSaveAndClose">
<Button
v-if="mode === 'create'"
variant="outline"
data-action="save-new"
hotkey="mod+shift+s"
:title="shortcutHint(t('backend::lang.form.create_and_new'), 'mod+shift+s')"
:disabled="busy || loading || !schema || activeUploads > 0"
@click="onSaveAndNew"
>
{{ t('backend::lang.form.create_and_new') }}
</Button>
<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 || activeUploads > 0"
@click="onSaveAndClose"
>
{{ t('backend::lang.form.save_and_close') }}
</Button>
<Button variant="primary" data-action="save" :disabled="busy || loading || !schema || activeUploads > 0" @click="onSave">
<Button
variant="primary"
data-action="save"
hotkey="mod+s"
:title="shortcutHint(t('backend::lang.form.save'), 'mod+s')"
:disabled="busy || loading || !schema || activeUploads > 0"
@click="onSave"
>
{{ busy ? t('backend::lang.form.saving') : t('backend::lang.form.save') }}
</Button>
</div>