-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: git mod - adding git ops modal (#38136)
## Description - Adds git modal - Adds pull saga - Adds ability to commit - Adds ability to merge - Adds ability to see status - Adds ability to discard - Improves on saga error handling Fixes #37826 Fixes #37803 Fixes #37804 Fixes #37807 ## Automation /ok-to-test tags="@tag.Git" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/12319147488> > Commit: ec461fd > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12319147488&attempt=2" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Git` > Spec: > <hr>Fri, 13 Dec 2024 17:27:36 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced new components for managing Git operations, including `ConflictErrorView`, `OpsModal`, `TabDeploy`, `TabMerge`, and `StatusChanges`. - Added loading and error handling components such as `StatusLoader` and `DiscardFailedError`. - Enhanced the `StatusTree` component for displaying hierarchical status changes. - **Bug Fixes** - Improved error handling across various sagas and components. - **Refactor** - Renamed several modal toggle functions for clarity and consistency. - Updated prop types for improved type safety in components. - **Chores** - Removed unused component `GitTest`. - Updated imports and exports for better organization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Loading branch information
Showing
79 changed files
with
2,604 additions
and
323 deletions.
There are no files selected for viewing
199 changes: 199 additions & 0 deletions
199
app/client/src/git/artifactHelpers/application/statusTransformer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.types"; | ||
import { objectKeys } from "@appsmith/utils"; | ||
import type { StatusTreeStruct } from "git/components/StatusChanges/StatusTree"; | ||
|
||
const ICON_LOOKUP = { | ||
query: "query", | ||
jsObject: "js", | ||
page: "page-line", | ||
datasource: "database-2-line", | ||
jsLib: "package", | ||
}; | ||
|
||
interface TreeNodeDef { | ||
subject: string; | ||
verb: string; | ||
type: keyof typeof ICON_LOOKUP; | ||
} | ||
|
||
function createTreeNode(nodeDef: TreeNodeDef) { | ||
return { | ||
icon: ICON_LOOKUP[nodeDef.type], | ||
message: `${nodeDef.subject} ${nodeDef.verb}`, | ||
}; | ||
} | ||
|
||
function determineVerbForDefs(defs: TreeNodeDef[]) { | ||
const isRemoved = defs.some((def) => def.verb === "removed"); | ||
const isAdded = defs.some((def) => def.verb === "added"); | ||
const isModified = defs.some((def) => def.verb === "modified"); | ||
|
||
let action = ""; | ||
|
||
if (isRemoved && !isAdded && !isModified) { | ||
action = "removed"; | ||
} else if (isAdded && !isRemoved && !isModified) { | ||
action = "added"; | ||
} else { | ||
action = "modified"; | ||
} | ||
|
||
return action; | ||
} | ||
|
||
function createTreeNodeGroup(nodeDefs: TreeNodeDef[], subject: string) { | ||
return { | ||
icon: ICON_LOOKUP[nodeDefs[0].type], | ||
message: `${nodeDefs.length} ${subject} ${determineVerbForDefs(nodeDefs)}`, | ||
children: nodeDefs.map(createTreeNode), | ||
}; | ||
} | ||
|
||
function statusPageTransformer(status: FetchStatusResponseData) { | ||
const { | ||
jsObjectsAdded, | ||
jsObjectsModified, | ||
jsObjectsRemoved, | ||
pagesAdded, | ||
pagesModified, | ||
pagesRemoved, | ||
queriesAdded, | ||
queriesModified, | ||
queriesRemoved, | ||
} = status; | ||
const pageEntityDefLookup: Record<string, TreeNodeDef[]> = {}; | ||
const addToPageEntityDefLookup = ( | ||
files: string[], | ||
type: keyof typeof ICON_LOOKUP, | ||
verb: string, | ||
) => { | ||
files.forEach((file) => { | ||
const [page, subject] = file.split("/"); | ||
|
||
pageEntityDefLookup[page] ??= []; | ||
pageEntityDefLookup[page].push({ subject, verb, type }); | ||
}); | ||
}; | ||
|
||
addToPageEntityDefLookup(queriesModified, "query", "modified"); | ||
addToPageEntityDefLookup(queriesAdded, "query", "added"); | ||
addToPageEntityDefLookup(queriesRemoved, "query", "removed"); | ||
addToPageEntityDefLookup(jsObjectsModified, "jsObject", "modified"); | ||
addToPageEntityDefLookup(jsObjectsAdded, "jsObject", "added"); | ||
addToPageEntityDefLookup(jsObjectsRemoved, "jsObject", "removed"); | ||
|
||
const pageDefLookup: Record<string, TreeNodeDef> = {}; | ||
const addToPageDefLookup = (pages: string[], verb: string) => { | ||
pages.forEach((page) => { | ||
pageDefLookup[page] = { subject: page, verb, type: "page" }; | ||
}); | ||
}; | ||
|
||
addToPageDefLookup(pagesModified, "modified"); | ||
addToPageDefLookup(pagesAdded, "added"); | ||
addToPageDefLookup(pagesRemoved, "removed"); | ||
|
||
const tree = [] as StatusTreeStruct[]; | ||
|
||
objectKeys(pageEntityDefLookup).forEach((page) => { | ||
const queryDefs = pageEntityDefLookup[page].filter( | ||
(def) => def.type === "query", | ||
); | ||
const jsObjectDefs = pageEntityDefLookup[page].filter( | ||
(def) => def.type === "jsObject", | ||
); | ||
const children = [] as StatusTreeStruct[]; | ||
|
||
if (queryDefs.length > 0) { | ||
const subject = queryDefs.length === 1 ? "query" : "queries"; | ||
|
||
children.push(createTreeNodeGroup(queryDefs, subject)); | ||
} | ||
|
||
if (jsObjectDefs.length > 0) { | ||
const subject = jsObjectDefs.length === 1 ? "jsObject" : "jsObjects"; | ||
|
||
children.push(createTreeNodeGroup(jsObjectDefs, subject)); | ||
} | ||
|
||
let pageDef = pageDefLookup[page]; | ||
|
||
if (!pageDef) { | ||
pageDef = { subject: page, verb: "modified", type: "page" }; | ||
} | ||
|
||
tree.push({ ...createTreeNode(pageDef), children }); | ||
}); | ||
|
||
objectKeys(pageDefLookup).forEach((page) => { | ||
if (!pageEntityDefLookup[page]) { | ||
tree.push(createTreeNode(pageDefLookup[page])); | ||
} | ||
}); | ||
|
||
return tree; | ||
} | ||
|
||
function statusDatasourceTransformer(status: FetchStatusResponseData) { | ||
const { datasourcesAdded, datasourcesModified, datasourcesRemoved } = status; | ||
const defs = [] as TreeNodeDef[]; | ||
|
||
datasourcesModified.forEach((datasource) => { | ||
defs.push({ subject: datasource, verb: "modified", type: "datasource" }); | ||
}); | ||
|
||
datasourcesAdded.forEach((datasource) => { | ||
defs.push({ subject: datasource, verb: "added", type: "datasource" }); | ||
}); | ||
|
||
datasourcesRemoved.forEach((datasource) => { | ||
defs.push({ subject: datasource, verb: "removed", type: "datasource" }); | ||
}); | ||
|
||
const tree = [] as StatusTreeStruct[]; | ||
|
||
if (defs.length > 0) { | ||
tree.push(createTreeNodeGroup(defs, "datasource")); | ||
} | ||
|
||
return tree; | ||
} | ||
|
||
function statusJsLibTransformer(status: FetchStatusResponseData) { | ||
const { jsLibsAdded, jsLibsModified, jsLibsRemoved } = status; | ||
const defs = [] as TreeNodeDef[]; | ||
|
||
jsLibsModified.forEach((jsLib) => { | ||
defs.push({ subject: jsLib, verb: "modified", type: "jsLib" }); | ||
}); | ||
|
||
jsLibsAdded.forEach((jsLib) => { | ||
defs.push({ subject: jsLib, verb: "added", type: "jsLib" }); | ||
}); | ||
|
||
jsLibsRemoved.forEach((jsLib) => { | ||
defs.push({ subject: jsLib, verb: "removed", type: "jsLib" }); | ||
}); | ||
|
||
const tree = [] as StatusTreeStruct[]; | ||
|
||
if (defs.length > 0) { | ||
const subject = defs.length === 1 ? "jsLib" : "jsLibs"; | ||
|
||
tree.push(createTreeNodeGroup(defs, subject)); | ||
} | ||
|
||
return tree; | ||
} | ||
|
||
export default function applicationStatusTransformer( | ||
status: FetchStatusResponseData, | ||
) { | ||
const tree = [ | ||
...statusPageTransformer(status), | ||
...statusDatasourceTransformer(status), | ||
...statusJsLibTransformer(status), | ||
] as StatusTreeStruct[]; | ||
|
||
return tree; | ||
} |
72 changes: 72 additions & 0 deletions
72
app/client/src/git/components/ConflictError/ConflictErrorView.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import React, { useCallback, useMemo } from "react"; | ||
import styled from "styled-components"; | ||
import { | ||
createMessage, | ||
GIT_CONFLICTING_INFO, | ||
LEARN_MORE, | ||
OPEN_REPO, | ||
} from "ee/constants/messages"; | ||
import { Button, Callout } from "@appsmith/ads"; | ||
|
||
const Row = styled.div` | ||
display: flex; | ||
align-items: center; | ||
`; | ||
|
||
const StyledButton = styled(Button)` | ||
margin-right: ${(props) => props.theme.spaces[3]}px; | ||
`; | ||
|
||
const StyledCallout = styled(Callout)` | ||
margin-bottom: 12px; | ||
`; | ||
|
||
const ConflictInfoContainer = styled.div` | ||
margin-top: ${(props) => props.theme.spaces[7]}px; | ||
margin-bottom: ${(props) => props.theme.spaces[7]}px; | ||
`; | ||
|
||
interface ConflictErrorViewProps { | ||
learnMoreUrl: string; | ||
repoUrl: string; | ||
} | ||
|
||
export default function ConflictErrorView({ | ||
learnMoreUrl, | ||
repoUrl, | ||
}: ConflictErrorViewProps) { | ||
const handleClickOnOpenRepo = useCallback(() => { | ||
window.open(repoUrl, "_blank", "noopener,noreferrer"); | ||
}, [repoUrl]); | ||
|
||
const calloutLinks = useMemo( | ||
() => [ | ||
{ | ||
children: createMessage(LEARN_MORE), | ||
to: learnMoreUrl, | ||
}, | ||
], | ||
[learnMoreUrl], | ||
); | ||
|
||
return ( | ||
<ConflictInfoContainer data-testid="t--conflict-info-container"> | ||
<StyledCallout | ||
data-testid="t--conflict-info-error-warning" | ||
kind="error" | ||
links={calloutLinks} | ||
> | ||
{createMessage(GIT_CONFLICTING_INFO)} | ||
</StyledCallout> | ||
<Row> | ||
<StyledButton | ||
data-testid="t--git-repo-button" | ||
kind="secondary" | ||
onClick={handleClickOnOpenRepo} | ||
> | ||
{createMessage(OPEN_REPO)} | ||
</StyledButton> | ||
</Row> | ||
</ConflictInfoContainer> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import React from "react"; | ||
|
||
import { useGitContext } from "../GitContextProvider"; | ||
import GitConflictErrorView from "./ConflictErrorView"; | ||
|
||
export default function ConflictError() { | ||
const { gitMetadata } = useGitContext(); | ||
|
||
// ! case: learnMoreUrl comes from pullError | ||
const learnMoreUrl = | ||
"https://docs.appsmith.com/advanced-concepts/version-control-with-git"; | ||
const repoUrl = gitMetadata?.browserSupportedRemoteUrl || ""; | ||
|
||
return <GitConflictErrorView learnMoreUrl={learnMoreUrl} repoUrl={repoUrl} />; | ||
} |
93 changes: 93 additions & 0 deletions
93
app/client/src/git/components/ConflictErrorModal/ConflictErrorModalView.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import React, { useCallback } from "react"; | ||
import styled from "styled-components"; | ||
import { Overlay, Classes } from "@blueprintjs/core"; | ||
import { | ||
createMessage, | ||
CONFLICTS_FOUND_WHILE_PULLING_CHANGES, | ||
} from "ee/constants/messages"; | ||
|
||
import { Button } from "@appsmith/ads"; | ||
import noop from "lodash/noop"; | ||
import ConflictError from "../ConflictError"; | ||
|
||
const StyledGitErrorPopup = styled.div` | ||
& { | ||
.${Classes.OVERLAY} { | ||
position: fixed; | ||
bottom: 0; | ||
left: 0; | ||
right: 0; | ||
display: flex; | ||
justify-content: center; | ||
.${Classes.OVERLAY_CONTENT} { | ||
overflow: hidden; | ||
bottom: 52px; | ||
left: 12px; | ||
background-color: #ffffff; | ||
} | ||
} | ||
.git-error-popup { | ||
width: 364px; | ||
padding: ${(props) => props.theme.spaces[7]}px; | ||
display: flex; | ||
flex-direction: column; | ||
} | ||
} | ||
`; | ||
|
||
interface ConflictErrorModalViewProps { | ||
isConflictErrorModalOpen?: boolean; | ||
toggleConflictErrorModal?: (open: boolean) => void; | ||
} | ||
|
||
function ConflictErrorModalView({ | ||
isConflictErrorModalOpen = false, | ||
toggleConflictErrorModal = noop, | ||
}: ConflictErrorModalViewProps) { | ||
const handleClose = useCallback(() => { | ||
toggleConflictErrorModal(false); | ||
}, [toggleConflictErrorModal]); | ||
|
||
return ( | ||
<StyledGitErrorPopup> | ||
<Overlay | ||
hasBackdrop | ||
isOpen={isConflictErrorModalOpen} | ||
onClose={handleClose} | ||
transitionDuration={25} | ||
usePortal={false} | ||
> | ||
<div className={Classes.OVERLAY_CONTENT}> | ||
<div className="git-error-popup"> | ||
<div | ||
// ! case: remove inline styles | ||
style={{ | ||
display: "flex", | ||
justifyContent: "space-between", | ||
}} | ||
> | ||
<div style={{ display: "flex", alignItems: "center" }}> | ||
<span className="title"> | ||
{createMessage(CONFLICTS_FOUND_WHILE_PULLING_CHANGES)} | ||
</span> | ||
</div> | ||
<Button | ||
isIconButton | ||
kind="tertiary" | ||
onClick={handleClose} | ||
size="sm" | ||
startIcon="close-modal" | ||
/> | ||
</div> | ||
<ConflictError /> | ||
</div> | ||
</div> | ||
</Overlay> | ||
</StyledGitErrorPopup> | ||
); | ||
} | ||
|
||
export default ConflictErrorModalView; |
14 changes: 14 additions & 0 deletions
14
app/client/src/git/components/ConflictErrorModal/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import React from "react"; | ||
import ConflictErrorModalView from "./ConflictErrorModalView"; | ||
import { useGitContext } from "../GitContextProvider"; | ||
|
||
export default function ConflictErrorModal() { | ||
const { conflictErrorModalOpen, toggleConflictErrorModal } = useGitContext(); | ||
|
||
return ( | ||
<ConflictErrorModalView | ||
isConflictErrorModalOpen={conflictErrorModalOpen} | ||
toggleConflictErrorModal={toggleConflictErrorModal} | ||
/> | ||
); | ||
} |
Oops, something went wrong.