From b7aae0fb7c1e953530a6591000b954c7e7c55b7e Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 1 Nov 2024 02:56:38 +0300 Subject: [PATCH 1/2] feat: add static image accepter support to new ui --- lib/static/modules/action-names.ts | 7 +- lib/static/modules/actions/static-accepter.ts | 45 ++++++- lib/static/modules/default-state.ts | 9 ++ lib/static/modules/reducers/is-initialized.js | 3 +- lib/static/modules/reducers/loading.js | 24 ++++ .../modules/reducers/static-image-accepter.js | 28 +++- lib/static/modules/static-image-accepter.ts | 18 +-- lib/static/modules/utils/index.js | 2 +- lib/static/new-ui/app/App.tsx | 45 ++++--- .../components/AssertViewResult/index.tsx | 10 ++ .../components/AssertViewStatus/index.tsx | 16 ++- .../AttemptPickerItem/index.module.css | 10 +- .../GuiniToolbarOverlay/index.module.css | 106 +++++++++++++++ .../components/GuiniToolbarOverlay/index.tsx | 126 ++++++++++++++++++ .../new-ui/components/LoadingBar/index.tsx | 18 +-- .../ToolbarOverlay/index.module.css | 38 ++++++ .../components/ToolbarOverlay/index.tsx | 80 +++++++++++ .../UiModeHintNotification/index.module.css | 64 +-------- .../UiModeHintNotification/index.tsx | 10 +- .../ScreenshotsTreeViewItem/index.tsx | 36 +++-- .../components/VisualChecksPage/index.tsx | 26 +++- lib/static/new-ui/types/index.ts | 5 + lib/static/new-ui/types/store.ts | 50 ++++++- lib/static/new-ui/utils/index.tsx | 14 +- lib/static/styles.css | 8 +- 25 files changed, 649 insertions(+), 149 deletions(-) create mode 100644 lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css create mode 100644 lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx create mode 100644 lib/static/new-ui/components/ToolbarOverlay/index.module.css create mode 100644 lib/static/new-ui/components/ToolbarOverlay/index.tsx diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index cb00e3f14..335bb524c 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -22,6 +22,8 @@ export default { STATIC_ACCEPTER_STAGE_SCREENSHOT: 'STATIC_ACCEPTER_STAGE_SCREENSHOT', STATIC_ACCEPTER_UNSTAGE_SCREENSHOT: 'STATIC_ACCEPTER_UNSTAGE_SCREENSHOT', STATIC_ACCEPTER_COMMIT_SCREENSHOT: 'STATIC_ACCEPTER_COMMIT_SCREENSHOT', + STATIC_ACCEPTER_UPDATE_TOOLBAR_POSITION: 'STATIC_ACCEPTER_UPDATE_TOOLBAR_POSITION', + STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE: 'STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE', CLOSE_SECTIONS: 'CLOSE_SECTIONS', TOGGLE_STATE_RESULT: 'TOGGLE_STATE_RESULT', TOGGLE_LOADING: 'TOGGLE_LOADING', @@ -64,5 +66,8 @@ export default { SUITES_PAGE_SET_SECTION_EXPANDED: 'SUITES_PAGE_SET_SECTION_EXPANDED', SUITES_PAGE_SET_STEPS_EXPANDED: 'SUITES_PAGE_SET_STEPS_EXPANDED', VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE: 'VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE', - UPDATE_LOADING_PROGRESS: 'UPDATE_LOADING_PROGRESS' + UPDATE_LOADING_PROGRESS: 'UPDATE_LOADING_PROGRESS', + UPDATE_LOADING_VISIBILITY: 'UPDATE_LOADING_VISIBILITY', + UPDATE_LOADING_TITLE: 'UPDATE_LOADING_TITLE', + UPDATE_LOADING_IS_IN_PROGRESS: 'UPDATE_LOADING_IS_IN_PROGRESS' } as const; diff --git a/lib/static/modules/actions/static-accepter.ts b/lib/static/modules/actions/static-accepter.ts index 27e13017b..57788db96 100644 --- a/lib/static/modules/actions/static-accepter.ts +++ b/lib/static/modules/actions/static-accepter.ts @@ -8,6 +8,7 @@ import actionNames from '../action-names'; import defaultState from '../default-state'; import type {Action, Dispatch, Store} from './types'; import {ThunkAction} from 'redux-thunk'; +import {Point} from '@/static/new-ui/types'; type StaticAccepterDelayScreenshotPayload = {imageId: string, stateName: string, stateNameImageId: string}[]; type StaticAccepterDelayScreenshotAction = Action @@ -47,6 +48,10 @@ type StaticAccepterConfig = typeof defaultState['config']['staticImageAccepter'] type StaticAccepterPayload = {id: string, stateNameImageId: string, image: string, path: string}[]; type StaticAccepterCommitScreenshotOptions = Pick & {message: string}; +export interface CommitResult { + error?: Error; +} + type StaticAccepterCommitScreenshotAction = Action; export const staticAccepterCommitScreenshot = ( imagesInfo: StaticAccepterPayload, @@ -58,10 +63,13 @@ export const staticAccepterCommitScreenshot = ( axiosRequestOptions = {}, meta }: StaticAccepterCommitScreenshotOptions -): ThunkAction, Store, void, StaticAccepterCommitScreenshotAction> => { - return async (dispatch: Dispatch) => { +): ThunkAction, Store, void, StaticAccepterCommitScreenshotAction> => { + return async (dispatch: Dispatch): Promise => { dispatch({type: actionNames.PROCESS_BEGIN}); dispatch(staticAccepterCloseConfirm()); + dispatch({type: actionNames.UPDATE_LOADING_IS_IN_PROGRESS, payload: true}); + dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: `Preparing images to commit: 0 of ${imagesInfo.length}`}); + dispatch({type: actionNames.UPDATE_LOADING_VISIBILITY, payload: true}); try { const payload = new FormData(); @@ -74,13 +82,21 @@ export const staticAccepterCommitScreenshot = ( payload.append('meta', JSON.stringify(meta)); } - await Promise.all(imagesInfo.map(async imageInfo => { + await Promise.all(imagesInfo.map(async (imageInfo, index) => { + dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: `Preparing images to commit: ${index + 1} of ${imagesInfo.length}`}); + const blob = await getBlob(imageInfo.image); payload.append('image', blob, imageInfo.path); })); - const response = await axios.post(serviceUrl, payload, axiosRequestOptions); + dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: 'Uploading images'}); + const response = await axios.post(serviceUrl, payload, { + ...axiosRequestOptions, + onUploadProgress: (e) => { + dispatch({type: actionNames.UPDATE_LOADING_PROGRESS, payload: {'static-accepter-commit': e.loaded / (e.total ?? e.loaded)}}); + } + }); const commitedImageIds = imagesInfo.map(imageInfo => imageInfo.id); const commitedImages = imagesInfo.map(imageInfo => ({ @@ -93,20 +109,37 @@ export const staticAccepterCommitScreenshot = ( storeCommitInLocalStorage(commitedImages); + dispatch({type: actionNames.UPDATE_LOADING_IS_IN_PROGRESS, payload: false}); + dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: 'All images committed!'}); dispatch(createNotification('commitScreenshot', 'success', 'Screenshots were successfully committed')); } else { const errorMessage = [ - `Unexpected statuscode from the service: ${response.status}.`, + `Unexpected status code from the service: ${response.status}.`, `Server response: '${response.data}'` ].join('\n'); throw new Error(errorMessage); } } catch (e) { - console.error('Error while comitting screenshot:', e); + console.error('An error occurred while commiting screenshot:', e); dispatch(createNotificationError('commitScreenshot', e)); + + return {error: e as Error}; } finally { + dispatch({type: actionNames.UPDATE_LOADING_VISIBILITY, payload: false}); dispatch({type: actionNames.PROCESS_END}); } + + return {}; }; }; + +type StaticAccepterUpdateToolbarPositionAction = Action; +export const staticAccepterUpdateToolbarPosition = (payload: {position: Point}): StaticAccepterUpdateToolbarPositionAction => { + return {type: actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_POSITION, payload}; +}; + +type StaticAccepterUpdateCommitMessageAction = Action; +export const staticAccepterUpdateCommitMessage = (payload: {commitMessage: string}): StaticAccepterUpdateCommitMessageAction => { + return {type: actionNames.STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE, payload}; +}; diff --git a/lib/static/modules/default-state.ts b/lib/static/modules/default-state.ts index 86fcd502f..43f891920 100644 --- a/lib/static/modules/default-state.ts +++ b/lib/static/modules/default-state.ts @@ -102,13 +102,22 @@ export default Object.assign({config: configDefaults}, { currentNamedImageId: null }, loading: { + taskTitle: 'Loading Testplane UI', + isVisible: true, + isInProgress: true, progress: {} + }, + staticImageAccepterModal: { + commitMessage: 'chore: update screenshot references' } }, ui: { suitesPage: { expandedSectionsById: {}, expandedStepsByResultId: {} + }, + staticImageAccepterToolbar: { + position: {x: 0, y: 0} } } }) satisfies State; diff --git a/lib/static/modules/reducers/is-initialized.js b/lib/static/modules/reducers/is-initialized.js index 00a9bb564..25b9bd198 100644 --- a/lib/static/modules/reducers/is-initialized.js +++ b/lib/static/modules/reducers/is-initialized.js @@ -1,10 +1,11 @@ import actionNames from '../action-names'; +import {applyStateUpdate} from '@/static/modules/utils'; export default (state, action) => { switch (action.type) { case actionNames.INIT_GUI_REPORT: case actionNames.INIT_STATIC_REPORT: - return {...state, app: {...state.app, isInitialized: true}}; + return applyStateUpdate(state, {app: {isInitialized: true, loading: {isVisible: false}}}); default: return state; diff --git a/lib/static/modules/reducers/loading.js b/lib/static/modules/reducers/loading.js index aefc5b5b6..3a281ad05 100644 --- a/lib/static/modules/reducers/loading.js +++ b/lib/static/modules/reducers/loading.js @@ -15,6 +15,30 @@ export default (state, action) => { }); } + case actionNames.UPDATE_LOADING_IS_IN_PROGRESS: { + return applyStateUpdate(state, { + app: { + loading: {isInProgress: action.payload} + } + }); + } + + case actionNames.UPDATE_LOADING_VISIBILITY: { + return applyStateUpdate(state, { + app: { + loading: {isVisible: action.payload} + } + }); + } + + case actionNames.UPDATE_LOADING_TITLE: { + return applyStateUpdate(state, { + app: { + loading: {taskTitle: action.payload} + } + }); + } + default: return state; } diff --git a/lib/static/modules/reducers/static-image-accepter.js b/lib/static/modules/reducers/static-image-accepter.js index a86d49724..6a82fadfa 100644 --- a/lib/static/modules/reducers/static-image-accepter.js +++ b/lib/static/modules/reducers/static-image-accepter.js @@ -2,7 +2,7 @@ import {get, set, last, groupBy} from 'lodash'; import actionNames from '../action-names'; import {checkIsEnabled, getLocalStorageCommitedImageIds} from '../static-image-accepter'; import {applyStateUpdate, isAcceptable, isNodeSuccessful} from '../utils'; -import {COMMITED, STAGED} from '../../../constants'; +import {COMMITED, EditScreensFeature, STAGED} from '../../../constants'; export default (state, action) => { switch (action.type) { @@ -11,7 +11,7 @@ export default (state, action) => { return state; } - return {...state, staticImageAccepter: initStaticImageAccepter(action.payload.tree)}; + return applyStateUpdate(state, {app: {availableFeatures: [EditScreensFeature]}, staticImageAccepter: initStaticImageAccepter(action.payload.tree)}); } case actionNames.STATIC_ACCEPTER_DELAY_SCREENSHOT: { @@ -38,7 +38,7 @@ export default (state, action) => { for (const imageId of imageIdsToStage) { const stateImageIds = getStateImageIds(state.tree, imageId); - const stagedImageId = stateImageIds.find(imageId => acceptableImages[imageId].commitStatus === STAGED); + const stagedImageId = stateImageIds.find(imageId => acceptableImages[imageId]?.commitStatus === STAGED); set(acceptableImagesDiff, [imageId, 'commitStatus'], STAGED); @@ -78,7 +78,7 @@ export default (state, action) => { for (const imageId of action.payload) { const stateImageIds = getStateImageIds(state.tree, imageId); - const commitedImageId = stateImageIds.find(imageId => acceptableImages[imageId].commitStatus === COMMITED); + const commitedImageId = stateImageIds.find(imageId => acceptableImages[imageId]?.commitStatus === COMMITED); if (commitedImageId) { set(acceptableImagesDiff, [commitedImageId, 'commitStatus'], null); @@ -93,6 +93,26 @@ export default (state, action) => { return applyStateUpdate(state, diff); } + case actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_POSITION: { + return applyStateUpdate(state, { + ui: { + staticImageAccepterToolbar: { + position: action.payload.position + } + } + }); + } + + case actionNames.STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE: { + return applyStateUpdate(state, { + app: { + staticImageAccepterModal: { + commitMessage: action.payload.commitMessage + } + } + }); + } + default: return state; } diff --git a/lib/static/modules/static-image-accepter.ts b/lib/static/modules/static-image-accepter.ts index 832350578..91c933cd8 100644 --- a/lib/static/modules/static-image-accepter.ts +++ b/lib/static/modules/static-image-accepter.ts @@ -3,10 +3,11 @@ import {get} from 'lodash'; import type {ReporterConfig} from '../../types'; import {COMMITED, STAGED} from '../../constants'; import * as localStorage from './local-storage-wrapper'; +import {ImageEntity, ImageEntityFail} from '@/static/new-ui/types/store'; let isEnabled: boolean | null = null; -interface AcceptableImage { +export interface AcceptableImage { id: string; parentId: string; stateName: string; @@ -15,14 +16,7 @@ interface AcceptableImage { originalStatus: string; } -type ImagesById = Record +type ImagesById = Record; interface LocalStorageValue { date: string, @@ -69,16 +63,16 @@ export const formatCommitPayload = ( .map(image => ({imageId: image.id, stateNameImageId: image.stateNameImageId})) .concat(extraImages); - if (imagesToCommit.find(({imageId}) => !imagesById[imageId].refImg.relativePath)) { + if (imagesToCommit.find(({imageId}) => !imagesById[imageId].refImg?.relativePath)) { throw new Error(`The version of your tool does not support static image accepter: missing "relativePath"`); } return imagesToCommit.map(({imageId, stateNameImageId}) => ({ id: imageId, stateNameImageId, - image: imagesById[imageId].actualImg.path, + image: (imagesById[imageId] as ImageEntityFail).actualImg.path, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - path: imagesById[imageId].refImg.relativePath! + path: (imagesById[imageId] as ImageEntityFail).refImg.relativePath! })); }; diff --git a/lib/static/modules/utils/index.js b/lib/static/modules/utils/index.js index ce8b3ca4f..056cf977c 100644 --- a/lib/static/modules/utils/index.js +++ b/lib/static/modules/utils/index.js @@ -48,7 +48,7 @@ export function isNodeSuccessful(node) { * @returns {boolean} */ export function isAcceptable({status, error}) { - return isErrorStatus(status) && isNoRefImageError(error) || isFailStatus(status) || isSkippedStatus(status) || isInvalidRefImageError(error); + return isErrorStatus(status) && (isNoRefImageError(error) || isInvalidRefImageError(error)) || isFailStatus(status) || isSkippedStatus(status); } function isScreenGuiRevertable({gui, image, isLastResult}) { diff --git a/lib/static/new-ui/app/App.tsx b/lib/static/new-ui/app/App.tsx index 7a4be1653..c88f5cfd7 100644 --- a/lib/static/new-ui/app/App.tsx +++ b/lib/static/new-ui/app/App.tsx @@ -1,17 +1,18 @@ -import {ThemeProvider} from '@gravity-ui/uikit'; +import {Eye, ListCheck} from '@gravity-ui/icons'; +import {ThemeProvider, ToasterComponent, ToasterProvider} from '@gravity-ui/uikit'; +import '@gravity-ui/uikit/styles/fonts.css'; +import '@gravity-ui/uikit/styles/styles.css'; import React, {ReactNode, StrictMode} from 'react'; -import {MainLayout} from '../components/MainLayout'; +import {Provider} from 'react-redux'; import {HashRouter, Navigate, Route, Routes} from 'react-router-dom'; -import {Eye, ListCheck} from '@gravity-ui/icons'; + +import {LoadingBar} from '@/static/new-ui/components/LoadingBar'; +import {GuiniToolbarOverlay} from '@/static/new-ui/components/GuiniToolbarOverlay'; +import {MainLayout} from '../components/MainLayout'; import {SuitesPage} from '../features/suites/components/SuitesPage'; import {VisualChecksPage} from '../features/visual-checks/components/VisualChecksPage'; - -import '@gravity-ui/uikit/styles/fonts.css'; -import '@gravity-ui/uikit/styles/styles.css'; import '../../new-ui.css'; -import {Provider} from 'react-redux'; import store from '../../modules/store'; -import {LoadingBar} from '@/static/new-ui/components/LoadingBar'; export function App(): ReactNode { const pages = [ @@ -20,24 +21,28 @@ export function App(): ReactNode { url: '/suites', icon: ListCheck, element: , - children: [} />] + children: [} />] }, {title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: } ]; return - - - - - - } path={'/'}/> - {pages.map(page => {page.children})} - - - - + + + + + + + } path={'/'}/> + {pages.map(page => {page.children})} + + + + + + + ; } diff --git a/lib/static/new-ui/components/AssertViewResult/index.tsx b/lib/static/new-ui/components/AssertViewResult/index.tsx index cb0679e46..e015b1dff 100644 --- a/lib/static/new-ui/components/AssertViewResult/index.tsx +++ b/lib/static/new-ui/components/AssertViewResult/index.tsx @@ -28,6 +28,16 @@ function AssertViewResultInternal({result, diffMode, style}: AssertViewResultPro ; + } else if (result.status === TestStatus.STAGED) { + return
+ + +
; + } else if (result.status === TestStatus.COMMITED) { + return
+ + +
; } return null; diff --git a/lib/static/new-ui/components/AssertViewStatus/index.tsx b/lib/static/new-ui/components/AssertViewStatus/index.tsx index b8dcdb4b1..93411773e 100644 --- a/lib/static/new-ui/components/AssertViewStatus/index.tsx +++ b/lib/static/new-ui/components/AssertViewStatus/index.tsx @@ -2,7 +2,17 @@ import React, {ReactNode} from 'react'; import {ImageEntity, ImageEntityError} from '@/static/new-ui/types/store'; import {TestStatus} from '@/constants'; import {Icon} from '@gravity-ui/uikit'; -import {FileCheck, CircleCheck, SquareExclamation, SquareXmark, FileLetterX, FileExclamation, ArrowRightArrowLeft} from '@gravity-ui/icons'; +import { + ArrowRightArrowLeft, + CircleCheck, + FileCheck, + FileExclamation, + FileLetterX, + FilePlus, + SquareExclamation, + SquareXmark, + FileArrowUp +} from '@gravity-ui/icons'; import {isInvalidRefImageError, isNoRefImageError} from '@/common-utils'; import styles from './index.module.css'; @@ -17,6 +27,10 @@ export function AssertViewStatus({image}: AssertViewStatusProps): ReactNode { status = <>Image is absent; } else if (image.status === TestStatus.SUCCESS) { status = <>Images match; + } else if (image.status === TestStatus.STAGED) { + status = <>Image is staged; + } else if (image.status === TestStatus.COMMITED) { + status = <>Image was committed; } else if (isNoRefImageError((image as ImageEntityError).error)) { status = <>Reference not found; } else if (isInvalidRefImageError((image as ImageEntityError).error)) { diff --git a/lib/static/new-ui/components/AttemptPickerItem/index.module.css b/lib/static/new-ui/components/AttemptPickerItem/index.module.css index 915cd7906..df6eb526f 100644 --- a/lib/static/new-ui/components/AttemptPickerItem/index.module.css +++ b/lib/static/new-ui/components/AttemptPickerItem/index.module.css @@ -9,12 +9,18 @@ .attempt-picker-item--staged { background-color: rgba(255, 235, 206, 1); - border-color: rgba(255, 235, 206, 1); + color: hsl(32 100% 48% / 1); + --box-shadow-color: hsl(32deg 100% 48% / 42%); + --g-button-background-color-hover: hsl(32deg 100% 48% / 20%); + --g-button-text-color-hover: hsl(32 100% 48% / 1); } .attempt-picker-item--commited { background-color: rgba(207, 231, 252, 1); - border-color: rgba(207, 231, 252, 1); + color: hsl(208 88% 48% / 1); + --box-shadow-color: hsl(208deg 88% 48% / 38%); + --g-button-background-color-hover: hsl(208 88% 84% / 1); + --g-button-text-color-hover: hsl(208 88% 48% / 1); } .attempt-picker-item--success { diff --git a/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css b/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css new file mode 100644 index 000000000..cc25bbb65 --- /dev/null +++ b/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css @@ -0,0 +1,106 @@ +.container { + bottom: 20px; + left: 50%; + --x: 0; + --y: 0; + transform: translate(calc(-50% + var(--x) * 1px), calc(var(--y) * 1px)) !important; +} + +.buttons-container { + display: flex; + gap: 8px; + margin-left: auto; +} + +.title { + font-weight: 500; +} + +.button { + composes: regular-button from global; +} + +.primary-button { + --g-button-text-color: hsl(252 100% 38% / 1); + --g-button-text-color-hover: hsl(252 100% 38% / 1); +} + +.modalContainer { + --g-modal-border-radius: 10px; + padding: 20px; + width: 512px; + --g-color-base-modal: white; +} + +.modalContainer a, .modalContainer a:visited { + color: var(--color-link); +} + +.modalContainer a:hover { + color: var(--color-link-hover); +} + +.modalDescription { + color: var(--g-color-private-black-400); + margin-top: 12px; + line-height: 1.4; +} + +.modalFieldLabel { + margin-top: 20px; + font-weight: 450; +} + +.modalInput { + margin-top: 12px; +} + +.modalButtonsContainer { + margin-top: 20px; + display: flex; + justify-content: end; + gap: 8px; +} + +.modalButtonPrimary { + composes: regular-button from global, action-button from global; +} + +.errorToaster { + border: 1px solid rgb(229, 231, 235); + box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px !important; + margin-right: 10px; + margin-bottom: 20px !important; +} + +@keyframes toast-appear { + from { + opacity: 0; + scale: 0.95; + } + + to { + opacity: 1; + scale: 1; + } +} + +.errorToaster:global(.g-toast-animation-desktop_enter_active) { + animation: toast-appear .15s forwards ease; +} + +.errorToaster:global(.g-toast-animation-desktop_exit_active) { + animation: toast-appear .15s forwards ease reverse; +} + +.errorToaster :global(.g-toast__title) { + font-weight: 450; +} + +.errorToaster :global(.g-toast__content) { + color: var(--g-color-private-black-400); +} + +.errorToasterIcon { + color: var(--g-color-private-red-600-solid); +} diff --git a/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx b/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx new file mode 100644 index 000000000..b0df8610e --- /dev/null +++ b/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx @@ -0,0 +1,126 @@ +import React, {ReactNode, useEffect, useState} from 'react'; +import {ToolbarOverlay} from '@/static/new-ui/components/ToolbarOverlay'; +import {Button, Icon, Modal, TextInput, useToaster} from '@gravity-ui/uikit'; +import {CloudArrowUpIn, TriangleExclamation} from '@gravity-ui/icons'; + +import styles from './index.module.css'; +import classNames from 'classnames'; +import {useDispatch, useSelector} from 'react-redux'; +import {State} from '@/static/new-ui/types/store'; +import { + CommitResult, + staticAccepterCommitScreenshot, + staticAccepterUnstageScreenshot, staticAccepterUpdateCommitMessage, + staticAccepterUpdateToolbarPosition +} from '@/static/modules/actions'; +import {Point} from '@/static/new-ui/types'; +import {useLocation} from 'react-router-dom'; +import {TestStatus} from '@/constants'; +import {formatCommitPayload} from '@/static/modules/static-image-accepter'; +import {pick} from 'lodash'; + +export function GuiniToolbarOverlay(): ReactNode { + const dispatch = useDispatch(); + const toaster = useToaster(); + + const isInProgress = useSelector((state: State) => state.processing); + const allImagesById = useSelector((state: State) => state.tree.images.byId); + const acceptableImages = useSelector((state: State) => state.staticImageAccepter.acceptableImages); + const delayedImages = useSelector((state: State) => state.staticImageAccepter.accepterDelayedImages); + const stagedImages = Object.values(acceptableImages) + .filter(image => image.commitStatus === TestStatus.STAGED); + + const staticAccepterConfig = useSelector((state: State) => state.config.staticImageAccepter); + const pullRequestUrl = useSelector((state: State) => state.config.staticImageAccepter.pullRequestUrl); + const position = useSelector((state: State) => state.ui.staticImageAccepterToolbar.position); + const location = useLocation(); + + const [isVisible, setIsVisible] = useState(null); + const [isModalVisible, setIsModalVisible] = useState(false); + + const commitMessage = useSelector((state: State) => state.app.staticImageAccepterModal.commitMessage); + + useEffect(() => { + const newIsVisible = stagedImages.length > 0 && + !isInProgress && + !isModalVisible && + (location.pathname.startsWith('/suites') || location.pathname.startsWith('/visual-checks')); + if (Boolean(newIsVisible) !== Boolean(isVisible)) { + setIsVisible(newIsVisible); + } + }, [stagedImages, location, isModalVisible]); + + const onPositionChange = (pos: Point): void => { + dispatch(staticAccepterUpdateToolbarPosition({position: pos})); + }; + + const onCommitClick = (): void => { + setIsModalVisible(true); + }; + + const onCancelClick = (): void => { + for (const image of stagedImages) { + dispatch(staticAccepterUnstageScreenshot(image.id)); + } + }; + + const onModalCancelClick = (): void => { + setIsModalVisible(false); + }; + + const onModalCommitClick = async (): Promise => { + const imagesInfo = formatCommitPayload( + Object.values(acceptableImages), + allImagesById, + delayedImages + ); + + const opts = { + ...pick(staticAccepterConfig, [ + 'repositoryUrl', + 'pullRequestUrl', + 'serviceUrl', + 'axiosRequestOptions', + 'meta' + ]), + message: commitMessage + }; + + setIsModalVisible(false); + const result = (await dispatch(staticAccepterCommitScreenshot(imagesInfo, opts))) as CommitResult; + + if (result.error) { + toaster.add({ + name: 'static-accepter-error', + title: 'Failed to commit images', + content: result.error.message + '. See console for details.', + isClosable: true, + autoHiding: 5000, + renderIcon: () => , + className: styles.errorToaster + }); + } + }; + + const onCommitMessageUpdate = (newCommitMessage: string): void => { + dispatch(staticAccepterUpdateCommitMessage({commitMessage: newCommitMessage})); + }; + + return +
{stagedImages.length} {stagedImages.length > 1 ? 'images are' : 'image is'} staged for commit
+
+ + +
+ setIsModalVisible(false)} contentClassName={styles.modalContainer}> +
Commit images
+
Commit with {stagedImages.length} {stagedImages.length > 1 ? 'images' : 'image'} will be added to your pull request at {pullRequestUrl}.
+
Commit Message
+ +
+ + +
+
+
; +} diff --git a/lib/static/new-ui/components/LoadingBar/index.tsx b/lib/static/new-ui/components/LoadingBar/index.tsx index fa8faecb8..9c597968c 100644 --- a/lib/static/new-ui/components/LoadingBar/index.tsx +++ b/lib/static/new-ui/components/LoadingBar/index.tsx @@ -7,30 +7,32 @@ import {State} from '@/static/new-ui/types/store'; import classNames from 'classnames'; export function LoadingBar(): ReactNode { - const isLoaded = useSelector((state: State) => state.app.isInitialized); - const isLoadedRef = useRef(isLoaded); + const isVisible = useSelector((state: State) => state.app.loading.isVisible); + const isInProgress = useSelector((state: State) => state.app.loading.isInProgress); + const isVisibleRef = useRef(isVisible); const progress = useSelector(getTotalLoadingProgress); + const taskTitle = useSelector((state: State) => state.app.loading.taskTitle); const [hidden, setHidden] = React.useState(true); // Delay is needed for smoother experience: when loading is fast, it prevents notification bar from appearing and // hiding immediately. When loading a lot of data, it helps avoid freezes when everything is loaded. useEffect(() => { - isLoadedRef.current = isLoaded; + isVisibleRef.current = isVisible; const timeoutId = setTimeout(() => { - if (isLoaded === isLoadedRef.current) { - setHidden(isLoaded); + if (isVisible === isVisibleRef.current) { + setHidden(!isVisible); } }, 500); return () => clearTimeout(timeoutId); - }, [isLoaded]); + }, [isVisible]); return
- Loading Testplane UI -
+ {taskTitle} + {isInProgress &&
}
diff --git a/lib/static/new-ui/components/ToolbarOverlay/index.module.css b/lib/static/new-ui/components/ToolbarOverlay/index.module.css new file mode 100644 index 000000000..e487679fe --- /dev/null +++ b/lib/static/new-ui/components/ToolbarOverlay/index.module.css @@ -0,0 +1,38 @@ +.container { + position: fixed; + z-index: 999; + background: #6c47ff; + padding: 12px 12px 12px 6px; + border-radius: 10px; + color: rgba(255, 255, 255, .9); + fill: rgba(255, 255, 255, .9); + width: 700px; + display: flex; + box-shadow: 0 0 16px 0 #00000036; + align-items: center; + + opacity: 0; + scale: 0.95; + + visibility: hidden; + transition: scale .15s ease, visibility 0.15s ease, opacity 0.15s ease; +} + +.dragging { + scale: 1.01 !important; +} + +.visible { + visibility: visible; + opacity: 1; + scale: 1; +} + +.icon-container { + margin-right: 6px; + cursor: grab; +} + +.dragging .icon-container { + cursor: grabbing; +} diff --git a/lib/static/new-ui/components/ToolbarOverlay/index.tsx b/lib/static/new-ui/components/ToolbarOverlay/index.tsx new file mode 100644 index 000000000..9fbcd549c --- /dev/null +++ b/lib/static/new-ui/components/ToolbarOverlay/index.tsx @@ -0,0 +1,80 @@ +import {Icon} from '@gravity-ui/uikit'; +import {Grip} from '@gravity-ui/icons'; +import classNames from 'classnames'; +import React, {ReactNode, useState} from 'react'; +import {createPortal} from 'react-dom'; + +import styles from './index.module.css'; +import {Point} from '@/static/new-ui/types'; + +interface ToolbarOverlayProps { + isVisible: boolean | null; + className?: string; + children: ReactNode; + draggable?: { + position: Point; + onPositionChange: (position: Point) => void; + } +} + +export function ToolbarOverlay(props: ToolbarOverlayProps): ReactNode { + const [dragging, setDragging] = useState(false); + const [offset, setOffset] = useState({x: 0, y: 0}); + + const handleMouseDown = (e: React.MouseEvent): void => { + if (!props.draggable) { + return; + } + + setDragging(true); + setOffset({ + x: e.clientX - props.draggable.position.x, + y: e.clientY - props.draggable.position.y + }); + }; + + const handleMouseMove = (e: MouseEvent): void => { + if (!dragging || !props.draggable) { + return; + } + + const newPosition = { + x: e.clientX - offset.x, + y: e.clientY - offset.y + }; + props.draggable.onPositionChange(newPosition); + }; + + const handleMouseUp = (): void => { + setDragging(false); + }; + + React.useEffect(() => { + if (dragging) { + document.body.style.userSelect = 'none'; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + } else { + document.body.style.userSelect = 'auto'; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + } + + return () => { + document.body.style.userSelect = 'auto'; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [dragging]); + + return createPortal(
+
{props.draggable && }
+ {props.children} +
, document.body); +} diff --git a/lib/static/new-ui/components/UiModeHintNotification/index.module.css b/lib/static/new-ui/components/UiModeHintNotification/index.module.css index 9e341dc51..02cf4a0aa 100644 --- a/lib/static/new-ui/components/UiModeHintNotification/index.module.css +++ b/lib/static/new-ui/components/UiModeHintNotification/index.module.css @@ -1,69 +1,7 @@ -@keyframes notification-appear { - 0% { - opacity: 0; - visibility: visible; - filter: blur(20px); - } - - 30% { - opacity: 1; - } - - 80% { - filter: blur(0px); - transform: scale(1.02); - } - - 100% { - filter: blur(0px); - transform: scale(1); - visibility: visible; - } -} - -@keyframes notification-disappear { - 0% { - opacity: 1; - visibility: visible; - } - - 70% { - opacity: 1; - } - - 100% { - filter: blur(20px); - opacity: 0; - visibility: hidden; - } -} - .container { - filter: blur(0px); - position: fixed; bottom: 6px; - left: 62px; - z-index: 999; - background: #6c47ff; - padding: 14px 14px; - border-radius: 10px; - color: rgba(255, 255, 255, .9); - fill: rgba(255, 255, 255, .9); - width: 700px; - display: flex; gap: 8px; - box-shadow: 0 0 16px 0 #00000036; - align-items: center; - - visibility: hidden; -} - -.visible { - animation: notification-appear .6s linear forwards; -} - -.hidden { - animation: notification-disappear .4s linear forwards; + left: 62px; } @keyframes arrow-shake { diff --git a/lib/static/new-ui/components/UiModeHintNotification/index.tsx b/lib/static/new-ui/components/UiModeHintNotification/index.tsx index b793221f5..5c394a529 100644 --- a/lib/static/new-ui/components/UiModeHintNotification/index.tsx +++ b/lib/static/new-ui/components/UiModeHintNotification/index.tsx @@ -1,9 +1,8 @@ -import {createPortal} from 'react-dom'; import React, {ReactNode} from 'react'; import {ArrowLeft, Xmark} from '@gravity-ui/icons'; import styles from './index.module.css'; -import classNames from 'classnames'; +import {ToolbarOverlay} from '@/static/new-ui/components/ToolbarOverlay'; interface HintNotificationProps { isVisible: boolean | null; @@ -11,10 +10,7 @@ interface HintNotificationProps { } export function UiModeHintNotification(props: HintNotificationProps): ReactNode { - return createPortal(
+ return
Hint
@@ -22,5 +18,5 @@ export function UiModeHintNotification(props: HintNotificationProps): ReactNode
You can always switch back to the old UI in Settings
props.onClose?.()}/> -
, document.body); + ; } diff --git a/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx index b5c763044..88db450e5 100644 --- a/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx +++ b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx @@ -4,9 +4,15 @@ import React, {ReactNode} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import {AssertViewResult} from '@/static/new-ui/components/AssertViewResult'; -import {ImageEntity, ImageEntityFail, State} from '@/static/new-ui/types/store'; +import {ImageEntity, State} from '@/static/new-ui/types/store'; import {DiffModeId, DiffModes, EditScreensFeature, TestStatus} from '@/constants'; -import {acceptTest, changeDiffMode, undoAcceptImage} from '@/static/modules/actions'; +import { + acceptTest, + changeDiffMode, + staticAccepterStageScreenshot, + staticAccepterUnstageScreenshot, + undoAcceptImage +} from '@/static/modules/actions'; import {isAcceptable, isScreenRevertable} from '@/static/modules/utils'; import {getCurrentBrowser, getCurrentResult} from '@/static/new-ui/features/suites/selectors'; import {AssertViewStatus} from '@/static/new-ui/components/AssertViewStatus'; @@ -22,30 +28,42 @@ export function ScreenshotsTreeViewItem(props: ScreenshotsTreeViewItemProps): Re const diffMode = useSelector((state: State) => state.view.diffMode); const isEditScreensAvailable = useSelector((state: State) => state.app.availableFeatures) .find(feature => feature.name === EditScreensFeature.name); + const isStaticImageAccepterEnabled = useSelector((state: State) => state.staticImageAccepter.enabled); const isRunning = useSelector((state: State) => state.running); + const isProcessing = useSelector((state: State) => state.processing); const isGui = useSelector((state: State) => state.gui); + const isDiffModeSwitcherVisible = props.image.status === TestStatus.FAIL && props.image.diffImg; + const currentBrowser = useSelector(getCurrentBrowser); const currentResult = useSelector(getCurrentResult); const isLastResult = currentResult && currentBrowser && currentResult.id === currentBrowser.resultIds[currentBrowser.resultIds.length - 1]; - const isUndoAvailable = isScreenRevertable({gui: isGui, image: props.image, isLastResult, isStaticImageAccepterEnabled: false}); + const isUndoAvailable = isScreenRevertable({gui: isGui, image: props.image, isLastResult, isStaticImageAccepterEnabled}); const onDiffModeChangeHandler = (diffMode: DiffModeId): void => { dispatch(changeDiffMode(diffMode)); }; const onScreenshotAccept = (): void => { - dispatch(acceptTest(props.image.id)); + if (isStaticImageAccepterEnabled) { + dispatch(staticAccepterStageScreenshot([props.image.id])); + } else { + dispatch(acceptTest(props.image.id)); + } }; const onScreenshotUndo = (): void => { - dispatch(undoAcceptImage(props.image.id)); + if (isStaticImageAccepterEnabled) { + dispatch(staticAccepterUnstageScreenshot(props.image.id)); + } else { + dispatch(undoAcceptImage(props.image.id)); + } }; return
{props.image.status !== TestStatus.SUCCESS &&
- {!(props.image as ImageEntityFail).diffImg && + {!isDiffModeSwitcherVisible && } - {(props.image as ImageEntityFail).diffImg && + {isDiffModeSwitcherVisible &&
{Object.values(DiffModes).map(diffMode => @@ -65,10 +83,10 @@ export function ScreenshotsTreeViewItem(props: ScreenshotsTreeViewItemProps): Re
} {isEditScreensAvailable &&
- {isUndoAvailable && } - {isAcceptable(props.image) && }
} diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx index 797886084..d4eb0985c 100644 --- a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx @@ -19,7 +19,7 @@ import {CompactAttemptPicker} from '@/static/new-ui/components/CompactAttemptPic import {DiffModeId, DiffModes, EditScreensFeature} from '@/constants'; import { acceptTest, - changeDiffMode, + changeDiffMode, staticAccepterStageScreenshot, staticAccepterUnstageScreenshot, undoAcceptImage, visualChecksPageSetCurrentNamedImage } from '@/static/modules/actions'; @@ -45,18 +45,32 @@ export function VisualChecksPage(): ReactNode { dispatch(changeDiffMode(diffMode)); }; + const isStaticImageAccepterEnabled = useSelector((state: State) => state.staticImageAccepter.enabled); const isEditScreensAvailable = useSelector((state: State) => state.app.availableFeatures) .find(feature => feature.name === EditScreensFeature.name); const isRunning = useSelector((state: State) => state.running); + const isProcessing = useSelector((state: State) => state.processing); const isGui = useSelector((state: State) => state.gui); const onScreenshotAccept = (): void => { - if (currentImage) { + if (!currentImage) { + return; + } + + if (isStaticImageAccepterEnabled) { + dispatch(staticAccepterStageScreenshot([currentImage.id])); + } else { dispatch(acceptTest(currentImage.id)); } }; const onScreenshotUndo = (): void => { - if (currentImage) { + if (!currentImage) { + return; + } + + if (isStaticImageAccepterEnabled) { + dispatch(staticAccepterUnstageScreenshot(currentImage.id)); + } else { dispatch(undoAcceptImage(currentImage.id)); } }; @@ -66,7 +80,7 @@ export function VisualChecksPage(): ReactNode { const currentResultId = currentImage?.parentId; const isLastResult = Boolean(currentResultId && currentBrowser && currentResultId === currentBrowser.resultIds[currentBrowser.resultIds.length - 1]); - const isUndoAvailable = isScreenRevertable({gui: isGui, image: currentImage ?? {}, isLastResult, isStaticImageAccepterEnabled: false}); + const isUndoAvailable = isScreenRevertable({gui: isGui, image: currentImage ?? {}, isLastResult, isStaticImageAccepterEnabled}); const isInitialized = useSelector((state: State) => state.app.isInitialized); @@ -93,9 +107,9 @@ export function VisualChecksPage(): ReactNode { )} {isEditScreensAvailable &&
- {isUndoAvailable && } - {currentImage && isAcceptable(currentImage) && }
}
diff --git a/lib/static/new-ui/types/index.ts b/lib/static/new-ui/types/index.ts index 46b3378e4..b496c59fe 100644 --- a/lib/static/new-ui/types/index.ts +++ b/lib/static/new-ui/types/index.ts @@ -2,3 +2,8 @@ export interface TreeViewItem { data: T; children?: TreeViewItem[]; } + +export interface Point { + x: number; + y: number; +} diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index a87a7ece4..8b038c622 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -1,7 +1,9 @@ import {DiffModeId, Feature, TestStatus, ViewMode} from '@/constants'; -import {BrowserItem, ImageFile, ReporterConfig, TestError, TestStepCompressed} from '@/types'; +import {BrowserItem, ImageFile, RefImageFile, ReporterConfig, TestError, TestStepCompressed} from '@/types'; import {HtmlReporterValues} from '@/plugin-api'; import {CoordBounds} from 'looks-same'; +import {Point} from '@/static/new-ui/types/index'; +import {AcceptableImage} from '@/static/modules/static-image-accepter'; export interface SuiteEntityNode { id: string; @@ -65,12 +67,28 @@ export interface ImageEntitySuccess extends ImageEntityCommon { status: TestStatus.SUCCESS; stateName: string; expectedImg: ImageFile; + refImg: RefImageFile; } export interface ImageEntityUpdated extends ImageEntityCommon { status: TestStatus.UPDATED; stateName: string; expectedImg: ImageFile; + refImg: RefImageFile; +} + +export interface ImageEntityStaged extends ImageEntityCommon { + status: TestStatus.STAGED; + stateName: string; + actualImg: ImageFile; + refImg: RefImageFile; +} + +export interface ImageEntityCommitted extends ImageEntityCommon { + status: TestStatus.COMMITED; + stateName: string; + actualImg: ImageFile; + refImg: RefImageFile; } export interface ImageEntityError extends ImageEntityCommon { @@ -78,6 +96,7 @@ export interface ImageEntityError extends ImageEntityCommon { stateName?: string; actualImg: ImageFile; error?: TestError; + refImg?: RefImageFile; } export interface ImageEntityFail extends ImageEntityCommon { @@ -87,9 +106,10 @@ export interface ImageEntityFail extends ImageEntityCommon { diffImg: ImageFile; actualImg: ImageFile; expectedImg: ImageFile; + refImg: RefImageFile; } -export type ImageEntity = ImageEntityError | ImageEntityFail | ImageEntitySuccess | ImageEntityUpdated; +export type ImageEntity = ImageEntityError | ImageEntityFail | ImageEntitySuccess | ImageEntityUpdated | ImageEntityStaged | ImageEntityCommitted; export const isImageEntityFail = (image: ImageEntity): image is ImageEntityFail => Boolean((image as ImageEntityFail).stateName); @@ -140,15 +160,26 @@ export interface State { currentNamedImageId: string | null; }; loading: { + /** @note Determines whether the loading bar is visible */ + isVisible: boolean; + /** @note Determines visibility of bouncing dots at the end of the task title */ + isInProgress: boolean; + taskTitle: string; /** @note Maps ID of a resource to its loading progress. E.g. dbUrl: 88. Progress is measured from 0 to 1. */ progress: Record; - } + }; + staticImageAccepterModal: { + commitMessage: string; + }; }; ui: { suitesPage: { expandedSectionsById: Record; expandedStepsByResultId: Record>; - }, + }; + staticImageAccepterToolbar: { + position: Point; + }; }; browsers: BrowserItem[]; tree: TreeEntity; @@ -161,7 +192,18 @@ export interface State { baseHost: string; }; running: boolean; + processing: boolean; gui: boolean; apiValues: HtmlReporterValues; config: ReporterConfig; + staticImageAccepter: { + enabled: boolean; + acceptableImages: Record; + accepterDelayedImages: { + imageId: string; + stateName: string; + stateNameImageId: string; + }[]; + imagesToCommitCount: number; + }; } diff --git a/lib/static/new-ui/utils/index.tsx b/lib/static/new-ui/utils/index.tsx index 0415d12a2..00b62f40a 100644 --- a/lib/static/new-ui/utils/index.tsx +++ b/lib/static/new-ui/utils/index.tsx @@ -1,4 +1,12 @@ -import {ArrowRotateLeft, CircleCheck, CircleDashed, CircleMinus, CircleXmark, ArrowsRotateLeft} from '@gravity-ui/icons'; +import { + ArrowRotateLeft, + ArrowsRotateLeft, + CircleCheck, + CircleDashed, + CircleMinus, + CircleXmark, + CloudCheck +} from '@gravity-ui/icons'; import {Spin} from '@gravity-ui/uikit'; import React from 'react'; @@ -14,10 +22,12 @@ export const getIconByStatus = (status: TestStatus): React.JSX.Element => { return ; } else if (status === TestStatus.RETRY) { return ; - } else if (status === TestStatus.UPDATED) { + } else if (status === TestStatus.UPDATED || status === TestStatus.STAGED) { return ; } else if (status === TestStatus.RUNNING) { return ; + } else if (status === TestStatus.COMMITED) { + return ; } return ; diff --git a/lib/static/styles.css b/lib/static/styles.css index 5fa41a936..0b8a0064a 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -983,9 +983,13 @@ h1, h2, h3, h4, h5, h6 { } .icon-retry { - color: var(--g-color-private-orange-600-solid); + color: var(--g-color-private-cool-grey-600-solid); } .icon-updated { - color: var(--color-pink-600); + color: hsl(32 100% 48% / 1); +} + +.icon-committed { + color: hsl(208 88% 48% / 1); } From a4d21a18ebef230e3750aca76f9785bffa8860ca Mon Sep 17 00:00:00 2001 From: shadowusr Date: Thu, 7 Nov 2024 14:48:29 +0300 Subject: [PATCH 2/2] fix: fix review issues --- lib/static/modules/action-names.ts | 2 +- lib/static/modules/actions/static-accepter.ts | 6 ++-- lib/static/modules/default-state.ts | 2 +- .../modules/reducers/static-image-accepter.js | 4 +-- .../GuiniToolbarOverlay/index.module.css | 30 +++++++++---------- .../components/GuiniToolbarOverlay/index.tsx | 12 ++++---- .../components/ToolbarOverlay/index.tsx | 24 +++++++-------- lib/static/new-ui/types/store.ts | 2 +- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index 335bb524c..6b88ae5f2 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -22,7 +22,7 @@ export default { STATIC_ACCEPTER_STAGE_SCREENSHOT: 'STATIC_ACCEPTER_STAGE_SCREENSHOT', STATIC_ACCEPTER_UNSTAGE_SCREENSHOT: 'STATIC_ACCEPTER_UNSTAGE_SCREENSHOT', STATIC_ACCEPTER_COMMIT_SCREENSHOT: 'STATIC_ACCEPTER_COMMIT_SCREENSHOT', - STATIC_ACCEPTER_UPDATE_TOOLBAR_POSITION: 'STATIC_ACCEPTER_UPDATE_TOOLBAR_POSITION', + STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET: 'STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET', STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE: 'STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE', CLOSE_SECTIONS: 'CLOSE_SECTIONS', TOGGLE_STATE_RESULT: 'TOGGLE_STATE_RESULT', diff --git a/lib/static/modules/actions/static-accepter.ts b/lib/static/modules/actions/static-accepter.ts index 57788db96..376a69a6d 100644 --- a/lib/static/modules/actions/static-accepter.ts +++ b/lib/static/modules/actions/static-accepter.ts @@ -134,9 +134,9 @@ export const staticAccepterCommitScreenshot = ( }; }; -type StaticAccepterUpdateToolbarPositionAction = Action; -export const staticAccepterUpdateToolbarPosition = (payload: {position: Point}): StaticAccepterUpdateToolbarPositionAction => { - return {type: actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_POSITION, payload}; +type StaticAccepterUpdateToolbarPositionAction = Action; +export const staticAccepterUpdateToolbarOffset = (payload: {offset: Point}): StaticAccepterUpdateToolbarPositionAction => { + return {type: actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET, payload}; }; type StaticAccepterUpdateCommitMessageAction = Action; diff --git a/lib/static/modules/default-state.ts b/lib/static/modules/default-state.ts index 43f891920..a10e83cd7 100644 --- a/lib/static/modules/default-state.ts +++ b/lib/static/modules/default-state.ts @@ -117,7 +117,7 @@ export default Object.assign({config: configDefaults}, { expandedStepsByResultId: {} }, staticImageAccepterToolbar: { - position: {x: 0, y: 0} + offset: {x: 0, y: 0} } } }) satisfies State; diff --git a/lib/static/modules/reducers/static-image-accepter.js b/lib/static/modules/reducers/static-image-accepter.js index 6a82fadfa..0a1ffff8e 100644 --- a/lib/static/modules/reducers/static-image-accepter.js +++ b/lib/static/modules/reducers/static-image-accepter.js @@ -93,11 +93,11 @@ export default (state, action) => { return applyStateUpdate(state, diff); } - case actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_POSITION: { + case actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET: { return applyStateUpdate(state, { ui: { staticImageAccepterToolbar: { - position: action.payload.position + offset: action.payload.offset } } }); diff --git a/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css b/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css index cc25bbb65..882561da8 100644 --- a/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css +++ b/lib/static/new-ui/components/GuiniToolbarOverlay/index.module.css @@ -3,7 +3,7 @@ left: 50%; --x: 0; --y: 0; - transform: translate(calc(-50% + var(--x) * 1px), calc(var(--y) * 1px)) !important; + transform: translate(calc(-50% + var(--x) * 1px), calc(var(--y) * 1px)); } .buttons-container { @@ -25,48 +25,48 @@ --g-button-text-color-hover: hsl(252 100% 38% / 1); } -.modalContainer { +.modal-container { --g-modal-border-radius: 10px; padding: 20px; width: 512px; --g-color-base-modal: white; } -.modalContainer a, .modalContainer a:visited { +.modal-container a, .modal-container a:visited { color: var(--color-link); } -.modalContainer a:hover { +.modal-container a:hover { color: var(--color-link-hover); } -.modalDescription { +.modal-description { color: var(--g-color-private-black-400); margin-top: 12px; line-height: 1.4; } -.modalFieldLabel { +.modal-field-label { margin-top: 20px; font-weight: 450; } -.modalInput { +.modal-input { margin-top: 12px; } -.modalButtonsContainer { +.modal-buttons-container { margin-top: 20px; display: flex; justify-content: end; gap: 8px; } -.modalButtonPrimary { +.modal-button-primary { composes: regular-button from global, action-button from global; } -.errorToaster { +.error-toaster { border: 1px solid rgb(229, 231, 235); box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px !important; margin-right: 10px; @@ -85,22 +85,22 @@ } } -.errorToaster:global(.g-toast-animation-desktop_enter_active) { +.error-toaster:global(.g-toast-animation-desktop_enter_active) { animation: toast-appear .15s forwards ease; } -.errorToaster:global(.g-toast-animation-desktop_exit_active) { +.error-toaster:global(.g-toast-animation-desktop_exit_active) { animation: toast-appear .15s forwards ease reverse; } -.errorToaster :global(.g-toast__title) { +.error-toaster :global(.g-toast__title) { font-weight: 450; } -.errorToaster :global(.g-toast__content) { +.error-toaster :global(.g-toast__content) { color: var(--g-color-private-black-400); } -.errorToasterIcon { +.error-toaster-icon { color: var(--g-color-private-red-600-solid); } diff --git a/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx b/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx index b0df8610e..481dc60c1 100644 --- a/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx +++ b/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx @@ -11,7 +11,7 @@ import { CommitResult, staticAccepterCommitScreenshot, staticAccepterUnstageScreenshot, staticAccepterUpdateCommitMessage, - staticAccepterUpdateToolbarPosition + staticAccepterUpdateToolbarOffset } from '@/static/modules/actions'; import {Point} from '@/static/new-ui/types'; import {useLocation} from 'react-router-dom'; @@ -32,7 +32,7 @@ export function GuiniToolbarOverlay(): ReactNode { const staticAccepterConfig = useSelector((state: State) => state.config.staticImageAccepter); const pullRequestUrl = useSelector((state: State) => state.config.staticImageAccepter.pullRequestUrl); - const position = useSelector((state: State) => state.ui.staticImageAccepterToolbar.position); + const offset = useSelector((state: State) => state.ui.staticImageAccepterToolbar.offset); const location = useLocation(); const [isVisible, setIsVisible] = useState(null); @@ -44,14 +44,14 @@ export function GuiniToolbarOverlay(): ReactNode { const newIsVisible = stagedImages.length > 0 && !isInProgress && !isModalVisible && - (location.pathname.startsWith('/suites') || location.pathname.startsWith('/visual-checks')); + ['/suites', '/visual-checks'].some((path) => location.pathname.startsWith(path)); if (Boolean(newIsVisible) !== Boolean(isVisible)) { setIsVisible(newIsVisible); } }, [stagedImages, location, isModalVisible]); - const onPositionChange = (pos: Point): void => { - dispatch(staticAccepterUpdateToolbarPosition({position: pos})); + const onOffsetChange = (offset: Point): void => { + dispatch(staticAccepterUpdateToolbarOffset({offset})); }; const onCommitClick = (): void => { @@ -106,7 +106,7 @@ export function GuiniToolbarOverlay(): ReactNode { dispatch(staticAccepterUpdateCommitMessage({commitMessage: newCommitMessage})); }; - return + return
{stagedImages.length} {stagedImages.length > 1 ? 'images are' : 'image is'} staged for commit
diff --git a/lib/static/new-ui/components/ToolbarOverlay/index.tsx b/lib/static/new-ui/components/ToolbarOverlay/index.tsx index 9fbcd549c..5270c8560 100644 --- a/lib/static/new-ui/components/ToolbarOverlay/index.tsx +++ b/lib/static/new-ui/components/ToolbarOverlay/index.tsx @@ -1,7 +1,7 @@ import {Icon} from '@gravity-ui/uikit'; import {Grip} from '@gravity-ui/icons'; import classNames from 'classnames'; -import React, {ReactNode, useState} from 'react'; +import React, {ReactNode, useState, useEffect} from 'react'; import {createPortal} from 'react-dom'; import styles from './index.module.css'; @@ -12,14 +12,14 @@ interface ToolbarOverlayProps { className?: string; children: ReactNode; draggable?: { - position: Point; - onPositionChange: (position: Point) => void; + offset: Point; + onOffsetChange: (position: Point) => void; } } export function ToolbarOverlay(props: ToolbarOverlayProps): ReactNode { const [dragging, setDragging] = useState(false); - const [offset, setOffset] = useState({x: 0, y: 0}); + const [startingPoint, setStartingPoint] = useState({x: 0, y: 0}); const handleMouseDown = (e: React.MouseEvent): void => { if (!props.draggable) { @@ -27,9 +27,9 @@ export function ToolbarOverlay(props: ToolbarOverlayProps): ReactNode { } setDragging(true); - setOffset({ - x: e.clientX - props.draggable.position.x, - y: e.clientY - props.draggable.position.y + setStartingPoint({ + x: e.clientX - props.draggable.offset.x, + y: e.clientY - props.draggable.offset.y }); }; @@ -39,17 +39,17 @@ export function ToolbarOverlay(props: ToolbarOverlayProps): ReactNode { } const newPosition = { - x: e.clientX - offset.x, - y: e.clientY - offset.y + x: e.clientX - startingPoint.x, + y: e.clientY - startingPoint.y }; - props.draggable.onPositionChange(newPosition); + props.draggable.onOffsetChange(newPosition); }; const handleMouseUp = (): void => { setDragging(false); }; - React.useEffect(() => { + useEffect(() => { if (dragging) { document.body.style.userSelect = 'none'; document.addEventListener('mousemove', handleMouseMove); @@ -68,7 +68,7 @@ export function ToolbarOverlay(props: ToolbarOverlayProps): ReactNode { }, [dragging]); return createPortal(
>; }; staticImageAccepterToolbar: { - position: Point; + offset: Point; }; }; browsers: BrowserItem[];