mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
refactor: extract RoutingRulesEditor component, revert log levels to debug
- Extract ~250 lines of inline routing rules UI from BotForm into a dedicated RoutingRulesEditor component - Revert stage interrupt and event prevented-default log levels from warning back to debug (these are normal flow, not errors) - Remove message content from log lines to avoid leaking user data
This commit is contained in:
@@ -247,8 +247,8 @@ class RuntimePipeline:
|
||||
await self._check_output(query, result)
|
||||
|
||||
if result.result_type == pipeline_entities.ResultType.INTERRUPT:
|
||||
self.ap.logger.warning(
|
||||
f'Stage {stage_container.inst_name} interrupted query {query.query_id}, message: {str(query.message_chain)[:100]}'
|
||||
self.ap.logger.debug(
|
||||
f'Stage {stage_container.inst_name} interrupted query {query.query_id}'
|
||||
)
|
||||
break
|
||||
elif result.result_type == pipeline_entities.ResultType.CONTINUE:
|
||||
@@ -263,8 +263,8 @@ class RuntimePipeline:
|
||||
await self._check_output(query, sub_result)
|
||||
|
||||
if sub_result.result_type == pipeline_entities.ResultType.INTERRUPT:
|
||||
self.ap.logger.warning(
|
||||
f'Stage {stage_container.inst_name} interrupted query {query.query_id}, message: {str(query.message_chain)[:100]}'
|
||||
self.ap.logger.debug(
|
||||
f'Stage {stage_container.inst_name} interrupted query {query.query_id}'
|
||||
)
|
||||
break
|
||||
elif sub_result.result_type == pipeline_entities.ResultType.CONTINUE:
|
||||
@@ -327,8 +327,8 @@ class RuntimePipeline:
|
||||
event_ctx = await self.ap.plugin_connector.emit_event(event_obj, bound_plugins)
|
||||
|
||||
if event_ctx.is_prevented_default():
|
||||
self.ap.logger.warning(
|
||||
f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}, message: {str(query.message_chain)[:100]}'
|
||||
self.ap.logger.debug(
|
||||
f'MessageReceived event prevented default for query {query.query_id}, pipeline={pipeline_name}'
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
|
||||
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
||||
else:
|
||||
self.ap.logger.warning(
|
||||
f'NormalMessageReceived event prevented default for query {query.query_id} without reply, message: {str(query.message_chain)[:100]}'
|
||||
self.ap.logger.debug(
|
||||
f'NormalMessageReceived event prevented default for query {query.query_id} without reply'
|
||||
)
|
||||
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
|
||||
else:
|
||||
|
||||
@@ -13,13 +13,10 @@ import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import { UUID } from 'uuidjs';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import {
|
||||
Bot,
|
||||
PipelineRoutingRule,
|
||||
RoutingRuleOperator,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { Bot } from '@/app/infra/entities/api';
|
||||
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||
import { ExternalLink, Plus, Trash2 } from 'lucide-react';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import RoutingRulesEditor from './RoutingRulesEditor';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -492,303 +489,10 @@ export default function BotForm({
|
||||
/>
|
||||
|
||||
{/* Pipeline Routing Rules */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<FormLabel>{t('bots.routingRules')}</FormLabel>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('bots.routingRulesDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const rules =
|
||||
form.getValues('pipeline_routing_rules') || [];
|
||||
form.setValue(
|
||||
'pipeline_routing_rules',
|
||||
[
|
||||
...rules,
|
||||
{
|
||||
type: 'launcher_type' as const,
|
||||
operator: 'eq' as const,
|
||||
value: '',
|
||||
pipeline_uuid: '',
|
||||
},
|
||||
],
|
||||
{ shouldDirty: true },
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t('bots.addRoutingRule')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(form.watch('pipeline_routing_rules') || []).map(
|
||||
(rule, index) => {
|
||||
// Determine which operators are available for the current type
|
||||
const operatorsForType: {
|
||||
value: RoutingRuleOperator;
|
||||
labelKey: string;
|
||||
}[] =
|
||||
rule.type === 'launcher_type'
|
||||
? [
|
||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||
]
|
||||
: rule.type === 'launcher_id'
|
||||
? [
|
||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||
{
|
||||
value: 'contains',
|
||||
labelKey: 'bots.operatorContains',
|
||||
},
|
||||
{
|
||||
value: 'not_contains',
|
||||
labelKey: 'bots.operatorNotContains',
|
||||
},
|
||||
{
|
||||
value: 'regex',
|
||||
labelKey: 'bots.operatorRegex',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||
{
|
||||
value: 'contains',
|
||||
labelKey: 'bots.operatorContains',
|
||||
},
|
||||
{
|
||||
value: 'not_contains',
|
||||
labelKey: 'bots.operatorNotContains',
|
||||
},
|
||||
{
|
||||
value: 'starts_with',
|
||||
labelKey: 'bots.operatorStartsWith',
|
||||
},
|
||||
{
|
||||
value: 'regex',
|
||||
labelKey: 'bots.operatorRegex',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 mt-2 p-3 border rounded-md bg-muted/30"
|
||||
>
|
||||
{/* Field selector */}
|
||||
<Select
|
||||
value={rule.type}
|
||||
onValueChange={(val) => {
|
||||
const rules = [
|
||||
...(form.getValues('pipeline_routing_rules') ||
|
||||
[]),
|
||||
];
|
||||
const newType = val as PipelineRoutingRule['type'];
|
||||
// Reset operator to 'eq' when switching type
|
||||
rules[index] = {
|
||||
...rules[index],
|
||||
type: newType,
|
||||
operator: 'eq',
|
||||
value: '',
|
||||
};
|
||||
form.setValue('pipeline_routing_rules', rules, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="launcher_type">
|
||||
{t('bots.ruleTypeLauncherType')}
|
||||
</SelectItem>
|
||||
<SelectItem value="launcher_id">
|
||||
{t('bots.ruleTypeLauncherId')}
|
||||
</SelectItem>
|
||||
<SelectItem value="message_content">
|
||||
{t('bots.ruleTypeMessageContent')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Operator selector */}
|
||||
<Select
|
||||
value={rule.operator || 'eq'}
|
||||
onValueChange={(val) => {
|
||||
const rules = [
|
||||
...(form.getValues('pipeline_routing_rules') ||
|
||||
[]),
|
||||
];
|
||||
rules[index] = {
|
||||
...rules[index],
|
||||
operator: val as RoutingRuleOperator,
|
||||
};
|
||||
form.setValue('pipeline_routing_rules', rules, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{operatorsForType.map((op) => (
|
||||
<SelectItem key={op.value} value={op.value}>
|
||||
{t(op.labelKey)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Value input */}
|
||||
{rule.type === 'launcher_type' ? (
|
||||
<Select
|
||||
value={rule.value}
|
||||
onValueChange={(val) => {
|
||||
const rules = [
|
||||
...(form.getValues('pipeline_routing_rules') ||
|
||||
[]),
|
||||
];
|
||||
rules[index] = { ...rules[index], value: val };
|
||||
form.setValue('pipeline_routing_rules', rules, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue
|
||||
placeholder={t('bots.ruleValuePlaceholder')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="person">
|
||||
{t('bots.sessionTypePerson')}
|
||||
</SelectItem>
|
||||
<SelectItem value="group">
|
||||
{t('bots.sessionTypeGroup')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
className="flex-1"
|
||||
placeholder={
|
||||
rule.type === 'launcher_id'
|
||||
? t('bots.ruleValueLauncherIdPlaceholder')
|
||||
: rule.operator === 'regex'
|
||||
? t('bots.ruleValueRegexpPlaceholder')
|
||||
: t('bots.ruleValueMessagePlaceholder')
|
||||
}
|
||||
value={rule.value}
|
||||
onChange={(e) => {
|
||||
const rules = [
|
||||
...(form.getValues('pipeline_routing_rules') ||
|
||||
[]),
|
||||
];
|
||||
rules[index] = {
|
||||
...rules[index],
|
||||
value: e.target.value,
|
||||
};
|
||||
form.setValue('pipeline_routing_rules', rules, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="text-sm text-muted-foreground shrink-0">
|
||||
→
|
||||
</span>
|
||||
|
||||
{/* Pipeline selector */}
|
||||
<Select
|
||||
value={rule.pipeline_uuid}
|
||||
onValueChange={(val) => {
|
||||
const rules = [
|
||||
...(form.getValues('pipeline_routing_rules') ||
|
||||
[]),
|
||||
];
|
||||
rules[index] = {
|
||||
...rules[index],
|
||||
pipeline_uuid: val,
|
||||
};
|
||||
form.setValue('pipeline_routing_rules', rules, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
{rule.pipeline_uuid ? (
|
||||
(() => {
|
||||
const p = pipelineNameList.find(
|
||||
(p) => p.value === rule.pipeline_uuid,
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{p?.emoji && (
|
||||
<span className="text-sm shrink-0">
|
||||
{p.emoji}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{p?.label ?? rule.pipeline_uuid}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<SelectValue
|
||||
placeholder={t('bots.selectPipeline')}
|
||||
/>
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{pipelineNameList.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.emoji && (
|
||||
<span className="text-sm shrink-0">
|
||||
{item.emoji}
|
||||
</span>
|
||||
)}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
onClick={() => {
|
||||
const rules = [
|
||||
...(form.getValues('pipeline_routing_rules') ||
|
||||
[]),
|
||||
];
|
||||
rules.splice(index, 1);
|
||||
form.setValue('pipeline_routing_rules', rules, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<RoutingRulesEditor
|
||||
form={form}
|
||||
pipelineNameList={pipelineNameList}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
258
web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx
Normal file
258
web/src/app/home/bots/components/bot-form/RoutingRulesEditor.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import {
|
||||
PipelineRoutingRule,
|
||||
RoutingRuleOperator,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { FormLabel } from '@/components/ui/form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface PipelineOption {
|
||||
value: string;
|
||||
label: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
interface RoutingRulesEditorProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
form: UseFormReturn<any>;
|
||||
pipelineNameList: PipelineOption[];
|
||||
}
|
||||
|
||||
const OPERATORS_BY_TYPE: Record<
|
||||
PipelineRoutingRule['type'],
|
||||
{ value: RoutingRuleOperator; labelKey: string }[]
|
||||
> = {
|
||||
launcher_type: [
|
||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||
],
|
||||
launcher_id: [
|
||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||
{ value: 'contains', labelKey: 'bots.operatorContains' },
|
||||
{ value: 'not_contains', labelKey: 'bots.operatorNotContains' },
|
||||
{ value: 'regex', labelKey: 'bots.operatorRegex' },
|
||||
],
|
||||
message_content: [
|
||||
{ value: 'eq', labelKey: 'bots.operatorEq' },
|
||||
{ value: 'neq', labelKey: 'bots.operatorNeq' },
|
||||
{ value: 'contains', labelKey: 'bots.operatorContains' },
|
||||
{ value: 'not_contains', labelKey: 'bots.operatorNotContains' },
|
||||
{ value: 'starts_with', labelKey: 'bots.operatorStartsWith' },
|
||||
{ value: 'regex', labelKey: 'bots.operatorRegex' },
|
||||
],
|
||||
};
|
||||
|
||||
function getValuePlaceholder(
|
||||
t: (key: string) => string,
|
||||
rule: PipelineRoutingRule,
|
||||
): string {
|
||||
if (rule.type === 'launcher_id') return t('bots.ruleValueLauncherIdPlaceholder');
|
||||
if (rule.operator === 'regex') return t('bots.ruleValueRegexpPlaceholder');
|
||||
return t('bots.ruleValueMessagePlaceholder');
|
||||
}
|
||||
|
||||
export default function RoutingRulesEditor({
|
||||
form,
|
||||
pipelineNameList,
|
||||
}: RoutingRulesEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const rules: PipelineRoutingRule[] =
|
||||
form.watch('pipeline_routing_rules') || [];
|
||||
|
||||
const updateRules = (newRules: PipelineRoutingRule[]) => {
|
||||
form.setValue('pipeline_routing_rules', newRules, { shouldDirty: true });
|
||||
};
|
||||
|
||||
const addRule = () => {
|
||||
updateRules([
|
||||
...rules,
|
||||
{
|
||||
type: 'launcher_type',
|
||||
operator: 'eq',
|
||||
value: '',
|
||||
pipeline_uuid: '',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const updateRule = (index: number, patch: Partial<PipelineRoutingRule>) => {
|
||||
const updated = [...rules];
|
||||
updated[index] = { ...updated[index], ...patch };
|
||||
updateRules(updated);
|
||||
};
|
||||
|
||||
const removeRule = (index: number) => {
|
||||
const updated = [...rules];
|
||||
updated.splice(index, 1);
|
||||
updateRules(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<FormLabel>{t('bots.routingRules')}</FormLabel>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('bots.routingRulesDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addRule}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t('bots.addRoutingRule')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{rules.map((rule, index) => {
|
||||
const operatorsForType = OPERATORS_BY_TYPE[rule.type] || OPERATORS_BY_TYPE.message_content;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 mt-2 p-3 border rounded-md bg-muted/30"
|
||||
>
|
||||
{/* Field selector */}
|
||||
<Select
|
||||
value={rule.type}
|
||||
onValueChange={(val) => {
|
||||
updateRule(index, {
|
||||
type: val as PipelineRoutingRule['type'],
|
||||
operator: 'eq',
|
||||
value: '',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="launcher_type">
|
||||
{t('bots.ruleTypeLauncherType')}
|
||||
</SelectItem>
|
||||
<SelectItem value="launcher_id">
|
||||
{t('bots.ruleTypeLauncherId')}
|
||||
</SelectItem>
|
||||
<SelectItem value="message_content">
|
||||
{t('bots.ruleTypeMessageContent')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Operator selector */}
|
||||
<Select
|
||||
value={rule.operator || 'eq'}
|
||||
onValueChange={(val) => {
|
||||
updateRule(index, { operator: val as RoutingRuleOperator });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{operatorsForType.map((op) => (
|
||||
<SelectItem key={op.value} value={op.value}>
|
||||
{t(op.labelKey)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Value input */}
|
||||
{rule.type === 'launcher_type' ? (
|
||||
<Select
|
||||
value={rule.value}
|
||||
onValueChange={(val) => updateRule(index, { value: val })}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue
|
||||
placeholder={t('bots.ruleValuePlaceholder')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="person">
|
||||
{t('bots.sessionTypePerson')}
|
||||
</SelectItem>
|
||||
<SelectItem value="group">
|
||||
{t('bots.sessionTypeGroup')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
className="flex-1"
|
||||
placeholder={getValuePlaceholder(t, rule)}
|
||||
value={rule.value}
|
||||
onChange={(e) => updateRule(index, { value: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className="text-sm text-muted-foreground shrink-0">→</span>
|
||||
|
||||
{/* Pipeline selector */}
|
||||
<Select
|
||||
value={rule.pipeline_uuid}
|
||||
onValueChange={(val) =>
|
||||
updateRule(index, { pipeline_uuid: val })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
{rule.pipeline_uuid ? (
|
||||
(() => {
|
||||
const p = pipelineNameList.find(
|
||||
(p) => p.value === rule.pipeline_uuid,
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{p?.emoji && (
|
||||
<span className="text-sm shrink-0">{p.emoji}</span>
|
||||
)}
|
||||
<span>{p?.label ?? rule.pipeline_uuid}</span>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<SelectValue placeholder={t('bots.selectPipeline')} />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{pipelineNameList.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.emoji && (
|
||||
<span className="text-sm shrink-0">{item.emoji}</span>
|
||||
)}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
onClick={() => removeRule(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user