Skip to content

Commit b54562c

Browse files
authored
Merge pull request #67 from artemploxoyy/feature/select-field-type
feat: добавлена поддержка типа select для настроек плагинов
2 parents dbb4155 + e340fbe commit b54562c

4 files changed

Lines changed: 158 additions & 29 deletions

File tree

backend/src/ai/plugin-assistant-system-prompt.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,26 @@ bot.events.on('core:raw_message', (rawText, jsonMsg) => {
412412
"defaultPath": "config/default.json"
413413
},
414414

415+
"mode": {
416+
"type": "select",
417+
"label": "Режим работы",
418+
"description": "Выберите режим работы плагина",
419+
"options": ["easy", "normal", "hard"],
420+
"default": "normal"
421+
},
422+
423+
"language": {
424+
"type": "select",
425+
"label": "Язык",
426+
"description": "Выберите язык интерфейса",
427+
"options": [
428+
{ "value": "ru", "label": "Русский" },
429+
{ "value": "en", "label": "English" },
430+
{ "value": "uk", "label": "Українська" }
431+
],
432+
"default": "ru"
433+
},
434+
415435
"proxy": {
416436
"type": "proxy",
417437
"label": "Прокси для запросов",
@@ -434,6 +454,7 @@ bot.events.on('core:raw_message', (rawText, jsonMsg) => {
434454
| `string[]` | Массив строк | `["a", "b"]` |
435455
| `json` | JSON объект | `{"key": "value"}` |
436456
| `json_file` | JSON из файла | Путь к файлу |
457+
| `select` | Выпадающий список | `"normal"` (строка или объект) |
437458
| `proxy` | Выбор прокси (из списка или вручную) | `{ enabled: true, host: "...", port: 1080 }` |
438459

439460
### Структура объекта proxy
@@ -454,6 +475,44 @@ bot.events.on('core:raw_message', (rawText, jsonMsg) => {
454475

455476
Если прокси отключен: `{ enabled: false }`
456477

478+
### Структура настройки select
479+
480+
Настройка типа `select` позволяет пользователю выбрать одно значение из предопределенного списка опций.
481+
482+
**Формат options:**
483+
484+
1. **Простой массив строк** - когда значение и label совпадают:
485+
486+
```javascript
487+
"options": ["easy", "normal", "hard"]
488+
```
489+
490+
1. **Массив объектов** - когда нужны разные value и label:
491+
492+
```javascript
493+
"options": [
494+
{ "value": "ru", "label": "Русский" },
495+
{ "value": "en", "label": "English" },
496+
{ "value": "uk", "label": "Українська" }
497+
]
498+
```
499+
500+
**Результат:**
501+
Сохраненное значение всегда будет строкой (например: `"normal"`, `"ru"`)
502+
503+
**Пример использования в плагине:**
504+
505+
```javascript
506+
module.exports = (bot, { settings }) => {
507+
const mode = settings.mode; // "easy" | "normal" | "hard"
508+
const language = settings.language; // "ru" | "en" | "uk"
509+
510+
if (mode === 'hard') {
511+
console.log('Включён сложный режим');
512+
}
513+
};
514+
```
515+
457516
### Доступ к настройкам
458517

459518
```javascript

frontend/src/components/PluginSettingsDialog.jsx

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { apiHelper } from '@/lib/api';
1515
import PluginDetailInfo from './PluginDetailInfo';
1616
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
1717
import { useAppStore } from '@/stores/appStore';
18+
import { shouldShowField } from '@/lib/pluginSettingsUtils';
1819

1920
function JsonEditorDialog({ initialValue, onSave, onCancel }) {
2021
const [jsonString, setJsonString] = useState('');
@@ -201,6 +202,29 @@ function SettingField({ settingKey, config, value, onChange, readOnly }) {
201202
{config.description && <p className="text-sm text-muted-foreground">{config.description}</p>}
202203
</div>
203204
);
205+
case 'select':
206+
return (
207+
<div className="space-y-2">
208+
<Label htmlFor={id}>{config.label}</Label>
209+
<Select value={value ?? config.default ?? ''} onValueChange={readOnly ? undefined : ((newValue) => onChange(settingKey, newValue))} disabled={readOnly}>
210+
<SelectTrigger id={id}>
211+
<SelectValue placeholder="Выберите значение" />
212+
</SelectTrigger>
213+
<SelectContent>
214+
{(config.options || []).map((option) => {
215+
const optionValue = typeof option === 'string' ? option : option.value;
216+
const optionLabel = typeof option === 'string' ? option : option.label;
217+
return (
218+
<SelectItem key={optionValue} value={optionValue}>
219+
{optionLabel}
220+
</SelectItem>
221+
);
222+
})}
223+
</SelectContent>
224+
</Select>
225+
{config.description && <p className="text-sm text-muted-foreground">{config.description}</p>}
226+
</div>
227+
);
204228
case 'json_file':
205229
return (
206230
<div className="space-y-2">
@@ -372,16 +396,19 @@ export default function PluginSettingsDialog({ bot, plugin, onOpenChange, onSave
372396
<AccordionItem key={categoryKey} value={categoryKey} className="border rounded-lg bg-muted/20 px-4">
373397
<AccordionTrigger className="text-base font-semibold hover:no-underline">{categoryConfig.label}</AccordionTrigger>
374398
<AccordionContent className="pt-4 border-t space-y-4">
375-
{Object.entries(categoryConfig).filter(([key]) => key !== 'label').map(([key, config]) => (
376-
<SettingField
377-
key={key}
378-
settingKey={key}
379-
config={config}
380-
value={settings[key]}
381-
onChange={handleSettingChange}
382-
readOnly={readOnly}
383-
/>
384-
))}
399+
{Object.entries(categoryConfig)
400+
.filter(([key]) => key !== 'label')
401+
.filter(([key, config]) => shouldShowField(key, settings?.actionsPreset))
402+
.map(([key, config]) => (
403+
<SettingField
404+
key={key}
405+
settingKey={key}
406+
config={config}
407+
value={settings[key]}
408+
onChange={handleSettingChange}
409+
readOnly={readOnly}
410+
/>
411+
))}
385412
</AccordionContent>
386413
</AccordionItem>
387414
))}
@@ -391,16 +418,18 @@ export default function PluginSettingsDialog({ bot, plugin, onOpenChange, onSave
391418

392419
return (
393420
<div className="space-y-4">
394-
{Object.entries(manifestSettings).map(([key, config]) => (
395-
<SettingField
396-
key={key}
397-
settingKey={key}
398-
config={config}
399-
value={settings[key]}
400-
onChange={handleSettingChange}
401-
readOnly={readOnly}
402-
/>
403-
))}
421+
{Object.entries(manifestSettings)
422+
.filter(([key, config]) => shouldShowField(key, settings?.actionsPreset))
423+
.map(([key, config]) => (
424+
<SettingField
425+
key={key}
426+
settingKey={key}
427+
config={config}
428+
value={settings[key]}
429+
onChange={handleSettingChange}
430+
readOnly={readOnly}
431+
/>
432+
))}
404433
</div>
405434
);
406435
};

frontend/src/components/PluginSettingsForm.jsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Edit } from 'lucide-react';
99
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, DialogTrigger } from "@/components/ui/dialog";
1010
import Editor from '@monaco-editor/react';
1111
import { useAppStore } from '@/stores/appStore';
12+
import { shouldShowField } from '@/lib/pluginSettingsUtils';
1213

1314
function JsonEditorDialog({ initialValue, onSave, onCancel }) {
1415
const [jsonString, setJsonString] = useState('');
@@ -197,6 +198,29 @@ function SettingField({ settingKey, config, value, onChange }) {
197198
{config.description && <p className="text-sm text-muted-foreground">{config.description}</p>}
198199
</div>
199200
);
201+
case 'select':
202+
return (
203+
<div className="space-y-2">
204+
<Label htmlFor={id}>{config.label}</Label>
205+
<Select value={value ?? config.default ?? ''} onValueChange={(newValue) => onChange(settingKey, newValue)}>
206+
<SelectTrigger id={id}>
207+
<SelectValue placeholder="Выберите значение" />
208+
</SelectTrigger>
209+
<SelectContent>
210+
{(config.options || []).map((option) => {
211+
const optionValue = typeof option === 'string' ? option : option.value;
212+
const optionLabel = typeof option === 'string' ? option : option.label;
213+
return (
214+
<SelectItem key={optionValue} value={optionValue}>
215+
{optionLabel}
216+
</SelectItem>
217+
);
218+
})}
219+
</SelectContent>
220+
</Select>
221+
{config.description && <p className="text-sm text-muted-foreground">{config.description}</p>}
222+
</div>
223+
);
200224
case 'json':
201225
case 'json_file':
202226
return (
@@ -236,15 +260,17 @@ export default function PluginSettingsForm({ plugin, onSettingsChange }) {
236260

237261
return (
238262
<div className="space-y-6">
239-
{Object.entries(plugin.manifest.settings).map(([key, config]) => (
240-
<SettingField
241-
key={key}
242-
settingKey={key}
243-
config={config}
244-
value={plugin.settings[key]}
245-
onChange={handleFieldChange}
246-
/>
247-
))}
263+
{Object.entries(plugin.manifest.settings)
264+
.filter(([key, config]) => shouldShowField(key, plugin.settings?.actionsPreset))
265+
.map(([key, config]) => (
266+
<SettingField
267+
key={key}
268+
settingKey={key}
269+
config={config}
270+
value={plugin.settings[key]}
271+
onChange={handleFieldChange}
272+
/>
273+
))}
248274
</div>
249275
);
250276
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Определяет, должно ли поле настройки плагина отображаться
3+
* на основе условий (например, actionsPreset)
4+
*
5+
* @param {string} key - Ключ поля настройки
6+
* @param {any} actionsPresetValue - Значение поля actionsPreset (если есть)
7+
* @returns {boolean} - Должно ли поле отображаться
8+
*/
9+
export const shouldShowField = (key, actionsPresetValue) => {
10+
// Если есть поле actionsPreset, то поля enable* показываем только при custom
11+
if (actionsPresetValue !== undefined && key.startsWith('enable')) {
12+
return actionsPresetValue === 'custom';
13+
}
14+
return true;
15+
};

0 commit comments

Comments
 (0)