diff --git a/.ai/rules/branches.md b/.ai/rules/branches.md index 98e084f7ab..3549524258 100644 --- a/.ai/rules/branches.md +++ b/.ai/rules/branches.md @@ -29,18 +29,20 @@ ric/RI-666/custom-prefix ## Allowed Branch Types (GitHub Actions Enforced) -- `feature/` - New features and refactoring -- `bugfix/` - Bug fixes -- `fe/feature/` - Frontend-only features -- `fe/bugfix/` - Frontend-only bug fixes -- `be/feature/` - Backend-only features -- `be/bugfix/` - Backend-only bug fixes +- `feature/` - New features and refactoring (affects multiple areas) +- `bugfix/` - Bug fixes (affects multiple areas) +- `fe/feature/` - Frontend-only features (only `redisinsight/ui/` folder) +- `fe/bugfix/` - Frontend-only bug fixes (only `redisinsight/ui/` folder) +- `be/feature/` - Backend-only features (only `redisinsight/api/` folder) +- `be/bugfix/` - Backend-only bug fixes (only `redisinsight/api/` folder) - `docs/` - Documentation changes - `test/` - Test-related changes - `e2e/` - End-to-end test changes - `release/` - Release branches - `ric/` - Custom prefix for special cases +**Note:** When a bug fix affects only the `redisinsight/ui/` folder, use `fe/bugfix/` prefix instead of `bugfix/`. + ## Issue References - **Internal**: `RI-XXX` (JIRA ticket) diff --git a/.ai/rules/frontend.md b/.ai/rules/frontend.md index 6509479036..c797dcb843 100644 --- a/.ai/rules/frontend.md +++ b/.ai/rules/frontend.md @@ -66,10 +66,13 @@ ComponentName/ Keep all component styles in dedicated .style.ts files and import them with a namespace. +**CRITICAL: `import * as S` is reserved for local styles only** (e.g., from `ComponentName.styles.ts`). When you need to use styled components from external components, create a local styles file that re-exports them. + #### ✅ Good ```typescript -import * as S from './Component.styles' +// ComponentName.tsx +import * as S from './ComponentName.styles' return ( @@ -77,19 +80,19 @@ return ( Content ) + +// ComponentName.styles.ts (when re-exporting from external component) +export { ExternalStyledComponent } from '../ExternalComponent/ExternalComponent.styles' ``` #### ❌ Bad ```typescript -import { Container, Title, Content } from './Component.styles' +// ❌ BAD: Importing styled components directly from external component +import * as S from '../ExternalComponent/ExternalComponent.styles' -return ( - - Title - Content - -) +// ❌ BAD: Named imports instead of namespace +import { Container, Title, Content } from './Component.styles' ``` ### Use Layout Components Instead of div diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/EnablementArea.spec.tsx b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/EnablementArea.spec.tsx index 0261516629..7f4a9eb9c3 100644 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/EnablementArea.spec.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/EnablementArea.spec.tsx @@ -192,7 +192,7 @@ describe('EnablementArea', () => { expect(screen.getByTestId('upload-tutorial-form')).toBeInTheDocument() }) - it('should render open form with tutorials', () => { + it('should render form directly when no tutorials', () => { const customTutorials = [ { ...MOCK_CUSTOM_TUTORIALS_ITEMS[0], children: [] }, ] @@ -202,9 +202,6 @@ describe('EnablementArea', () => { customTutorials={customTutorials} />, ) - expect(screen.getByTestId('welcome-my-tutorials')).toBeInTheDocument() - - fireEvent.click(screen.getByTestId('upload-tutorial-btn')) expect(screen.getByTestId('upload-tutorial-form')).toBeInTheDocument() }) @@ -254,17 +251,6 @@ describe('EnablementArea', () => { ) }) - it('should not render welcome screen if at least one tutorial uploaded', () => { - render( - , - ) - expect( - screen.queryByTestId('welcome-my-tutorials'), - ).not.toBeInTheDocument() - }) }) describe('Telemetry', () => { diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.styles.ts b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.styles.ts new file mode 100644 index 0000000000..0911af6d5b --- /dev/null +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.styles.ts @@ -0,0 +1,2 @@ +export { GroupHeaderButton } from '../Group/Group.styles' + diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.tsx b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.tsx index 614807c9b2..8fb780b4b9 100644 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.tsx @@ -7,6 +7,8 @@ import { DeleteIcon } from 'uiSrc/components/base/icons' import { Text } from 'uiSrc/components/base/text' import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' import { RiPopover } from 'uiSrc/components/base' + +import * as S from './DeleteTutorialButton.styles' import styles from './styles.module.scss' export interface Props { @@ -20,7 +22,10 @@ const DeleteTutorialButton = (props: Props) => { const { id, label, onDelete, isLoading } = props const [isPopoverDeleteOpen, setIsPopoverDeleteOpen] = useState(false) - const handleClickDelete = () => { + const handleClickDelete = ( + e: React.MouseEvent, + ) => { + e.stopPropagation() setIsPopoverDeleteOpen((v) => !v) } @@ -32,14 +37,13 @@ const DeleteTutorialButton = (props: Props) => { closePopover={() => setIsPopoverDeleteOpen(false)} panelPaddingSize="l" button={ -
-
+ } onClick={(e) => e.stopPropagation()} data-testid={`delete-tutorial-popover-${id}`} diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.styles.ts b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.styles.ts new file mode 100644 index 0000000000..295883346f --- /dev/null +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.styles.ts @@ -0,0 +1,23 @@ +import React from 'react' +import styled from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' + +export const GroupHeaderButton = styled.div< + React.HTMLAttributes +>` + display: flex; + align-items: center; + justify-content: center; + + width: ${({ theme }: { theme: Theme }) => theme.core.space.space300}; + height: ${({ theme }: { theme: Theme }) => theme.core.space.space300}; + border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space050}; + cursor: pointer; + + &:hover { + color: ${({ theme }: { theme: Theme }) => + theme.semantic.color.text.neutral100}; + background-color: ${({ theme }: { theme: Theme }) => + theme.semantic.color.background.neutral200}; + } +` diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.tsx b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.tsx index dcaa82de88..9fcbb1ba53 100644 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.tsx @@ -10,16 +10,16 @@ import { } from 'uiSrc/telemetry' import { workbenchCustomTutorialsSelector } from 'uiSrc/slices/workbench/wb-custom-tutorials' import { EAItemActions } from 'uiSrc/constants' -import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' import { RiAccordion } from 'uiSrc/components/base/display/accordion/RiAccordion' import { Col, Row } from 'uiSrc/components/base/layout/flex' -import { RiTooltip, OnboardingTour } from 'uiSrc/components' +import { RiTooltip } from 'uiSrc/components' import { Text } from 'uiSrc/components/base/text' import { RiIcon } from 'uiSrc/components/base/icons/RiIcon' import DeleteTutorialButton from '../DeleteTutorialButton' +import * as S from './Group.styles' import './styles.scss' export interface Props { @@ -33,9 +33,9 @@ export interface Props { initialIsOpen?: boolean forceState?: 'open' | 'closed' onToggle?: (isOpen: boolean) => void - isPageOpened?: boolean isShowActions?: boolean isShowFolder?: boolean + hasChildren?: boolean } const Group = (props: Props) => { @@ -51,8 +51,8 @@ const Group = (props: Props) => { onToggle, onCreate, onDelete, - isPageOpened, isShowFolder, + hasChildren = true, } = props const { deleting: deletingCustomTutorials } = useSelector( workbenchCustomTutorialsSelector, @@ -87,25 +87,17 @@ const Group = (props: Props) => { const actionsContent = ( <> {actions?.includes(EAItemActions.Create) && + hasChildren && (isGroupOpen || forceState === 'open') && ( - - -
- -
-
-
+ + + + + )} {actions?.includes(EAItemActions.Delete) && ( { [], ) + // Open form when onboarding is triggered and form is not visible + useEffect(() => { + if ( + isCustomTutorialsOnboarding && + customTutorials?.length > 0 && + !isCreateOpen + ) { + setIsCreateOpen(true) + } + }, [isCustomTutorialsOnboarding, customTutorials?.length, isCreateOpen]) + const submitCreate = ({ file, link }: FormValues) => { const formData = new FormData() @@ -157,6 +167,7 @@ const Navigation = (props: Props) => { const currentManifestPath = `${manifestPath}/${key}` const isCustomTutorials = id === CUSTOM_TUTORIALS_ID && level === 0 + const hasChildren = (children?.length ?? 0) > 0 switch (type) { case EnablementAreaComponent.Group: @@ -171,7 +182,7 @@ const Navigation = (props: Props) => { )} onCreate={() => setIsCreateOpen((v) => !v)} onDelete={onDeleteCustomTutorial} - isPageOpened={isInternalPageVisible} + hasChildren={hasChildren} forceState={ isCustomTutorials && isCustomTutorialsOnboarding ? 'open' @@ -179,24 +190,25 @@ const Navigation = (props: Props) => { } {...args} > - {isCustomTutorials && actions?.includes(EAItemActions.Create) && ( -
- {!isCreateOpen && children?.length === 0 && ( - - setIsCreateOpen(true)} - /> - - - )} - {isCreateOpen && ( + {isCustomTutorials && + actions?.includes(EAItemActions.Create) && + (children?.length === 0 ? ( + + + + + ) : ( + isCreateOpen && ( setIsCreateOpen(false)} + isPageOpened={isInternalPageVisible} /> - )} -
- )} + ) + ))} {renderTreeView( children ? getManifestItems(children) : [], { diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.styles.ts b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.styles.ts new file mode 100644 index 0000000000..5ed49a3bcc --- /dev/null +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.styles.ts @@ -0,0 +1,11 @@ +import React from 'react' +import styled from 'styled-components' +import { Theme } from 'uiSrc/components/base/theme/types' + +export const Wrapper = styled.div>` + border: 1px solid + ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral600}; + border-radius: ${({ theme }: { theme: Theme }) => + theme.components.card.borderRadius}; + padding: ${({ theme }: { theme: Theme }) => theme.core.space.space200}; +` diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.tsx b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.tsx index 39f43cab3a..49d2704d5c 100644 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.tsx @@ -2,13 +2,16 @@ import React, { useState } from 'react' import { useFormik } from 'formik' import { FormikErrors } from 'formik/dist/types' import { isEmpty } from 'lodash' +import cx from 'classnames' import { TextInput } from 'uiSrc/components/base/inputs' import { Nullable } from 'uiSrc/utils' import validationErrors from 'uiSrc/constants/validationErrors' -import { RiFilePicker, RiTooltip } from 'uiSrc/components' +import { RiFilePicker, RiTooltip, OnboardingTour } from 'uiSrc/components' +import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' import { Spacer } from 'uiSrc/components/base/layout/spacer' +import { Row } from 'uiSrc/components/base/layout/flex' import { PrimaryButton, SecondaryButton, @@ -16,6 +19,8 @@ import { import { InfoIcon } from 'uiSrc/components/base/icons' import { Text } from 'uiSrc/components/base/text' import CreateTutorialLink from '../CreateTutorialLink' + +import * as S from './UploadTutorialForm.styles' import styles from './styles.module.scss' export interface FormValues { @@ -26,10 +31,11 @@ export interface FormValues { export interface Props { onSubmit: (data: FormValues) => void onCancel?: () => void + isPageOpened?: boolean } const UploadTutorialForm = (props: Props) => { - const { onSubmit, onCancel } = props + const { onSubmit, onCancel, isPageOpened } = props const [errors, setErrors] = useState>({}) const initialValues: FormValues = { @@ -75,70 +81,75 @@ const UploadTutorialForm = (props: Props) => { } return ( -
-
- Add new Tutorial - -
-
- -
-
OR
- formik.setFieldValue('link', value)} - className={styles.input} - data-testid="tutorial-link-field" + + + Add new tutorial + + +
+
+ - -
- -
+
+
OR
+ formik.setFieldValue('link', value)} + className={styles.input} + data-testid="tutorial-link-field" + /> + +
+ + + {onCancel && ( onCancel?.()} + onClick={() => onCancel()} data-testid="cancel-upload-tutorial-btn" > Cancel - + formik.handleSubmit()} + icon={isSubmitDisabled ? InfoIcon : undefined} + disabled={isSubmitDisabled} + data-testid="submit-upload-tutorial-btn" > - formik.handleSubmit()} - icon={isSubmitDisabled ? InfoIcon : undefined} - disabled={isSubmitDisabled} - data-testid="submit-upload-tutorial-btn" - > - Submit - - -
-
+ Submit + + +
-
+ ) } diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/styles.module.scss b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/styles.module.scss index 202dabcf05..631a5cd550 100644 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/styles.module.scss +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/styles.module.scss @@ -1,93 +1,83 @@ -.outerWrapper { - padding: 12px 10px; - - .wrapper { - padding: 18px; - background: var(--euiColorLightestShade); - border-radius: 4px; - - .input { - height: 34px !important; - background: var(--euiColorEmptyShade) !important; - } - - .uploadFileWrapper { - text-align: right; - margin-top: -8px; - } +.wrapper { + .input { + height: 34px !important; + background: var(--euiColorEmptyShade) !important; + } - .uploadFileName { - display: flex; - align-items: center; - padding-left: 4px; - } + .uploadFileWrapper { + text-align: right; + margin-top: -8px; + } - .uploadFileNameTitle { - max-width: calc(100% - 30px); - } + .uploadFileName { + display: flex; + align-items: center; + padding-left: 4px; } - .btnSubmit { - margin-left: 6px; + .uploadFileNameTitle { + max-width: calc(100% - 30px); + } +} - svg { - width: 16px !important; - height: 16px !important; - margin-top: 0; - } +.btnSubmit { + svg { + width: 16px !important; + height: 16px !important; + margin-top: 0; } +} - .fileDrop { - width: 100%; - margin-top: 14px; +.fileDrop { + width: 100%; + margin-top: 14px; - :global { - .RI-File-Picker__clearButton, .RI-File-Picker__clearButton .euiButtonEmpty__text { - color: var(--externalLinkColor) !important; - text-transform: lowercase; - } + :global { + .RI-File-Picker__clearButton, .RI-File-Picker__clearButton .euiButtonEmpty__text { + color: var(--externalLinkColor) !important; + text-transform: lowercase; + } - .RI-File-Picker__showDrop .RI-File-Picker__prompt, .RI-File-Picker__input:focus + .RI-File-Picker__prompt { - background-color: var(--euiColorEmptyShade); - } + .RI-File-Picker__showDrop .RI-File-Picker__prompt, .RI-File-Picker__input:focus + .RI-File-Picker__prompt { + background-color: var(--euiColorEmptyShade); + } - .RI-File-Picker__prompt { - background-color: var(--euiColorEmptyShade); - height: 120px; - border-radius: 4px; - box-shadow: none; - border: 1px dashed var(--controlsBorderColor); - } + .RI-File-Picker__prompt { + background-color: var(--euiColorEmptyShade); + height: 120px; + border-radius: 4px; + box-shadow: none; + border: 1px dashed var(--controlsBorderColor); + } - .RI-File-Picker__clearButton { - margin-top: 4px; - } + .RI-File-Picker__clearButton { + margin-top: 4px; } } +} - .hr { - margin: 12px 0; - width: 100%; - text-align: center; - position: relative; - - &:before, &:after { - content: ''; - display: block; - width: 40%; - height: 1px; - background: var(--tableDarkestBorderColor); - position: absolute; - top: 50%; - } +.hr { + margin: 12px 0; + width: 100%; + text-align: center; + position: relative; + + &:before, &:after { + content: ''; + display: block; + width: 40%; + height: 1px; + background: var(--tableDarkestBorderColor); + position: absolute; + top: 50%; + } - &:before { - left: 0; - } + &:before { + left: 0; + } - &:after { - right: 0; - } + &:after { + right: 0; } } @@ -96,9 +86,4 @@ align-items: center; justify-content: space-between; - .footerButtons { - display: flex; - align-items: center; - justify-content: flex-end; - } } diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.spec.tsx b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.spec.tsx deleted file mode 100644 index b1b2f12d65..0000000000 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.spec.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' - -import WelcomeMyTutorials from './WelcomeMyTutorials' - -describe('WelcomeMyTutorials', () => { - it('should render', () => { - expect( - render(), - ).toBeTruthy() - }) - - it('should call handleOpenUpload', () => { - const mockHandleOpenUpload = jest.fn() - render() - - fireEvent.click(screen.getByTestId('upload-tutorial-btn')) - - expect(mockHandleOpenUpload).toBeCalled() - }) -}) diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.styles.ts b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.styles.ts deleted file mode 100644 index 2c7223c916..0000000000 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.styles.ts +++ /dev/null @@ -1,13 +0,0 @@ -import styled from 'styled-components' - -import { Row } from 'uiSrc/components/base/layout/flex' - -export const StyledRow = styled(Row)` - padding: ${({ theme }) => theme.core.space.space100}; -` - -export const TutorialText = styled.span` - @media only screen and (max-width: 1440px) { - display: none; - } -` diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.tsx b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.tsx deleted file mode 100644 index c1f0f9b3ee..0000000000 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/WelcomeMyTutorials.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react' - -import { PrimaryButton } from 'uiSrc/components/base/forms/buttons' -import { Card } from 'uiSrc/components/base/layout' -import CreateTutorialLink from '../CreateTutorialLink' -import { StyledRow, TutorialText } from './WelcomeMyTutorials.styles' - -export interface Props { - handleOpenUpload: () => void -} - -const WelcomeMyTutorials = ({ handleOpenUpload }: Props) => ( - - - - handleOpenUpload()} - data-testid="upload-tutorial-btn" - > - + Upload tutorial - - - -) - -export default WelcomeMyTutorials diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/index.ts b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/index.ts deleted file mode 100644 index 906eb17a0e..0000000000 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/WelcomeMyTutorials/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import WelcomeMyTutorials from './WelcomeMyTutorials' - -export default WelcomeMyTutorials