All files / src/form/fields ObjectSelect.tsx

45.87% Statements 128/279
51.19% Branches 107/209
37.17% Functions 29/78
45.35% Lines 122/269

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812                                                                                      66x                           66x         41x 37x 37x 37x       37x   37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x 37x   37x 37x   37x 12x 5x       37x 8x                 37x 8x 5x           37x 16x 8x   8x 8x                                   8x                             8x       37x   37x 37x     37x                                                             7x 7x 6x       6x 6x                     6x   3x 3x       3x       3x             3x 3x       1x   1x       1x               1x     1x 1x 1x 2x 2x 2x       2x   1x 1x           37x     8x   8x                   37x 8x 8x           37x 8x 7x 1x                     1x   6x 6x                 37x 37x               37x                                                                                           37x         37x                   37x 37x 4x   33x 15x   28x             28x     37x     37x                                                                                                                   37x         37x                 37x                                                                                                                 37x                                                                                                                           37x                                                                                                                 37x                                                                                   37x                                           37x 37x 37x 12x   37x   11x           12x               37x 15x   37x                                                                                                                  
import React, { ReactElement, useEffect, useState, useContext } from 'react';
 
import { Dropdown, Menu, Modal, Select, Spin } from 'antd';
import {
  FormOutlined, SettingOutlined, PlusOutlined, SearchOutlined
} from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
 
import {
  fetchCurrentValueNoRelationshipColumns,
  fetchCurrentValues,
  searchObjectByKeyword,
  fetchCanCreate,
  fetchSearchResult
} from "@utils/FetchUtils";
import {
  ObjectMetaProps,
  SaveRecordProps,
  ObjectSelectProps,
  RecordProps,
  ContinueOperate,
  DataApiResultProps,
  EnumMetaProps,
} from "@props/RecordProps";
import {
  TRIGGER_SEARCH_MINIMAL_TIME_MS, TRIGGER_SEARCH_MINIMAL_INPUT_LENGTH,
  CustomIcon, EditableComponentDefaultStyle, EditableObjectSelectDefaultStyle, OnlyLabelFieldFetchType
} from '@config/base';
import { getObjectLabelFromCache, getLabelToDisplay, isObject, setObjectLabelToCache } from "@utils/ObjectUtils";
import { openErrorNotification } from "@utils/NotificationUtils";
import { getReadOnlyClass } from '@utils/ComponentUtils';
import { CloseIcon, SuffixIcon } from '../../components';
import { UpdateComponent, CreateComponent, ListComponent, modalPropsBuilder, SaveLoadingButtonType } from '../../form';
import { fullDomainNameToHumanReadable } from "@utils/StringUtils";
import { clearFinderConditions } from '@kernel/ServerSideFinder';
import { isArray } from 'lodash';
import { removeDuplicate } from '@utils/ArrayUtils';
import { CreateAndUseText, SearchAndUseText } from '@utils/Constants';
import { useLocation } from 'react-router-dom';
import { useDomainPermit } from '@utils/DomainPermitUtils';
import { useHotkeys } from 'react-hotkeys-hook';
import { ModalLockContext } from '@utils/context';
 
const { Option } = Select;
 
/**
 * 渲染对象的选择框
 * @param props 渲染需要传递的相关参数,包括数据和相关显示模式的信息均在其中
 * domainName: 对象的名称,可能是类似 tech_muyan_User 或者直接不带 package 的 User 的形式
 * mode: 空,或者tags 或者 multiple,空表示单选, tags表示多选,且可自定义添加, multiple 表示多选,后两种模式还未实现
 * placeholder:没有值时候的默认显示提示信息
 * value:默认值
 * notFoundContent: 在下拉框搜索时,如果后台没有找到,则显示该 notFoundContent
 * updatable: 是否可更新,如果不传,默认为 true 如果不可更新,则渲染为只读的下拉框,否则渲染为可操作的下拉框
 * labelField: 可选,该对象在下拉框中,显示给用户的字段
 * @constructor
 */
export const ObjectSelect = (props: ObjectSelectProps): ReactElement => {
  const {
    domainName: rawDomainName, mode, placeholder, value, notFoundContent, form, style,
    updatable, labelField, ownerClass, fieldName, defaultValue, className,
    onValuesChange, options, zIndex, column,
  } = props;
  const { t } = useTranslation();
  let domainName: string = rawDomainName;
  Iif (column.type === 'object') {
    domainName = column.elementDomain ?? rawDomainName;
  }
 
  const modalStyle = { minWidth: "410px", marginTop: "1%" };
 
  const [data, setData] = useState<Array<ObjectMetaProps> | undefined>(undefined);
  const [fetching, setFetching] = useState<boolean>(false);
  const [canCreate, setCanCreate] = useState<boolean>();
  const [showCreateModal, setShowCreateModal] = useState<boolean>();
  const [showSearchModal, setShowSearchModal] = useState<boolean>();
  const [showUpdateModal, setShowUpdateModal] = useState<boolean>();
  const [showDetailModal, setShowDetailModal] = useState<boolean>();
  const [newDataId, setNewDataId] = useState<string | number | Array<number>>();
  const [candidate, setCandidate] = useState<Array<number>>();
  const domainTitle = fullDomainNameToHumanReadable(domainName);
  const [triggerSave, setTriggerSave] = useState<false | number>(false);
  const [closeUpdateModal, setCloseUpdateModal] = useState<boolean>(true);
  const [closeDetailModal, setCloseDetailModal] = useState<boolean>(true);
  const [saveLoadingButton, setSaveLoadingButton] = useState<SaveLoadingButtonType>(undefined);
  const [defaultOptions, setDefaultOptions] = useState<Array<EnumMetaProps>>();
  const isSingleSelect = (mode == null);
  const isMultipleMode = (mode === "multiple");
  const isTagMode = (mode === "tags");
  const dataIsNotNull = (data != null && data.length > 0);
  const isMultipleOrTagMode = (isMultipleMode || isTagMode);
  const location = useLocation();
  const currentUrl = location.pathname;
  const { acquireLock, releaseLock, hasLock } = useContext(ModalLockContext);
  const [lockId, setLockId] = useState(0);
  const domainPermit = useDomainPermit(domainName, data?.[0]?.value);
 
  const labelFieldToUse = labelField ?? column?.labelField;
  const defaultOptionsCondition = column?.extInfo?.defaultOptionsCondition;
 
  useEffect(() => {
    data?.forEach((d) => {
      setObjectLabelToCache(domainName, d.value, d.label);
    });
  }, [data, domainName]);
 
  useEffect(() => {
    Iif (value && isObject(value)) {
      const recordProps = value as RecordProps;
      const label = getLabelToDisplay(recordProps, labelFieldToUse);
      if (recordProps.id && label) {
        setObjectLabelToCache(domainName, recordProps.id, label);
      }
    }
  }, [value, domainName, labelFieldToUse]);
 
  useEffect(() => {
    fetchCanCreate(domainName)
      .then(json => setCanCreate(json.create))
      .catch(e => {
        console.error(`Failed to get canCreate of ${domainName}: ${e}`);
      });
  }, [domainName]);
 
  useEffect(() => {
    if (defaultOptions != null) {
      return;
    }
    const queryJson = column?.extInfo?.queryJson;
    Iif (queryJson) {
      fetchSearchResult({
        searchConditions: {},
        domainName,
        queryJson,
        max: 20,
        current: 0,
        fetchType: OnlyLabelFieldFetchType,
        useCache: true,
      }).then((result: DataApiResultProps) => {
        const dv: Array<EnumMetaProps> = result.data.map(r => {
          return {
            label: getLabelToDisplay(r as RecordProps, labelFieldToUse),
            value: r.id.toString()
          };
        });
        setDefaultOptions(dv);
      });
    } else Iif (defaultOptionsCondition) {
      fetchSearchResult({
        domainName, searchConditions: defaultOptionsCondition,
        max: 20, current: 0, fetchType: OnlyLabelFieldFetchType,
        useCache: true,
      }).then((result: DataApiResultProps) => {
        const dv: Array<EnumMetaProps> = result.data.map(r => {
          return {
            label: getLabelToDisplay(r as RecordProps, labelFieldToUse),
            value: r.id.toString()
          };
        });
        setDefaultOptions(dv);
      });
    } else {
      setDefaultOptions([]);
    }
  }, [domainName, column, setDefaultOptions, defaultOptions, defaultOptionsCondition, labelFieldToUse]);
 
  const disabled = (updatable == null) ? false : !updatable;
 
  const getPromptingMessage = (): string => {
    return (data == null) ? t(placeholder) : t(notFoundContent);
  };
 
  const onSelectValue = (value: unknown): void => {
    if (value != null) {
      const numberValue = (typeof value === 'string') ? parseInt(value) : value;
      const currentVal = form?.getFieldValue(fieldName);
      if (isMultipleOrTagMode) {
        const currentValNumberType = currentVal?.map((v: "string" | "number") => parseInt(v));
        if (Array.isArray(currentValNumberType) && currentValNumberType?.includes(numberValue)) {
          setFieldValueAndRefreshForm(currentValNumberType);
          return;
        }
        if (['number', 'string'].includes(typeof currentValNumberType)) {
          if (numberValue != null) {
            const newVal = [currentValNumberType, numberValue];
            setFieldValueAndRefreshForm(newVal);
          }
        } else if (Array.isArray(currentValNumberType)) {
          currentValNumberType.push(numberValue);
          setFieldValueAndRefreshForm(currentValNumberType);
        } else if (currentVal == null) {
          setFieldValueAndRefreshForm([numberValue]);
        } else {
          console.warn(`Value of ${currentVal}(${domainName}) is not of \
                type [array, string, number] or undefined, detail search will not work`);
        }
      } else {
        setFieldValueAndRefreshForm(numberValue);
      }
    }
  };
 
  function refreshData(targetValue: string | number | Array<number>): void {
    const dataIsEmptyAndTargetValueIsNotEmpty = (targetValue != null && !dataIsNotNull);
    if (isSingleSelect) {
      const targetValueNotInDataList = (targetValue != null && data != null &&
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        !data.map(d => d.value).includes(targetValue));
      Eif (dataIsEmptyAndTargetValueIsNotEmpty || targetValueNotInDataList || newDataId != null) {
        Iif (typeof targetValue === 'number' && getObjectLabelFromCache(domainName, targetValue)) {
          const initData = [{
            value: targetValue,
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            label: getObjectLabelFromCache(domainName, targetValue)!,
          }];
          setData(initData);
          setNewDataId(undefined);
        } else {
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-ignore
          fetchCurrentValueNoRelationshipColumns(domainName, targetValue).then(json => {
 
            const displayVal = getLabelToDisplay(json, labelFieldToUse);
            const initData = [{
              value: json?.id,
              label: displayVal
            }];
            const initIds = initData.map(d => d.value);
            // The follow logic is to handle scenario user edit label field of
            // related object in second level modal , then we need to retrieve the
            // object to refresh label displayed on the input field
            Iif (dataIsNotNull) {
              data?.forEach(d => {
                if (!initIds.includes(d.value)) {
                  initData.push(d);
                }
              });
            }
            setData(initData);
            setNewDataId(undefined);
          });
        }
      }
    } else Eif (isMultipleMode) {
      const addOrRemoveFromArray = (
        Array.isArray(targetValue) && Array.isArray(data)
        && targetValue.length !== data.length
      );
 
      Iif (Array.isArray(targetValue) && targetValue.every((id) => getObjectLabelFromCache(domainName, id))) {
        const newData: ObjectMetaProps[] = targetValue.map((id) => ({
          value: id,
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          label: getObjectLabelFromCache(domainName, id)!,
        }));
        setData(newData);
        setNewDataId(undefined);
      } else Eif (dataIsEmptyAndTargetValueIsNotEmpty || addOrRemoveFromArray) {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        fetchCurrentValues(domainName, targetValue).then(json => {
          const newData = [] as Array<ObjectMetaProps>;
          for (let i = 0; i < json.length; i++) {
            const valueFromServer = json[i];
            const displayVal = getLabelToDisplay(valueFromServer, labelFieldToUse);
            const appendData = {
              value: valueFromServer.id,
              label: displayVal
            };
            newData.push(appendData);
          }
          setData(newData);
          setNewDataId(undefined);
        });
      }
    }
  }
 
  useEffect(() => {
    // showCreateModal 或者 showUpdateModal 等于 false 表示是执行过关闭弹窗的操作
    // 不能用 !showCreateModal || !showUpdateModal 的形式,因为刚打开弹窗时,这两个值都是空,这样的条件会命中
    Iif ((showCreateModal === false) || (showUpdateModal === false)) {
      releaseLock(lockId);
    } else Iif (showCreateModal || showUpdateModal) {
      if (hasLock(lockId)) {
        return;
      } else {
        const newLockId = acquireLock();
        setLockId(newLockId);
      }
    }
  }, [showCreateModal, showUpdateModal, lockId, acquireLock, releaseLock, hasLock]);
 
  useEffect(() => {
    Eif (newDataId == null) {
      return;
    }
    refreshData(newDataId);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [newDataId]);
 
  useEffect(() => {
    if (defaultValue != null && defaultValue !== '') {
      if (isArray(defaultValue) && defaultValue.length > 0) {
        Iif (options != null && options.length > 0) {
          const newDataOpts: Array<ObjectMetaProps> = options.map(opt => {
            return {
              value: parseInt(opt.value),
              label: opt.label,
            } as ObjectMetaProps;
          }).filter(opt => {
            return defaultValue.includes(opt.value);
          });
          setData(newDataOpts);
        } else {
          refreshData(defaultValue);
        }
      } else Eif (defaultValue != null) {
        refreshData(defaultValue);
      }
      // Only trigger refresh by default value once
      // So the dependencies list is empty
      // eslint-disable-next-line
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [defaultValue]);
 
  let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
  let lastSearchKeyword = "";
 
  //********************************** Attention **********************************************/
  // To reduce server side pressure and don't send too much nonsense search request to backend
  // variable shouldNotSearch is to control throughout of request to backend are:
  // 1. There will be only 1 request to search within TRIGGER_SEARCH_MINIMAL_TIME_MS ms
  // 2. Only send request if the input size is longer than TRIGGER_SEARCH_MINIMAL_INPUT_LENGTH
  //********************************** Attention **********************************************/
  const handleSearch = (value: string): void => {
 
    if (column.extInfo?.disableObjectSearch) {
      return;
    }
 
    lastSearchKeyword = value;
 
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    const x = () => {
      return new Promise(function(resolve) {
        if (timeout != null) {
          clearTimeout(timeout);
        }
        timeout = setTimeout(function() {
          resolve('done!');
        }, TRIGGER_SEARCH_MINIMAL_TIME_MS);
      });
    };
 
    x().then(() => {
      const shouldNotSearch = (lastSearchKeyword === undefined) || value.length < TRIGGER_SEARCH_MINIMAL_INPUT_LENGTH;
      if (shouldNotSearch) {
        return;
      }
      setFetching(true);
      searchObjectByKeyword(ownerClass, fieldName, domainName, lastSearchKeyword)
        .then(json => {
          const errorFromServerSide = ("error" in json);
          if (errorFromServerSide) {
            openErrorNotification(
              `Failed to search for ${fieldName} with keyword ${lastSearchKeyword}: ${JSON.stringify(json)}`
            );
            setData([]);
          } else {
            setData(json);
          }
          setFetching(false);
        }).catch((error) => {
          openErrorNotification(
            `Failed to search for ${fieldName} with keyword ${lastSearchKeyword}: ${JSON.stringify(error)}`,
          );
        });
    });
  };
 
  const selectStyle = {
    ...(style ?? (updatable ? EditableObjectSelectDefaultStyle : EditableComponentDefaultStyle)),
    maxWidth: 'calc(100% - 31px)',
  };
  // eslint-disable-next-line  @typescript-eslint/no-explicit-any
  const setFieldValueAndRefreshForm = (newVal: any): void => {
    const fieldValue = { [fieldName]: newVal };
    form?.setFieldsValue(fieldValue);
    setNewDataId(newVal);
 
    if (onValuesChange != null) {
      onValuesChange(fieldValue, form?.getFieldsValue());
    }
  };
 
  const convertNewValue = (): undefined | string | string[] => {
    if (value == null || value === '') {
      return undefined;
    }
    if (isMultipleMode && Array.isArray(value)) {
      return value?.map(v => v?.toString());
    }
    Iif (isObject(value)) {
      const recordProps = value as RecordProps;
      const cachedLabel = getObjectLabelFromCache(domainName, recordProps.id);
      if (cachedLabel) {
        return recordProps.id.toString();
      }
    }
    return value?.toString().split(',').map(v => v?.toString());
  };
 
  const newValue = convertNewValue();
 
  const searchModal = (
    <Modal
      className="search-modal"
      okText={t("Select")}
      open={showSearchModal}
      centered={false}
      title={t('SearchFormFormTitle', { domainTitle })}
      onCancel={() => {
        setShowSearchModal(false);
        clearFinderConditions(domainName);
      }}
      maskClosable={true}
      //zIndex should be higher than 1050, which is zIndex of dropdown menu
      zIndex={zIndex + 3}
      destroyOnClose={false}
      width="90%"
      key={`${domainName}_search_modal`}
      closeIcon={<CloseIcon onClick={(visible) => setShowSearchModal(visible)} />}
      style={modalStyle}
      onOk={() => {
        if (candidate != null && candidate.length > 0) {
          const currentVal = form?.getFieldValue(fieldName);
          if (isMultipleOrTagMode) { //多选模式
            if (['number', 'string'].includes(typeof currentVal)) {
              setFieldValueAndRefreshForm(removeDuplicate([currentVal, ...candidate]));
            } else if (Array.isArray(currentVal)) {
              setFieldValueAndRefreshForm(removeDuplicate([...currentVal, ...candidate]));
            } else if (currentVal == null) {
              setFieldValueAndRefreshForm(candidate);
            } else {
              console.warn(`Value of ${currentVal}(${domainName}) is not of \
                type [array, string, number] or undefined, detail search will not work`);
            }
          } else { // 单选模式
            // If is single mode, just put first element of candidate to form value
            setFieldValueAndRefreshForm(candidate[0]);
          }
          setCandidate(undefined);
        }
        clearFinderConditions(domainName);
        setShowSearchModal(false);
      }}
    >
      <ListComponent
        tableMode="finder"
        inline={true}
        domainName={domainName}
        onSelectRow={(selectedRows) => {
          setCandidate(selectedRows?.map(r => r.id));
          selectedRows?.forEach(r => setObjectLabelToCache(domainName, r.id, getLabelToDisplay(r, labelFieldToUse)));
        }}
        zIndex={zIndex + 1}
        ownerClass={ownerClass}
        columnNameInOwnerClass={fieldName}
        multiple={isMultipleMode}
        defaultSearchConditions={defaultOptionsCondition}
      />
    </Modal>);
 
  const onSaveAndClose = (): void => {
    setTriggerSave(Math.random());
    setSaveLoadingButton("saveClose");
  };
 
  useHotkeys('ctrl+enter', () => {
    if (hasLock(lockId)) {
      onSaveAndClose();
    }
  }, {
    enableOnFormTags: true
  });
 
  function createModal(): ReactElement | undefined | false {
    return updatable && canCreate &&
      <Modal
        {...modalPropsBuilder({
          mode: 'create-related',
          domainName,
          isValid: true,
          onCancel: () => {
            setShowCreateModal(false);
          },
          ownerClass,
          ownerId: form?.getFieldValue('id'),
          columnNameInOwnerClass: fieldName,
          onSaveAndClose,
          onSaveAndContinue: undefined,
          onSaveAndContinueEdit: undefined,
          open: showCreateModal,
          hasRelateObjectField: false,
          zIndex: zIndex + 1,
          saveLoadingButton,
          t,
          refererUrl: currentUrl,
        })}
        closeIcon={<CloseIcon onClick={(visible) => setShowCreateModal(visible)} />}
        style={modalStyle}
      >
        <CreateComponent
          showContinueOperate={false}
          domainName={domainName}
          triggerSave={triggerSave}
          ownerClass={ownerClass}
          ownerId={form?.getFieldValue('id')}
          columnNameInOwnerClass={fieldName}
          callback={(createProps: {
            data?: SaveRecordProps;
            continueOperate: ContinueOperate;
          }) => {
            const { data: createdData } = createProps;
            if (createdData != null) {
              onSelectValue(createdData.id);
            }
            setShowCreateModal(false);
            setTriggerSave(false);
            setSaveLoadingButton(undefined);
          }}
          validationCallback={(valid: boolean) => {
            //Reset the save button loading status and triggerSave flag
            if (!valid) {
              setTriggerSave(false);
              setSaveLoadingButton(undefined);
            }
          }}
          zIndex={zIndex + 1}
        />
      </Modal>;
  }
 
  function editModal(): ReactElement | undefined | false {
    return updatable && domainPermit.canUpdate && isSingleSelect && data != null && data.length > 0 &&
      <Modal
        {...modalPropsBuilder({
          id: data[0].value,
          mode: 'edit-related',
          domainName,
          isValid: true,
          onCancel: () => {
            setShowUpdateModal(false);
          },
          onSaveAndClose: () => {
            setCloseUpdateModal(true);
            setTriggerSave(Math.random());
          },
          onSaveAndContinue: () => {
            setCloseUpdateModal(false);
            setTriggerSave(Math.random());
          },
          ownerClass,
          ownerId: form?.getFieldValue('id'),
          columnNameInOwnerClass: fieldName,
          onSaveAndContinueEdit: undefined,
          open: showUpdateModal,
          hasRelateObjectField: true,
          zIndex: zIndex + 1,
          t,
          refererUrl: currentUrl,
          // **** Attention ****
          // zIndex of the select dropdown should higher than the zIndex of modal(by
          // default 1051) because the dropdown might be displayed on a modal
          // https://stackoverflow.com/questions/53926911/antd-select-not-working-inside-a-full-screen-dialog
          // Set in app.less by .ant-select-dropdown
        })}
        closeIcon={<CloseIcon onClick={(visible) => setShowUpdateModal(visible)} />}
        style={modalStyle}
      >
        <UpdateComponent
          id={data[0].value}
          domainName={domainName}
          triggerSave={triggerSave}
          ownerClass={ownerClass}
          ownerId={form?.getFieldValue('id')}
          columnNameInOwnerClass={fieldName}
          callback={(updateProps: {
            data?: SaveRecordProps;
            continueOperate: ContinueOperate;
          }) => {
            const { data: updateData } = updateProps;
            if (updateData != null && form != null) {
              setFieldValueAndRefreshForm(updateData.id);
            }
            setTriggerSave(false);
            if (closeUpdateModal) {
              setShowUpdateModal(false);
            }
          }}
          zIndex={zIndex + 1}
        />
      </Modal>;
  }
 
  function detailModal(): ReactElement | undefined | false {
    return isSingleSelect && data != null && data.length > 0 &&
      <Modal
        {...modalPropsBuilder({
          id: data[0].value,
          mode: 'edit-related',
          domainName,
          isValid: true,
          onCancel: () => {
            setShowDetailModal(false);
          },
          onSaveAndClose: () => {
            setCloseDetailModal(true);
          },
          onSaveAndContinue: () => {
            setCloseDetailModal(false);
          },
          onSaveAndContinueEdit: undefined,
          open: showDetailModal,
          hasRelateObjectField: true,
          zIndex: zIndex + 1,
          t,
          refererUrl: currentUrl,
          readonly: true,
          // **** Attention ****
          // zIndex of the select dropdown should higher than the zIndex of modal(by
          // default 1051) because the dropdown might be displayed on a modal
          // https://stackoverflow.com/questions/53926911/antd-select-not-working-inside-a-full-screen-dialog
          // Set in app.less by .ant-select-dropdown
        })}
        closeIcon={<CloseIcon onClick={(visible) => setShowDetailModal(visible)} />}
        style={modalStyle}
      >
        <UpdateComponent
          id={data[0].value}
          domainName={domainName}
          triggerSave={triggerSave}
          ownerClass={ownerClass}
          callback={(updateProps: {
            data?: SaveRecordProps;
            continueOperate: ContinueOperate;
          }) => {
            const { data: updateData } = updateProps;
            if (updateData != null && form != null) {
              setFieldValueAndRefreshForm(updateData.id);
            }
            setTriggerSave(false);
            if (closeDetailModal) {
              setShowDetailModal(false);
            }
          }}
          zIndex={zIndex + 1}
          readonly={true}
        />
      </Modal>;
  }
 
  const overlayMenu = (
    <Menu>
      {!column.extInfo?.disableObjectSearch && <Menu.Item
          key="search"
          icon={<SearchOutlined />}
          onClick={() => {
            setShowSearchModal(true);
          }}
      >
        {t(SearchAndUseText)}
      </Menu.Item>}
      {canCreate && <Menu.Item
        key="create"
        icon={<PlusOutlined />}
        onClick={() => {
          setShowCreateModal(true);
        }}
      >
        {t(CreateAndUseText)}
      </Menu.Item>}
      {isSingleSelect && dataIsNotNull &&
        <Menu.Item
          key="show"
          icon={<CustomIcon type="icon-link" />}
          onClick={() => setShowDetailModal(true)}
        >
          {t('ShowObjectDetail', { label: data?.[0].label })}
        </Menu.Item>
      }
      {domainPermit.canUpdate && isSingleSelect && dataIsNotNull &&
        <Menu.Item
          key="update"
          icon={<FormOutlined />}
          onClick={() => {
            setShowUpdateModal(true);
          }}
        >
          {t('EditObjectDetail', { label: data?.[0].label })}
        </Menu.Item>}
    </Menu>
  );
 
  function objectOperateDropdown(): ReactElement | undefined | false {
    return !column?.extInfo?.disableObjectOperations && updatable && <Dropdown
      className="object-select-menu"
      overlayClassName="object-select-menu-overlay"
      overlayStyle={{ zIndex: zIndex + 1 }}
      overlay={overlayMenu}
      trigger={["click"]}
    >
      <div style={{ flex: 1 }}>
        <a
          href="/#"
          onClick={(event) => event.preventDefault()}
          className="object-select-suffix"
        >
          <SettingOutlined
            title={t("Object operations")}
          />
        </a>
      </div>
    </Dropdown>;
  }
 
  function allOptions(): ReactElement {
    const dataValues: Array<string> | undefined = data?.map(d => d?.value?.toString());
    const allOptions = [...(options ?? []), ...(defaultOptions ?? [])];
    const deduplicatedOpts = allOptions.filter(
      opt => !dataValues?.includes(opt.value.toString()));
 
    return (<>
      {
        data != null && data.map(d => <Option
          key={d?.value?.toString()}
          value={d?.value?.toString()}>{d.label}
        </Option>)
      }
      {
        (options != null || defaultOptions != null) && deduplicatedOpts?.map(d => <Option
          key={d?.value?.toString()}
          value={d?.value?.toString()}>{d.label}
        </Option>)
      }
    </>);
  }
 
  const tdv = (defaultValue === "") ? undefined :
    (Array.isArray(defaultValue)) ? defaultValue.map(v => v?.toString()) : defaultValue?.toString();
 
  return (
    <div style={{ display: "flex" }} className="object-select-container">
      <Select
        style={selectStyle}
        onChange={props.onChange}
        mode={mode}
        showSearch
        disabled={disabled}
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        value={newValue}
        placeholder={t(placeholder)}
        defaultActiveFirstOption={true}
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        defaultValue={tdv}
        showArrow={true}
        onSearch={handleSearch}
        notFoundContent={fetching ? <Spin size="small" /> : getPromptingMessage()}
        dropdownMatchSelectWidth={true}
        optionFilterProp="children"
        filterOption={(input, option) => {
          // query keyword might not on the display label,
          // So can not filter by display label.
          // For example, when there's "name" and "label" in an domain object
          // User input name as search query keyword, and server side returns
          // List of (id, label) as display, if we filter by option display
          // Then the option will be hidden, that's not the desire behavior.
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-ignore
          return !(option == null || option.children == null);
        }}
        suffixIcon={<SuffixIcon updatable={updatable} />}
        className={`object ${getReadOnlyClass(updatable)} ${className}`}
        allowClear={true}
        // **** Attention ****
        // zIndex of the select dropdown should higher than or equal to the
        // zIndex of modal(by default 1051) because the dropdown might be
        // displayed on a modal
        // https://stackoverflow.com/questions/53926911/antd-select-not-working-inside-a-full-screen-dialog
        dropdownStyle={{ zIndex: 2000 }}
        onSelect={(value: unknown): void => {
          onSelectValue(value);
        }}
      >
        {allOptions()}
      </Select>
      {objectOperateDropdown()}
      {!column.extInfo?.disableObjectSearch && searchModal}
      {createModal()}
      {detailModal()}
      {editModal()}
    </div>
  );
};
 
export default ObjectSelect;