From 0a5267e5949135c8d0d54268f72baa854362896f Mon Sep 17 00:00:00 2001 From: Maria Lordwill Date: Fri, 7 Aug 2026 19:59:46 +0400 Subject: [PATCH 01/40] 48741 feat(fieldsets): add usage banner with linked templates dropdown --- .../src/public/__stubs__/fieldsets.factory.ts | 1 + .../Fieldsets/FieldsetCard/FieldsetCard.tsx | 2 + .../__tests__/FieldsetCard.test.tsx | 1 + .../FieldsetDetails/FieldsetDetails.css | 40 +++++++++++++++++++ .../FieldsetDetails/FieldsetDetails.tsx | 26 ++++++++++++ .../__tests__/KickoffRedux.test.tsx | 1 + .../UI/DropdownList/DropdownList.tsx | 3 ++ frontend/src/public/lang/locales/en_US.ts | 4 ++ frontend/src/public/lang/locales/ru_RU.ts | 4 ++ frontend/src/public/types/fieldset.ts | 3 +- 10 files changed, 84 insertions(+), 1 deletion(-) diff --git a/frontend/src/public/__stubs__/fieldsets.factory.ts b/frontend/src/public/__stubs__/fieldsets.factory.ts index 9fea0f4c6..f12a6c5c9 100644 --- a/frontend/src/public/__stubs__/fieldsets.factory.ts +++ b/frontend/src/public/__stubs__/fieldsets.factory.ts @@ -50,6 +50,7 @@ export const makeFieldsetCatalogItem = (overrides: Partial title: '', rules: [], fields: [], + usage: [], ...overrides, }); diff --git a/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx b/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx index 710c9269b..689a70fb7 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx @@ -26,6 +26,7 @@ export function FieldsetCard({ title, rules, fields, + usage, }: IFieldsetCatalogItem) { const { formatMessage } = useIntl(); const dispatch = useDispatch(); @@ -51,6 +52,7 @@ export function FieldsetCard({ title, rules, fields, + usage, })); dispatch(openEditModal()); }; diff --git a/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx b/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx index d2c0ea1dd..ccfcb7ff9 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx @@ -134,6 +134,7 @@ describe('FieldsetCard', () => { title: props.title, rules: props.rules, fields: props.fields, + usage: props.usage, }), ); expect(mockDispatch).toHaveBeenCalledWith(openEditModal()); diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css index a6efe6e6f..b2a8d2415 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css @@ -284,3 +284,43 @@ background-repeat: no-repeat; background-size: 200px 100%; } + +.usage-banner { + padding: 1.2rem 2rem; + border-radius: 0.8rem; + margin-bottom: 2rem; + @mixin text-base; +} + +.usage-banner--linked { + background-color: var(--pneumatic-color-beige); +} + +.usage-banner--not-linked { + background-color: var(--pneumatic-color-black8); + color: var(--pneumatic-color-black48); +} + +.usage-banner__row { + display: flex; + justify-content: space-between; +} + +.usage-banner .usage-banner__row .usage-banner__dropdown .usage-banner__menu-list { + min-width: 24rem !important; + max-width: 24rem; +} + +.usage-banner .usage-banner__row .usage-banner__dropdown .usage-banner__option { + display: block !important; + @mixin text-truncate; +} + +.usage-banner__dropdown button { + background: none; + + p { + color: var(--pneumatic-color-link-dark); + font-weight: normal; + } +} diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx index f6ecda629..342bf2118 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx @@ -17,6 +17,7 @@ import { ERoutes } from '../../../constants/routes'; import { ModifyDropdown, Button, FilterSelect } from '../../UI'; import { EModifyDropdownToggle } from '../../UI/ModifyDropdown/types'; +import { DropdownList } from '../../UI/DropdownList'; import { NotificationManager } from '../../UI/Notifications'; import { FieldsetModal } from '../FieldsetModal/FieldsetModal'; import { EFieldsetModalType } from '../FieldsetModal/types'; @@ -281,6 +282,31 @@ const FieldsetDetails = ({ + {fieldset.usage.length > 0 ? ( +
+
+ + {formatMessage({ id: 'fieldsets.usage.linked' }, { count: fieldset.usage.length })} + + ({ label: template.name }))} + placement="left" + classNames={{ + menuList: () => styles['usage-banner__menu-list'], + option: () => styles['usage-banner__option'], + }} + /> +
+
+ ) : ( +
+ {formatMessage({ id: 'fieldsets.usage.not-linked' })} +
+ )} +

{formatMessage({ id: 'fieldsets.settings-section' })}

diff --git a/frontend/src/public/components/TemplateEdit/KickoffRedux/__tests__/KickoffRedux.test.tsx b/frontend/src/public/components/TemplateEdit/KickoffRedux/__tests__/KickoffRedux.test.tsx index d9cb12e71..b487925cd 100644 --- a/frontend/src/public/components/TemplateEdit/KickoffRedux/__tests__/KickoffRedux.test.tsx +++ b/frontend/src/public/components/TemplateEdit/KickoffRedux/__tests__/KickoffRedux.test.tsx @@ -87,6 +87,7 @@ jest.mock('../../TaskOutputFlow/FieldsetIconPicker', () => ({ labelPosition: 'top', layout: 'vertical', title: '', + usage: [], } as IFieldsetCatalogItem), }, 'Add fieldset New Set', diff --git a/frontend/src/public/components/UI/DropdownList/DropdownList.tsx b/frontend/src/public/components/UI/DropdownList/DropdownList.tsx index 6b451a944..99508e85e 100644 --- a/frontend/src/public/components/UI/DropdownList/DropdownList.tsx +++ b/frontend/src/public/components/UI/DropdownList/DropdownList.tsx @@ -167,12 +167,15 @@ const DropdownIndicator = ({ isDisabled }: any) => { }; const MenuSM = ({ children, ...props }: any) => { + const customClassName = props.selectProps.classNames?.menuList?.(props); + return (
{children} diff --git a/frontend/src/public/lang/locales/en_US.ts b/frontend/src/public/lang/locales/en_US.ts index c0cb1b801..647f5b90e 100644 --- a/frontend/src/public/lang/locales/en_US.ts +++ b/frontend/src/public/lang/locales/en_US.ts @@ -1545,4 +1545,8 @@ export const enMessages = { 'template.fieldset-picker.empty': 'No fieldsets available. Create one first.', 'fieldset.group-title': 'Fieldset: {name}', 'fieldset.validation-error.sum-max': 'The sum of numeric fields in "{name}" must not exceed {value}.', + 'fieldsets.usage.linked': 'Used in {count} {count, plural, one {template} other {templates}}. Editing is locked.', + 'fieldsets.usage.not-linked': 'Not used in any template', + 'fieldsets.usage.show': 'Show', + 'fieldsets.usage.hide': 'Hide', }; diff --git a/frontend/src/public/lang/locales/ru_RU.ts b/frontend/src/public/lang/locales/ru_RU.ts index f64e615ed..ca54343dc 100644 --- a/frontend/src/public/lang/locales/ru_RU.ts +++ b/frontend/src/public/lang/locales/ru_RU.ts @@ -1435,6 +1435,10 @@ export const ruMessages = { 'template.fieldset-picker.empty': 'Нет доступных наборов полей. Сначала создайте один.', 'fieldset.group-title': 'Набор полей: {name}', 'fieldset.validation-error.sum-max': 'Сумма числовых полей в «{name}» не должна превышать {value}.', + 'fieldsets.usage.linked': 'Используется в {count} {count, plural, one {шаблоне} few {шаблонах} other {шаблонах}}. Редактирование заблокировано.', + 'fieldsets.usage.not-linked': 'Не используется ни в одном шаблоне', + 'fieldsets.usage.show': 'Показать', + 'fieldsets.usage.hide': 'Скрыть', 'user-info.vacation.title': 'Статус', 'user-info.vacation.status': 'Опции', 'user-info.vacation.active': 'Вы в отпуске/отсутствуете', diff --git a/frontend/src/public/types/fieldset.ts b/frontend/src/public/types/fieldset.ts index 9ed920b01..02b6dc769 100644 --- a/frontend/src/public/types/fieldset.ts +++ b/frontend/src/public/types/fieldset.ts @@ -41,9 +41,10 @@ export interface IFieldsetCatalogItem { title: string; rules: IFieldsetTemplateRule[]; fields: IFieldsetField[]; + usage: { id: number; name: string }[]; } -export interface IFieldsetBinding extends Omit { +export interface IFieldsetBinding extends Omit { sharedFieldsetId: number; } From a21c94cfe4e1e792184acbb85995d40d4df2b994 Mon Sep 17 00:00:00 2001 From: Maria Lordwill Date: Tue, 11 Aug 2026 11:33:39 +0400 Subject: [PATCH 02/40] 48741 feat(fieldset): add fieldset details disabled/readonly view --- .../public/assets/css/mixins/fieldsets.pcss | 19 +++ .../src/public/components/Field/Field.tsx | 1 + frontend/src/public/components/Field/types.ts | 1 + .../FieldsetDetails/FieldsetDetails.css | 149 +++++++++++++++++- .../FieldsetDetails/FieldsetDetails.tsx | 86 +++++++--- .../FieldsetUnsavedChangesModal.tsx | 13 +- .../FieldsetUnsavedChangesModal.test.tsx | 19 ++- .../Fieldsets/FieldsetDetails/types.ts | 1 + .../public/components/Fieldsets/constants.ts | 2 + .../Checkbox/ExtraFieldCheckbox.css | 1 + .../Checkbox/ExtraFieldCheckbox.tsx | 2 + .../Creatable/ExtraFieldCreatable.css | 10 +- .../Creatable/ExtraFieldCreatable.tsx | 1 + .../ExtraFields/Radio/ExtraFieldRadio.css | 1 + .../ExtraFields/Radio/ExtraFieldRadio.tsx | 2 + .../ExtraFields/utils/FieldLabel.tsx | 1 + .../ExtraFields/utils/FieldWithName.tsx | 1 + .../UI/Fields/Checkbox/Checkbox.css | 1 + .../UI/Fields/RadioButton/RadioButton.css | 1 + frontend/src/public/lang/locales/en_US.ts | 1 + frontend/src/public/lang/locales/ru_RU.ts | 3 +- 21 files changed, 272 insertions(+), 44 deletions(-) create mode 100644 frontend/src/public/assets/css/mixins/fieldsets.pcss diff --git a/frontend/src/public/assets/css/mixins/fieldsets.pcss b/frontend/src/public/assets/css/mixins/fieldsets.pcss new file mode 100644 index 000000000..e692e84aa --- /dev/null +++ b/frontend/src/public/assets/css/mixins/fieldsets.pcss @@ -0,0 +1,19 @@ +@define-mixin fieldset-field-readonly { + cursor: default; + color: var(--pneumatic-color-black72); + background-color: var(--pneumatic-color-black4); + border: 0.1rem solid transparent; + opacity: 1; +} + +@define-mixin fieldset-textarea-readonly { + @mixin fieldset-field-readonly; + resize: none; +} + +@define-mixin fieldset-option-readonly { + field-sizing: content; + width: auto; + padding: 0.4rem 1.2rem; + border-radius: 0.6rem; +} diff --git a/frontend/src/public/components/Field/Field.tsx b/frontend/src/public/components/Field/Field.tsx index d6a19f254..fd91a680f 100644 --- a/frontend/src/public/components/Field/Field.tsx +++ b/frontend/src/public/components/Field/Field.tsx @@ -95,6 +95,7 @@ export function Field({ onBlur={() => setTouched(true)} placeholder={placeholder} disabled={disabled} + data-use-input={inputRest['data-use-input']} /> ); case EFieldTagName.RichText: diff --git a/frontend/src/public/components/Field/types.ts b/frontend/src/public/components/Field/types.ts index d406445ad..21ad3745e 100644 --- a/frontend/src/public/components/Field/types.ts +++ b/frontend/src/public/components/Field/types.ts @@ -23,6 +23,7 @@ export interface IFieldProps extends NativeInputProps { isFromConditionValueField?: boolean; value?: string | number | string[]; innerRef?: React.Ref; + 'data-use-input'?: boolean; } export enum EFieldTagName { diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css index b2a8d2415..c3115faab 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css @@ -23,7 +23,8 @@ .section-title { margin-bottom: 1.6rem; font-size: 1.8rem; - font-weight: bold; + font-weight: 800; + color: var(--pneumatic-color-black72); @mixin text-base; } @@ -83,6 +84,10 @@ &:focus { border-color: var(--pneumatic-color-blue); } + + &:disabled { + @mixin fieldset-textarea-readonly; + } } .settings-select { @@ -102,6 +107,22 @@ &:focus { border-color: var(--pneumatic-color-blue); } + + &:disabled { + @mixin fieldset-field-readonly; + } +} + +.readonly-badge { + margin-left: 1.2rem; + padding: 0.2rem 0.8rem; + font-size: 1.2rem; + font-weight: 600; + color: var(--pneumatic-color-black72); + background-color: var(--pneumatic-color-black4); + border-radius: 0.4rem; + vertical-align: middle; + opacity: 0.55; } .settings-input { @@ -138,8 +159,86 @@ display: none; } +.components_disabled { + opacity: 0.45; + pointer-events: none; +} + .fields { margin-top: 1.6rem; + + [data-use-input]:disabled { + cursor: default !important; + resize: none !important; + color: var(--pneumatic-color-black72) !important; + background-color: var(--pneumatic-color-black4) !important; + border: 1px solid transparent !important; + border-radius: 0.8rem !important; + opacity: 1 !important; + } + + [class*="kick-off-input__name-textarea"]:disabled, + [class*="kickoff-create-field-name-input"]:disabled, + [class*="extra-field-file__input-name--template"]:disabled { + padding: 0 0.8rem !important; + border-radius: 0.6rem !important; + } + + [class*="kick-off-input__name"]:has(:disabled) { + background: transparent !important; + } + + [class*="kick-off-required-sign"] { + background-color: transparent !important; + } + + [class*="kickoff-create-field-option__input"]:disabled { + @mixin fieldset-option-readonly; + max-width: 100%; + } + + [class*="labeled-checkbox__input"]:disabled, + [class*="labeled-radiobutton__input"]:disabled { + @mixin fieldset-option-readonly; + max-width: calc(100% - 3.2rem); + } + + [class*="checkbox__text"], + [class*="radio__text"], + [class*="checkbox__title"], + [class*="radio__title"] { + color: var(--pneumatic-color-black88) !important; + } + + [data-use-input]::placeholder { + color: var(--pneumatic-color-black48) !important; + -webkit-text-fill-color: var(--pneumatic-color-black48) !important; + opacity: 1 !important; + } + + &.fieldset_readonly [class*="kickoff-create-field-container"], + &.fieldset_readonly [class*="extra-field-file__conteiner--template"] { + max-width: 100% !important; + } + + [class*="extra-field-file__upload-button--template"] { + color: var(--pneumatic-color-black48) !important; + border-color: var(--pneumatic-color-black16) !important; + opacity: 0.55 !important; + cursor: default !important; + } + + [class*="checkbox__input"]:disabled + [class*="checkbox__box"], + [class*="radio__input"]:disabled + [class*="radio__box"] { + cursor: default !important; + border-color: var(--pneumatic-color-black16) !important; + opacity: 0.32 !important; + } + + [class*="edit-name-button"], + [class*="kick-off-edit-name"] { + display: none !important; + } } .save-bar { @@ -212,6 +311,20 @@ .rule-fields-select { flex: 1; min-width: 0; + [aria-disabled="true"] [class*="clear-button"], + [class*="clear-button"][aria-disabled="true"] { + display: none; + } + + [class*="active-value"] { + font-weight: bold; + color: var(--pneumatic-color-black88); + opacity: 1; + } + + [class*="filter-select_disabled"] { + opacity: 1; + } } .rule-type-label { @@ -234,6 +347,10 @@ &:focus { border-color: var(--pneumatic-color-blue); } + + &:disabled { + @mixin fieldset-field-readonly; + } } .rule-delete-btn { @@ -247,10 +364,15 @@ @mixin text-small; - &:hover { + &:hover:not(:disabled) { color: var(--pneumatic-color-red); border-color: var(--pneumatic-color-red); } + + &:disabled { + @mixin fieldset-field-readonly; + opacity: 0.55; + } } .add-rule-btn { @@ -266,10 +388,17 @@ @mixin text-base; - &:hover { + &:hover:not(:disabled) { background-color: var(--pneumatic-color-blue-light); border-color: var(--pneumatic-color-blue); } + + &:disabled { + cursor: default; + color: var(--pneumatic-color-black32); + border-color: var(--pneumatic-color-black16); + opacity: 0.55; + } } .header-skeleton { @@ -286,9 +415,13 @@ } .usage-banner { + display: flex; + align-items: center; + min-height: 5.6rem; padding: 1.2rem 2rem; border-radius: 0.8rem; margin-bottom: 2rem; + @mixin text-base; } @@ -297,18 +430,24 @@ } .usage-banner--not-linked { - background-color: var(--pneumatic-color-black8); - color: var(--pneumatic-color-black48); + background-color: var(--pneumatic-success-color-light); + color: var(--pneumatic-color-black80); } .usage-banner__row { display: flex; justify-content: space-between; + align-items: center; + width: 100%; } .usage-banner .usage-banner__row .usage-banner__dropdown .usage-banner__menu-list { min-width: 24rem !important; max-width: 24rem; + + :global(.ps) { + max-height: 20.4rem !important; + } } .usage-banner .usage-banner__row .usage-banner__dropdown .usage-banner__option { diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx index 342bf2118..0362548c0 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { useEffect, useState, useMemo, useCallback, ChangeEvent } from 'react'; +import classnames from 'classnames'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; @@ -46,6 +47,7 @@ import { useDatasetOptions } from '../../TemplateEdit/ExtraFields/utils/useDatas import { normalizeFieldsForUI } from './fieldsetFieldMappers'; import { validateFieldsetRules } from '../validators'; import { + FIELDSET_READONLY_TEXT, FIELDSET_LABEL_POSITION_OPTIONS, FIELDSET_RULE_TYPES, FIELDSET_RULE_VALUE_PLACEHOLDER_BY_TYPE, @@ -244,6 +246,9 @@ const FieldsetDetails = ({ return null; } + const isLinked = fieldset.usage.length > 0; + const readOnlyBadge = isLinked ? {FIELDSET_READONLY_TEXT} : null; + const handleCloneFieldset = () => { if (isChanged) { NotificationManager.warning({ @@ -256,7 +261,7 @@ const FieldsetDetails = ({ return (
- +

{fieldset.name}

@@ -282,7 +287,7 @@ const FieldsetDetails = ({
- {fieldset.usage.length > 0 ? ( + {isLinked ? (
@@ -308,7 +313,10 @@ const FieldsetDetails = ({ )}
-

{formatMessage({ id: 'fieldsets.settings-section' })}

+

+ {formatMessage({ id: 'fieldsets.settings-section' })} + {readOnlyBadge} +

@@ -321,6 +329,7 @@ const FieldsetDetails = ({ value={detailFieldset.description} placeholder={formatMessage({ id: 'fieldsets.settings.description-placeholder' })} onChange={handleSettingsDescriptionChange} + disabled={isLinked} />
@@ -333,6 +342,7 @@ const FieldsetDetails = ({ className={styles['settings-select']} value={detailFieldset.labelPosition} onChange={handleSettingsLabelPositionChange} + disabled={isLinked} > {FIELDSET_LABEL_POSITION_OPTIONS.map((option) => (
-

{formatMessage({ id: 'fieldsets.fields-section' })}

+

+ {formatMessage({ id: 'fieldsets.fields-section' })} + {readOnlyBadge} +

-
+
{ExtraFieldsMap.map((x) => ( - handleCreateField(x.id)} /> + handleCreateField(x.id)} + /> ))}
{sortedFields.length > 0 && ( -
+
{sortedFields.map((field, index) => ( @@ -382,7 +400,10 @@ const FieldsetDetails = ({
-

{formatMessage({ id: 'fieldsets.rules-section' })}

+

+ {formatMessage({ id: 'fieldsets.rules-section' })} + {readOnlyBadge} +

{detailFieldset.rules.length === 0 && (

{formatMessage({ id: 'fieldsets.no-rules' })}

@@ -395,6 +416,7 @@ const FieldsetDetails = ({ onChange={(e) => handleEditRuleType(index, e.target.value as EFieldsetRuleType)} className={styles['rule-value-input']} style={{ flex: 'none', minWidth: '10rem' }} + disabled={isLinked} > {FIELDSET_RULE_TYPES.map((ruleType) => (
- -
- )}
+ {!isLinked && ( +
+
+ )} +
); diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetUnsavedChangesModal.tsx b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetUnsavedChangesModal.tsx index a4c00e875..f1a6c2968 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetUnsavedChangesModal.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetUnsavedChangesModal.tsx @@ -8,7 +8,7 @@ import { Button, RouteLeavingGuard } from '../../UI'; import { getCurrentFieldset } from '../../../redux/selectors/fieldsets'; import { TFieldsetUnsavedChangesModalProps } from './types'; -export function FieldsetUnsavedChangesModal({ isChanged }: TFieldsetUnsavedChangesModalProps) { +export function FieldsetUnsavedChangesModal({ isChanged, onSave }: TFieldsetUnsavedChangesModalProps) { const { formatMessage } = useIntl(); const fieldset = useSelector(getCurrentFieldset); @@ -42,9 +42,18 @@ export function FieldsetUnsavedChangesModal({ isChanged }: TFieldsetUnsavedChang
diff --git a/frontend/src/public/components/UI/Fields/Checkbox/Checkbox.css b/frontend/src/public/components/UI/Fields/Checkbox/Checkbox.css index 5fe668073..af8af5282 100644 --- a/frontend/src/public/components/UI/Fields/Checkbox/Checkbox.css +++ b/frontend/src/public/components/UI/Fields/Checkbox/Checkbox.css @@ -56,6 +56,7 @@ &__title { margin: 0; + overflow-wrap: anywhere; } &:hover { diff --git a/frontend/src/public/components/UI/Fields/RadioButton/RadioButton.css b/frontend/src/public/components/UI/Fields/RadioButton/RadioButton.css index 3ec8ef045..e3ffb53c9 100644 --- a/frontend/src/public/components/UI/Fields/RadioButton/RadioButton.css +++ b/frontend/src/public/components/UI/Fields/RadioButton/RadioButton.css @@ -47,6 +47,7 @@ .radio__title { font-size: 1.5rem; line-height: 2rem; + overflow-wrap: anywhere; } .radio:hover .radio__box, diff --git a/frontend/src/public/lang/locales/en_US.ts b/frontend/src/public/lang/locales/en_US.ts index 647f5b90e..066069600 100644 --- a/frontend/src/public/lang/locales/en_US.ts +++ b/frontend/src/public/lang/locales/en_US.ts @@ -1522,6 +1522,7 @@ export const enMessages = { 'fieldsets.leave-unsaved-message': 'Your changes are not saved.', 'fieldsets.leave-unsaved-stay': 'Keep editing', 'fieldsets.leave-unsaved-leave': 'Discard', + 'fieldsets.leave-unsaved-save': 'Save', 'fieldsets.clone-unsaved-warning': 'Please save or discard unsaved changes before cloning', 'fieldsets.settings-section': 'Settings', 'fieldsets.settings.description': 'Description', diff --git a/frontend/src/public/lang/locales/ru_RU.ts b/frontend/src/public/lang/locales/ru_RU.ts index ca54343dc..21adbe95f 100644 --- a/frontend/src/public/lang/locales/ru_RU.ts +++ b/frontend/src/public/lang/locales/ru_RU.ts @@ -1410,7 +1410,8 @@ export const ruMessages = { 'fieldsets.leave-unsaved-title': 'Отменить изменения для набора полей {name}?', 'fieldsets.leave-unsaved-message': 'Ваши изменения не сохранены.', 'fieldsets.leave-unsaved-stay': 'Продолжить редактирование', - 'fieldsets.leave-unsaved-leave': 'Отменить', + 'fieldsets.leave-unsaved-leave': 'Убрать', + 'fieldsets.leave-unsaved-save': 'Сохранить', 'fieldsets.clone-unsaved-warning': 'Пожалуйста, сохраните или отмените несохраненные изменения перед клонированием', 'fieldsets.settings-section': 'Настройки', From 587ad2cb4dbe002d05a4184ce7cff2bdd456f645 Mon Sep 17 00:00:00 2001 From: Maria Lordwill Date: Tue, 11 Aug 2026 12:32:12 +0400 Subject: [PATCH 03/40] 48741 feat(fieldsets): disable edit and delete actions in ModifyDropdown --- .../Fieldsets/FieldsetCard/FieldsetCard.tsx | 43 ++++++------------- .../FieldsetDetails/FieldsetDetails.tsx | 1 + .../components/UI/Dropdown/Dropdown.css | 10 +++++ .../components/UI/Dropdown/Dropdown.tsx | 36 +++++++++++++--- .../UI/ModifyDropdown/ModifyDropdown.tsx | 10 +++++ .../components/UI/ModifyDropdown/types.ts | 1 + frontend/src/public/lang/locales/en_US.ts | 1 + frontend/src/public/lang/locales/ru_RU.ts | 1 + 8 files changed, 68 insertions(+), 35 deletions(-) diff --git a/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx b/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx index 689a70fb7..2d2292bc5 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetCard/FieldsetCard.tsx @@ -4,8 +4,8 @@ import { useDispatch } from 'react-redux'; import { useIntl } from 'react-intl'; import classnames from 'classnames'; -import { Dropdown, TDropdownOption } from '../../UI'; -import { MoreIcon, PencilIcon, TrashIcon, UnionIcon } from '../../icons'; +import { ModifyDropdown } from '../../UI'; +import { EModifyDropdownToggle } from '../../UI/ModifyDropdown/types'; import { WarningPopup } from '../../UI/WarningPopup'; import { openEditModal, deleteFieldsetAction, cloneFieldsetAction, setCurrentFieldset } from '../../../redux/fieldsets/slice'; import { history } from '../../../utils/history'; @@ -68,29 +68,7 @@ export function FieldsetCard({ dispatch(cloneFieldsetAction({ id })); }; - const dropdownOptions: TDropdownOption[] = [ - { - label: formatMessage({ id: 'fieldsets.edit' }), - onClick: handleEditName, - Icon: PencilIcon, - size: 'sm', - }, - { - label: formatMessage({ id: 'fieldsets.clone' }), - onClick: handleCloneFieldset, - Icon: UnionIcon, - size: 'sm', - }, - { - label: formatMessage({ id: 'fieldsets.delete' }), - onClick: handleOpenDeleteModal, - Icon: TrashIcon, - color: 'red', - withUpperline: true, - size: 'sm', - }, - ]; - + const isLinked = Boolean(usage && usage.length > 0); const hasContent = fields.length > 0 || rules.length > 0; return ( @@ -118,11 +96,16 @@ export function FieldsetCard({ {sanitizeText(name)}
- ( - - )} - options={dropdownOptions} +
diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx index 0362548c0..4bb18795f 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx @@ -282,6 +282,7 @@ const FieldsetDetails = ({ editLabel={formatMessage({ id: 'fieldsets.edit' })} deleteLabel={formatMessage({ id: 'fieldsets.delete' })} cloneLabel={formatMessage({ id: 'fieldsets.clone' })} + isReadOnly={isLinked} toggleType={EModifyDropdownToggle.Modify} />
diff --git a/frontend/src/public/components/UI/Dropdown/Dropdown.css b/frontend/src/public/components/UI/Dropdown/Dropdown.css index 24a74f091..640fa91b0 100644 --- a/frontend/src/public/components/UI/Dropdown/Dropdown.css +++ b/frontend/src/public/components/UI/Dropdown/Dropdown.css @@ -88,6 +88,16 @@ } } } +.dropdown-item_disabled { + pointer-events: none; + opacity: 0.32; +} + +.dropdown-item-tooltip { + padding: 4px 8px; + text-align: center; +} + .dropdown-item-mobile { justify-content: space-between; } diff --git a/frontend/src/public/components/UI/Dropdown/Dropdown.tsx b/frontend/src/public/components/UI/Dropdown/Dropdown.tsx index fbd106e3c..e7fa0d98a 100644 --- a/frontend/src/public/components/UI/Dropdown/Dropdown.tsx +++ b/frontend/src/public/components/UI/Dropdown/Dropdown.tsx @@ -10,6 +10,7 @@ import { ArrowRightIcon } from '../../icons'; import styles from './Dropdown.css'; import { useCheckDevice } from '../../../hooks/useCheckDevice'; +import { Tooltip } from '../Tooltip'; type TDropdownItemColor = 'black' | 'green' | 'red' | 'orange'; @@ -23,6 +24,8 @@ export type TDropdownOption = { customSubOption?: React.ReactElement; color?: TDropdownItemColor; isHidden?: boolean; + isDisabled?: boolean; + disabledTooltip?: string; size?: 'lg' | 'sm'; className?: string; Icon?(props: React.SVGAttributes): JSX.Element; @@ -115,6 +118,8 @@ export function Dropdown({ withConfirmation, initialConfirmationState, isHidden, + isDisabled: isOptionDisabled, + disabledTooltip, className: optionClassName, Icon, onClick, @@ -160,20 +165,25 @@ export function Dropdown({ ); } - return ( + const optionContent = (
{withUpperline &&
} { onClick?.(() => setIsOpen(false)); closeDropdown(); }, })} cssModule={{ - 'dropdown-item': classnames(styles['dropdown-item'], getDropdownItemColorClass(color), { - [styles['dropdown-item-mobile']]: isMobile && isFromBreakdownItem, - }), + 'dropdown-item': classnames( + styles['dropdown-item'], + getDropdownItemColorClass(color), + { + [styles['dropdown-item-mobile']]: isMobile && isFromBreakdownItem, + [styles['dropdown-item_disabled']]: isOptionDisabled, + }, + ), }} withConfirmation={withConfirmation} initialConfirmationState={initialConfirmationState} @@ -185,6 +195,22 @@ export function Dropdown({
); + + if (isOptionDisabled && disabledTooltip) { + return ( + + {optionContent} + + ); + } + + return optionContent; }); const isWide = items.every((item) => item.size === 'lg'); const Menu = CustomDropdownMenu || DefaultDropdownMenu; diff --git a/frontend/src/public/components/UI/ModifyDropdown/ModifyDropdown.tsx b/frontend/src/public/components/UI/ModifyDropdown/ModifyDropdown.tsx index 144e4abed..3df1fe481 100644 --- a/frontend/src/public/components/UI/ModifyDropdown/ModifyDropdown.tsx +++ b/frontend/src/public/components/UI/ModifyDropdown/ModifyDropdown.tsx @@ -24,16 +24,22 @@ export function ModifyDropdown({ editLabel, cloneLabel, deleteLabel, + isReadOnly, className, toggleType, }: IModifyDropdownProps) { const { formatMessage } = useIntl(); + const options: TDropdownOption[] = [ { label: editLabel, onClick: onEdit, Icon: PencilIcon, size: 'sm', + ...(isReadOnly && { + isDisabled: true, + disabledTooltip: formatMessage({ id: 'fieldsets.usage.disabled-tooltip' }), + }), }, ...(cloneLabel ? [{ label: cloneLabel, @@ -49,6 +55,10 @@ export function ModifyDropdown({ withUpperline: true, withConfirmation: true, size: 'sm' as const, + ...(isReadOnly && { + isDisabled: true, + disabledTooltip: formatMessage({ id: 'fieldsets.usage.disabled-tooltip' }), + }), }, ]; diff --git a/frontend/src/public/components/UI/ModifyDropdown/types.ts b/frontend/src/public/components/UI/ModifyDropdown/types.ts index 0c4cb5863..7d6baba7c 100644 --- a/frontend/src/public/components/UI/ModifyDropdown/types.ts +++ b/frontend/src/public/components/UI/ModifyDropdown/types.ts @@ -10,6 +10,7 @@ export interface IModifyDropdownProps { deleteLabel: string; onClone?: () => void; cloneLabel?: string; + isReadOnly?: boolean; className?: string; toggleType: EModifyDropdownToggle; } diff --git a/frontend/src/public/lang/locales/en_US.ts b/frontend/src/public/lang/locales/en_US.ts index 066069600..709d7c409 100644 --- a/frontend/src/public/lang/locales/en_US.ts +++ b/frontend/src/public/lang/locales/en_US.ts @@ -1550,4 +1550,5 @@ export const enMessages = { 'fieldsets.usage.not-linked': 'Not used in any template', 'fieldsets.usage.show': 'Show', 'fieldsets.usage.hide': 'Hide', + 'fieldsets.usage.disabled-tooltip': 'Fieldset is used in templates. Editing and deletion are locked.', }; diff --git a/frontend/src/public/lang/locales/ru_RU.ts b/frontend/src/public/lang/locales/ru_RU.ts index 21adbe95f..41ca083ab 100644 --- a/frontend/src/public/lang/locales/ru_RU.ts +++ b/frontend/src/public/lang/locales/ru_RU.ts @@ -1440,6 +1440,7 @@ export const ruMessages = { 'fieldsets.usage.not-linked': 'Не используется ни в одном шаблоне', 'fieldsets.usage.show': 'Показать', 'fieldsets.usage.hide': 'Скрыть', + 'fieldsets.usage.disabled-tooltip': 'Набор полей используется в шаблонах. Редактирование и удаление заблокированы.', 'user-info.vacation.title': 'Статус', 'user-info.vacation.status': 'Опции', 'user-info.vacation.active': 'Вы в отпуске/отсутствуете', From 7deca164cf9b94d5e5cf751c7f632f164cf1362d Mon Sep 17 00:00:00 2001 From: Maria Lordwill Date: Tue, 11 Aug 2026 13:52:05 +0400 Subject: [PATCH 04/40] 48741 fix(fieldsets): resolve AI comment feedback and prepare tests for code updates --- .../__tests__/FieldsetCard.test.tsx | 58 +++++++++++-------- .../FieldsetDetails/FieldsetDetails.css | 4 +- .../FieldsetDetails/FieldsetDetails.tsx | 7 ++- .../FieldsetUnsavedChangesModal.tsx | 18 +++--- .../__tests__/FieldsetDetails.test.tsx | 40 ++++++++++++- .../FieldsetUnsavedChangesModal.test.tsx | 45 ++++++++++++-- .../Fieldsets/FieldsetDetails/types.ts | 2 +- 7 files changed, 130 insertions(+), 44 deletions(-) diff --git a/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx b/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx index ccfcb7ff9..2f3beb407 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetCard/__tests__/FieldsetCard.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { useDispatch } from 'react-redux'; import { FieldsetCard } from '../FieldsetCard'; -import { Dropdown } from '../../../UI'; +import { ModifyDropdown } from '../../../UI'; import { WarningPopup } from '../../../UI/WarningPopup'; import { openEditModal, @@ -33,7 +33,7 @@ jest.mock('../../../../redux/fieldsets/slice', () => ({ })); jest.mock('../../../UI', () => ({ - Dropdown: jest.fn(() => null), + ModifyDropdown: jest.fn(() => null), })); jest.mock('../../../UI/WarningPopup', () => ({ @@ -50,12 +50,10 @@ jest.mock('../../../icons', () => ({ describe('FieldsetCard', () => { const mockDispatch = jest.fn(); - const formatMsg = (id: string) => intlMock.formatMessage({ id }); - const EDIT_LABEL = formatMsg('fieldsets.edit'); - const CLONE_LABEL = formatMsg('fieldsets.clone'); - const DELETE_LABEL = formatMsg('fieldsets.delete'); - const FIELDS_STATS = (count: number) => intlMock.formatMessage({ id: 'fieldsets.stats.fields' }, { count }); - const RULES_STATS = (count: number) => intlMock.formatMessage({ id: 'fieldsets.stats.rules' }, { count }); + const formatMsg = (id: string, values?: Record): string => + intlMock.formatMessage({ id }, values) as string; + const FIELDS_STATS = (count: number) => formatMsg('fieldsets.stats.fields', { count }); + const RULES_STATS = (count: number) => formatMsg('fieldsets.stats.rules', { count }); let fieldCounter = 0; @@ -69,14 +67,10 @@ describe('FieldsetCard', () => { const makeProps = makeFieldsetCatalogItem; - const getDropdownOptions = () => { - const mock = Dropdown as unknown as jest.Mock; + const getModifyDropdownProps = () => { + const mock = ModifyDropdown as jest.Mock; const lastCall = mock.mock.calls[mock.mock.calls.length - 1]; - return lastCall[0].options; - }; - - const findDropdownOption = (label: string) => { - return getDropdownOptions().find((opt: { label: string }) => opt.label === label); + return lastCall[0]; }; const getWarningPopupProps = () => { @@ -119,8 +113,7 @@ describe('FieldsetCard', () => { const props = makeProps(); render(React.createElement(FieldsetCard, props)); - const editOption = findDropdownOption(EDIT_LABEL); - editOption.onClick(); + act(() => { getModifyDropdownProps().onEdit(); }); expect(mockDispatch).toHaveBeenCalledWith( setCurrentFieldset({ @@ -148,8 +141,7 @@ describe('FieldsetCard', () => { expect(getWarningPopupProps().isOpen).toBe(false); - const deleteOption = findDropdownOption(DELETE_LABEL); - act(() => { deleteOption.onClick(); }); + act(() => { getModifyDropdownProps().onDelete(); }); expect(getWarningPopupProps().isOpen).toBe(true); }); @@ -157,7 +149,7 @@ describe('FieldsetCard', () => { it('dispatches deleteFieldsetAction on WarningPopup confirm', () => { render(React.createElement(FieldsetCard, makeProps({ id: 10 }))); - act(() => { findDropdownOption(DELETE_LABEL).onClick(); }); + act(() => { getModifyDropdownProps().onDelete(); }); act(() => { getWarningPopupProps().onConfirm(); }); expect(mockDispatch).toHaveBeenCalledTimes(1); @@ -169,7 +161,7 @@ describe('FieldsetCard', () => { it('closes WarningPopup without dispatch on cancel', () => { render(React.createElement(FieldsetCard, makeProps())); - act(() => { findDropdownOption(DELETE_LABEL).onClick(); }); + act(() => { getModifyDropdownProps().onDelete(); }); expect(getWarningPopupProps().isOpen).toBe(true); act(() => { getWarningPopupProps().onReject(); }); @@ -179,20 +171,38 @@ describe('FieldsetCard', () => { }); }); - describe('Dropdown — Clone', () => { it('dispatches cloneFieldsetAction on Clone click', () => { const props = makeProps({ id: 10 }); render(React.createElement(FieldsetCard, props)); - const cloneOption = findDropdownOption(CLONE_LABEL); - cloneOption.onClick(); + act(() => { getModifyDropdownProps().onClone(); }); expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledWith(cloneFieldsetAction({ id: 10 })); }); }); + describe('ModifyDropdown — isReadOnly state (linked fieldsets)', () => { + it('passes isReadOnly=true to ModifyDropdown when usage contains linked templates', () => { + const props = makeProps({ + usage: [ + { id: 1, name: 'Template 1' }, + ], + }); + render(React.createElement(FieldsetCard, props)); + + expect(getModifyDropdownProps().isReadOnly).toBe(true); + }); + + it('passes isReadOnly=false to ModifyDropdown when usage is empty', () => { + const props = makeProps({ usage: [] }); + render(React.createElement(FieldsetCard, props)); + + expect(getModifyDropdownProps().isReadOnly).toBe(false); + }); + }); + describe('Statistics footer', () => { it('shows field count and rule count when both are > 0', () => { const props = makeProps({ diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css index c3115faab..3e2aec373 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css @@ -235,8 +235,8 @@ opacity: 0.32 !important; } - [class*="edit-name-button"], - [class*="kick-off-edit-name"] { + &.fieldset_readonly [class*="edit-name-button"], + &.fieldset_readonly [class*="kick-off-edit-name"] { display: none !important; } } diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx index 4bb18795f..af54dcd0b 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx @@ -196,8 +196,8 @@ const FieldsetDetails = ({ setDetailFieldsetChanges((prev) => ({ ...prev, rules })); }; - const handleSave = () => { - if (!fieldset || !isChanged) return; + const handleSave = (): boolean => { + if (!fieldset || !isChanged) return false; if (detailFieldsetChanges.rules) { const ruleErrorMessageKey = validateFieldsetRules(detailFieldsetChanges.rules, detailFieldset.fields); @@ -206,7 +206,7 @@ const FieldsetDetails = ({ NotificationManager.warning({ message: formatMessage({ id: ruleErrorMessageKey }), }); - return; + return false; } } @@ -233,6 +233,7 @@ const FieldsetDetails = ({ } dispatch(updateFieldsetAction(payload)); + return true; }; const getRuleValuePlaceholder = (ruleType: EFieldsetRuleType) => diff --git a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetUnsavedChangesModal.tsx b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetUnsavedChangesModal.tsx index f1a6c2968..17ea96207 100644 --- a/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetUnsavedChangesModal.tsx +++ b/frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetUnsavedChangesModal.tsx @@ -46,14 +46,18 @@ export function FieldsetUnsavedChangesModal({ isChanged, onSave }: TFieldsetUnsa size="md" />
@@ -385,7 +386,7 @@ const FieldsetDetails = ({