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
53 changes: 53 additions & 0 deletions dashboard/src/components/config/DynamicConfigForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,59 @@ describe('DynamicConfigForm', () => {
expect(markup).not.toContain('dynamic-object__manage');
});

it('does not add a blank option when the current enum value is valid', () => {
const markup = renderToStaticMarkup(
<I18nextProvider i18n={i18n}>
<ConfigGroup
fieldsFromValue
metadata={{
type: 'object',
items: {
segment_mode: {
labels: ['Regular expression', 'Word list'],
options: ['regex', 'word_list'],
type: 'string',
},
},
}}
onChange={() => undefined}
translationPath="config"
value={{ segment_mode: 'regex' }}
variant="inline"
/>
</I18nextProvider>,
);

expect(markup).not.toContain('<option disabled="" hidden="" value=""></option>');
expect(markup).toContain('<option value="0" selected="">Regular expression</option>');
expect(markup).toContain('<option value="1">Word list</option>');
});

it('keeps a hidden placeholder for an unmatched enum value', () => {
const markup = renderToStaticMarkup(
<I18nextProvider i18n={i18n}>
<ConfigGroup
fieldsFromValue
metadata={{
type: 'object',
items: {
segment_mode: {
options: ['regex', 'word_list'],
type: 'string',
},
},
}}
onChange={() => undefined}
translationPath="config"
value={{ segment_mode: 'legacy_mode' }}
variant="inline"
/>
</I18nextProvider>,
);

expect(markup).toContain('<option disabled="" hidden="" value="" selected=""></option>');
});

it('renders the embedding dimension detector for special metadata', () => {
const markup = renderToStaticMarkup(
<I18nextProvider i18n={i18n}>
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/components/config/DynamicConfigForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@
);
}

function ConfigControl({

Check warning on line 450 in dashboard/src/components/config/DynamicConfigForm.tsx

View workflow job for this annotation

GitHub Actions / lint, test, and build

Function 'ConfigControl' has a complexity of 50. Maximum allowed is 35

Check warning on line 450 in dashboard/src/components/config/DynamicConfigForm.tsx

View workflow job for this annotation

GitHub Actions / lint, test, and build

Function 'ConfigControl' has a complexity of 50. Maximum allowed is 35
configKey = '',
configRoot,
embeddingDimensionLoading,
Expand Down Expand Up @@ -535,7 +535,7 @@

if (type === 'bool') {
return (
<label className="dynamic-switch">

Check warning on line 538 in dashboard/src/components/config/DynamicConfigForm.tsx

View workflow job for this annotation

GitHub Actions / lint, test, and build

A form label must have accessible text

Check warning on line 538 in dashboard/src/components/config/DynamicConfigForm.tsx

View workflow job for this annotation

GitHub Actions / lint, test, and build

A form label must have accessible text
<input
checked={Boolean(value)}
disabled={disabled}
Expand Down Expand Up @@ -599,7 +599,7 @@
onChange={(event) => onChange(metadata.options?.[Number(event.target.value)])}
value={selectedIndex < 0 ? '' : selectedIndex}
>
<option disabled hidden value="" />
{selectedIndex < 0 && <option disabled hidden value="" />}
{metadata.options.map((option, index) => (
<option key={String(option)} value={index}>
{String(labels[index] ?? option)}
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/components/content/Markdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ describe('Streamdown Markdown renderer', () => {
expect(html).not.toContain('markdown-body--streaming');
});

it('marks external links as link-safety controls', () => {
const html = renderToStaticMarkup(<Markdown content="Visit [AstrBot](https://astrbot.app) for documentation." />);

expect(html).toContain('<button');
expect(html).toContain('data-streamdown="link"');
expect(html).toContain('>AstrBot</button>');
});

it('defers rich controls while content is still streaming', () => {
const html = renderToStaticMarkup(<Markdown content={'```ts\nconst value = 1;\n```'} streaming />);

Expand Down
26 changes: 25 additions & 1 deletion dashboard/src/components/content/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,35 @@

.markdown-body a,
.markdown-body [data-streamdown='link'] {
color: var(--astrbot-text);
display: inline;
margin: 0;
padding: 0;
border: 0;
appearance: none;
background: transparent;
color: var(--astrbot-primary);
cursor: pointer;
font: inherit;
line-height: inherit;
text-align: inherit;
text-decoration: none;
vertical-align: baseline;
}

.markdown-body a:hover,
.markdown-body [data-streamdown='link']:hover {
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}

.markdown-body a:focus-visible,
.markdown-body [data-streamdown='link']:focus-visible {
border-radius: 3px;
outline: 2px solid color-mix(in srgb, var(--astrbot-primary) 38%, transparent);
outline-offset: 2px;
}

.markdown-body [data-streamdown='strong'] {
font-weight: 700;
}
Expand Down
51 changes: 51 additions & 0 deletions dashboard/src/components/ui/DisclosureButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { forwardRef, type ButtonHTMLAttributes } from 'react';

import { MdiIcon } from '@/components/icons/MdiIcon';

export type DisclosureButtonProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'aria-expanded' | 'aria-label' | 'children'
> & {
collapseLabel: string;
compact?: boolean;
direction?: 'down' | 'right';
expanded: boolean;
expandLabel: string;
label?: string;
};

export const DisclosureButton = forwardRef<HTMLButtonElement, DisclosureButtonProps>(function DisclosureButton(
{
className = '',
collapseLabel,
compact = false,
direction = 'down',
expanded,
expandLabel,
label = '',
title,
type = 'button',
...props
},
ref,
) {
const actionLabel = expanded ? collapseLabel : expandLabel;
const accessibleLabel = label ? `${actionLabel}: ${label}` : actionLabel;

return (
<button
aria-expanded={expanded}
aria-label={accessibleLabel}
className={`ui-disclosure-button${compact ? ' ui-disclosure-button--compact' : ''}${direction === 'right' ? ' ui-disclosure-button--tree' : ''}${className ? ` ${className}` : ''}`}
ref={ref}
title={title ?? accessibleLabel}
type={type}
{...props}
>
<MdiIcon
className="ui-disclosure-button__icon"
name={direction === 'right' ? 'mdi-chevron-right' : 'mdi-chevron-down'}
/>
</button>
);
});
27 changes: 27 additions & 0 deletions dashboard/src/components/ui/primitives.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest';

import { Button } from './Button';
import { DataTable } from './DataTable';
import { DisclosureButton } from './DisclosureButton';
import { DialogActions } from './DialogActions';
import { Pagination } from './Pagination';
import { SearchField } from './SearchField';
Expand Down Expand Up @@ -59,6 +60,32 @@ describe('shared UI primitives', () => {
expect(markup).toContain('ui-status-chip--success');
});

it('keeps disclosure controls centered and exposes their current state', () => {
const collapsed = renderToStaticMarkup(
<DisclosureButton collapseLabel="Collapse" expanded={false} expandLabel="Expand" label="Config file" />,
);
const expanded = renderToStaticMarkup(
<DisclosureButton
collapseLabel="Collapse"
compact
direction="right"
expanded
expandLabel="Expand"
label="Tool details"
/>,
);

expect(collapsed).toContain('aria-expanded="false"');
expect(collapsed).toContain('aria-label="Expand: Config file"');
expect(collapsed).toContain('ui-disclosure-button__icon');
expect(expanded).toContain('aria-expanded="true"');
expect(expanded).toContain('aria-label="Collapse: Tool details"');
expect(expanded).toContain('ui-disclosure-button--compact');
expect(expanded).toContain('ui-disclosure-button--tree');
expect(expanded).toContain('mdi-chevron-right');
expect(primitiveStyles).toContain(".ui-disclosure-button[aria-expanded='true']");
});

it('shares table selection, empty state and pagination structure', () => {
const table = renderToStaticMarkup(
<DataTable
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/en-US/core/actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"clear": "Clear",
"save": "Save",
"close": "Close",
"expand": "Expand",
"collapse": "Collapse"
}
1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/ru-RU/core/actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"clear": "Очистить",
"save": "Сохранить",
"close": "Закрыть",
"expand": "Развернуть",
"collapse": "Свернуть"
}
1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/zh-CN/core/actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"clear": "清空",
"save": "保存",
"close": "关闭",
"expand": "展开",
"collapse": "收起"
}
16 changes: 9 additions & 7 deletions dashboard/src/routes/configuration/PlatformPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ConfigGroup, MetadataConfigEditor } from '@/components/config/DynamicCo
import type { ConfigGroupMetadata, ConfigRecord } from '@/components/config/configFormModel';
import { Dialog, DialogClose } from '@/components/headless/Dialog';
import { MdiIcon } from '@/components/icons/MdiIcon';
import { DisclosureButton } from '@/components/ui/DisclosureButton';
import { DEFAULT_CONFIG_ID } from '@/config/defaults';
import { useBrowserCapabilities } from '@/platform/BrowserCapabilitiesProvider';
import { i18n } from '@/i18n';
Expand Down Expand Up @@ -748,16 +749,17 @@ function PlatformEditor({
{t('createDialog.configHint')} {t('createDialog.configDefaultHint')}
</p>
</div>
<button
aria-expanded={showConfigSection}
<DisclosureButton
aria-controls="platform-editor-config-profiles"
collapseLabel={i18n.t('core.actions.collapse')}
expanded={showConfigSection}
expandLabel={i18n.t('core.actions.expand')}
label={t('createDialog.configFileTitle')}
onClick={() => setShowConfigSection((current) => !current)}
type="button"
>
<MdiIcon name={showConfigSection ? 'mdi-chevron-up' : 'mdi-chevron-down'} />
</button>
/>
</div>
{showConfigSection && (
<div className="platform-editor__profiles">
<div className="platform-editor__profiles" id="platform-editor-config-profiles">
<label>
<input
checked={configMode === 'existing'}
Expand Down
37 changes: 30 additions & 7 deletions dashboard/src/routes/extensions/ExtensionSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
import { MdiIcon } from '@/components/icons/MdiIcon';
import { Dialog } from '@/components/headless/Dialog';
import { MonacoEditor } from '@/components/editor/MonacoEditor';
import { DisclosureButton } from '@/components/ui/DisclosureButton';
import { useUnsavedChangesGuard } from '@/components/ui/useUnsavedChangesGuard';
import { confirmAction, toast } from '@/stores/feedback';
import { useBrowserCapabilities } from '@/platform/BrowserCapabilitiesProvider';
Expand Down Expand Up @@ -74,6 +75,10 @@ export function ComponentsSection() {
const c = (key: string, options?: Record<string, unknown>) => t(`features.command.${key}`, options);
const u = (key: string, options?: Record<string, unknown>) => t(`features.tooluse.${key}`, options);
const e = (key: string) => t(`features.extension.${key}`);
const disclosureLabels = {
collapse: t('core.actions.collapse'),
expand: t('core.actions.expand'),
};
const [commands, setCommands] = useState<JsonObject[]>([]);
const [tools, setTools] = useState<JsonObject[]>([]);
const [summary, setSummary] = useState({ conflicts: 0, disabled: 0 });
Expand Down Expand Up @@ -394,6 +399,7 @@ export function ComponentsSection() {
expanded={expandedGroups.has(recordId(item, 'handler_full_name'))}
item={item}
key={recordId(item, 'handler_full_name') || index}
labels={disclosureLabels}
onDetails={setDetails}
onPermission={commandPermission}
onRename={(command) =>
Expand All @@ -413,6 +419,7 @@ export function ComponentsSection() {
expanded={expandedTools.has(recordId(item, 'name'))}
item={item}
key={recordId(item, 'name') || index}
labels={disclosureLabels}
onPermission={toolPermission}
onToggle={toggleTool}
onToggleExpand={(tool) => toggleSet(setExpandedTools, recordId(tool, 'name'))}
Expand Down Expand Up @@ -615,6 +622,7 @@ function CommandFilters({
function CommandRow({
expanded,
item,
labels,
onDetails,
onPermission,
onRename,
Expand All @@ -624,6 +632,7 @@ function CommandRow({
}: {
expanded: boolean;
item: JsonObject;
labels: { collapse: string; expand: string };
onDetails: (item: JsonObject) => void;
onPermission: (item: JsonObject, value: 'admin' | 'member') => Promise<void>;
onRename: (item: JsonObject) => void;
Expand All @@ -650,9 +659,15 @@ function CommandRow({
<td>
<div className="component-command-name">
{isGroup && subCommands.length ? (
<button onClick={() => onToggleExpand(item)} type="button">
<MdiIcon name={expanded ? 'mdi-chevron-down' : 'mdi-chevron-right'} />
</button>
<DisclosureButton
collapseLabel={labels.collapse}
compact
direction="right"
expanded={expanded}
expandLabel={labels.expand}
label={String(item.effective_command || item.current_fragment || item.original_command || '')}
onClick={() => onToggleExpand(item)}
/>
) : type === 'sub_command' ? (
<span className="component-command-indent" />
) : null}
Expand Down Expand Up @@ -722,13 +737,15 @@ function CommandRow({
function ToolRow({
expanded,
item,
labels,
onPermission,
onToggle,
onToggleExpand,
t,
}: {
expanded: boolean;
item: JsonObject;
labels: { collapse: string; expand: string };
onPermission: (item: JsonObject, value: 'admin' | 'member') => Promise<void>;
onToggle: (item: JsonObject) => Promise<void>;
onToggleExpand: (item: JsonObject) => void;
Expand All @@ -744,9 +761,15 @@ function ToolRow({
<>
<tr>
<td>
<button className="component-expand" onClick={() => onToggleExpand(item)} type="button">
<MdiIcon name={expanded ? 'mdi-chevron-up' : 'mdi-chevron-down'} />
</button>
<DisclosureButton
className="component-expand"
collapseLabel={labels.collapse}
compact
expanded={expanded}
expandLabel={labels.expand}
label={id}
onClick={() => onToggleExpand(item)}
/>
</td>
<td>
<div className="component-tool-name">
Expand Down Expand Up @@ -900,7 +923,7 @@ function RenameCommandDialog({
/>
</label>
<section>
<button onClick={() => setAliasesOpen((value) => !value)} type="button">
<button aria-expanded={aliasesOpen} onClick={() => setAliasesOpen((value) => !value)} type="button">
<span>{t('dialogs.rename.aliases')}</span>
<MdiIcon name={aliasesOpen ? 'mdi-chevron-up' : 'mdi-chevron-down'} />
</button>
Expand Down
Loading
Loading