@cloud-ru/uikit-product-fields-predefined 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +935 -0
- package/LICENSE +201 -0
- package/README.md +151 -0
- package/package.json +63 -0
- package/src/components/AIDisclaimer/AIDisclaimer.tsx +18 -0
- package/src/components/AIDisclaimer/index.ts +1 -0
- package/src/components/AIDisclaimer/styles.module.scss +19 -0
- package/src/components/FieldAi/FieldAi.tsx +145 -0
- package/src/components/FieldAi/components/CheckItem/CheckItem.tsx +44 -0
- package/src/components/FieldAi/components/CheckItem/index.ts +1 -0
- package/src/components/FieldAi/components/CheckItem/styles.module.scss +28 -0
- package/src/components/FieldAi/components/MobileFieldAi/MobileFieldAi.tsx +57 -0
- package/src/components/FieldAi/components/MobileFieldAi/index.ts +1 -0
- package/src/components/FieldAi/components/MobileFieldAi/styles.module.scss +81 -0
- package/src/components/FieldAi/components/PasswordValidation/PasswordValidation.tsx +78 -0
- package/src/components/FieldAi/components/PasswordValidation/index.ts +1 -0
- package/src/components/FieldAi/components/PasswordValidation/styles.module.scss +32 -0
- package/src/components/FieldAi/components/TextArea/TextArea.tsx +113 -0
- package/src/components/FieldAi/components/TextArea/index.ts +1 -0
- package/src/components/FieldAi/components/TextArea/styles.module.scss +35 -0
- package/src/components/FieldAi/components/WithPasswordValidation/WithPasswordValidation.tsx +51 -0
- package/src/components/FieldAi/components/WithPasswordValidation/index.ts +1 -0
- package/src/components/FieldAi/components/WithPasswordValidation/styles.module.scss +7 -0
- package/src/components/FieldAi/index.ts +1 -0
- package/src/components/FieldAi/styles.module.scss +33 -0
- package/src/components/FieldAi/utils.ts +25 -0
- package/src/components/FieldChat/FieldChat.tsx +101 -0
- package/src/components/FieldChat/components/Attachments/Attachments.tsx +32 -0
- package/src/components/FieldChat/components/Attachments/index.ts +1 -0
- package/src/components/FieldChat/components/Attachments/styles.module.scss +9 -0
- package/src/components/FieldChat/index.ts +1 -0
- package/src/components/FieldChat/styles.module.scss +13 -0
- package/src/components/FieldPhone/FieldPhone.tsx +226 -0
- package/src/components/FieldPhone/__tests__/constants.ts +26 -0
- package/src/components/FieldPhone/__tests__/formatPhoneNumber.spec.ts +15 -0
- package/src/components/FieldPhone/__tests__/matchMedia.ts +15 -0
- package/src/components/FieldPhone/constants.ts +1 -0
- package/src/components/FieldPhone/countries.tsx +1755 -0
- package/src/components/FieldPhone/hooks/index.ts +2 -0
- package/src/components/FieldPhone/hooks/useCountries.ts +40 -0
- package/src/components/FieldPhone/hooks/useMapCountryToOptions.ts +25 -0
- package/src/components/FieldPhone/index.ts +7 -0
- package/src/components/FieldPhone/styles.module.scss +9 -0
- package/src/components/FieldPhone/types.ts +32 -0
- package/src/components/FieldPhone/utils.ts +71 -0
- package/src/components/SelectCreate/SelectCreate.tsx +122 -0
- package/src/components/SelectCreate/SelectFooter/SelectFooter.tsx +29 -0
- package/src/components/SelectCreate/SelectFooter/index.ts +1 -0
- package/src/components/SelectCreate/SelectFooter/styles.module.scss +9 -0
- package/src/components/SelectCreate/index.ts +1 -0
- package/src/components/SelectCreate/types.ts +35 -0
- package/src/components/SelectCreate/useSelectDataStates.tsx +53 -0
- package/src/components/index.ts +5 -0
- package/src/helperComponents/FieldSubmitButton/FieldSubmitButton.tsx +42 -0
- package/src/helperComponents/FieldSubmitButton/index.ts +1 -0
- package/src/helperComponents/TextAreaActionsFooter/TextAreaActionsFooter.tsx +18 -0
- package/src/helperComponents/TextAreaActionsFooter/index.ts +1 -0
- package/src/helperComponents/TextAreaActionsFooter/styles.module.scss +23 -0
- package/src/helpers/capitalize.ts +3 -0
- package/src/helpers/getSymbolsRangeFromMask.ts +17 -0
- package/src/helpers/index.ts +3 -0
- package/src/helpers/isTouchDevice.ts +5 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useOpen.ts +22 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ALL_COUNTRY_CODES } from '../countries';
|
|
4
|
+
import { Country, CountrySettings, FieldPhoneOptionsProps } from '../types';
|
|
5
|
+
import { uniqueSet } from '../utils';
|
|
6
|
+
import { useMapCountryToOptions } from './useMapCountryToOptions';
|
|
7
|
+
|
|
8
|
+
export function useCountries(optionsSettings?: CountrySettings): FieldPhoneOptionsProps[] {
|
|
9
|
+
const mapCountryToOption = useMapCountryToOptions();
|
|
10
|
+
|
|
11
|
+
const filterCountries = useMemo(() => {
|
|
12
|
+
if (!optionsSettings) {
|
|
13
|
+
return () => true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { includedCountries, excludedCountries } = optionsSettings;
|
|
17
|
+
const uniqueKey = 'value' satisfies keyof Country;
|
|
18
|
+
|
|
19
|
+
if (includedCountries) {
|
|
20
|
+
const includedSet = uniqueSet(includedCountries, uniqueKey);
|
|
21
|
+
return (country: Country) => includedSet.has(country[uniqueKey]);
|
|
22
|
+
}
|
|
23
|
+
if (excludedCountries) {
|
|
24
|
+
const excludedSet = uniqueSet(excludedCountries, uniqueKey);
|
|
25
|
+
return (country: Country) => !excludedSet.has(country[uniqueKey]);
|
|
26
|
+
}
|
|
27
|
+
return () => true;
|
|
28
|
+
}, [optionsSettings]);
|
|
29
|
+
|
|
30
|
+
return useMemo(() => {
|
|
31
|
+
if (optionsSettings?.overriddenOptions) return optionsSettings.overriddenOptions;
|
|
32
|
+
|
|
33
|
+
return ALL_COUNTRY_CODES.reduce<FieldPhoneOptionsProps[]>((result, country) => {
|
|
34
|
+
if (filterCountries(country)) {
|
|
35
|
+
result.push(mapCountryToOption(country));
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}, []);
|
|
39
|
+
}, [optionsSettings, filterCountries, mapCountryToOption]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
4
|
+
|
|
5
|
+
import { Country, FieldPhoneOptionsProps } from '../types';
|
|
6
|
+
|
|
7
|
+
export function useMapCountryToOptions() {
|
|
8
|
+
const { t } = useLocale('FieldsPredefined');
|
|
9
|
+
|
|
10
|
+
const mapCountryToOption = useCallback(
|
|
11
|
+
({ value: id, mask, caption, beforeContent, iso2 }: Country): FieldPhoneOptionsProps => ({
|
|
12
|
+
id,
|
|
13
|
+
mask,
|
|
14
|
+
content: {
|
|
15
|
+
option: t(`FieldPhone.${id}`),
|
|
16
|
+
caption: caption,
|
|
17
|
+
},
|
|
18
|
+
beforeContent,
|
|
19
|
+
iso2,
|
|
20
|
+
}),
|
|
21
|
+
[t],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return mapCountryToOption;
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { useIMask } from 'react-imask';
|
|
3
|
+
|
|
4
|
+
import { ALL_COUNTRY_CODES } from './countries';
|
|
5
|
+
|
|
6
|
+
export type FieldPhoneOptionsProps = {
|
|
7
|
+
id: string;
|
|
8
|
+
beforeContent: ReactNode;
|
|
9
|
+
content: {
|
|
10
|
+
option: string;
|
|
11
|
+
caption: string;
|
|
12
|
+
};
|
|
13
|
+
mask: string;
|
|
14
|
+
iso2: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type MaskOptions = Parameters<typeof useIMask>[0];
|
|
18
|
+
|
|
19
|
+
export type Country = (typeof ALL_COUNTRY_CODES)[number];
|
|
20
|
+
|
|
21
|
+
type OneOf<T extends object> = {
|
|
22
|
+
[K in keyof T]: { [P in K]: T[P] } & { [P in Exclude<keyof T, K>]?: never };
|
|
23
|
+
}[keyof T];
|
|
24
|
+
|
|
25
|
+
export type CountrySettings = OneOf<{
|
|
26
|
+
/** Список элементов выпадающего списка со странами, который будет передан в селект-компонент и переопределит список объявленных стран внутри пакета */
|
|
27
|
+
overriddenOptions: FieldPhoneOptionsProps[];
|
|
28
|
+
/** Список стран, которые должны отображаться в селекторе (будут выбраны из перечня стран, объявленных внутри пакета) */
|
|
29
|
+
includedCountries: Country[];
|
|
30
|
+
/** Список стран, которые будут исключены из списка объявленных стран внутри пакета */
|
|
31
|
+
excludedCountries: Country[];
|
|
32
|
+
}>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parsePhoneNumber } from 'awesome-phonenumber';
|
|
2
|
+
|
|
3
|
+
import { ABKHAZIA_COUNTRY_CODE, RUSSIA_COUNTRY_CODE } from './countries';
|
|
4
|
+
import { FieldPhoneOptionsProps } from './types';
|
|
5
|
+
|
|
6
|
+
export const uniqueSet = <T, K extends keyof T>(arr: T[], key: K): Set<T[K]> => new Set(arr.map(obj => obj[key]));
|
|
7
|
+
|
|
8
|
+
/* Приходит +79878887879/+7987****875, на основании mask возвращается +7 987-888-78-79/+7 987-***-*8-75 */
|
|
9
|
+
export const formatPhoneNumber = <T extends { caption: string; mask: string }>(
|
|
10
|
+
phone: string,
|
|
11
|
+
countries: readonly T[],
|
|
12
|
+
): string => {
|
|
13
|
+
const digits = phone.replace(/[^\d*]/g, '');
|
|
14
|
+
|
|
15
|
+
const country = countries.find(item => digits.startsWith(item.caption.replace('+', '')));
|
|
16
|
+
|
|
17
|
+
if (!country) {
|
|
18
|
+
return phone;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const countryCode = country.caption.replace('+', '');
|
|
22
|
+
const restNumber = digits.slice(countryCode.length);
|
|
23
|
+
|
|
24
|
+
let formatted = '';
|
|
25
|
+
let index = 0;
|
|
26
|
+
for (const char of country.mask) {
|
|
27
|
+
if (char === 'X') {
|
|
28
|
+
formatted += restNumber.charAt(index) || '';
|
|
29
|
+
index++;
|
|
30
|
+
} else {
|
|
31
|
+
formatted += char;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `${country.caption} ${formatted}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const byId = (id: string) => (option: FieldPhoneOptionsProps) => option.id === id;
|
|
39
|
+
|
|
40
|
+
export function detectCountryByPhone(text: string, options: FieldPhoneOptionsProps[]) {
|
|
41
|
+
const cleaned = text.replace(/[^\d+]/g, '');
|
|
42
|
+
|
|
43
|
+
const withPlus = cleaned.startsWith('+') ? cleaned : `+${cleaned}`;
|
|
44
|
+
const parsedNumber = parsePhoneNumber(withPlus);
|
|
45
|
+
const regionCode = parsedNumber.regionCode;
|
|
46
|
+
|
|
47
|
+
/* У Абхазии iso2 совпадает с Россией */
|
|
48
|
+
if (/^\+?7(840|940)/.test(cleaned)) {
|
|
49
|
+
let russia: FieldPhoneOptionsProps | undefined;
|
|
50
|
+
|
|
51
|
+
for (const option of options) {
|
|
52
|
+
const id = option.id;
|
|
53
|
+
const iso = option.iso2;
|
|
54
|
+
|
|
55
|
+
if (id === ABKHAZIA_COUNTRY_CODE.value) {
|
|
56
|
+
return option;
|
|
57
|
+
}
|
|
58
|
+
if (!russia && (id === RUSSIA_COUNTRY_CODE.value || iso === RUSSIA_COUNTRY_CODE.iso2)) {
|
|
59
|
+
russia = option;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return russia;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (regionCode === RUSSIA_COUNTRY_CODE.iso2) {
|
|
67
|
+
return options.find(byId(RUSSIA_COUNTRY_CODE.value));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return options.find(opt => opt.iso2 === regionCode);
|
|
71
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { PlusSVG } from '@cloud-ru/uikit-product-icons';
|
|
4
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
5
|
+
import { AdaptiveFieldSelect } from '@cloud-ru/uikit-product-mobile-fields';
|
|
6
|
+
import { AdaptiveDrawer, AdaptiveModal, DrawerProps, ModalProps } from '@cloud-ru/uikit-product-mobile-modal';
|
|
7
|
+
import { extractSupportProps, WithLayoutType, WithSupportProps } from '@cloud-ru/uikit-product-utils';
|
|
8
|
+
import { ButtonFunction } from '@snack-uikit/button';
|
|
9
|
+
import { IconPredefinedProps } from '@snack-uikit/icon-predefined';
|
|
10
|
+
import { TooltipProps, WithTooltip } from '@snack-uikit/tooltip';
|
|
11
|
+
|
|
12
|
+
import { useOpen } from '../../hooks';
|
|
13
|
+
import { SelectFooter } from './SelectFooter';
|
|
14
|
+
import { EntityName, FieldSelectProps, LayoutProps } from './types';
|
|
15
|
+
import { useSelectDataStates } from './useSelectDataStates';
|
|
16
|
+
|
|
17
|
+
export type SelectCreateProps = WithSupportProps<
|
|
18
|
+
WithLayoutType &
|
|
19
|
+
LayoutProps & {
|
|
20
|
+
/** Тип объекта для создания новой опции (в единственном числе вин.падеже для кнопки Создать <entityName> и множественном числе) */
|
|
21
|
+
entityName: EntityName;
|
|
22
|
+
/** Коллбек создания новой опции, при успешном выполнении возвращает value новой опции */
|
|
23
|
+
submitHandler: () => Promise<string | void>;
|
|
24
|
+
/** Пропсы прокидываемые в селект */
|
|
25
|
+
selectProps: FieldSelectProps;
|
|
26
|
+
/** Коллбек рефетча запроса на получение списка опций в случае ошибки (при передаче dataError в selectProps). */
|
|
27
|
+
onRefetch?: VoidFunction;
|
|
28
|
+
className?: string;
|
|
29
|
+
/** Коллбек после закрытия модального окна/дровера */
|
|
30
|
+
afterClose?: VoidFunction;
|
|
31
|
+
/** Иконка сервиса */
|
|
32
|
+
entityIcon?: IconPredefinedProps['icon'];
|
|
33
|
+
/** Управление состоянием компонента в зависимости от прав пользователя (по дефолту permission = 'canCreate') */
|
|
34
|
+
permission?: 'none' | 'canRead' | 'canCreate';
|
|
35
|
+
}
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export const SelectCreate = memo(function SelectCreate({
|
|
39
|
+
selectProps,
|
|
40
|
+
createLayoutProps,
|
|
41
|
+
createLayoutType = 'drawer',
|
|
42
|
+
entityName,
|
|
43
|
+
submitHandler,
|
|
44
|
+
onRefetch,
|
|
45
|
+
className,
|
|
46
|
+
afterClose,
|
|
47
|
+
entityIcon,
|
|
48
|
+
permission = 'canCreate',
|
|
49
|
+
layoutType,
|
|
50
|
+
...rest
|
|
51
|
+
}: SelectCreateProps) {
|
|
52
|
+
const { t } = useLocale('FieldsPredefined');
|
|
53
|
+
const { onClose, onOpen, isOpen } = useOpen();
|
|
54
|
+
const selectDataStates = useSelectDataStates({ entityName, entityIcon, onRefetch });
|
|
55
|
+
const selectSize: FieldSelectProps['size'] = selectProps.size || 'm';
|
|
56
|
+
|
|
57
|
+
const handleClose = useCallback(() => {
|
|
58
|
+
onClose();
|
|
59
|
+
afterClose?.();
|
|
60
|
+
}, [afterClose, onClose]);
|
|
61
|
+
|
|
62
|
+
const handleCreate = useCallback(async () => {
|
|
63
|
+
const newOption = await submitHandler();
|
|
64
|
+
selectProps.onChange?.(newOption);
|
|
65
|
+
handleClose();
|
|
66
|
+
}, [handleClose, selectProps, submitHandler]);
|
|
67
|
+
|
|
68
|
+
const buttons = useMemo<Pick<ModalProps, 'approveButton' | 'cancelButton'>>(
|
|
69
|
+
() => ({
|
|
70
|
+
approveButton: { label: t('SelectCreate.buttonCreate'), onClick: handleCreate },
|
|
71
|
+
cancelButton: { label: t('SelectCreate.buttonCancel'), onClick: onClose },
|
|
72
|
+
}),
|
|
73
|
+
[t, handleCreate, onClose],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const formLayout = useMemo(() => {
|
|
77
|
+
const layoutProps = { ...createLayoutProps, ...buttons, open: isOpen, onClose: handleClose, layoutType };
|
|
78
|
+
|
|
79
|
+
return createLayoutType === 'modal' ? (
|
|
80
|
+
<AdaptiveModal {...(layoutProps as WithLayoutType<ModalProps>)} />
|
|
81
|
+
) : (
|
|
82
|
+
<AdaptiveDrawer {...(layoutProps as WithLayoutType<DrawerProps>)} />
|
|
83
|
+
);
|
|
84
|
+
}, [createLayoutProps, buttons, isOpen, handleClose, layoutType, createLayoutType]);
|
|
85
|
+
|
|
86
|
+
const createBtnLabel = `${t('SelectCreate.buttonCreate')} ${entityName.single.toLocaleLowerCase()}`;
|
|
87
|
+
|
|
88
|
+
const tooltipProps: TooltipProps = {
|
|
89
|
+
tip: t('SelectCreate.noPermission'),
|
|
90
|
+
placement: 'top',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className={className} {...extractSupportProps(rest)} data-test-id='select-create__wrapper'>
|
|
95
|
+
<WithTooltip tooltip={permission === 'none' ? tooltipProps : undefined}>
|
|
96
|
+
<AdaptiveFieldSelect
|
|
97
|
+
layoutType={layoutType}
|
|
98
|
+
placeholder={t('SelectCreate.selectPlaceholder')}
|
|
99
|
+
{...selectDataStates}
|
|
100
|
+
{...selectProps}
|
|
101
|
+
size={selectSize}
|
|
102
|
+
footer={
|
|
103
|
+
<SelectFooter onClick={onOpen} createButtonLabel={createBtnLabel} canCreate={permission === 'canCreate'} />
|
|
104
|
+
}
|
|
105
|
+
disabled={permission === 'none'}
|
|
106
|
+
/>
|
|
107
|
+
<WithTooltip tooltip={permission === 'canRead' ? tooltipProps : undefined}>
|
|
108
|
+
<ButtonFunction
|
|
109
|
+
label={createBtnLabel}
|
|
110
|
+
icon={<PlusSVG />}
|
|
111
|
+
iconPosition='before'
|
|
112
|
+
onClick={onOpen}
|
|
113
|
+
size={selectSize}
|
|
114
|
+
data-test-id='select-create__create-button'
|
|
115
|
+
disabled={permission !== 'canCreate'}
|
|
116
|
+
/>
|
|
117
|
+
</WithTooltip>
|
|
118
|
+
</WithTooltip>
|
|
119
|
+
{formLayout}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PlusSVG } from '@cloud-ru/uikit-product-icons';
|
|
2
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
3
|
+
import { ButtonFunction } from '@snack-uikit/button';
|
|
4
|
+
import { WithTooltip } from '@snack-uikit/tooltip';
|
|
5
|
+
|
|
6
|
+
import styled from './styles.module.scss';
|
|
7
|
+
type SelectFooterProps = {
|
|
8
|
+
onClick: VoidFunction;
|
|
9
|
+
createButtonLabel: string;
|
|
10
|
+
canCreate: boolean;
|
|
11
|
+
};
|
|
12
|
+
export function SelectFooter({ onClick, createButtonLabel, canCreate }: SelectFooterProps) {
|
|
13
|
+
const { t } = useLocale('FieldsPredefined');
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className={styled.footerWrapper}>
|
|
17
|
+
<WithTooltip tooltip={canCreate ? undefined : { tip: t('SelectCreate.noPermission'), placement: 'top' }}>
|
|
18
|
+
<ButtonFunction
|
|
19
|
+
label={createButtonLabel}
|
|
20
|
+
onClick={onClick}
|
|
21
|
+
icon={<PlusSVG />}
|
|
22
|
+
iconPosition='before'
|
|
23
|
+
data-test-id='select-create__create-button-footer'
|
|
24
|
+
disabled={!canCreate}
|
|
25
|
+
/>
|
|
26
|
+
</WithTooltip>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SelectFooter';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SelectCreate';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { FieldSelectMultipleProps, FieldSelectSingleProps } from '@cloud-ru/uikit-product-mobile-fields';
|
|
4
|
+
import { AdaptiveDrawerProps, AdaptiveModalProps } from '@cloud-ru/uikit-product-mobile-modal';
|
|
5
|
+
|
|
6
|
+
type CommonModalDrawerProps = 'open' | 'onClose' | 'approveButton' | 'cancelButton' | 'layoutType';
|
|
7
|
+
type CreateLayoutModalProps = Omit<AdaptiveModalProps, CommonModalDrawerProps> & { content: ReactNode };
|
|
8
|
+
type CreateLayoutDrawerProps = Omit<AdaptiveDrawerProps, CommonModalDrawerProps>;
|
|
9
|
+
|
|
10
|
+
export type LayoutProps =
|
|
11
|
+
| {
|
|
12
|
+
/** По клику на кнопку создания открывать модальное окно или дровер */
|
|
13
|
+
createLayoutType: 'modal';
|
|
14
|
+
/** Пропсы передаваемые в модалку или дровер создания новой опции */
|
|
15
|
+
createLayoutProps: CreateLayoutModalProps;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
createLayoutType: 'drawer';
|
|
19
|
+
createLayoutProps: CreateLayoutDrawerProps;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type EntityName = {
|
|
23
|
+
plural: string;
|
|
24
|
+
single: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type OmittedSelectProps = 'footer' | 'noResultState' | 'noDataState' | 'errorDataState';
|
|
28
|
+
|
|
29
|
+
export type FieldSelectProps =
|
|
30
|
+
| (Omit<FieldSelectSingleProps, OmittedSelectProps> & {
|
|
31
|
+
selection?: 'single';
|
|
32
|
+
})
|
|
33
|
+
| (Omit<FieldSelectMultipleProps, OmittedSelectProps> & {
|
|
34
|
+
selection: 'multiple';
|
|
35
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { SearchSVG, UpdateSVG } from '@cloud-ru/uikit-product-icons';
|
|
4
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
5
|
+
import { FieldSelectProps } from '@cloud-ru/uikit-product-mobile-fields';
|
|
6
|
+
import { ButtonTonal } from '@snack-uikit/button';
|
|
7
|
+
import { IconPredefinedProps } from '@snack-uikit/icon-predefined';
|
|
8
|
+
|
|
9
|
+
import { capitalize } from '../../helpers';
|
|
10
|
+
import { EntityName } from './types';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
onRefetch?: VoidFunction;
|
|
14
|
+
entityName: EntityName;
|
|
15
|
+
entityIcon?: IconPredefinedProps['icon'];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function useSelectDataStates({ onRefetch, entityName, entityIcon }: Props) {
|
|
19
|
+
const { t } = useLocale('FieldsPredefined');
|
|
20
|
+
|
|
21
|
+
return useMemo<Pick<FieldSelectProps, 'noResultsState' | 'noDataState' | 'errorDataState'>>(
|
|
22
|
+
() => ({
|
|
23
|
+
noDataState: {
|
|
24
|
+
icon: { icon: entityIcon ?? SearchSVG },
|
|
25
|
+
description: `${capitalize(entityName.plural)} ${t('SelectCreate.noData')}`,
|
|
26
|
+
},
|
|
27
|
+
noResultsState: {
|
|
28
|
+
icon: { icon: SearchSVG },
|
|
29
|
+
description: (
|
|
30
|
+
<>
|
|
31
|
+
{capitalize(entityName.plural)} {t('SelectCreate.noResult')}.
|
|
32
|
+
<br />
|
|
33
|
+
{t('SelectCreate.changeRequest')} {entityName.single.toLocaleLowerCase()}
|
|
34
|
+
</>
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
errorDataState: {
|
|
38
|
+
description: `${t('SelectCreate.loadError')} ${entityName.plural.toLocaleLowerCase()}`,
|
|
39
|
+
footer: onRefetch ? (
|
|
40
|
+
<ButtonTonal
|
|
41
|
+
label={t('SelectCreate.buttonRefetch')}
|
|
42
|
+
icon={<UpdateSVG />}
|
|
43
|
+
appearance='neutral'
|
|
44
|
+
onClick={onRefetch}
|
|
45
|
+
/>
|
|
46
|
+
) : (
|
|
47
|
+
<></>
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
[entityIcon, entityName, t, onRefetch],
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ArrowUpSVG } from '@cloud-ru/uikit-product-icons';
|
|
2
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
3
|
+
import { ButtonFilled } from '@snack-uikit/button';
|
|
4
|
+
import { Tooltip } from '@snack-uikit/tooltip';
|
|
5
|
+
|
|
6
|
+
export type FieldSubmitButtonProps = {
|
|
7
|
+
active: boolean;
|
|
8
|
+
handleClick(): void;
|
|
9
|
+
size?: 'xs' | 's';
|
|
10
|
+
className?: string;
|
|
11
|
+
fullWidth?: boolean;
|
|
12
|
+
showTooltip?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function FieldSubmitButton({
|
|
16
|
+
active,
|
|
17
|
+
handleClick,
|
|
18
|
+
size = 'xs',
|
|
19
|
+
className,
|
|
20
|
+
fullWidth,
|
|
21
|
+
showTooltip = true,
|
|
22
|
+
}: FieldSubmitButtonProps) {
|
|
23
|
+
const { t } = useLocale('FieldsPredefined');
|
|
24
|
+
|
|
25
|
+
if (active) {
|
|
26
|
+
return (
|
|
27
|
+
<Tooltip tip={t('FieldAi.submit.tooltip')} hoverDelayOpen={600} open={showTooltip ? undefined : false}>
|
|
28
|
+
<ButtonFilled
|
|
29
|
+
fullWidth={fullWidth}
|
|
30
|
+
icon={<ArrowUpSVG />}
|
|
31
|
+
size={size}
|
|
32
|
+
appearance='primary'
|
|
33
|
+
type='submit'
|
|
34
|
+
onClick={handleClick}
|
|
35
|
+
className={className}
|
|
36
|
+
/>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return <ButtonFilled icon={<ArrowUpSVG />} size={size} disabled className={className} fullWidth={fullWidth} />;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FieldSubmitButton';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import styles from './styles.module.scss';
|
|
5
|
+
|
|
6
|
+
type TextAreaActionsFooterProps = {
|
|
7
|
+
left?: ReactNode;
|
|
8
|
+
right?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function TextAreaActionsFooter({ left, right }: TextAreaActionsFooterProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.actionsFooter}>
|
|
14
|
+
<div className={styles.actionsWrapper}>{left}</div>
|
|
15
|
+
<div className={cn(styles.actionsWrapper, styles.right)}>{right}</div>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './TextAreaActionsFooter';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as ste;
|
|
2
|
+
|
|
3
|
+
.actionsFooter {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: row;
|
|
6
|
+
justify-content: space-between;
|
|
7
|
+
align-items: center;
|
|
8
|
+
width: 100%;
|
|
9
|
+
padding-bottom: 7px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.actionsWrapper {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: ste.$dimension-050m;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.right {
|
|
20
|
+
justify-content: flex-end;
|
|
21
|
+
width: 100%;
|
|
22
|
+
padding-right: 8px;
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Возвращает кол-во символов для маски - со скобками и без
|
|
3
|
+
* @function helper
|
|
4
|
+
*/
|
|
5
|
+
export function getSymbolsRangeFromMask(mask: string = '') {
|
|
6
|
+
// Считаем все 'X' и цифры в строке
|
|
7
|
+
const maxNumberLength = (mask.match(/[X\d]/g) || []).length;
|
|
8
|
+
|
|
9
|
+
// Удаляем содержимое внутри скобок, включая сами скобки для подсчёта без учёта их содержимого
|
|
10
|
+
const stringWithoutBrackets = mask.replace(/\[[^\]]*\]/g, '');
|
|
11
|
+
|
|
12
|
+
// Считаем 'X' и цифры после удаления содержимого в скобках
|
|
13
|
+
const minNumberLength = (stringWithoutBrackets.match(/[X\d]/g) || []).length;
|
|
14
|
+
|
|
15
|
+
// Возвращаем оба значения: без учёта и с учётом содержимого в скобках
|
|
16
|
+
return { minNumberLength, maxNumberLength };
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useOpen';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useOpen(defaultState = false) {
|
|
4
|
+
const [isOpen, setIsOpen] = useState(defaultState);
|
|
5
|
+
|
|
6
|
+
const onClose = useCallback(() => {
|
|
7
|
+
setIsOpen(false);
|
|
8
|
+
}, []);
|
|
9
|
+
|
|
10
|
+
const onOpen = useCallback(() => {
|
|
11
|
+
setIsOpen(true);
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
return useMemo(
|
|
15
|
+
() => ({
|
|
16
|
+
isOpen,
|
|
17
|
+
onClose,
|
|
18
|
+
onOpen,
|
|
19
|
+
}),
|
|
20
|
+
[isOpen, onClose, onOpen],
|
|
21
|
+
);
|
|
22
|
+
}
|
package/src/index.ts
ADDED