antd#FormInstance TypeScript Examples
The following examples show how to use
antd#FormInstance.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts From posthog-foss with MIT License | 7 votes |
doFieldRequirementsMatch = (
form: FormInstance<any>,
targetFieldName: string | undefined,
targetFieldValue: string | undefined
): boolean => {
const formActualValue = form.getFieldValue(targetFieldName || '') || ''
const targetAnyValue = typeof targetFieldValue === 'undefined'
const formValueSet = !!formActualValue
return (targetAnyValue && formValueSet) || targetFieldValue === formActualValue
}
Example #2
Source File: useAntdForm.ts From jmix-frontend with Apache License 2.0 | 6 votes |
export function useAntdForm<TEntity>(form: FormInstance, item: EntityInstance<TEntity>, entityName: string) {
const metadata = useMetadata();
useEffect(() => {
if (item != null && metadata != null) {
form.setFieldsValue(
jmixFront_to_ant(item, entityName, metadata)
);
}
}, [form, item, metadata, entityName]);
}
Example #3
Source File: MultipleFilesForm.tsx From next-basics with GNU General Public License v3.0 | 6 votes |
export function LegacyMultipleFilesForm(
{ fieldList, onFinish, onFinishFailed }: MultipleFilesFormProps,
ref: React.Ref<FormInstance>
): React.ReactElement {
const [form] = Form.useForm();
useImperativeHandle(ref, () => form);
const handleFinish = (data: UploadFormData): void => {
const formData = processFileData(data);
onFinish?.(formData);
};
return (
<Form
form={form}
name="filesForm"
data-testid="files-form"
onFinish={handleFinish}
onFinishFailed={onFinishFailed}
>
{fieldList?.map((item) => (
<Form.Item key={item.name} name={item.name} label={item.name}>
<Upload
maxCount={isMultipleFiles(item.type) ? null : 1}
beforeUpload={() => false}
>
<Button icon={<UploadOutlined />}>upload</Button>
</Upload>
</Form.Item>
))}
</Form>
);
}
Example #4
Source File: index.tsx From erda-ui with GNU Affero General Public License v3.0 | 6 votes |
formatSearchList = () => {
const searchList = this.props.searchList.map((search) => {
return {
required: false,
initialValue: this.getFormInitialValue(search.type),
extraProps: {
colon: false,
required: false,
},
...search,
};
});
searchList.push({
getComp: ({ form }: { form: FormInstance }) => (
<React.Fragment>
<Button
className="ops-bar-btn ops-bar-default-btn ops-bar-reset-btn"
type="default"
onClick={(e: any) => this.handleReset(e, form)}
>
{i18n.t('reset')}
</Button>
<Button className="ops-bar-btn" type="primary" ghost onClick={() => this.handleSubmit(form)}>
{i18n.t('search')}
</Button>
</React.Fragment>
),
});
return searchList;
};
Example #5
Source File: SourcePage.utils.tsx From jitsu with MIT License | 6 votes |
getAntdFormAndKeyByOauthFieldKey = (
forms: {
[key: string]: {
form: FormInstance<PlainObjectWithPrimitiveValues>
patchConfigOnFormValuesChange?: (values: PlainObjectWithPrimitiveValues) => void
}
},
oauthFieldKey: string
): [
{
form: FormInstance<PlainObjectWithPrimitiveValues>
patchConfigOnFormValuesChange?: (values: PlainObjectWithPrimitiveValues) => void
} | null,
string | null
] => {
let allFormsKeys: string[] = []
const allFormsWithValues: {
[key: string]: {
form: {
form: FormInstance<PlainObjectWithPrimitiveValues>
patchConfigOnFormValuesChange?: (values: PlainObjectWithPrimitiveValues) => void
}
values: PlainObjectWithPrimitiveValues
}
} = Object.entries(forms).reduce((result, [formKey, formData]) => {
const values = formData.form.getFieldsValue()
allFormsKeys = [...allFormsKeys, ...Object.keys(values)]
return {
...result,
[formKey]: {
form: formData,
values,
},
}
}, {})
const formKey =
allFormsKeys.find(_formKey => {
const formKeyNameEnd = _formKey.split(".").pop() // gets access_token from config.config.access_token
const formKey = formKeyNameEnd.replace("_", "").toLowerCase() // viewid <- viewId, accesstoken <- access_token
const parsedOauthFieldKey = oauthFieldKey.replace("_", "").toLowerCase()
return formKey === parsedOauthFieldKey
}) ?? null
const { form } = formKey ? Object.values(allFormsWithValues).find(({ values }) => formKey in values) : { form: null }
return [form, formKey]
}
Example #6
Source File: useMultipleFiltersModal.tsx From condo with MIT License | 6 votes |
function getModalComponents <T> (filters: IFilters, filterMetas: Array<FiltersMeta<T>>, form: FormInstance): React.ReactElement[] { if (!form) return return filterMetas.map(filterMeta => { const { keyword, component } = filterMeta const modalFilterComponentWrapper = get(component, 'modalFilterComponentWrapper') if (!modalFilterComponentWrapper) return const size = get(modalFilterComponentWrapper, 'size') const label = get(modalFilterComponentWrapper, 'label') const formItemProps = get(modalFilterComponentWrapper, 'formItemProps') const type = get(component, 'type') let Component if (type === ComponentType.Custom) { const componentGetter = get(component, 'modalFilterComponent') Component = isFunction(componentGetter) ? componentGetter(form) : componentGetter } else Component = getModalFilterComponentByMeta(filters, keyword, component, form) const queryToValueProcessor = getQueryToValueProcessorByType(type) return ( <FilterComponent key={keyword} name={keyword} filters={filters} size={size} label={label} formItemProps={formItemProps} queryToValueProcessor={queryToValueProcessor} > {Component} </FilterComponent> ) }) }
Example #7
Source File: FilterAction.tsx From datart with Apache License 2.0 | 6 votes |
FilterAction: FC<{
config: ChartDataSectionField;
dataset?: ChartDataSetDTO;
dataView?: ChartDataView;
dataConfig?: ChartDataConfig;
aggregation?: boolean;
onConfigChange: (
config: ChartDataSectionField,
needRefresh?: boolean,
) => void;
form?: FormInstance;
}> = memo(
({
config,
dataset,
dataView,
dataConfig,
onConfigChange,
aggregation,
form,
}) => {
const handleFetchDataFromField = async fieldId => {
// TODO: to be implement to get fields
return await Promise.resolve(['a', 'b', 'c'].map(f => `${fieldId}-${f}`));
};
return (
<FilterControlPanel
aggregation={aggregation}
config={config}
dataset={dataset}
dataConfig={dataConfig}
dataView={dataView}
onConfigChange={onConfigChange}
fetchDataByField={handleFetchDataFromField}
form={form}
/>
);
},
)
Example #8
Source File: createUseAntdForm.ts From jmix-frontend with Apache License 2.0 | 5 votes |
export function createUseAntdForm<TEntity>(form: FormInstance): (item: EntityInstance<TEntity>, entityName: string) => void {
return useAntdForm.bind(null, form);
}
Example #9
Source File: DendronLookupPanel.tsx From dendron with GNU Affero General Public License v3.0 | 5 votes |
function LookupViewForm({
ide,
form,
}: {
ide: ideSlice.IDEState;
form: FormInstance;
}) {
const pressed = ide.lookupModifiers
? ide.lookupModifiers.filter((mod) => {
return mod.pressed;
})
: [];
// update selection type form
const selectionTypeState = pressed.filter((mod) => {
if (Object.keys(LookupSelectionTypeEnum).includes(mod.type)) {
form.setFieldsValue({ selection: mod.type });
return true;
}
return false;
});
if (selectionTypeState.length === 0) {
form.setFieldsValue({ selection: undefined });
}
// update note type form
const noteTypeState = pressed.filter((mod) => {
if (Object.keys(LookupNoteTypeEnum).includes(mod.type)) {
form.setFieldsValue({ note: mod.type });
return true;
}
return false;
});
if (noteTypeState.length === 0) {
form.setFieldsValue({ note: undefined });
}
// update effect type form
form.setFieldsValue({
effect: pressed
.filter((mod) => {
return Object.keys(LookupEffectTypeEnum).includes(mod.type);
})
.map((mod) => mod.type),
});
// update horizontal split switch
form.setFieldsValue({
horizontalSplit:
pressed.filter((mod) => {
return Object.keys(LookupSplitTypeEnum).includes(mod.type);
}).length === 1,
});
// update direct child only switch
form.setFieldsValue({
directChildOnly:
pressed.filter((mod) => {
return Object.keys(LookupFilterTypeEnum).includes(mod.type);
}).length === 1,
});
return (
<>
<Form form={form}>
<SelectionTypeFormItem />
<EffectTypeFormItem />
<SplitTypeFormItem />
<FilterTypeFormItem />
</Form>
</>
);
}
Example #10
Source File: index.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
private _form = createRef<FormInstance>();
Example #11
Source File: index.tsx From erda-ui with GNU Affero General Public License v3.0 | 5 votes |
handleSubmit = (form: FormInstance) => {
const formValue = form.getFieldsValue();
this.props.onUpdateOps && this.props.onUpdateOps(formValue);
};
Example #12
Source File: SourcePage.utils.tsx From jitsu with MIT License | 5 votes |
sourcePageUtils = {
getSourceType: (sourceConnector: SourceConnector) =>
sourceConnector?.protoType ? sourceConnector?.protoType : snakeCase(sourceConnector?.id),
getSourcePrototype: (sourceConnector: SourceConnector): string => snakeCase(sourceConnector?.id),
getSourceId: (sourceProtoType: string, sourcesIds: string[]) => {
sourceProtoType = sourceProtoType.replace("airbyte-source-", "").replace("singer-tap-", "")
const isUniqueSourceId = !sourcesIds.find(id => id === sourceProtoType)
if (isUniqueSourceId) {
return sourceProtoType
}
return getUniqueAutoIncId(sourceProtoType, sourcesIds)
},
getPromptMessage: (tabs: Tab[]) => () =>
tabs.some(tab => tab.touched) ? "You have unsaved changes. Are you sure you want to leave the page?" : undefined,
testConnection: async (
src: SourceData,
hideMessage?: boolean,
_options?: { skipHandleError?: boolean }
): Promise<TestConnectionResponse> => {
const options = _options ?? {}
let connectionTestMessagePrefix: string | undefined
try {
const response = await ApplicationServices.get().backendApiClient.post("/sources/test", Marshal.toPureJson(src))
if (response["status"] === "pending") {
actionNotification.loading(
"Please, allow some time for the connector source installation to complete. Once the connector source is installed, we will test the connection and send a push notification with the result."
)
connectionTestMessagePrefix = `Source ${src.sourceId} connection test result: `
const POLLING_INTERVAL_MS = 2000
const POLLING_TIMEOUT_MS = 60_000
const poll = new Poll<void>(
(end, fail) => async () => {
try {
const response = await ApplicationServices.get().backendApiClient.post(
"/sources/test",
Marshal.toPureJson(src)
)
const status = response["status"]
if (status !== "pending") end()
else if (status !== "ok")
fail(new Error(`Tap connection test returned an error. ${response["error"] ?? "Unknown error."}`))
} catch (error) {
fail(error)
}
},
POLLING_INTERVAL_MS,
POLLING_TIMEOUT_MS
)
poll.start()
await poll.wait()
}
if (!hideMessage) {
const message = "Successfully connected"
actionNotification.success(
connectionTestMessagePrefix ? `${connectionTestMessagePrefix}${message.toLowerCase()}` : message
)
}
return {
connected: true,
connectedErrorType: undefined,
connectedErrorMessage: undefined,
connectedErrorPayload: undefined,
}
} catch (error) {
if (!hideMessage) {
const message = "Connection test failed"
const prefixedMessage = connectionTestMessagePrefix
? `${connectionTestMessagePrefix}${message.toLowerCase()}`
: message
if (!options.skipHandleError) handleError(error, prefixedMessage)
}
const errorType: TestConnectionErrorType = `${error}`.includes("selected streams unavailable")
? "streams_changed"
: "general"
return {
connected: false,
connectedErrorType: errorType,
connectedErrorMessage: error.message ?? "Failed to connect",
connectedErrorPayload: error._response?.payload,
}
}
},
applyOauthValuesToAntdForms: (
forms: {
[key: string]: {
form: FormInstance<PlainObjectWithPrimitiveValues>
patchConfigOnFormValuesChange?: (values: PlainObjectWithPrimitiveValues) => void
}
},
oauthValues: PlainObjectWithPrimitiveValues
): boolean => {
const oauthFieldsSuccessfullySet: string[] = []
const oauthFieldsNotSet: string[] = []
Object.entries(oauthValues).forEach(([oauthFieldKey, oauthFieldValue]) => {
const [formToApplyValue, fieldKeyToApplyValue] = getAntdFormAndKeyByOauthFieldKey(forms, oauthFieldKey)
if (!formToApplyValue || !fieldKeyToApplyValue) {
oauthFieldsNotSet.push(oauthFieldKey)
return
}
const newValues = { ...formToApplyValue.form.getFieldsValue() }
newValues[fieldKeyToApplyValue] = oauthFieldValue
formToApplyValue.form.setFieldsValue(newValues)
formToApplyValue.patchConfigOnFormValuesChange?.(newValues)
oauthFieldsSuccessfullySet.push(oauthFieldKey)
})
if (oauthFieldsSuccessfullySet.length > 0) {
actionNotification.success(`Authorization Successful`)
return true
}
/* handles the case when failed to set all fields */
if (oauthFieldsNotSet.length > 0 && oauthFieldsSuccessfullySet.length === 0) {
const messagePostfix =
"Did you forget to select OAuth authorization type in the form below? If you believe that this is an error, please, contact us at [email protected] or file an issue to our github."
const secretsNamesSeparator = oauthFieldsNotSet.length === 2 ? " and " : ", "
const message = `Failed to paste ${oauthFieldsNotSet
.map(key => toTitleCase(key, { separator: "_" }))
.join(secretsNamesSeparator)} secret${oauthFieldsNotSet.length > 1 ? "s" : ""}. ${messagePostfix}`
actionNotification.warn(message)
return false
}
},
}
Example #13
Source File: index.tsx From ant-simple-draw with MIT License | 5 votes |
OlUl: FC<{ keyName: string; form: FormInstance<Store>; showEditPropsData: any }> = memo(
({ keyName, form, showEditPropsData }) => {
const [type, setType] = useState<string | undefined>(undefined);
const ulType = [
{ label: '小圆点', value: 'disc' },
{ label: '空心圆圈', value: 'circle' },
{ label: '小方块', value: 'square' },
];
const olType = [
{ label: '1', value: '1' },
{ label: 'A', value: 'A' },
{ label: 'I', value: 'I' },
];
useEffect(() => {
if (showEditPropsData[keyName]) {
// 将数据流的数据同步一下
setType(showEditPropsData[keyName].type);
}
}, [showEditPropsData, keyName]);
const selectData = useMemo(() => {
return type === 'ol' ? olType : ulType;
}, [type]);
return (
<>
<Form.Item label={null} name={[keyName, 'type']} style={{ marginBottom: '16px' }}>
<Radio.Group
onChange={(val) => {
setType(val.target.value);
form.setFieldsValue({
[keyName]: {
attrType: undefined,
},
});
}}
>
<Radio value={'ol'}>有序列表</Radio>
<Radio value={'ul'}>无序列表</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label={'序列'} name={[keyName, 'attrType']} style={{ marginBottom: '16px' }}>
<Selects data={selectData} valKey="value" valName="label" />
</Form.Item>
<Form.List name={[keyName, 'list']}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space
key={key}
style={{ display: 'flex', marginBottom: 8, justifyContent: 'space-around' }}
align="baseline"
>
<Form.Item {...restField} name={[name, 'text']} style={{ marginBottom: '16px' }}>
<Input />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} title="移除" />
</Space>
))}
<Form.Item style={{ marginBottom: '16px' }}>
<Button onClick={() => add()} block icon={<PlusOutlined />}>
添加数据
</Button>
</Form.Item>
</>
)}
</Form.List>
</>
);
},
)
Example #14
Source File: useModalFilterClassifiers.tsx From condo with MIT License | 5 votes |
useTicketClassifierSelect = ({
onChange,
keyword,
}) => {
const intl = useIntl()
const SelectMessage = intl.formatMessage({ id: 'Select' })
const [classifiers, setClassifiersFromRules] = useState([])
const [searchClassifiers, setSearchClassifiers] = useState([])
const [stateForm, setForm] = useState<FormInstance>(null)
const classifiersRef = useRef(null)
const optionsRef = useRef([])
const setClassifiers = (classifiers) => {
setClassifiersFromRules(classifiers)
setSearchClassifiers([])
}
function setSelected (value) {
stateForm && stateForm.setFieldsValue({ [keyword]: value })
}
const Setter = useMemo(() => ({
all: setClassifiers,
one: setSelected,
search: setSearchClassifiers,
}), [])
useEffect(() => {
optionsRef.current = uniqBy([...classifiers, ...searchClassifiers], 'id')
}, [classifiers, searchClassifiers])
const handleChange = (form: FormInstance, value) => {
if (isFunction(onChange)) onChange(value)
form.setFieldsValue({ [keyword]: value })
}
const SelectComponent = useCallback( (props) => {
const { disabled, style, form } = props
if (!stateForm)
setForm(stateForm)
return (
<Select
showSearch
showArrow
style={style}
onChange={(value) => handleChange(form, value)}
optionFilterProp={'title'}
disabled={disabled}
value={form.getFieldValue(keyword)}
showAction={SHOW_SELECT_ACTIONS}
mode={'multiple'}
placeholder={SelectMessage}
getPopupContainer={getFiltersModalPopupContainer}
>
{
Array.isArray(optionsRef.current) && optionsRef.current.map(classifier => (
<Option value={classifier.id} key={classifier.id} title={classifier.name}>
{classifier.name}
</Option>
))
}
</Select>
)
}, [SelectMessage, handleChange, keyword, stateForm])
return {
SelectComponent,
set: Setter,
ref: classifiersRef,
}
}
Example #15
Source File: create.tsx From dashboard with Apache License 2.0 | 5 votes |
CreateCustomerMassMsg: React.FC = () => {
const [currentCustomerMassMsg] = useState<CustomerMassMsgItem>();
const formRef = useRef<FormInstance>();
return (
<PageContainer
onBack={() => history.goBack()}
backIcon={<LeftOutlined />}
header={{
title: '创建群发',
}}
>
<ProCard>
<CustomerMassMsgForm
formRef={formRef}
mode={'create'}
onFinish={async (values) => {
const params = { ...values };
const hide = message.loading('处理中');
const res: CommonResp = await Create(params);
hide();
if (res.code === 0) {
history.push('/staff-admin/customer-conversion/customer-mass-msg');
message.success('添加成功');
return true;
}
if (res.message) {
message.error(res.message);
return false;
}
message.error('添加失败');
return false;
}}
initialValues={currentCustomerMassMsg}
/>
</ProCard>
</PageContainer>
);
}
Example #16
Source File: InviteForm.tsx From datart with Apache License 2.0 | 5 votes |
InviteForm = memo(
({ formProps, afterClose, ...modalProps }: ModalFormProps) => {
const [options, setOptions] = useState<ValueType[]>([]);
const formRef = useRef<FormInstance>();
const t = useI18NPrefix('member.form');
const tgv = useI18NPrefix('global.validation');
const debouncedSearchUser = useMemo(() => {
const searchUser = async (val: string) => {
if (!val.trim()) {
setOptions([]);
} else {
const { data } = await request<User[]>(
`/users/search?keyword=${val}`,
);
setOptions(
data.map(({ email, username, name }) => ({
key: username,
value: email,
label: `${name ? `[${name}]` : ''}${email}`,
})),
);
}
};
return debounce(searchUser, DEFAULT_DEBOUNCE_WAIT);
}, []);
const filterOption = useCallback((keywords: string, option) => {
const { key, value, label } = option;
return (
key?.includes(keywords) ||
value.toString().includes(keywords) ||
label?.toString().includes(keywords)
);
}, []);
const onAfterClose = useCallback(() => {
formRef.current?.resetFields();
setOptions([]);
afterClose && afterClose();
}, [afterClose]);
return (
<ModalForm
formProps={formProps}
{...modalProps}
afterClose={onAfterClose}
ref={formRef}
>
<Form.Item
name="emails"
rules={[
{ required: true, message: `${t('email')}${tgv('required')}` },
]}
>
<Select<ValueType>
mode="tags"
placeholder={t('search')}
options={options}
filterOption={filterOption}
onSearch={debouncedSearchUser}
/>
</Form.Item>
<Form.Item name="sendMail" valuePropName="checked" initialValue={true}>
<Checkbox>{t('needConfirm')}</Checkbox>
</Form.Item>
</ModalForm>
);
},
)
Example #17
Source File: interfaceJobsLogic.ts From posthog-foss with MIT License | 4 votes |
interfaceJobsLogic = kea<interfaceJobsLogicType>({
props: {} as {
jobName: string
pluginConfigId: number
pluginId: number
jobSpecPayload: JobSpec['payload']
},
key: (props) => {
return `${props.pluginId}_${props.jobName}`
},
path: (key) => ['scenes', 'plugins', 'edit', 'interface-jobs', 'interfaceJobsLogic', key],
connect: {
actions: [pluginsLogic, ['showPluginLogs']],
},
actions: {
setIsJobModalOpen: (isOpen: boolean) => ({ isOpen }),
setRunJobAvailable: (isAvailable: boolean) => ({ isAvailable }),
runJob: (form: FormInstance<any>) => ({ form }),
playButtonOnClick: (form: FormInstance<any>, jobHasEmptyPayload: boolean) => ({ form, jobHasEmptyPayload }),
setRunJobAvailableTimeout: (timeout: NodeJS.Timeout) => ({ timeout }),
},
reducers: {
isJobModalOpen: [
false,
{
setIsJobModalOpen: (_, { isOpen }) => isOpen,
},
],
runJobAvailable: [
true,
{
setRunJobAvailable: (_, { isAvailable }) => isAvailable,
},
],
runJobAvailableTimeout: [
null as NodeJS.Timeout | null,
{
setRunJobAvailableTimeout: (_, { timeout }) => timeout,
},
],
},
listeners: ({ actions, props, values }) => ({
runJob: async ({ form }) => {
try {
await form.validateFields()
} catch {
return
}
actions.setIsJobModalOpen(false)
const formValues = form.getFieldsValue()
for (const [fieldKey, fieldValue] of Object.entries(formValues)) {
if (props.jobSpecPayload?.[fieldKey].type === 'date') {
if (!!formValues[fieldKey]) {
formValues[fieldKey] = (fieldValue as moment.Moment).toISOString()
} else {
formValues[fieldKey] = null
}
}
}
try {
await api.create(`api/plugin_config/${props.pluginConfigId}/job`, {
job: {
type: props.jobName,
payload: form.getFieldsValue(),
},
})
} catch (error) {
errorToast(`Enqueuing job '${props.jobName}' failed`)
return
}
actions.showPluginLogs(props.pluginId)
// temporary handling to prevent people from rage
// clicking and creating multiple jobs - this will be
// subsituted by better feedback tools like progress bars
actions.setRunJobAvailable(false)
if (values.runJobAvailableTimeout) {
clearTimeout(values.runJobAvailableTimeout)
}
setTimeout(() => {
const timeout = actions.setRunJobAvailable(true)
actions.setRunJobAvailableTimeout(timeout)
}, 15000)
toast.success('Job enqueued succesfully.')
},
playButtonOnClick: ({ form, jobHasEmptyPayload }) => {
if (!values.runJobAvailable) {
return
}
if (jobHasEmptyPayload) {
actions.runJob(form)
return
}
actions.setIsJobModalOpen(true)
},
}),
events: ({ values }) => ({
beforeUnmount: () => {
if (values.runJobAvailableTimeout) {
clearTimeout(values.runJobAvailableTimeout)
}
},
}),
})
Example #18
Source File: cmd-rename-node-modal.tsx From XFlow with MIT License | 4 votes |
function showModal(node: NsGraph.INodeConfig, getAppContext: IGetAppCtx) {
/** showModal 返回一个Promise */
const defer = new Deferred<string | void>()
/** modal确认保存逻辑 */
class ModalCache {
static modal: IModalInstance
static form: FormInstance<IFormProps>
}
/** modal确认保存逻辑 */
const onOk = async () => {
const { form, modal } = ModalCache
const appContext = getAppContext()
const { updateNodeNameService, graphMeta } = appContext
try {
modal.update({ okButtonProps: { loading: true } })
await form.validateFields()
const values = await form.getFieldsValue()
const newName: string = values.newNodeName
/** 执行 backend service */
if (updateNodeNameService) {
const { err, nodeName } = await updateNodeNameService(newName, node, graphMeta)
if (err) {
throw new Error(err)
}
defer.resolve(nodeName)
}
/** 更新成功后,关闭modal */
onHide()
} catch (error) {
console.error(error)
/** 如果resolve空字符串则不更新 */
modal.update({ okButtonProps: { loading: false } })
}
}
/** modal销毁逻辑 */
const onHide = () => {
modal.destroy()
ModalCache.form = null as any
ModalCache.modal = null as any
container.destroy()
}
/** modal内容 */
const ModalContent = () => {
const [form] = Form.useForm<IFormProps>()
/** 缓存form实例 */
ModalCache.form = form
return (
<div>
<ConfigProvider>
<Form form={form} {...layout} initialValues={{ newNodeName: node.label }}>
<Form.Item
name="newNodeName"
label="节点名"
rules={[
{ required: true, message: '请输入新节点名' },
{ min: 3, message: '节点名不能少于3个字符' },
]}
>
<Input />
</Form.Item>
</Form>
</ConfigProvider>
</div>
)
}
/** 创建modal dom容器 */
const container = createContainer()
/** 创建modal */
const modal = Modal.confirm({
title: '重命名',
content: <ModalContent />,
getContainer: () => {
return container.element
},
okButtonProps: {
onClick: e => {
e.stopPropagation()
onOk()
},
},
onCancel: () => {
onHide()
},
afterClose: () => {
onHide()
},
})
/** 缓存modal实例 */
ModalCache.modal = modal
/** showModal 返回一个Promise,用于await */
return defer.promise
}
Example #19
Source File: index.test.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
describe('ConfigurableFilter', () => {
const externalFieldsList = [
{
label: '',
type: 'input',
outside: true,
key: 'keyword',
placeholder: 'search by keywords',
customProps: {
autoComplete: 'off',
},
},
];
const fieldsList = [
{
label: 'APP_NAME',
type: 'input',
key: 'keyword',
placeholder: 'search by keywords',
customProps: {
autoComplete: 'off',
},
},
{
label: 'ITERATION',
type: 'select',
key: 'iteration',
placeholder: 'please select iteration',
options: [
{ label: 'iteration-1.1', value: 123 },
{ label: 'iteration-1.2', value: 124 },
{ label: 'iteration-1.3', value: 125 },
],
},
{
key: 'createdAtStartEnd',
label: 'CREATE_AT',
type: 'dateRange',
},
];
const initialInsideFieldsValue = fieldsList.reduce(
(prev, curr) => ({
...prev,
[curr.key]: undefined,
}),
{},
);
beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
_.debounce = (fn: Function) => fn;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
_.throttle = (fn: Function) => fn;
});
afterAll(() => {
jest.resetAllMocks();
});
const setUp = (props?: Partial<IProps>) => {
const filterFn = jest.fn();
const closeFn = jest.fn();
const clearFn = jest.fn();
const configChangeFn = jest.fn();
const saveFilterFn = jest.fn();
const deleteFilterFn = jest.fn();
const filterRef: React.Ref<{ form: FormInstance }> = React.createRef();
let firstOpen = true;
const Comp = (c_props: Partial<IProps>) => {
const newProps = {
fieldsList,
...c_props,
} as IProps;
return (
<ConfigurableFilter
{...newProps}
onFilter={filterFn}
onClose={closeFn}
onSaveFilter={saveFilterFn}
onDeleteFilter={deleteFilterFn}
onClear={clearFn}
onConfigChange={configChangeFn}
ref={filterRef}
/>
);
};
const result = render(<Comp {...props} />);
const rerender = (n_props: Partial<IProps>) => {
result.rerender(<Comp {...n_props} />);
};
const openFilter = async () => {
fireEvent.click(result.baseElement.querySelector('.erda-configurable-filter-btn')!);
if (firstOpen) {
await waitFor(() =>
expect(result.baseElement.querySelector('.erda-configurable-filter')).not.toHaveStyle({ display: 'none' }),
);
} else {
await waitFor(() =>
expect(result.baseElement).not.isExistClass('.erda-configurable-filter', 'ant-popover-hidden'),
);
}
firstOpen = false;
};
return {
result,
rerender,
filterRef,
filterFn,
closeFn,
clearFn,
configChangeFn,
saveFilterFn,
deleteFilterFn,
openFilter,
};
};
it('should work well without insideFields', async () => {
const { result, rerender, filterFn } = setUp({ fieldsList: externalFieldsList });
expect(result.container).isExist('.erda-configurable-filter-btn', 0);
await act(async () => {
fireEvent.change(result.getByPlaceholderText('Search by keywords'), { target: { value: 'erda' } });
await flushPromises();
});
expect(filterFn).toHaveBeenLastCalledWith({ keyword: 'erda' });
filterFn.mockReset();
rerender({
fieldsList: externalFieldsList,
value: { keyword: 'erda cloud' },
});
await act(async () => {
fireEvent.change(result.getByPlaceholderText('Search by keywords'), { target: { value: 'erda cloud' } });
await flushPromises();
});
expect(filterFn).not.toHaveBeenCalled();
});
it('should work well with insideFields', async () => {
const { result, rerender, filterFn, closeFn, openFilter, clearFn } = setUp({ zIndex: 10, hideSave: true });
await openFilter();
fireEvent.click(result.getByText('Cancel'));
await waitFor(() => expect(result.baseElement).isExistClass('.erda-configurable-filter', 'ant-popover-hidden'));
expect(closeFn).toHaveBeenCalledTimes(1);
await openFilter();
fireEvent.click(result.baseElement.querySelector('[name="guanbi"]')!);
expect(closeFn).toHaveBeenCalledTimes(2);
await openFilter();
fireEvent.change(result.getByPlaceholderText('Search by keywords'), { target: { value: 'erda' } });
fireEvent.mouseDown(result.baseElement.querySelector('.ant-select-selector')!);
await waitFor(() => expect(result.baseElement).isExist('.ant-select-dropdown', 1));
fireEvent.click(result.getByText('iteration-1.1'));
await act(async () => {
fireEvent.click(result.getByText('Filter', { selector: 'button span' }));
await flushPromises();
});
expect(filterFn).toHaveBeenLastCalledWith({
...initialInsideFieldsValue,
iteration: [123],
keyword: 'erda',
});
rerender({
zIndex: 10,
hideSave: true,
value: {
...initialInsideFieldsValue,
iteration: [123],
keyword: 'erda',
},
});
expect(result.baseElement).isExist('.erda-configurable-filter-clear-btn', 1);
await act(async () => {
fireEvent.click(result.baseElement.querySelector('.erda-configurable-filter-clear-btn')!);
await flushPromises();
});
expect(filterFn).toHaveBeenLastCalledWith(initialInsideFieldsValue);
expect(clearFn).toHaveBeenCalledTimes(1);
rerender({
zIndex: 10,
hideSave: true,
value: {
...initialInsideFieldsValue,
},
});
expect(result.baseElement).isExist('.erda-configurable-filter-clear-btn', 0);
});
it('should work well with insideFields and haveSave', async () => {
const configList = [
{
id: 'all',
isPreset: true,
label: 'openAll',
values: {},
},
{
id: 'defaultState',
isPreset: true,
label: 'presetFilter',
values: {
iteration: [123],
},
},
{
id: 'customFilter',
isPreset: false,
label: 'customFilter',
values: {
keyword: 'cloud',
},
},
];
const { result, rerender, filterFn, closeFn, openFilter, clearFn, configChangeFn, saveFilterFn } = setUp({
zIndex: 10,
hideSave: false,
configList,
});
await openFilter();
expect(result.baseElement).isExistClass('.erda-configurable-filter', 'w-[960px]');
await act(async () => {
fireEvent.click(result.getByText('presetFilter').closest('.filter-config-selector-item')!);
await flushPromises();
});
expect(filterFn).toHaveBeenLastCalledWith({
...initialInsideFieldsValue,
iteration: [123],
});
fireEvent.change(result.getByPlaceholderText('Search by keywords'), { target: { value: 'erda' } });
fireEvent.click(result.getByText('new filter'));
await waitFor(() => expect(result.baseElement).isExist('.erda-configurable-filter-add', 1));
fireEvent.click(result.getByText('Cancel', { selector: '.erda-configurable-filter-add button span' }));
expect(result.baseElement).isExistClass('.erda-configurable-filter-add', 'ant-popover-hidden');
fireEvent.click(result.getByText('new filter'));
expect(result.baseElement).not.isExistClass('.erda-configurable-filter-add', 'ant-popover-hidden');
fireEvent.change(result.getByPlaceholderText('Please enter, within 10 characters'), {
target: { value: 'ErdaFilter' },
});
await act(async () => {
fireEvent.click(result.getByText('OK'));
await flushPromises();
});
expect(saveFilterFn).toHaveBeenLastCalledWith('ErdaFilter', {
createdAtStartEnd: undefined,
iteration: [123],
keyword: 'erda',
});
await act(async () => {
fireEvent.click(
result
.getByText('openAll', { selector: '.filter-config-selector-item .truncate' })
.closest('.filter-config-selector-item')!,
);
await flushPromises();
});
fireEvent.change(result.getByPlaceholderText('Search by keywords'), { target: { value: 'cloud' } });
expect(result.getByText('customFilter').closest('.filter-config-selector-item')).toHaveClass('bg-default-04');
});
});
Example #20
Source File: index.tsx From react-resume-site with GNU General Public License v3.0 | 4 votes |
HeaderBar = observer(() => {
const { templateStore } = useStores();
const { setTempTheme , tempTheme, theme, color, setColor, setTheme, setPreview, mdContent, isPreview } = templateStore;
const [isModalVisible, setIsModalVisible] = useState(false);
const [isExportVisible, setIsExportVisible] = useState(false);
const [isUsageVisible, setIsUsageVisible] = useState(false);
const [isUpdateVisible, setIsUpdateVisible] = useState(is_update);
const formRef = useRef<FormInstance>(null);
const handleOk = async () => {
// 更新模板
await updateTempalte(tempTheme, color, setColor);
// 设置模板
setTheme(tempTheme);
// 关闭弹窗
setIsModalVisible(false);
};
const uploadMdFile = useCallback((e: any) => {
let resultFile = e.target.files[0];
var reader = new FileReader();
reader.readAsText(resultFile);
reader.onload = (e) => {
if (e.target?.result) {
mdEditorRef && (mdEditorRef.setValue(e.target.result));
setPreview(false);
renderViewStyle(color);
}
};
}, []);
const exportMdFile = useCallback(() => {
const file = new Blob([mdContent]);
const url = URL.createObjectURL(file);
downloadDirect(url, "木及简历.md");
}, [mdContent]);
const templateContent = (
<div className="template-wrapper">
{themes.map((item) => {
return (
<div
className={`template ${item.id === tempTheme ? "active" : ""}`}
key={item.id}
onClick={(e) => {
e.preventDefault();
setTempTheme(item.id);
}}
>
<img className="template-img" src={item.src}></img>
<p className="template-title">{item.name}
{item.isColor && <Tag color="#2db7f5">可换色</Tag>}
</p>
</div>
);
})}
</div>
);
const filesMenu = (
<Menu>
<Menu.Item>
<label htmlFor="uploadMdFile">
<a rel="noopener noreferrer">导入md</a>
<input
type="file"
id="uploadMdFile"
accept=".md"
className="uploadMd"
onChange={uploadMdFile}
></input>
</label>
</Menu.Item>
<Menu.Item>
<a rel="noopener noreferrer" onClick={exportMdFile}>
导出md
</a>
</Menu.Item>
</Menu>
);
const handleExport = () => {
formRef.current?.submit();
setIsExportVisible(false);
};
const exportPdf = async ({
name,
isOnePage,
isMark,
}: {
name: string;
isOnePage: boolean;
isMark: boolean;
}) => {
// 设置渲染
const rsViewer = document.querySelector(".rs-view") as HTMLElement;
if (!isPreview) {
setPreview(true);
htmlParser(rsViewer);
}
const pages = rsViewer.dataset.pages || '1';
const rsLine = document.querySelectorAll('.rs-line-split');
rsLine.forEach(item => item.parentNode?.removeChild(item));
const content = localStorage.getItem(LOCAL_STORE.MD_RESUME);
if (content) {
const htmlContent = document.querySelector('.rs-view-inner')?.innerHTML.replace(/(\n|\r)/g, "");
let hide = message.loading("正在为你生成简历...", 0);
if (globalEditorCount < 2) {
try {
hide();
const curThemes = themes.filter(item => item.id === theme);
await downloadFetch(curThemes[0].defaultUrl, name ? `${name}.pdf` : "木及简历.pdf");
} catch (e) {
hide();
}
return;
}
const themeColor = getComputedStyle(document.body).getPropertyValue(
"--bg"
);
try {
let data = await getPdf({
htmlContent: String(htmlContent),
theme,
themeColor,
isMark,
isOnePage,
pages
});
await downloadFetch(data.url, name ? `${name}.pdf` : "木及简历.pdf");
hide();
message.success("恭喜你,导出成功!")
} catch (e) {
hide();
message.error("生成简历出错,请稍再试!");
}
setPreview(false);
renderViewStyle(color);
}
};
useEffect(() => {
getTheme(theme);
}, []);
return (
<div className="rs-header-bar rs-link">
<div className="rs-header-bar__left">
{/* <a className="rs-logo rs-link">
<img src="https://s3.qiufeng.blue/muji/muji-logo.jpg" alt=""/>
木及简历
</a> */}
<Dropdown overlay={filesMenu} trigger={["click"]}>
<a
className="ant-dropdown-link rs-link"
onClick={(e) => e.preventDefault()}
>
文件
</a>
</Dropdown>
<a className="ant-dropdown-link rs-link" onClick={() => {
setIsModalVisible(true);
}}>
选择模板
</a>
<a className="ant-dropdown-link rs-link" onClick={() => {
setIsUsageVisible(true);
}}>
使用教程
</a>
<Shortcuts></Shortcuts>
<History></History>
<a
href="#"
className="rs-link"
onClick={() => {
setIsExportVisible(true);
}}
>
导出 pdf
</a>
</div>
<Modal
title="请选择模板"
visible={isModalVisible}
onOk={handleOk}
onCancel={() => {
setTempTheme(theme);
setIsModalVisible(false);
}}
cancelText="取消"
okText="确定"
width={1100}
>
{templateContent}
</Modal>
<Modal
title="使用教程"
visible={isUsageVisible}
width={700}
cancelText="取消"
okText="确定"
onOk={() => {
setIsUsageVisible(false);
}}
onCancel={() => {
setIsUsageVisible(false);
}}
>
<div className="rs-article-container" dangerouslySetInnerHTML={{
__html: markdownParserArticle.render(TUTORIALS_GUIDE)
}}></div>
</Modal>
{<Modal
title="更新日志"
visible={isUpdateVisible}
cancelText="取消"
okText="确定"
width={700}
onOk={() => {
localStorage.setItem(LOCAL_STORE.MD_UPDATE_LOG, `${UPDATE_LOG_VERSION}`);
setIsUpdateVisible(false);
}}
onCancel={() => {
localStorage.setItem(LOCAL_STORE.MD_UPDATE_LOG, `${UPDATE_LOG_VERSION}`);
setIsUpdateVisible(false);
}}
>
<div className="rs-article-container" dangerouslySetInnerHTML={{
__html: markdownParserArticle.render(UPDATE_CONTENT)
}}></div>
</Modal>}
{isExportVisible && (
<Modal
title="导出确认"
visible={isExportVisible}
onOk={handleExport}
onCancel={() => {
setIsExportVisible(false);
}}
cancelText="取消"
okText="确认"
>
<Form
ref={formRef}
labelCol={{ span: 6 }}
wrapperCol={{ span: 14 }}
layout="horizontal"
initialValues={{
isMark: true,
}}
onFinish={(values: any) => {
exportPdf({
...values,
isMark: false
});
}}
>
<Form.Item name="name" label="简历名称">
<Input placeholder="不填则系统命名" />
</Form.Item>
<Form.Item name="isOnePage" label="是否一页纸" valuePropName="checked">
<Switch />
</Form.Item>
</Form>
</Modal>
)}
</div>
);
})
Example #21
Source File: CommentForm.tsx From condo with MIT License | 4 votes |
CommentForm: React.FC<ICommentFormProps> = ({
ticket,
initialValue,
action,
fieldName,
editableComment,
sending,
FileModel,
relationField,
setSending,
}) => {
const intl = useIntl()
const PlaceholderMessage = intl.formatMessage({ id: 'Comments.form.placeholder' })
const HelperMessage = intl.formatMessage({ id: 'Comments.form.helper' })
const { InputWithCounter, Counter, setTextLength: setCommentLength, textLength: commentLength } = useInputWithCounter(Input.TextArea, MAX_COMMENT_LENGTH)
const [form, setForm] = useState<FormInstance>()
const { organization } = useOrganization()
const editableCommentFiles = get(editableComment, 'files')
const { UploadComponent, syncModifiedFiles, resetModifiedFiles, filesCount } = useMultipleFileUploadHook({
Model: FileModel,
relationField: relationField,
initialFileList: editableCommentFiles,
initialCreateValues: { organization: organization.id, ticket: ticket.id },
dependenciesForRerenderUploadComponent: [editableComment],
})
useEffect(() => {
if (editableComment && form) {
const editableCommentContent = editableComment.content
form.setFieldsValue({ [fieldName]: editableCommentContent })
setCommentLength(get(editableCommentContent, 'length', 0))
}
}, [editableComment, fieldName, form, setCommentLength])
const handleKeyUp = useCallback(async (event, form) => {
if (event.keyCode === ENTER_KEY_CODE && !event.shiftKey) {
const content = form.getFieldValue(fieldName)
if (content && content.trim().length > 0 || filesCount > 0) {
setSending(true)
}
form.submit()
setCommentLength(0)
}
}, [fieldName, filesCount, setCommentLength, setSending])
const handleKeyDown = useCallback((event) => {
if (event.keyCode === ENTER_KEY_CODE) {
event.preventDefault()
}
}, [])
const { requiredValidator, trimValidator } = useValidations()
const validations = useMemo(() => ({
comment: filesCount > 0 ? [] : [requiredValidator, trimValidator],
}), [filesCount, requiredValidator, trimValidator])
const actionWithSyncComments = useCallback(async (values) => {
values.content = form.getFieldValue(fieldName)
form.setFieldsValue({ [fieldName]: null })
await action(values, syncModifiedFiles)
await resetModifiedFiles()
setSending(false)
}, [action, fieldName, form, resetModifiedFiles, setSending, syncModifiedFiles])
const MemoizedUploadComponent = useCallback(() => (
<UploadComponent
initialFileList={editableCommentFiles}
UploadButton={
<Button type={'text'}>
<ClipIcon />
</Button>
}
uploadProps={{
iconRender: (file) => {
return getIconByMimetype(file.type)
},
}}
/>
), [UploadComponent, editableComment, sending])
const initialCommentFormValues = useMemo(() => ({
[fieldName]: initialValue,
}), [fieldName, initialValue])
const showHelperMessage = useMemo(() => commentLength > 0 || editableComment, [commentLength, editableComment])
return (
<FormWithAction
initialValues={initialCommentFormValues}
action={actionWithSyncComments}
resetOnComplete={true}
>
{({ handleSave, isLoading, form: formInstance }) => {
if (!form) {
setForm(formInstance)
}
return (
<Holder>
{
showHelperMessage && (
<Row justify={'space-between'} style={COMMENT_HELPERS_ROW_STYLES}>
<CommentHelperWrapper>
<Typography.Text>
{HelperMessage}
</Typography.Text>
</CommentHelperWrapper>
<CommentHelperWrapper>
<Counter />
</CommentHelperWrapper>
</Row>
)
}
<div className={'wrapper'}>
<Form.Item
name={fieldName}
rules={validations.comment}
>
<InputWithCounter
maxLength={MAX_COMMENT_LENGTH}
placeholder={PlaceholderMessage}
className="white"
autoSize={INPUT_WITH_COUNTER_AUTOSIZE_CONFIG}
onKeyDown={handleKeyDown}
onKeyUp={(event) => {handleKeyUp(event, form)}}
/>
</Form.Item>
<MemoizedUploadComponent />
</div>
</Holder>
)
}}
</FormWithAction>
)
}
Example #22
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
AutoReply: React.FC<AutoReplyProps> = (props) => {
const {welcomeMsg, setWelcomeMsg, isFetchDone} = props;
const [modalVisible, setModalVisible] = useState(false);
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [currentMode, setCurrentMode] = useState<MsgType>('image');
const [linkFetching, setLinkFetching] = useState(false);
const [content, setContent] = useState('');
const contentRef = useRef<React.RefObject<HTMLElement>>();
const imageModalFormRef = useRef<FormInstance>();
const linkModalFormRef = useRef<FormInstance>();
const miniAppModalFormRef = useRef<FormInstance>();
const UploadFileFn = async (req: UploadRequestOption, ref: MutableRefObject<any | undefined>, inputName: string) => {
const file = req.file as File;
if (!file.name) {
message.error('非法参数');
return;
}
const hide = message.loading('上传中');
try {
const res = await GetSignedURL(file.name)
const data = res.data as GetSignedURLResult
if (res.code === 0) {
const uploadRes = (await fetch(data.upload_url, {
method: 'PUT',
body: file
}));
hide();
if (uploadRes.ok && ref) {
ref.current?.setFieldsValue({[inputName]: data.download_url});
return;
}
message.error('上传图片失败');
return;
}
hide();
message.error('获取上传地址失败');
return;
} catch (e) {
message.error('上传图片失败');
console.log(e);
}
};
useEffect(() => {
const formData = itemDataToFormData(welcomeMsg);
setAttachments(formData.attachments || []);
setContent(formData.text || '');
}, [isFetchDone]);
useEffect(() => {
setWelcomeMsg({
text: content || '',
attachments: attachments || [],
});
}, [content, attachments]);
return (
<>
<div className={styles.replyEditor}>
<div className={'preview-container'}>
<div className={styles.replyEditorPreview}>
<img src={phoneImage} className='bg'/>
<div className='content'>
<ul className='reply-list'>
{content && (
<li><img
src={avatarDefault}/>
<div className='msg text' dangerouslySetInnerHTML={{__html: content}}/>
</li>
)}
{attachments && attachments.length > 0 && (
attachments.map((attachment) => {
if (attachment.msgtype === 'image') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className={`msg image`}>
<img src={attachment.image?.pic_url}/>
</div>
</li>
);
}
if (attachment.msgtype === 'link') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className='msg link'><p className='title'>{attachment.link?.title}</p>
<div className='link-inner'><p
className='desc'>{attachment.link?.desc}</p>
<img src={attachment.link?.picurl}/>
</div>
</div>
</li>
);
}
if (attachment.msgtype === 'miniprogram') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className='msg miniprogram'>
<p className='m-title'>
<IconFont
type={'icon-weixin-mini-app'}
style={{marginRight: 4, fontSize: 14}}
/>
{attachment.miniprogram?.title}
</p>
<img src={attachment.miniprogram?.pic_media_id}/>
<p className='l-title'>
<IconFont type={'icon-weixin-mini-app'} style={{marginRight: 4}}/>
小程序
</p>
</div>
</li>
);
}
return '';
})
)}
</ul>
</div>
</div>
</div>
<div className='text-area-container'>
<div className={styles.msgTextareaContainer} style={{border: 'none'}}>
{props.enableQuickInsert && (
<div className='insert-btn '>
<span
className='clickable no-select'
onClick={() => {
setContent(`${content}[客户昵称]`);
}}
>[插入客户昵称]</span>
</div>
)}
<div className='textarea-container '>
<ContentEditable
// @ts-ignore
innerRef={contentRef}
onKeyDown={(event) => {
if (event.key === 'Enter') {
document.execCommand('insertLineBreak');
event.preventDefault();
}
}}
className={'textarea'}
html={content}
onChange={(e) => {
setContent(e.target.value);
}}/>
<div className='flex-row align-side'>
<p className='text-cnt'>{content.length}/600</p>
</div>
</div>
</div>
</div>
<div className='option-area-container'>
{attachments && attachments.length > 0 && (
<ReactSortable handle={'.draggable-button'} tag='ul' className={'select-msg-options'} list={attachments} setList={setAttachments}>
{attachments.map((attachment, index) => (
<li key={attachment.id} className='flex-row'>
<span>
<MinusCircleOutlined
onClick={() => {
const items = [...attachments];
items.splice(index, 1);
setAttachments(items);
}}
/>
【{msgTypes[attachment.msgtype]}】:
<span
className='col-1'>{attachment?.name}</span>
</span>
<span className='d-action-container'>
<EditOutlined
onClick={() => {
setCurrentMode(attachment.msgtype);
imageModalFormRef.current?.setFieldsValue(attachment.image);
linkModalFormRef.current?.setFieldsValue(attachment.link);
miniAppModalFormRef.current?.setFieldsValue(attachment.miniprogram);
setCurrentIndex(index);
setModalVisible(true);
}}
/>
<DragOutlined
className={'draggable-button'}
style={{cursor: 'grabbing'}}
/>
</span>
</li>
))}
</ReactSortable>
)}
<div className='option-container'>
<Dropdown
placement='topLeft'
trigger={['click']}
overlay={(
<Menu style={{minWidth: 120}}>
<Menu.Item
key={'image'}
icon={<FileImageOutlined/>}
onClick={() => {
setCurrentMode('image');
setCurrentIndex(attachments.length);
imageModalFormRef.current?.resetFields();
setModalVisible(true);
}}
>
图片
</Menu.Item>
<Menu.Item
key={'link'}
icon={<LinkOutlined/>}
onClick={() => {
setCurrentMode('link');
setCurrentIndex(attachments.length);
setModalVisible(true);
}}
>
链接
</Menu.Item>
<Menu.Item
key={'miniApp'}
icon={<IconFont type={'icon-weixin-mini-app'}/>}
onClick={() => {
setCurrentMode('miniprogram');
setCurrentIndex(attachments.length);
setModalVisible(true);
}}
>
小程序
</Menu.Item>
</Menu>
)}
>
<a className='ant-dropdown-link' onClick={e => e.preventDefault()}>
<PlusCircleOutlined/> 添加附件
</a>
</Dropdown>
</div>
</div>
</div>
<ModalForm
formRef={imageModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
visible={currentMode === 'image' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params: { title: string, pic_url: string, msgtype: MsgType }) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
image: {...params},
};
setAttachments(attachments);
return true;
}}
>
<h2 className='dialog-title'> 添加图片附件 </h2>
<ProForm.Item initialValue={'image'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='title'
label='图片名称'
placeholder={'请输入图片名称'}
width='md'
rules={[
{
required: true,
message: '请输入图片名称!',
},
]}
/>
<Form.Item
label='上传图片'
name='pic_url'
rules={[
{
required: true,
message: '请上传图片!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, imageModalFormRef, 'pic_url')
}}
/>
</Form.Item>
</ModalForm>
<ModalForm
formRef={linkModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
visible={currentMode === 'link' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
// @ts-ignore
link: {...params},
};
setAttachments(attachments);
return true;
}}
>
<Spin spinning={linkFetching}>
<h2 className='dialog-title'> 添加链接附件 </h2>
<ProForm.Item initialValue={'link'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='url'
label='链接地址'
width='md'
fieldProps={{
disabled: linkFetching,
addonAfter: (
<Tooltip title="点击抓取远程链接,自动填充标题,描述,图片">
<div
onClick={async () => {
setLinkFetching(true);
const res = await ParseURL(linkModalFormRef.current?.getFieldValue('url'))
setLinkFetching(false);
if (res.code !== 0) {
message.error(res.message);
} else {
message.success('解析链接成功');
linkModalFormRef?.current?.setFieldsValue({
customer_link_enable: 1,
title: res.data.title,
desc: res.data.desc,
picurl: res.data.img_url,
})
}
}}
style={{
cursor: "pointer",
width: 32,
height: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<SyncOutlined/>
</div>
</Tooltip>
)
}}
rules={[
{
required: true,
message: '请输入链接地址',
},
{
type: 'url',
message: '请填写正确的的URL,必须是http或https开头',
},
]}
/>
<ProFormSwitch
label={'高级设置'}
checkedChildren='开启'
unCheckedChildren='关闭'
name='customer_link_enable'
tooltip={'开启后可以自定义链接所有信息'}
/>
<ProFormDependency name={['customer_link_enable']}>
{({customer_link_enable}) => {
if (customer_link_enable) {
return (
<>
<ProFormText
name='title'
label='链接标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormTextArea
name='desc'
label='链接描述'
width='md'
/>
<Form.Item
label='链接封面'
name='picurl'
rules={[
{
required: true,
message: '请上传链接图片!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, linkModalFormRef, 'picurl')
}}
/>
</Form.Item>
</>
);
}
return <></>;
}}
</ProFormDependency>
</Spin>
</ModalForm>
<ModalForm
formRef={miniAppModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
labelCol={{
md: 6,
}}
visible={currentMode === 'miniprogram' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
// @ts-ignore
miniprogram: {...params},
};
setAttachments(attachments);
return true;
}}
>
<h2 className='dialog-title'> 添加小程序附件 </h2>
<Alert
showIcon={true}
type='info'
message={
'请填写企业微信后台绑定的小程序id和路径,否则会造成发送失败'
}
style={{marginBottom: 20}}
/>
<ProForm.Item initialValue={'miniprogram'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='title'
label='小程序标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormText
// 帮助指引
name='app_id'
label='小程序AppID'
width='md'
rules={[
{
required: true,
message: '请输入小程序AppID',
},
]}
/>
<ProFormText
name='page'
label='小程序路径'
width='md'
rules={[
{
required: true,
message: '请输入小程序路径',
},
]}
/>
<Form.Item
label='小程序封面'
name='pic_media_id'
rules={[
{
required: true,
message: '请小程序封面!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, miniAppModalFormRef, 'pic_media_id')
}}
/>
</Form.Item>
</ModalForm>
</>
);
}