Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/manual/en/09-articles.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ Articles support richer content than a plain note. They can include structured t

Ticket messages can reference articles using the `$` mention syntax in the message editor. In the rendered message, the mention is displayed as the article title. Each reference is recorded on the ticket and shown in a dedicated Articles section, sorted by title.

## Keyboard shortcuts

Like all paginated list views in the application, the articles list supports keyboard shortcuts for quick navigation. You can use `Alt+1` through `Alt+0` to open items in the currently visible list.

On the article form page, the following shortcuts are available to jump directly to specific fields:
* `Alt+1`: Jump to Title
* `Alt+2`: Jump to Tags
* `Alt+3`: Focus the Editor

These shortcuts work universally. See the **Navigation** chapter for more details.

## Why it matters

The articles feature turns day-to-day support experience into reusable organizational knowledge. Over time, that helps billetsys serve not only as a ticket system, but also as a knowledge platform for support teams and customers.
15 changes: 14 additions & 1 deletion doc/manual/en/11-tickets.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,20 @@ For roles that use the ticket queues, these keyboard shortcuts are available fro
* `Ctrl+C` opens **Closed tickets**
* `Ctrl++` opens **Create ticket**

When a ticket queue view is open, `Ctrl+1` through `Ctrl+9` open the first through ninth ticket in the currently visible list order, and `Ctrl+0` opens the tenth ticket.
When a ticket queue view is open, `Alt+1` through `Alt+9` open the first through ninth ticket in the currently visible list order, and `Alt+0` opens the tenth ticket.

On the ticket detail page, the following shortcuts are available to jump directly to specific fields or sections (and will automatically open dropdown menus):

* `Alt+1`: Jump to Summary / Title
* `Alt+2`: Jump to Category
* `Alt+3`: Jump to Status
* `Alt+4`: Jump to External Issue link
* `Alt+5`: Jump to Affects Version
* `Alt+6`: Jump to Resolved Version
* `Alt+7`: Jump to the Messages Thread
* `Alt+8`: Focus the Reply Editor

These shortcuts work universally, even if you are viewing read-only versions of the fields. If you are actively typing inside a text box (like the Reply Editor) and wish to use a shortcut to jump elsewhere, simply use the `Alt+Number` shortcut directly—they will gracefully navigate you out of the current field.

## Ticket search

Expand Down
14 changes: 14 additions & 0 deletions doc/manual/en/15-companies.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ From an operational perspective, companies make it easier to answer questions su

That makes the company record one of the key administrative foundations of the system.

## Keyboard shortcuts

Like all paginated list views in the application, the companies list supports keyboard shortcuts for quick navigation. You can use `Alt+1` through `Alt+0` to open items in the currently visible list.

On the company form page, the following shortcuts are available to jump directly to specific fields (and open dropdowns automatically):
* `Alt+1`: Jump to Name
* `Alt+2`: Jump to Phone number
* `Alt+3`: Jump to Address 1
* `Alt+4`: Jump to City
* `Alt+5`: Jump to Country
* `Alt+6`: Jump to Time zone

These shortcuts work universally. See the **Navigation** chapter for more details.

## Role perspective

Companies are most important for the roles that coordinate or administer broader sets of work, such as admins, superusers, TAMs, and support staff.
Expand Down
13 changes: 13 additions & 0 deletions doc/manual/en/16-users.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ This is essential because the quality of the user structure affects almost every
* Reporting scope
* Company coordination

## Keyboard shortcuts

Like all paginated list views in the application, the users list supports keyboard shortcuts for quick navigation. You can use `Alt+1` through `Alt+0` to open items in the currently visible list.

On the user form page, the following shortcuts are available to jump directly to specific fields (and open dropdowns automatically):
* `Alt+1`: Jump to Username
* `Alt+2`: Jump to Name
* `Alt+3`: Jump to Email

Only `Alt+1` through `Alt+3` are currently assigned on the user form page.

These shortcuts work universally. See the **Navigation** chapter for more details.

## Why it matters

Without a strong user model, billetsys would only be a ticket container. With structured user accounts and role assignment, it becomes a coordinated support platform where responsibilities, visibility, and communication all follow the same design.
10 changes: 10 additions & 0 deletions doc/manual/en/17-categories.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ That context can help administrators keep the classification system understandab

Categories can include attachments. This makes it possible to keep supporting examples, templates, or reference files directly on the category definition.

## Keyboard shortcuts

Like all paginated list views in the application, the categories list supports keyboard shortcuts for quick navigation. You can use `Alt+1` through `Alt+0` to open items in the currently visible list.

On the category form page, the following shortcuts are available to jump directly to specific fields (and open dropdowns automatically):
* `Alt+1`: Jump to Name
* `Alt+2`: Jump to Default

These shortcuts work universally. See the **Navigation** chapter for more details.

## Role perspective

Categories are mostly maintained by administrative roles, but they affect many other parts of the application. Users, support staff, TAMs, and superusers all benefit from clearer ticket classification even if they are not the ones managing the category definitions directly.
10 changes: 10 additions & 0 deletions doc/manual/en/18-entitlements.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ Over time, entitlements help teams answer questions such as:

This makes entitlements one of the key concepts behind consistent service-aware ticket handling.

## Keyboard shortcuts

Like all paginated list views in the application, the entitlements list supports keyboard shortcuts for quick navigation. You can use `Alt+1` through `Alt+0` to open items in the currently visible list.

On the entitlement form page, the following shortcuts are available to jump directly to specific fields:
* `Alt+1`: Jump to Name
* `Alt+2`: Jump to Description

These shortcuts work universally. See the **Navigation** chapter for more details.

## Role perspective

Entitlements are mainly configured and maintained by administrative roles, but their effects are visible throughout the application. Support roles, superusers, TAMs, and ticket creators all benefit from having a clear service context behind the cases they work with.
14 changes: 14 additions & 0 deletions doc/manual/en/19-levels.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,17 @@ Over time, levels help teams answer questions such as:
* How service expectations differ across customers

This makes the levels feature an important part of making support handling more structured and transparent.

## Keyboard shortcuts

Like all paginated list views in the application, the levels list supports keyboard shortcuts for quick navigation. You can use `Alt+1` through `Alt+0` to open items in the currently visible list.

On the level form page, the following shortcuts are available to jump directly to specific fields (and open dropdowns automatically):
* `Alt+1`: Jump to Name
* `Alt+2`: Jump to Level
* `Alt+3`: Jump to Color
* `Alt+4`: Jump to Country
* `Alt+5`: Jump to From day
* `Alt+6`: Jump to To day

These shortcuts work universally. See the **Navigation** chapter for more details.
6 changes: 4 additions & 2 deletions doc/manual/en/24-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ For example, navigation helps users:

## Keyboard shortcuts and pagination

Keyboard shortcuts such as `Ctrl+1` through `Ctrl+0` always refer to items **on the current page**. When a list spans multiple pages, moving to a different page updates the shortcut targets to match the visible items.
Keyboard shortcuts such as `Alt+1` through `Alt+0` always refer to items **on the current page** or **the current view**. On paginated lists, moving to a different page updates the shortcut targets to match the visible items.

This means that `Ctrl+1` on page 2 opens the first ticket on page 2 — not the first ticket across the entire dataset. This behavior keeps shortcuts predictable regardless of how many items exist.
This means that `Alt+1` on page 2 opens the first item on page 2 — not the first item across the entire dataset. This behavior keeps shortcuts predictable regardless of how many items exist. On detail pages, these same shortcuts are context-aware and jump focus between major sections and form fields.

These shortcuts work universally across the application, even if you are currently typing inside a text box or have a dropdown menu open. Using a shortcut will gracefully navigate you out of your current input and directly into the new target field or open the targeted dropdown. If you wish to manually unfocus any active input or dropdown without navigating away, you can press `Escape`.

## Why it matters

Expand Down
18 changes: 15 additions & 3 deletions src/frontend/src/components/editor/LexicalEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import { CodeHighlightNode, CodeNode } from "@lexical/code";
import {
AutoFocusExtension,
ClearEditorExtension,
DecoratorTextExtension,
HorizontalRuleExtension,
Expand Down Expand Up @@ -185,6 +184,7 @@ interface LexicalEditorProps {
required?: boolean;
ticketSuggestApiBase?: string;
excludeTicketId?: number | string;
"data-shortcut-index"?: string | number;
}

const SUPERSCRIPT: TextMatchTransformer = {
Expand Down Expand Up @@ -337,6 +337,7 @@ export default function LexicalEditor({
required,
ticketSuggestApiBase,
excludeTicketId,
"data-shortcut-index": dataShortcutIndex,
}: LexicalEditorProps) {
const [floatingAnchorElem, setFloatingAnchorElem] =
useState<HTMLDivElement | null>(null);
Expand All @@ -353,7 +354,6 @@ export default function LexicalEditor({
defineExtension({
dependencies: [
RichTextExtension,
AutoFocusExtension,
SelectionAlwaysOnDisplayExtension,
HistoryExtension,
configExtension(LinkExtension, {
Expand Down Expand Up @@ -401,7 +401,19 @@ export default function LexicalEditor({
);

return (
<div className="editor-shell bg-background overflow-hidden rounded-lg border shadow w-full">
<div
className="editor-shell bg-background overflow-hidden rounded-lg border shadow w-full"
data-shortcut-index={dataShortcutIndex}
tabIndex={dataShortcutIndex ? -1 : undefined}
onFocus={(event) => {
if (event.target === event.currentTarget) {
const editable = event.currentTarget.querySelector<HTMLElement>(
'[contenteditable="true"]',
);
editable?.focus();
}
}}
>
Comment on lines +404 to +416
<LexicalExtensionComposer extension={AppExtension} contentEditable={null}>
<TooltipProvider>
<div className="relative">
Expand Down
15 changes: 1 addition & 14 deletions src/frontend/src/components/layout/AuthenticatedHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useDarkModeToggle } from "../../hooks/useDarkModeToggle";
import { installationCompanyName } from "../../utils/installationBranding";
import { formatInstallationClock, toQueryString } from "../../utils/formatting";
import { SmartLink, normalizeClientPath } from "../../utils/routing";
import { isEditableTarget } from "../../utils/keyboard";
import {
ticketCountsApiPath,
ticketLabelForRole,
Expand Down Expand Up @@ -64,20 +65,6 @@ interface AuthenticatedHeaderProps {
session: Session | null;
}

function isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) {
return false;
}
const tagName = target.tagName.toLowerCase();
return (
target.isContentEditable ||
tagName === "input" ||
tagName === "textarea" ||
tagName === "select" ||
Boolean(target.closest("[contenteditable='true']"))
);
}

export default function AuthenticatedHeader({
session,
}: AuthenticatedHeaderProps) {
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/components/ui/aevr/country-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ interface CountryDropdownProps {
disabled?: boolean;
placeholder?: string;
slim?: boolean;
"data-shortcut-index"?: string | number;
}

const CountryDropdownComponent = (
Expand Down
111 changes: 111 additions & 0 deletions src/frontend/src/hooks/useNumberShortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Eclipse Public License - v 2.0
*
* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
* PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
* OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
*/

import { useEffect } from "react";
import { useNavigate } from "react-router-dom";

interface UseNumberShortcutsProps<T> {
items?: T[];
getPath?: (item: T) => string | undefined | null;
enableFieldJumps?: boolean;
}

export default function useNumberShortcuts<T = unknown>({
items,
getPath,
enableFieldJumps,
}: UseNumberShortcutsProps<T> = {}) {
const navigate = useNavigate();

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
return;
}

// Allow Escape key to blur any active input
if (event.key === "Escape") {
if (document.activeElement instanceof HTMLElement) {
event.preventDefault();
document.activeElement.blur();
}
return;
}

if (!event.altKey || event.ctrlKey || event.metaKey) {
return;
}
Comment on lines +26 to +42

const keyString = event.code
? event.code.replace("Digit", "").replace("Numpad", "")
: event.key;
const shortcutIndex =
keyString === "0" ? 10 : Number.parseInt(keyString, 10);

if (
Number.isNaN(shortcutIndex) ||
shortcutIndex < 1 ||
shortcutIndex > 10
) {
return;
}

let handled = false;

// 1. Try list items
if (items && getPath) {
const item = items[shortcutIndex - 1];
if (item) {
const path = getPath(item);
if (path) {
event.preventDefault();
navigate(path);
handled = true;
}
}
}

// 2. Try field jumps if enabled and not already handled
if (!handled && enableFieldJumps) {
const selector = `[data-shortcut-index="${shortcutIndex}"]`;
const target = document.querySelector<HTMLElement>(selector);
if (target) {
event.preventDefault();
target.focus({ preventScroll: true });
target.scrollIntoView({ behavior: "smooth", block: "center" });

// If the target is a dropdown trigger, open it automatically
if (
target.getAttribute("role") === "combobox" ||
target.getAttribute("data-slot") === "select-trigger" ||
target.tagName.toLowerCase() === "button"
) {
setTimeout(() => {
target.dispatchEvent(
new KeyboardEvent("keydown", {
key: "ArrowDown",
code: "ArrowDown",
bubbles: true,
cancelable: true,
}),
);
target.click();
}, 10);
}

handled = true;
}
}
};

document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [items, getPath, enableFieldJumps, navigate]);
}
6 changes: 6 additions & 0 deletions src/frontend/src/pages/ArticleFormPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useNavigate, useParams } from "react-router-dom";
import { toast } from "sonner";
import useJson from "../hooks/useJson";
import useSubmissionGuard from "../hooks/useSubmissionGuard";
import useNumberShortcuts from "../hooks/useNumberShortcuts";
import DataState from "../components/common/DataState";
import PageHeader from "../components/layout/PageHeader";
import LexicalEditor from "../components/editor/LexicalEditor";
Expand Down Expand Up @@ -50,6 +51,8 @@ export default function ArticleFormPage({ mode }: ArticleFormPageProps) {
const submissionGuard = useSubmissionGuard();
const isEdit = mode === "edit";

useNumberShortcuts({ enableFieldJumps: true });

const updateFormState = (field: keyof ArticleFormState, value: string) => {
if (saveState.error) {
setSaveState((current) => ({ ...current, error: "" }));
Expand Down Expand Up @@ -139,6 +142,7 @@ export default function ArticleFormPage({ mode }: ArticleFormPageProps) {
}
className="bg-background shadow-sm"
required
data-shortcut-index="1"
/>
<p className="mt-1 text-xs text-muted-foreground">
Article titles must be unique.
Expand All @@ -154,6 +158,7 @@ export default function ArticleFormPage({ mode }: ArticleFormPageProps) {
updateFormState("tags", event.target.value)
}
className="bg-background shadow-sm"
data-shortcut-index="2"
/>
</Field>
<Field className="md:col-span-2 mt-2">
Expand All @@ -167,6 +172,7 @@ export default function ArticleFormPage({ mode }: ArticleFormPageProps) {
inputRef={bodyInputRef}
rows={16}
required
data-shortcut-index="3"
/>
</div>
</Field>
Expand Down
Loading
Loading