feat(admin): dock ticket reply composer in conversation and move notes to a dialog

Put the reply composer at the bottom of the conversation card so the
timeline and composer form one viewport-height panel; the send button is
visible on load and Ctrl/Cmd+Enter submits. Internal notes now open in a
dialog instead of expanding inline, so the sidebar no longer grows.

Remove the shared ticket_composer helper and the inline note expansion CSS;
both dialogs share ticket_dialog_head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-31 15:10:13 +08:00
1 parent 54cc5301c3
commit b9000e1b93
8 files changed
+178 -73

No files matched your search

+12 -1
View File
@@ -44,7 +44,18 @@ test('workspace loads by ID independent of list filters and keeps the list conte
assert.match(closed, /aksrt-ticket-detail__closed/);
assert.match(closed, /data-aksrt-ticket-form="note"/);
assert.doesNotMatch(closed, /data-aksrt-ticket-form="(?:reply|status)"/);
for (const html of [open, closed]) assert.doesNotMatch(html, /<dialog|data-aksrt-dialog-close/);
// 回复框是会话卡片的底部,不是独立卡片;工作区唯一的弹窗是内部备注。
const conversation = open.match(/<section class="aksrt-ticket-conversation"[^]*?<\/section>/)?.[0] || '';
assert.match(conversation, /aksrt-ticket-conversation__scroll[^]*data-aksrt-ticket-form="reply"/);
assert.match(conversation, /data-aksrt-ticket-send-shortcut/);
assert.match(conversation, /<button type="submit" class="button button-primary">[^]*发送回复<\/button>/);
for (const html of [open, closed]) {
assert.equal((html.match(/<dialog /g) || []).length, 1);
assert.match(html, /<dialog id="aksrt-ticket-note-dialog"[^]*data-aksrt-ticket-form="note"[^]*<\/dialog>/);
assert.match(html, /data-aksrt-dialog-open="aksrt-ticket-note-dialog"/);
}
const sidebar = open.match(/<aside\b[^]*?<\/aside>/)?.[0] || '';
assert.doesNotMatch(sidebar, /<textarea|data-aksrt-ticket-form="note"/, 'note input lives only in the dialog');
});
test('result codes render as one native notice and unknown codes are ignored', () => {
+20 -6
View File
@@ -133,6 +133,14 @@ function makeRuntime({ href = 'https://site.test/wp-admin/users.php?page=aksrt_t
return node;
};
function makeDialog(id) {
const dialog = el('dialog', { id, class: 'aksrt-ticket-dialog' }, root);
dialog.open = false;
dialog.showModal = () => { dialog.open = true; };
dialog.close = () => { if (!dialog.open) return; dialog.open = false; dialog.fire('close'); };
return dialog;
}
const root = new Element('div', { class: 'aksrt-tickets-admin' });
const feedback = el('div', { 'data-aksrt-ticket-feedback': '', hidden: '' }, root);
el('p', {}, feedback);
@@ -157,15 +165,21 @@ function makeRuntime({ href = 'https://site.test/wp-admin/users.php?page=aksrt_t
nodes.warning = el('div', { 'data-aksrt-ticket-close-warning': '', hidden: '' }, status);
nodes.confirm = el('input', { type: 'checkbox', 'data-aksrt-ticket-close-confirm': '' }, nodes.warning);
nodes.statusSubmit = el('button', { type: 'submit' }, status);
const note = nodes.note = el('form', { action: `${REST}/41/notes`, 'data-aksrt-ticket-form': 'note' }, detail);
nodes.noteText = el('textarea', { id: 'aksrt-ticket-note-41', name: 'content', required: '', maxlength: '24', 'data-aksrt-ticket-editor': '' }, note);
nodes.noteOpener = el('button', { type: 'button', 'data-aksrt-dialog-open': 'aksrt-ticket-note-dialog' }, detail);
const noteDialog = nodes.noteDialog = makeDialog('aksrt-ticket-note-dialog');
const note = nodes.note = el('form', { action: `${REST}/41/notes`, 'data-aksrt-ticket-form': 'note' }, noteDialog);
nodes.noteClose = el('button', { type: 'button', 'data-aksrt-dialog-close': '' }, note);
nodes.noteFeedback = el('div', { 'data-aksrt-ticket-feedback': '', hidden: '' }, note);
el('p', {}, nodes.noteFeedback);
el('button', { type: 'button', 'data-aksrt-ticket-reload': '', hidden: '' }, nodes.noteFeedback);
nodes.noteText = el('textarea', { id: 'aksrt-ticket-note-41', name: 'content', required: '', autofocus: '', maxlength: '24', 'data-aksrt-ticket-editor': '' }, note);
el('button', { type: 'submit' }, note);
note.reset = () => { nodes.noteText.value = ''; };
nodes.replyText.attrs['data-aksrt-ticket-send-shortcut'] = '';
reply.requestSubmit = () => reply.fire('submit') && root.fire('submit', { target: reply });
} else {
nodes.opener = el('button', { type: 'button', 'data-aksrt-dialog-open': 'aksrt-ticket-create-dialog' }, root);
const dialog = nodes.dialog = el('dialog', { id: 'aksrt-ticket-create-dialog' }, root);
dialog.open = false;
dialog.showModal = () => { dialog.open = true; };
dialog.close = () => { if (!dialog.open) return; dialog.open = false; dialog.fire('close'); };
const dialog = nodes.dialog = makeDialog('aksrt-ticket-create-dialog');
const create = nodes.create = el('form', { action: REST, 'data-aksrt-ticket-form': 'create' }, dialog);
nodes.closeButton = el('button', { type: 'button', 'data-aksrt-dialog-close': '' }, create);
el('input', { type: 'number', name: 'user_id', required: '' }, create);