Skip to content

Commit

Permalink
chore: git mod - adding git ops modal (#38136)
Browse files Browse the repository at this point in the history
## 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
brayn003 authored Dec 13, 2024
1 parent 813279b commit e2916b2
Show file tree
Hide file tree
Showing 79 changed files with 2,604 additions and 323 deletions.
199 changes: 199 additions & 0 deletions app/client/src/git/artifactHelpers/application/statusTransformer.ts
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 app/client/src/git/components/ConflictError/ConflictErrorView.tsx
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>
);
}
15 changes: 15 additions & 0 deletions app/client/src/git/components/ConflictError/index.tsx
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} />;
}
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 app/client/src/git/components/ConflictErrorModal/index.tsx
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}
/>
);
}
Loading

0 comments on commit e2916b2

Please sign in to comment.