mirror of
https://github.com/harness/drone.git
synced 2025-05-21 11:29:52 +08:00
feat: [code-1687]: update ux of pr aida btn (#1164)
This commit is contained in:
parent
b02da4b78a
commit
38510ca0c1
@ -46,6 +46,20 @@
|
||||
padding: var(--spacing-small);
|
||||
|
||||
.cm-line {
|
||||
> .aidaGenText {
|
||||
color: var(--ai-purple-800) !important;
|
||||
}
|
||||
|
||||
> .aidaGenText::before {
|
||||
padding-top: 4px;
|
||||
padding-right: 3px;
|
||||
content: url('../../icons/spinner.svg?url');
|
||||
}
|
||||
> .highlightText {
|
||||
padding: 1px;
|
||||
background: var(--ai-purple-100) !important;
|
||||
}
|
||||
|
||||
&,
|
||||
* {
|
||||
@include markdown-font;
|
||||
|
@ -119,6 +119,11 @@
|
||||
// @include mono-font;
|
||||
// }
|
||||
// }
|
||||
.cm-line {
|
||||
> .aidaGenText {
|
||||
background: var(--ai-purple-800) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.md-editor {
|
||||
background-color: transparent !important;
|
||||
@ -270,3 +275,8 @@
|
||||
background: var(--grey-50);
|
||||
}
|
||||
}
|
||||
|
||||
.aidaGenText {
|
||||
background: var(--ai-purple-800) !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const aidaGenText: string
|
||||
export declare const buttonsBar: string
|
||||
export declare const container: string
|
||||
export declare const dialog: string
|
||||
|
@ -29,18 +29,25 @@ import {
|
||||
import type { IconName } from '@harnessio/icons'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import cx from 'classnames'
|
||||
import type { EditorView } from '@codemirror/view'
|
||||
import { keymap } from '@codemirror/view'
|
||||
import { DecorationSet, EditorView, Decoration, keymap } from '@codemirror/view'
|
||||
import { undo, redo, history } from '@codemirror/commands'
|
||||
import { EditorSelection } from '@codemirror/state'
|
||||
import { EditorSelection, StateEffect, StateField } from '@codemirror/state'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { useMutate } from 'restful-react'
|
||||
import { Editor } from 'components/Editor/Editor'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { CommentBoxOutletPosition, formatBytes, getErrorMessage, handleFileDrop, handlePaste } from 'utils/Utils'
|
||||
import {
|
||||
CommentBoxOutletPosition,
|
||||
formatBytes,
|
||||
getErrorMessage,
|
||||
handleFileDrop,
|
||||
handlePaste,
|
||||
removeSpecificTextOptimized
|
||||
} from 'utils/Utils'
|
||||
import { decodeGitContent, handleUpload, normalizeGitRef } from 'utils/GitUtils'
|
||||
import type { TypesRepository } from 'services/code'
|
||||
import { useEventListener } from 'hooks/useEventListener'
|
||||
import css from './MarkdownEditorWithPreview.module.scss'
|
||||
|
||||
enum MarkdownEditorTab {
|
||||
@ -74,6 +81,32 @@ const toolbar: ToolbarItem[] = [
|
||||
{ icon: 'main-code-yaml', action: ToolbarAction.CODE_BLOCK }
|
||||
]
|
||||
|
||||
// Define a unique effect to update decorations
|
||||
const addDecorationEffect = StateEffect.define<{ decoration: Decoration; from: number; to: number }[]>()
|
||||
const removeDecorationEffect = StateEffect.define<{}>() // No payload needed for removal in this simple case// Create a state field to hold decorations
|
||||
const decorationField = StateField.define<DecorationSet>({
|
||||
create() {
|
||||
return Decoration.none
|
||||
},
|
||||
update(decorations, tr) {
|
||||
decorations = decorations.map(tr.changes)
|
||||
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(addDecorationEffect)) {
|
||||
const add = []
|
||||
for (const { decoration, from, to } of effect.value) {
|
||||
add.push(decoration.range(from, to))
|
||||
}
|
||||
decorations = decorations.update({ add })
|
||||
} else if (effect.is(removeDecorationEffect)) {
|
||||
decorations = Decoration.none
|
||||
}
|
||||
}
|
||||
return decorations
|
||||
},
|
||||
provide: f => EditorView.decorations.from(f)
|
||||
})
|
||||
|
||||
interface MarkdownEditorWithPreviewProps {
|
||||
className?: string
|
||||
value?: string
|
||||
@ -151,6 +184,7 @@ export function MarkdownEditorWithPreview({
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/genai/change-summary`
|
||||
})
|
||||
const isDirty = useRef(dirty)
|
||||
const [data, setData] = useState({})
|
||||
|
||||
useEffect(
|
||||
function setDirtyRef() {
|
||||
@ -175,22 +209,54 @@ export function MarkdownEditorWithPreview({
|
||||
preventDefault: true
|
||||
}
|
||||
])
|
||||
const handleMouseDown = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(event: any) => {
|
||||
const editorDom = viewRef?.current?.dom
|
||||
if (!editorDom?.contains(event.target)) {
|
||||
// Clicked outside the editor
|
||||
viewRef?.current?.dispatch({
|
||||
effects: removeDecorationEffect.of({})
|
||||
})
|
||||
}
|
||||
},
|
||||
[viewRef]
|
||||
)
|
||||
|
||||
const dispatchContent = (content: string, userEvent: boolean) => {
|
||||
useEventListener('mousedown', handleMouseDown)
|
||||
|
||||
const dispatchContent = (content: string, userEvent: boolean, decoration = false) => {
|
||||
const view = viewRef.current
|
||||
const currentContent = view?.state.doc.toString()
|
||||
const { from, to } = view?.state.selection.main ?? { from: 0, to: 0 }
|
||||
const changeColorDecoration = Decoration.mark({ class: 'aidaGenText' })
|
||||
const highlightDecoration = Decoration.mark({ class: 'highlightText' })
|
||||
|
||||
view?.dispatch({
|
||||
changes: { from: 0, to: currentContent?.length, insert: content },
|
||||
userEvent: userEvent ? 'input' : 'ignore' // Marking this transaction as an input event makes it part of the undo history
|
||||
})
|
||||
if (decoration) {
|
||||
view?.dispatch({
|
||||
changes: { from: from, to: to, insert: content },
|
||||
effects: addDecorationEffect.of([
|
||||
{ decoration: changeColorDecoration, from: from, to: content?.length + from }
|
||||
]),
|
||||
userEvent: userEvent ? 'input' : 'ignore' // Marking this transaction as an input event makes it part of the undo history,
|
||||
})
|
||||
} else {
|
||||
view?.dispatch({
|
||||
effects: removeDecorationEffect.of({ from: from, to: to }),
|
||||
userEvent: userEvent ? 'input' : 'ignore' // Marking this transaction as an input event makes it part of the undo history
|
||||
})
|
||||
view?.dispatch({
|
||||
changes: { from: from, to: to, insert: content },
|
||||
effects: addDecorationEffect.of([{ decoration: highlightDecoration, from: from, to: content?.length + from }]),
|
||||
userEvent: userEvent ? 'input' : 'ignore' // Marking this transaction as an input event makes it part of the undo history
|
||||
})
|
||||
removeSpecificTextOptimized(viewRef, getString('aidaGenSummary'))
|
||||
}
|
||||
}
|
||||
|
||||
const [data, setData] = useState({})
|
||||
useEffect(() => {
|
||||
if (flag) {
|
||||
if (handleCopilotClick) {
|
||||
dispatchContent(getString('aidaGenSummary'), false)
|
||||
dispatchContent(getString('aidaGenSummary'), false, true)
|
||||
mutate({
|
||||
head_ref: normalizeGitRef(sourceGitRef),
|
||||
base_ref: normalizeGitRef(targetGitRef)
|
||||
@ -204,13 +270,13 @@ export function MarkdownEditorWithPreview({
|
||||
}
|
||||
setFlag?.(false)
|
||||
}
|
||||
}, [handleCopilotClick])
|
||||
}, [handleCopilotClick]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(data)) {
|
||||
dispatchContent(`${data}`, true)
|
||||
}
|
||||
}, [data])
|
||||
}, [data]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
const onToolbarAction = useCallback((action: ToolbarAction) => {
|
||||
const view = viewRef.current
|
||||
|
||||
@ -406,7 +472,7 @@ export function MarkdownEditorWithPreview({
|
||||
}))
|
||||
)
|
||||
}
|
||||
}, [markdownContent])
|
||||
}, [markdownContent]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
@ -536,7 +602,7 @@ export function MarkdownEditorWithPreview({
|
||||
</Container>
|
||||
<Container className={css.tabContent}>
|
||||
<Editor
|
||||
extensions={[myKeymap, history()]}
|
||||
extensions={[myKeymap, decorationField, history()]}
|
||||
routingId={routingId}
|
||||
standalone={standalone}
|
||||
repoMetadata={repoMetadata}
|
||||
|
@ -1002,8 +1002,8 @@ customMin: '{{minutes}}m'
|
||||
customSecond: '{{seconds}}s'
|
||||
reqChanges: 'Request changes'
|
||||
summary: Summary
|
||||
prGenSummary: Have Harness AIDA summarize the code changes in this pull request
|
||||
aidaGenSummary: '[AIDA is generating a summary...]'
|
||||
prGenSummary: AIDA generate PR summary, insert at the cursor or replace selected text only.
|
||||
aidaGenSummary: 'AIDA is generating a summary...'
|
||||
importFailed: Import Failed
|
||||
uploadAFileError: There is no image or video uploaded. Please upload an image or video.
|
||||
securitySettings:
|
||||
|
1
web/src/icons/spinner.svg
Normal file
1
web/src/icons/spinner.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="15" height="15" version="1.0" viewBox="0 0 128 128"><g><path fill="#592CD7" d="M122.5 69.25H96.47a33.1 33.1 0 0 0 0-10.5h26.05a5.25 5.25 0 0 1 0 10.5z"/><path fill="#3E1E96" fill-opacity=".3" d="M112.04 97.83 89.47 84.8a33.1 33.1 0 0 0 5.25-9.1l22.57 13.03a5.25 5.25 0 0 1-5.28 9.1zM88.68 117.35 75.65 94.78a33.1 33.1 0 0 0 9.1-5.25l13.02 22.57a5.25 5.25 0 1 1-9.1 5.25zM58.7 122.57V96.5a33.1 33.1 0 0 0 10.5 0v26.07a5.25 5.25 0 0 1-10.5 0M30.1 112.1l13.04-22.57a33.1 33.1 0 0 0 9.1 5.25L39.2 117.35a5.25 5.25 0 1 1-9.1-5.25M10.6 88.74 33.16 75.7a33.1 33.1 0 0 0 5.25 9.1L15.88 97.83a5.25 5.25 0 1 1-5.25-9.1z"/><path fill="#4723AC" fill-opacity=".4" d="M5.37 58.75h26.06a33.1 33.1 0 0 0 0 10.5H5.37a5.25 5.25 0 0 1 0-10.5"/><path fill="#4B25B6" fill-opacity=".5" d="M15.85 30.17 38.4 43.2a33.1 33.1 0 0 0-5.24 9.1L10.6 39.25a5.25 5.25 0 1 1 5.25-9.1z"/><path fill="#5027C1" fill-opacity=".6" d="m39.2 10.65 13.03 22.57a33.1 33.1 0 0 0-9.1 5.25l-13-22.57a5.25 5.25 0 1 1 9.1-5.25z"/><path fill="#5429CC" fill-opacity=".7" d="M69.2 5.43V31.5a33.1 33.1 0 0 0-10.5 0V5.42a5.25 5.25 0 1 1 10.5 0z"/><path fill="#5D2EE1" fill-opacity=".8" d="M97.77 15.9 84.75 38.47a33.1 33.1 0 0 0-9.1-5.25l13.03-22.57a5.25 5.25 0 1 1 9.1 5.25z"/><path fill="#6130EC" fill-opacity=".9" d="M117.3 39.26 94.7 52.3a33.1 33.1 0 0 0-5.25-9.1l22.57-13.03a5.25 5.25 0 0 1 5.25 9.1z"/><animateTransform attributeName="transform" calcMode="discrete" dur="1200ms" repeatCount="indefinite" type="rotate" values="0 64 64;30 64 64;60 64 64;90 64 64;120 64 64;150 64 64;180 64 64;210 64 64;240 64 64;270 64 64;300 64 64;330 64 64"/></g></svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -120,3 +120,11 @@
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.aidaIcon {
|
||||
:global(> svg) {
|
||||
border-radius: 4px !important;
|
||||
background: linear-gradient(317deg, #5e2ce0 11.36%, #a652f9 79.62%) !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const aidaIcon: string
|
||||
export declare const changesContainer: string
|
||||
export declare const generalTab: string
|
||||
export declare const hyperlink: string
|
||||
|
@ -66,7 +66,7 @@ import css from './Compare.module.scss'
|
||||
export default function Compare() {
|
||||
const { routes, standalone, hooks, routingId } = useAppContext()
|
||||
const [flag, setFlag] = useState(false)
|
||||
const { SEMANTIC_SEARCH_ENABLED: isSemanticSearchFFEnabled } = hooks?.useFeatureFlags()
|
||||
const { SEMANTIC_SEARCH_ENABLED } = hooks?.useFeatureFlags()
|
||||
const { getString } = useStrings()
|
||||
const history = useHistory()
|
||||
const { repoMetadata, error, loading, diffRefs } = useGetRepositoryMetadata()
|
||||
@ -304,13 +304,18 @@ export default function Compare() {
|
||||
outlets={{
|
||||
[CommentBoxOutletPosition.START_OF_MARKDOWN_EDITOR_TOOLBAR]: (
|
||||
<>
|
||||
{isSemanticSearchFFEnabled && !standalone ? (
|
||||
{SEMANTIC_SEARCH_ENABLED && !standalone ? (
|
||||
<Button
|
||||
size={ButtonSize.SMALL}
|
||||
variation={ButtonVariation.ICON}
|
||||
icon={'harness-copilot'}
|
||||
withoutCurrentColor
|
||||
iconProps={{ color: Color.AI_PURPLE_800, size: 16 }}
|
||||
iconProps={{
|
||||
color: Color.GREY_0,
|
||||
size: 22,
|
||||
className: css.aidaIcon
|
||||
}}
|
||||
className={css.aidaIcon}
|
||||
onClick={handleCopilotClick}
|
||||
tooltip={
|
||||
<Container padding={'small'} width={270}>
|
||||
|
@ -155,6 +155,7 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
const dataArr = groupedData[key]
|
||||
if (groupedData && dataArr) {
|
||||
const { minCreated, maxUpdated } = dataArr.reduce(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(acc: any, item: TypesCheck) => ({
|
||||
minCreated: item.created && item.created < acc.minCreated ? item.created : acc.minCreated,
|
||||
maxUpdated: item.updated && item.updated > acc.maxUpdated ? item.updated : acc.maxUpdated
|
||||
|
@ -274,3 +274,11 @@
|
||||
.rightTextPadding {
|
||||
padding-right: var(--spacing-xsmall);
|
||||
}
|
||||
|
||||
.aidaIcon {
|
||||
:global(> svg) {
|
||||
border-radius: 4px !important;
|
||||
background: linear-gradient(317deg, #5e2ce0 11.36%, #a652f9 79.62%) !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const aidaIcon: string
|
||||
export declare const ascContainer: string
|
||||
export declare const box: string
|
||||
export declare const bp3ButtonText: string
|
||||
|
@ -14,18 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container, useToaster } from '@harnessio/uicore'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Button, ButtonSize, ButtonVariation, Container, Layout, useToaster, Text } from '@harnessio/uicore'
|
||||
import cx from 'classnames'
|
||||
import { useMutate } from 'restful-react'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import { PopoverPosition } from '@blueprintjs/core'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { OpenapiUpdatePullReqRequest } from 'services/code'
|
||||
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
|
||||
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
|
||||
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import { CommentBoxOutletPosition, getErrorMessage } from 'utils/Utils'
|
||||
import Config from 'Config'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import type { ConversationProps } from './Conversation'
|
||||
import css from './Conversation.module.scss'
|
||||
|
||||
@ -41,6 +44,10 @@ export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
|
||||
standalone,
|
||||
routingId
|
||||
}) => {
|
||||
const { hooks } = useAppContext()
|
||||
|
||||
const [flag, setFlag] = useState(false)
|
||||
const { SEMANTIC_SEARCH_ENABLED } = hooks?.useFeatureFlags()
|
||||
const [edit, setEdit] = useState(false)
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const [originalContent, setOriginalContent] = useState(pullReqMetadata.description as string)
|
||||
@ -61,6 +68,11 @@ export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
|
||||
}
|
||||
}, [pullReqMetadata?.description, pullReqMetadata?.description?.length])
|
||||
|
||||
// write the above function handleCopilotClick in a callback
|
||||
const handleCopilotClick = useCallback(() => {
|
||||
setFlag(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container className={cx({ [css.box]: !edit, [css.desc]: !edit })}>
|
||||
<Container padding={!edit ? { left: 'small', bottom: 'small' } : undefined}>
|
||||
@ -70,6 +82,42 @@ export const DescriptionBox: React.FC<DescriptionBoxProps> = ({
|
||||
standalone={standalone}
|
||||
repoMetadata={repoMetadata}
|
||||
value={content}
|
||||
flag={flag}
|
||||
setFlag={setFlag}
|
||||
outlets={{
|
||||
[CommentBoxOutletPosition.START_OF_MARKDOWN_EDITOR_TOOLBAR]: (
|
||||
<>
|
||||
{SEMANTIC_SEARCH_ENABLED && !standalone ? (
|
||||
<Button
|
||||
size={ButtonSize.SMALL}
|
||||
variation={ButtonVariation.ICON}
|
||||
icon={'harness-copilot'}
|
||||
withoutCurrentColor
|
||||
iconProps={{
|
||||
color: Color.GREY_0,
|
||||
size: 22,
|
||||
className: css.aidaIcon
|
||||
}}
|
||||
className={css.aidaIcon}
|
||||
onClick={handleCopilotClick}
|
||||
tooltip={
|
||||
<Container padding={'small'} width={270}>
|
||||
<Layout.Vertical flex={{ align: 'center-center' }}>
|
||||
<Text font={{ variation: FontVariation.BODY }}>{getString('prGenSummary')}</Text>
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
}
|
||||
tooltipProps={{
|
||||
interactionKind: 'hover',
|
||||
usePortal: true,
|
||||
position: PopoverPosition.BOTTOM_LEFT,
|
||||
popoverClassName: cx(css.popover)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
onSave={value => {
|
||||
if (value?.split('\n').some(line => line.length > Config.MAX_TEXT_LINE_SIZE_LIMIT)) {
|
||||
return showError(getString('pr.descHasTooLongLine', { max: Config.MAX_TEXT_LINE_SIZE_LIMIT }), 0)
|
||||
|
@ -20,6 +20,7 @@ import moment from 'moment'
|
||||
import langMap from 'lang-map'
|
||||
import type { EditorDidMount } from 'react-monaco-editor'
|
||||
import type { editor } from 'monaco-editor'
|
||||
import type { EditorView } from '@codemirror/view'
|
||||
import type { EnumMergeMethod, TypesRuleViolations, TypesViolation, TypesCodeOwnerEvaluationEntry } from 'services/code'
|
||||
import type { GitInfoProps } from './GitUtils'
|
||||
|
||||
@ -650,3 +651,31 @@ export const PAGE_CONTAINER_WIDTH = '--page-container-width'
|
||||
export enum CommentBoxOutletPosition {
|
||||
START_OF_MARKDOWN_EDITOR_TOOLBAR = 'start_of_markdown_editor_toolbar'
|
||||
}
|
||||
|
||||
// Helper function to escape special characters for use in regex
|
||||
export function escapeRegExp(str: string) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
export function removeSpecificTextOptimized(
|
||||
viewRef: React.MutableRefObject<EditorView | undefined>,
|
||||
textToRemove: string
|
||||
) {
|
||||
const doc = viewRef?.current?.state.doc.toString()
|
||||
const regex = new RegExp(escapeRegExp(textToRemove), 'g')
|
||||
let match
|
||||
|
||||
// Initialize an array to hold all changes
|
||||
const changes = []
|
||||
|
||||
// Use regex to find all occurrences of the text
|
||||
while (doc && (match = regex.exec(doc)) !== null) {
|
||||
// Add a change object for each match to remove it
|
||||
changes.push({ from: match.index, to: match.index + match[0].length })
|
||||
}
|
||||
|
||||
// Dispatch a single transaction with all changes if any matches were found
|
||||
if (changes.length > 0) {
|
||||
viewRef?.current?.dispatch({ changes })
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user