mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
Refactor human input form item types
This commit is contained in:
parent
0c8dec3315
commit
6c4f293719
@ -15,7 +15,7 @@ const createInput = (overrides: Partial<FormInputItem>): FormInputItem => ({
|
||||
variable: 'field',
|
||||
required: false,
|
||||
max_length: 128,
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
default: {
|
||||
type: 'constant' as const,
|
||||
value: '',
|
||||
@ -57,7 +57,7 @@ describe('human-input utils', () => {
|
||||
it('should initialize text fields with constants and variable defaults', () => {
|
||||
const formInputs = [
|
||||
createInput({
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'name',
|
||||
default: { type: 'constant', value: 'John', selector: [] },
|
||||
}),
|
||||
@ -90,7 +90,7 @@ describe('human-input utils', () => {
|
||||
it('should fallback to empty string when variable default is missing', () => {
|
||||
const formInputs = [
|
||||
createInput({
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'summary',
|
||||
default: { type: 'variable', value: '', selector: [] },
|
||||
}),
|
||||
|
||||
@ -4,7 +4,10 @@ import dayjs from 'dayjs'
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import {
|
||||
isParagraphFormInput,
|
||||
UserActionButtonType,
|
||||
} from '@/app/components/workflow/nodes/human-input/types'
|
||||
import 'dayjs/locale/en'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import 'dayjs/locale/ja'
|
||||
@ -32,9 +35,9 @@ export const splitByOutputVar = (content: string): string[] => {
|
||||
}
|
||||
|
||||
export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Record<string, string> = {}) => {
|
||||
const initialInputs: Record<string, any> = {}
|
||||
const initialInputs: Record<string, string | undefined> = {}
|
||||
formInputs.forEach((item) => {
|
||||
if (item.type === 'text-input' || item.type === 'paragraph')
|
||||
if (isParagraphFormInput(item))
|
||||
initialInputs[item.output_variable_name] = item.default.type === 'variable' ? defaultValues[item.output_variable_name] || '' : item.default.value
|
||||
else
|
||||
initialInputs[item.output_variable_name] = undefined
|
||||
|
||||
@ -8,7 +8,10 @@ import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
createDefaultParagraphFormInput,
|
||||
isParagraphFormInput,
|
||||
} from '@/app/components/workflow/nodes/human-input/types'
|
||||
import ActionButton from '../../../action-button'
|
||||
import { VariableX } from '../../../icons/src/vender/workflow'
|
||||
import Modal from '../../../modal'
|
||||
@ -36,15 +39,7 @@ type HITLInputComponentUIProps = {
|
||||
const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
nodeId,
|
||||
varName,
|
||||
formInput = {
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: varName,
|
||||
default: {
|
||||
type: 'constant',
|
||||
selector: [],
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
formInput,
|
||||
onChange,
|
||||
onRename,
|
||||
onRemove,
|
||||
@ -56,6 +51,10 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const resolvedFormInput = formInput || createDefaultParagraphFormInput(varName)
|
||||
const paragraphDefault = isParagraphFormInput(resolvedFormInput)
|
||||
? resolvedFormInput.default
|
||||
: null
|
||||
const [isShowEditModal, {
|
||||
setTrue: showEditModal,
|
||||
setFalse: hideEditModal,
|
||||
@ -72,8 +71,7 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
if (editBtn)
|
||||
editBtn.removeEventListener('click', showEditModal)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, [showEditModal])
|
||||
|
||||
const removeBtnRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
@ -97,8 +95,8 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
}, [hideEditModal, onChange, onRename, varName])
|
||||
|
||||
const isDefaultValueVariable = useMemo(() => {
|
||||
return formInput.default?.type === 'variable'
|
||||
}, [formInput.default?.type])
|
||||
return paragraphDefault?.type === 'variable'
|
||||
}, [paragraphDefault])
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -117,7 +115,7 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
{/* Default Value Info */}
|
||||
{isDefaultValueVariable && (
|
||||
<VariableBlock
|
||||
variables={formInput.default?.selector}
|
||||
variables={paragraphDefault?.selector || []}
|
||||
workflowNodesMap={workflowNodesMap}
|
||||
getVarType={getVarType}
|
||||
environmentVariables={environmentVariables}
|
||||
@ -126,7 +124,9 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
/>
|
||||
)}
|
||||
{!isDefaultValueVariable && (
|
||||
<div className="max-w-full truncate system-xs-medium text-components-input-text-filled">{formInput.default?.value}</div>
|
||||
<div className="max-w-full truncate system-xs-medium text-components-input-text-filled">
|
||||
{paragraphDefault?.value ?? resolvedFormInput.type}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -166,7 +166,7 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
<InputField
|
||||
nodeId={nodeId}
|
||||
isEdit
|
||||
payload={formInput}
|
||||
payload={resolvedFormInput}
|
||||
onChange={handleChange}
|
||||
onCancel={hideEditModal}
|
||||
/>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FormInputItem, FormInputItemDefault } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { FormInputItem, FormInputItemDefault, ParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { produce } from 'immer'
|
||||
@ -6,7 +6,10 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
createDefaultParagraphFormInput,
|
||||
isParagraphFormInput,
|
||||
} from '@/app/components/workflow/nodes/human-input/types'
|
||||
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import PrePopulate from './pre-populate'
|
||||
|
||||
@ -19,11 +22,6 @@ type InputFieldProps = {
|
||||
onChange: (newPayload: FormInputItem) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
const defaultPayload: FormInputItem = {
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: '',
|
||||
default: { type: 'constant', selector: [], value: '' },
|
||||
}
|
||||
const InputField: React.FC<InputFieldProps> = ({
|
||||
nodeId,
|
||||
isEdit,
|
||||
@ -32,7 +30,13 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [tempPayload, setTempPayload] = useState(payload || defaultPayload)
|
||||
const [tempPayload, setTempPayload] = useState<FormInputItem>(() => payload || createDefaultParagraphFormInput())
|
||||
const paragraphPayload = useMemo<ParagraphFormInput>(() => {
|
||||
if (isParagraphFormInput(tempPayload))
|
||||
return tempPayload
|
||||
|
||||
return createDefaultParagraphFormInput(tempPayload.output_variable_name)
|
||||
}, [tempPayload])
|
||||
const nameValid = useMemo(() => {
|
||||
const name = tempPayload.output_variable_name.trim()
|
||||
if (!name)
|
||||
@ -46,12 +50,9 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
return
|
||||
onChange(tempPayload)
|
||||
}, [nameValid, onChange, tempPayload])
|
||||
const defaultValueConfig = tempPayload.default
|
||||
const handleDefaultValueChange = useCallback((key: keyof FormInputItemDefault) => {
|
||||
return (value: ValueSelector | string) => {
|
||||
const nextValue = produce(tempPayload, (draft) => {
|
||||
if (!draft.default)
|
||||
draft.default = { type: 'constant', selector: [], value: '' }
|
||||
const nextValue = produce(paragraphPayload, (draft) => {
|
||||
if (key === 'selector') {
|
||||
draft.default.type = 'variable'
|
||||
draft.default.selector = value as ValueSelector
|
||||
@ -66,7 +67,7 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
})
|
||||
setTempPayload(nextValue)
|
||||
}
|
||||
}, [tempPayload])
|
||||
}, [paragraphPayload])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@ -109,14 +110,14 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
{t(`${i18nPrefix}.prePopulateField`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<PrePopulate
|
||||
isVariable={defaultValueConfig?.type === 'variable'}
|
||||
isVariable={paragraphPayload.default.type === 'variable'}
|
||||
onIsVariableChange={(isVariable) => {
|
||||
handleDefaultValueChange('type')(isVariable ? 'variable' : 'constant')
|
||||
}}
|
||||
nodeId={nodeId}
|
||||
valueSelector={defaultValueConfig?.selector}
|
||||
valueSelector={paragraphPayload.default.selector}
|
||||
onValueSelectorChange={handleDefaultValueChange('selector')}
|
||||
value={defaultValueConfig?.value}
|
||||
value={paragraphPayload.default.value}
|
||||
onValueChange={handleDefaultValueChange('value')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -23,7 +23,7 @@ const createData = (overrides: Partial<HumanInputNodeType> = {}): HumanInputNode
|
||||
}],
|
||||
form_content: 'Please review this request',
|
||||
inputs: [{
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'review_result',
|
||||
default: {
|
||||
selector: [],
|
||||
|
||||
@ -103,7 +103,7 @@ vi.mock('../components/form-content', () => ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onFormInputsChange([{
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'email',
|
||||
default: {
|
||||
selector: [],
|
||||
@ -230,7 +230,7 @@ const createData = (overrides: Partial<HumanInputNodeType> = {}): HumanInputNode
|
||||
}],
|
||||
form_content: 'Please review this request',
|
||||
inputs: [{
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'review_result',
|
||||
default: {
|
||||
selector: [],
|
||||
|
||||
@ -86,10 +86,14 @@ describe('variable-in-markdown', () => {
|
||||
it('should render note values and replace node ids with labels for variable defaults', () => {
|
||||
const { rerender } = render(
|
||||
<Note
|
||||
defaultInput={{
|
||||
type: 'variable',
|
||||
selector: ['node-1', 'output'],
|
||||
value: '',
|
||||
input={{
|
||||
type: 'paragraph',
|
||||
output_variable_name: 'approval',
|
||||
default: {
|
||||
type: 'variable',
|
||||
selector: ['node-1', 'output'],
|
||||
value: '',
|
||||
},
|
||||
}}
|
||||
nodeName={nodeId => nodeId === 'node-1' ? 'Start Node' : nodeId}
|
||||
/>,
|
||||
@ -99,10 +103,14 @@ describe('variable-in-markdown', () => {
|
||||
|
||||
rerender(
|
||||
<Note
|
||||
defaultInput={{
|
||||
type: 'constant',
|
||||
value: 'Plain value',
|
||||
selector: [],
|
||||
input={{
|
||||
type: 'paragraph',
|
||||
output_variable_name: 'approval',
|
||||
default: {
|
||||
type: 'constant',
|
||||
value: 'Plain value',
|
||||
selector: [],
|
||||
},
|
||||
}}
|
||||
nodeName={nodeId => nodeId}
|
||||
/>,
|
||||
|
||||
@ -77,7 +77,7 @@ const createEmailConfig = (overrides: Partial<EmailConfig> = {}): EmailConfig =>
|
||||
})
|
||||
|
||||
const formInputs: FormInputItem[] = [{
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'name',
|
||||
default: {
|
||||
selector: ['start', 'name'],
|
||||
|
||||
@ -25,6 +25,7 @@ import { InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
import { useTestEmailSender } from '@/service/use-workflow'
|
||||
import { isParagraphFormInput } from '../../types'
|
||||
import { isOutput } from '../../utils'
|
||||
import EmailInput from './recipient/email-input'
|
||||
|
||||
@ -92,7 +93,7 @@ const EmailSenderModal = ({
|
||||
|
||||
const generatedInputs = useMemo(() => {
|
||||
const defaultValueSelectors = (formInputs || []).reduce((acc, input) => {
|
||||
if (input.default.type === 'variable') {
|
||||
if (isParagraphFormInput(input) && input.default.type === 'variable') {
|
||||
acc.push(input.default.selector)
|
||||
}
|
||||
return acc
|
||||
|
||||
@ -38,6 +38,21 @@ const FormContentPreview: FC<FormContentPreviewProps> = ({
|
||||
return node?.data.title || nodeId
|
||||
}, [nodes])
|
||||
|
||||
const renderInputPreview = React.useCallback(({ node }: { node?: { properties?: Record<string, unknown> } }) => {
|
||||
const name = String(node?.properties?.dataName ?? '')
|
||||
const input = formInputs.find(i => i.output_variable_name === name)
|
||||
if (!input) {
|
||||
return (
|
||||
<div>
|
||||
Can't find note:
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Note input={input} nodeName={nodeName} />
|
||||
}, [formInputs, nodeName])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-[112px] z-10 max-h-[calc(100vh-116px)] w-[600px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg py-3 shadow-xl"
|
||||
@ -64,22 +79,7 @@ const FormContentPreview: FC<FormContentPreviewProps> = ({
|
||||
}
|
||||
return <Variable path={newPath} />
|
||||
},
|
||||
section: ({ node }) => (() => {
|
||||
const name = String(node?.properties?.dataName ?? '')
|
||||
const input = formInputs.find(i => i.output_variable_name === name)
|
||||
if (!input) {
|
||||
return (
|
||||
<div>
|
||||
Can't find note:
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const defaultInput = input.default
|
||||
return (
|
||||
<Note defaultInput={defaultInput!} nodeName={nodeName} />
|
||||
)
|
||||
})(),
|
||||
section: renderInputPreview,
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex flex-wrap gap-1 py-1">
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type * as React from 'react'
|
||||
import type { FormInputItemDefault } from '../types'
|
||||
import type { FormInputItem } from '../types'
|
||||
import { isParagraphFormInput } from '../types'
|
||||
|
||||
const variableRegex = /\{\{#(.+?)#\}\}/g
|
||||
const noteRegex = /\{\{#\$(.+?)#\}\}/g
|
||||
@ -132,13 +133,21 @@ export const Variable: React.FC<{ path: string }> = ({ path }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const Note: React.FC<{ defaultInput: FormInputItemDefault, nodeName: (nodeId: string) => string }> = ({ defaultInput, nodeName }) => {
|
||||
const isVariable = defaultInput.type === 'variable'
|
||||
const path = `{{#${defaultInput.selector.join('.')}#}}`
|
||||
export const Note: React.FC<{ input: FormInputItem, nodeName: (nodeId: string) => string }> = ({ input, nodeName }) => {
|
||||
if (!isParagraphFormInput(input)) {
|
||||
return (
|
||||
<div className="my-3 rounded-[10px] bg-components-input-bg-normal px-2.5 py-2">
|
||||
<span>{input.type}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isVariable = input.default.type === 'variable'
|
||||
const path = `{{#${input.default.selector.join('.')}#}}`
|
||||
const newPath = path ? replaceNodeIdsWithNames(path, nodeName) : path
|
||||
return (
|
||||
<div className="my-3 rounded-[10px] bg-components-input-bg-normal px-2.5 py-2">
|
||||
{isVariable ? <Variable path={newPath} /> : <span>{defaultInput.value}</span>}
|
||||
{isVariable ? <Variable path={newPath} /> : <span>{input.default.value}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
|
||||
}))
|
||||
|
||||
const createFormInput = (overrides: Partial<FormInputItem> = {}): FormInputItem => ({
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'old_name',
|
||||
default: {
|
||||
selector: [],
|
||||
|
||||
@ -37,7 +37,7 @@ const createPayload = (overrides: Partial<HumanInputNodeType> = {}): HumanInputN
|
||||
delivery_methods: [],
|
||||
form_content: 'Summary: {{#start.topic#}}',
|
||||
inputs: [{
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'summary',
|
||||
default: {
|
||||
type: 'variable',
|
||||
|
||||
@ -8,6 +8,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { fetchHumanInputNodeStepRunForm, submitHumanInputNodeStepRunForm } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import useNodeCrud from '../../_base/hooks/use-node-crud'
|
||||
import { isParagraphFormInput } from '../types'
|
||||
import { isOutput } from '../utils'
|
||||
|
||||
const i18nPrefix = 'nodes.humanInput'
|
||||
@ -33,7 +34,7 @@ const useSingleRunFormParams = ({
|
||||
const [requiredInputs, setRequiredInputs] = useState<Record<string, string>>({})
|
||||
const generatedInputs = useMemo(() => {
|
||||
const defaultInputs = inputs.inputs.reduce((acc, input) => {
|
||||
if (input.default.type === 'variable') {
|
||||
if (isParagraphFormInput(input) && input.default.type === 'variable') {
|
||||
acc.push(`{{#${input.default.selector.join('.')}#}}`)
|
||||
}
|
||||
return acc
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type {
|
||||
CommonNodeType,
|
||||
InputVarType,
|
||||
ValueSelector,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
|
||||
export type HumanInputNodeType = CommonNodeType & {
|
||||
delivery_methods: DeliveryMethod[]
|
||||
@ -65,8 +65,77 @@ export type FormInputItemDefault = {
|
||||
value: string
|
||||
}
|
||||
|
||||
export type FormInputItem = {
|
||||
type: InputVarType
|
||||
type BaseFormInputItem = {
|
||||
output_variable_name: string
|
||||
}
|
||||
|
||||
export type ParagraphFormInput = BaseFormInputItem & {
|
||||
type: InputVarType.paragraph
|
||||
default: FormInputItemDefault
|
||||
}
|
||||
|
||||
export type SelectFormInput = BaseFormInputItem & {
|
||||
type: InputVarType.select
|
||||
}
|
||||
|
||||
export type FileFormInput = BaseFormInputItem & {
|
||||
type: InputVarType.singleFile
|
||||
}
|
||||
|
||||
export type FileListFormInput = BaseFormInputItem & {
|
||||
type: InputVarType.multiFiles
|
||||
}
|
||||
|
||||
export type FormInputItem
|
||||
= | ParagraphFormInput
|
||||
| SelectFormInput
|
||||
| FileFormInput
|
||||
| FileListFormInput
|
||||
|
||||
export const isParagraphFormInput = (
|
||||
input: FormInputItem,
|
||||
): input is ParagraphFormInput => {
|
||||
return input.type === InputVarType.paragraph
|
||||
}
|
||||
|
||||
export const isSelectFormInput = (
|
||||
input: FormInputItem,
|
||||
): input is SelectFormInput => {
|
||||
return input.type === InputVarType.select
|
||||
}
|
||||
|
||||
export const isFileFormInput = (
|
||||
input: FormInputItem,
|
||||
): input is FileFormInput => {
|
||||
return input.type === InputVarType.singleFile
|
||||
}
|
||||
|
||||
export const isFileListFormInput = (
|
||||
input: FormInputItem,
|
||||
): input is FileListFormInput => {
|
||||
return input.type === InputVarType.multiFiles
|
||||
}
|
||||
|
||||
export const isFileLikeFormInput = (
|
||||
input: FormInputItem,
|
||||
): input is FileFormInput | FileListFormInput => {
|
||||
return input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles
|
||||
}
|
||||
|
||||
export const supportsDefaultValue = (
|
||||
input: FormInputItem,
|
||||
): input is ParagraphFormInput => {
|
||||
return isParagraphFormInput(input)
|
||||
}
|
||||
|
||||
export const createDefaultParagraphFormInput = (
|
||||
output_variable_name = '',
|
||||
): ParagraphFormInput => ({
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name,
|
||||
default: {
|
||||
type: 'constant',
|
||||
selector: [],
|
||||
value: '',
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user