Skip to content
Open
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
23 changes: 19 additions & 4 deletions apps/web/src/components/work-item/editorSlashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
import { Extension } from '@tiptap/core';
import Suggestion from '@tiptap/suggestion';
import type { Editor, Range } from '@tiptap/core';
import i18n from '../../i18n';

interface SlashCommand {
/** i18n key stem, shared with the page editor's slash menu (editor.slash.<key>.*). */
key: string;
title: string;
description: string;
icon: string;
Expand All @@ -12,55 +15,63 @@ interface SlashCommand {

const COMMANDS: SlashCommand[] = [
{
key: 'heading1',
title: 'Heading 1',
description: 'Big section heading',
icon: 'H1',
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).toggleHeading({ level: 1 }).run(),
},
{
key: 'heading2',
title: 'Heading 2',
description: 'Medium section heading',
icon: 'H2',
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).toggleHeading({ level: 2 }).run(),
},
{
key: 'heading3',
title: 'Heading 3',
description: 'Small section heading',
icon: 'H3',
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).toggleHeading({ level: 3 }).run(),
},
{
key: 'bulletList',
title: 'Bullet list',
description: 'A simple bullet list',
icon: '••',
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).toggleBulletList().run(),
},
{
key: 'numberedList',
title: 'Numbered list',
description: 'An ordered list',
icon: '1.',
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).toggleOrderedList().run(),
},
{
key: 'quote',
title: 'Quote',
description: 'Block quote',
icon: '❝',
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).toggleBlockquote().run(),
},
{
key: 'codeBlock',
title: 'Code block',
description: 'Fenced code',
icon: '</>',
command: ({ editor, range }) =>
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
key: 'divider',
title: 'Divider',
description: 'Horizontal rule',
icon: '—',
Expand All @@ -69,6 +80,10 @@ const COMMANDS: SlashCommand[] = [
},
];

/** Localised label/description for a slash command, shared with the page editor. */
const cmdTitle = (c: SlashCommand) => i18n.t(`editor.slash.${c.key}.title`, c.title);
const cmdDesc = (c: SlashCommand) => i18n.t(`editor.slash.${c.key}.subtitle`, c.description);

/**
* TipTap extension that opens a slash-command palette when the user types `/`.
* Like the @-mention popup it uses a vanilla DOM list — keyboard nav (↑↓ + Enter)
Expand Down Expand Up @@ -96,7 +111,7 @@ export function createSlashCommands() {
const q = query.toLowerCase().trim();
if (!q) return COMMANDS;
return COMMANDS.filter(
(c) => c.title.toLowerCase().includes(q) || c.description.toLowerCase().includes(q),
(c) => cmdTitle(c).toLowerCase().includes(q) || cmdDesc(c).toLowerCase().includes(q),
);
},
render: () => {
Expand All @@ -111,7 +126,7 @@ export function createSlashCommands() {
popup.innerHTML = '';
if (currentItems.length === 0) {
const empty = document.createElement('div');
empty.textContent = 'No matches';
empty.textContent = i18n.t('editor.slash.noMatch', 'No matches');
empty.className = 'px-3 py-2 text-xs text-(--txt-tertiary)';
popup.appendChild(empty);
return;
Expand All @@ -133,10 +148,10 @@ export function createSlashCommands() {
body.className = 'min-w-0 flex-1';
const t = document.createElement('span');
t.className = 'block truncate';
t.textContent = cmd.title;
t.textContent = cmdTitle(cmd);
const d = document.createElement('span');
d.className = 'block truncate text-[11px] text-(--txt-tertiary)';
d.textContent = cmd.description;
d.textContent = cmdDesc(cmd);
body.appendChild(t);
body.appendChild(d);
btn.appendChild(body);
Expand Down
150 changes: 141 additions & 9 deletions apps/web/src/i18n/dynamicKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,34 @@ export function registerDynamicKeys(t: TFunction) {
t('settings.webhooks.event.issue_comment.label', 'Issue comments');
t('settings.webhooks.event.issue_comment.hint', 'New comments on issues');

// instance-admin sections (InstanceAdminLayout SECTIONS)
t('instanceAdmin.section.general', 'General');
t('instanceAdmin.section.admins', 'Admins');
t('instanceAdmin.section.workspace', 'Workspaces');
t('instanceAdmin.section.email', 'Email');
t('instanceAdmin.section.authentication', 'Authentication');
t('instanceAdmin.section.ai', 'Artificial intelligence');
t('instanceAdmin.section.image', 'Images in Devlane');
t('instanceAdmin.section.integrations', 'Integrations');
// instance-admin sections (InstanceAdminLayout SECTIONS) — label + description
t('instanceAdmin.section.general.label', 'General');
t('instanceAdmin.section.general.desc', 'Identify your instances and get key details.');
t('instanceAdmin.section.admins.label', 'Admins');
t('instanceAdmin.section.admins.desc', 'Manage instance administrators.');
t('instanceAdmin.section.workspace.label', 'Workspaces');
t('instanceAdmin.section.workspace.desc', 'Manage all workspaces on this instance.');
t('instanceAdmin.section.email.label', 'Email');
t('instanceAdmin.section.email.desc', 'Configure your SMTP controls.');
t('instanceAdmin.section.authentication.label', 'Authentication');
t('instanceAdmin.section.authentication.desc', 'Configure authentication modes.');
t('instanceAdmin.section.ai.label', 'Artificial intelligence');
t('instanceAdmin.section.ai.desc', 'Configure your OpenAI creds.');
t('instanceAdmin.section.image.label', 'Images in Devlane');
t('instanceAdmin.section.image.desc', 'Allow third-party image libraries.');
t('instanceAdmin.section.integrations.label', 'Integrations');
t('instanceAdmin.section.integrations.desc', 'Configure GitHub and other integrations.');

// instance-admin breadcrumb (InstanceAdminLayout BREADCRUMB_LABEL)
t('instanceAdmin.breadcrumb.settings', 'Settings');
t('instanceAdmin.breadcrumb.general', 'General');
t('instanceAdmin.breadcrumb.admins', 'Admins');
t('instanceAdmin.breadcrumb.workspace', 'Workspace');
t('instanceAdmin.breadcrumb.email', 'Email');
t('instanceAdmin.breadcrumb.authentication', 'Authentication');
t('instanceAdmin.breadcrumb.ai', 'Artificial Intelligence');
t('instanceAdmin.breadcrumb.image', 'Image');
t('instanceAdmin.breadcrumb.integrations', 'Integrations');

// project section switcher (ProjectSectionDropdown / ProjectSectionHeader)
t('header.section.issues', 'Work items');
Expand All @@ -118,6 +137,119 @@ export function registerDynamicKeys(t: TFunction) {
t('header.section.pages', 'Pages');
t('header.section.views', 'Views');

// project sidebar nav (Sidebar)
t('nav.project.issues', 'Work items');
t('nav.project.epics', 'Epics');
t('nav.project.cycles', 'Cycles');
t('nav.project.modules', 'Modules');
t('nav.project.views', 'Views');
t('nav.project.pages', 'Pages');
t('nav.project.intake', 'Intake');

// home widgets (WorkspaceHomePage)
t('home.widget.quicklinks', 'Quicklinks');
t('home.widget.recents', 'Recents');
t('home.widget.stickies', 'Your stickies');

// notifications tabs (NotificationsPage)
t('notifications.tab.all', 'All');
t('notifications.tab.mentions', 'Mentions');
t('notifications.tab.archived', 'Archived');

// pages tabs + sort (PagesPage)
t('pages.tab.public', 'Public');
t('pages.tab.private', 'Private');
t('pages.tab.archived', 'Archived');
t('pages.sort.updated_at', 'Date modified');
t('pages.sort.created_at', 'Date created');
t('pages.sort.name', 'Name');

// saved-view date presets (ProjectSavedViewActiveFilters)
t('views.filter.datePreset.1_week', 'Last 1 week');
t('views.filter.datePreset.2_weeks', 'Last 2 weeks');
t('views.filter.datePreset.1_month', 'Last 1 month');
t('views.filter.datePreset.2_months', 'Last 2 months');
t('views.filter.datePreset.custom', 'Custom range');

// estimate type options (ProjectEstimatesSettings)
t('settings.estimates.typeOption.points', 'Points');
t('settings.estimates.typeOption.categories', 'Categories');

// notification preference rows (NotificationPreferencesPanel) — label + desc
t('settings.notifications.row.property.label', 'Property changes');
t(
'settings.notifications.row.property.desc',
"Notify me when work items' properties like assignees, priority, or estimates change.",
);
t('settings.notifications.row.state.label', 'State change');
t(
'settings.notifications.row.state.desc',
'Notify me when a work item moves to a different state.',
);
t('settings.notifications.row.completed.label', 'Work item completed');
t('settings.notifications.row.completed.desc', 'Notify me when a work item is completed.');
t('settings.notifications.row.comments.label', 'Comments');
t('settings.notifications.row.comments.desc', 'Notify me when someone comments on a work item.');
t('settings.notifications.row.mentions.label', 'Mentions');
t(
'settings.notifications.row.mentions.desc',
'Notify me when someone mentions me in a comment or description.',
);

// page-editor colour names (ColorDropdown)
t('editor.colorName.gray', 'Gray');
t('editor.colorName.brown', 'Brown');
t('editor.colorName.orange', 'Orange');
t('editor.colorName.yellow', 'Yellow');
t('editor.colorName.green', 'Green');
t('editor.colorName.blue', 'Blue');
t('editor.colorName.purple', 'Purple');
t('editor.colorName.red', 'Red');

// page-editor slash menu (slashCommands ITEMS) — title + subtitle
t('editor.slash.text.title', 'Text');
t('editor.slash.text.subtitle', 'Plain paragraph');
t('editor.slash.heading1.title', 'Heading 1');
t('editor.slash.heading1.subtitle', 'Large section heading');
t('editor.slash.heading2.title', 'Heading 2');
t('editor.slash.heading2.subtitle', 'Medium section heading');
t('editor.slash.heading3.title', 'Heading 3');
t('editor.slash.heading3.subtitle', 'Small section heading');
t('editor.slash.bulletList.title', 'Bulleted list');
t('editor.slash.bulletList.subtitle', 'Unordered list');
t('editor.slash.numberedList.title', 'Numbered list');
t('editor.slash.numberedList.subtitle', 'Ordered list');
t('editor.slash.todoList.title', 'To-do list');
t('editor.slash.todoList.subtitle', 'Checklist');
t('editor.slash.quote.title', 'Quote');
t('editor.slash.quote.subtitle', 'Block quote');
t('editor.slash.codeBlock.title', 'Code block');
t('editor.slash.codeBlock.subtitle', 'Formatted code');
t('editor.slash.table.title', 'Table');
t('editor.slash.table.subtitle', '3x3 table');
t('editor.slash.image.title', 'Image');
t('editor.slash.image.subtitle', 'Embed by URL');
t('editor.slash.divider.title', 'Divider');
t('editor.slash.divider.subtitle', 'Horizontal rule');

// page-editor toolbar (PageEditorToolbar item.labelKey)
t('editor.toolbar.bold', 'Bold');
t('editor.toolbar.italic', 'Italic');
t('editor.toolbar.underline', 'Underline');
t('editor.toolbar.strikethrough', 'Strikethrough');
t('editor.toolbar.alignLeft', 'Left align');
t('editor.toolbar.alignCenter', 'Center align');
t('editor.toolbar.alignRight', 'Right align');
t('editor.toolbar.numberedList', 'Numbered list');
t('editor.toolbar.bulletedList', 'Bulleted list');
t('editor.toolbar.todoList', 'To-do list');
t('editor.toolbar.quote', 'Quote');
t('editor.toolbar.inlineCode', 'Inline code');
t('editor.toolbar.codeBlock', 'Code block');
t('editor.toolbar.insertTable', 'Insert table');
t('editor.toolbar.insertImage', 'Insert image');
t('editor.toolbar.imageUrlPrompt', 'Image URL');

// org-size select (constants/workspace.ts)
t('workspace.orgSize.selectRange', 'Select a range');
}
Loading
Loading