feat: adding branch persistence (#36622)
## Description Persisting latest branch. Re-opening the branch opens it in the latest branch Fixes https://github.com/appsmithorg/appsmith/issues/30321 ## 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/11325191494> > Commit: 570c6fba8b85f59c8577efda5863d19049b0ebe1 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11325191494&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Git` > Spec: > <hr>Mon, 14 Oct 2024 10:27:07 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** - Enhanced Git branch management functionality in the ApplicationCard component. - New utility functions for storing and retrieving the latest Git branch. - Improved error handling and response management for Git operations. - Added user-specific tracking for Git branches in the AppEditorEngine. - Introduced a new feature flag for Git branch persistence. - New Cypress test suite to verify Git branch persistence after switching branches. - Added methods for asserting branch visibility and URL consistency in the GitSync class. - **Bug Fixes** - Refined error handling for various sagas, ensuring accurate error dispatching and user feedback. - **Documentation** - Updated function signatures and error handling descriptions in the initialization and Git synchronization processes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
97f2560170
commit
f572d6dda4
|
|
@ -0,0 +1,45 @@
|
|||
import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags";
|
||||
import {
|
||||
agHelper,
|
||||
gitSync,
|
||||
homePage,
|
||||
} from "../../../../support/Objects/ObjectsCore";
|
||||
|
||||
let wsName: string;
|
||||
let appName: string;
|
||||
let repoName: string;
|
||||
|
||||
describe(
|
||||
"Git Persist Branch",
|
||||
{
|
||||
tags: ["@tag.Git", "@tag.GitPersistBranch"],
|
||||
},
|
||||
function () {
|
||||
before(() => {
|
||||
agHelper.GenerateUUID();
|
||||
cy.get("@guid").then((uid) => {
|
||||
wsName = "GitPB-" + uid;
|
||||
appName = "GitPB1-" + uid;
|
||||
homePage.CreateNewWorkspace(wsName, true);
|
||||
homePage.CreateAppInWorkspace(wsName, appName);
|
||||
gitSync.CreateNConnectToGit("test-git-perssit-branch", true, true);
|
||||
cy.get("@gitRepoName").then((resRepoName) => {
|
||||
repoName = resRepoName.toString();
|
||||
homePage.NavigateToHome();
|
||||
});
|
||||
});
|
||||
});
|
||||
it("Check if branch persist after changing branch and exiting the app", function () {
|
||||
featureFlagIntercept({ release_git_persist_branch_enabled: true }, true);
|
||||
homePage.EditAppFromAppHover(appName);
|
||||
gitSync.CreateGitBranch("b1", false);
|
||||
cy.get("@gitbranchName").then((resBranchName) => {
|
||||
const branchName = resBranchName.toString();
|
||||
homePage.NavigateToHome();
|
||||
homePage.EditAppFromAppHover(appName);
|
||||
gitSync.AssertBranchName(branchName);
|
||||
gitSync.AssertBranchNameInUrl(branchName);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { featureFlagIntercept } from "../../../../../support/Objects/FeatureFlags";
|
||||
import * as _ from "../../../../../support/Objects/ObjectsCore";
|
||||
import EditorNavigation, {
|
||||
EntityType,
|
||||
|
|
|
|||
|
|
@ -453,4 +453,16 @@ export class GitSync {
|
|||
}
|
||||
this.CloseGitSyncModal();
|
||||
}
|
||||
|
||||
public AssertBranchName(branch: string) {
|
||||
this.agHelper.AssertElementVisibility(this._branchButton);
|
||||
this.agHelper.AssertContains(branch);
|
||||
}
|
||||
|
||||
public AssertBranchNameInUrl(branch: string) {
|
||||
cy.location("search")
|
||||
.then((searchParams) => new URLSearchParams(searchParams))
|
||||
.invoke("get", "branch")
|
||||
.should("equal", branch);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export const FEATURE_FLAG = {
|
|||
rollout_side_by_side_enabled: "rollout_side_by_side_enabled",
|
||||
release_layout_conversion_enabled: "release_layout_conversion_enabled",
|
||||
release_anvil_toggle_enabled: "release_anvil_toggle_enabled",
|
||||
release_git_persist_branch_enabled: "release_git_persist_branch_enabled",
|
||||
release_ide_animations_enabled: "release_ide_animations_enabled",
|
||||
} as const;
|
||||
|
||||
|
|
@ -71,6 +72,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
|
|||
rollout_side_by_side_enabled: false,
|
||||
release_layout_conversion_enabled: false,
|
||||
release_anvil_toggle_enabled: false,
|
||||
release_git_persist_branch_enabled: false,
|
||||
release_ide_animations_enabled: false,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ import {
|
|||
reportSWStatus,
|
||||
waitForWidgetConfigBuild,
|
||||
} from "sagas/InitSagas";
|
||||
import { getCurrentGitBranch } from "selectors/gitSyncSelectors";
|
||||
import {
|
||||
getCurrentGitBranch,
|
||||
isGitPersistBranchEnabledSelector,
|
||||
} from "selectors/gitSyncSelectors";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import history from "utils/history";
|
||||
import type { AppEnginePayload } from ".";
|
||||
|
|
@ -50,7 +53,7 @@ import {
|
|||
} from "ee/sagas/userSagas";
|
||||
import { getFirstTimeUserOnboardingComplete } from "selectors/onboardingSelectors";
|
||||
import { isAirgapped } from "ee/utils/airgapHelpers";
|
||||
import { getAIPromptTriggered } from "utils/storage";
|
||||
import { getAIPromptTriggered, setLatestGitBranchInLocal } from "utils/storage";
|
||||
import { trackOpenEditorTabs } from "../../utils/editor/browserTabsTracking";
|
||||
import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import { waitForFetchEnvironments } from "ee/sagas/EnvironmentSagas";
|
||||
|
|
@ -68,6 +71,9 @@ import {
|
|||
import { getCurrentApplication } from "ee/selectors/applicationSelectors";
|
||||
import type { Span } from "@opentelemetry/api";
|
||||
import { endSpan, startNestedSpan } from "UITelemetry/generateTraces";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import type { User } from "constants/userConstants";
|
||||
import log from "loglevel";
|
||||
|
||||
export default class AppEditorEngine extends AppEngine {
|
||||
constructor(mode: APP_MODE) {
|
||||
|
|
@ -269,6 +275,27 @@ export default class AppEditorEngine extends AppEngine {
|
|||
getCurrentApplication,
|
||||
);
|
||||
|
||||
const isGitPersistBranchEnabled: boolean = yield select(
|
||||
isGitPersistBranchEnabledSelector,
|
||||
);
|
||||
|
||||
if (isGitPersistBranchEnabled) {
|
||||
const currentUser: User = yield select(getCurrentUser);
|
||||
const currentBranch: string = yield select(getCurrentGitBranch);
|
||||
|
||||
if (currentUser?.email && currentApplication?.baseId && currentBranch) {
|
||||
yield setLatestGitBranchInLocal(
|
||||
currentUser.email,
|
||||
currentApplication.baseId,
|
||||
currentBranch,
|
||||
);
|
||||
} else {
|
||||
log.error(
|
||||
`There was an error setting the latest git branch in local - userEmail: ${!!currentUser?.email}, applicationId: ${currentApplication?.baseId}, branch: ${currentBranch}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const [isAnotherEditorTabOpen, currentTabs] = yield call(
|
||||
trackOpenEditorTabs,
|
||||
currentApplication.id,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ import { getCurrentUser } from "actions/authActions";
|
|||
import Card, { ContextMenuTrigger } from "components/common/Card";
|
||||
import { generateEditedByText } from "./helpers";
|
||||
import { noop } from "lodash";
|
||||
import { getLatestGitBranchFromLocal } from "utils/storage";
|
||||
import { getCurrentUser as getCurrentUserSelector } from "selectors/usersSelectors";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
|
||||
interface ApplicationCardProps {
|
||||
application: ApplicationPayload;
|
||||
|
|
@ -98,6 +102,7 @@ export function ApplicationCard(props: ApplicationCardProps) {
|
|||
const theme = useContext(ThemeContext);
|
||||
const isSavingName = useSelector(getIsSavingAppName);
|
||||
const isErroredSavingName = useSelector(getIsErroredSavingAppName);
|
||||
const currentUser = useSelector(getCurrentUserSelector);
|
||||
const initialsAndColorCode = getInitialsAndColorCode(
|
||||
props.application.name,
|
||||
theme.colors.appCardColors,
|
||||
|
|
@ -116,7 +121,40 @@ export function ApplicationCard(props: ApplicationCardProps) {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const applicationId = props.application?.id;
|
||||
const baseApplicationId = props.application?.baseId;
|
||||
const showGitBadge = props.application?.gitApplicationMetadata?.branchName;
|
||||
const [editorParams, setEditorParams] = useState({});
|
||||
const isGitPersistBranchEnabled = useFeatureFlag(
|
||||
FEATURE_FLAG.release_git_persist_branch_enabled,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const storedLatestBranch = await getLatestGitBranchFromLocal(
|
||||
currentUser?.email ?? "",
|
||||
baseApplicationId,
|
||||
);
|
||||
|
||||
if (isGitPersistBranchEnabled && storedLatestBranch) {
|
||||
setEditorParams({ branch: storedLatestBranch });
|
||||
} else if (showGitBadge) {
|
||||
setEditorParams({ branch: showGitBadge });
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
baseApplicationId,
|
||||
currentUser?.email,
|
||||
showGitBadge,
|
||||
isGitPersistBranchEnabled,
|
||||
]);
|
||||
|
||||
const viewerParams = useMemo(() => {
|
||||
if (showGitBadge) {
|
||||
return { branch: showGitBadge };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}, [showGitBadge]);
|
||||
|
||||
useEffect(() => {
|
||||
let colorCode;
|
||||
|
|
@ -273,15 +311,6 @@ export function ApplicationCard(props: ApplicationCardProps) {
|
|||
initials += props.application.name[1].toUpperCase() || "";
|
||||
}
|
||||
|
||||
// should show correct branch of application when edit mode
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const params: any = {};
|
||||
|
||||
if (showGitBadge) {
|
||||
params.branch = showGitBadge;
|
||||
}
|
||||
|
||||
const handleMenuOnClose = (open: boolean) => {
|
||||
if (!open && !isDeleting) {
|
||||
setIsMenuOpen(false);
|
||||
|
|
@ -437,16 +466,16 @@ export function ApplicationCard(props: ApplicationCardProps) {
|
|||
|
||||
if (!basePageId) return "";
|
||||
|
||||
return builderURL({ basePageId, params });
|
||||
}, [props.application.defaultBasePageId, params]);
|
||||
return builderURL({ basePageId, params: editorParams });
|
||||
}, [props.application.defaultBasePageId, editorParams]);
|
||||
|
||||
const viewModeURL = useMemo(() => {
|
||||
const basePageId = props.application.defaultBasePageId;
|
||||
|
||||
if (!basePageId) return "";
|
||||
|
||||
return viewerURL({ basePageId, params });
|
||||
}, [props.application.defaultBasePageId, params]);
|
||||
return viewerURL({ basePageId, params: viewerParams });
|
||||
}, [props.application.defaultBasePageId, viewerParams]);
|
||||
|
||||
const launchApp = useCallback(() => {
|
||||
setURLParams();
|
||||
|
|
@ -463,11 +492,11 @@ export function ApplicationCard(props: ApplicationCardProps) {
|
|||
history.push(
|
||||
viewerURL({
|
||||
basePageId: props.application.defaultBasePageId,
|
||||
params,
|
||||
params: viewerParams,
|
||||
}),
|
||||
);
|
||||
dispatch(getCurrentUser());
|
||||
}, [props.application.defaultPageId]);
|
||||
}, [dispatch, props.application.defaultBasePageId, viewerParams]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
|
|
|||
|
|
@ -219,7 +219,6 @@ export function* getInitResponses({
|
|||
}: {
|
||||
applicationId?: string;
|
||||
basePageId?: string;
|
||||
branch?: string;
|
||||
mode?: APP_MODE;
|
||||
shouldInitialiseUserDetails?: boolean;
|
||||
// TODO: Fix this the next time the file is edited
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
getCurrentApplication,
|
||||
} from "ee/selectors/applicationSelectors";
|
||||
import type { Branch } from "entities/GitSync";
|
||||
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
|
||||
|
||||
export const getGitSyncState = (state: AppState): GitSyncReducerState =>
|
||||
state.ui.gitSync;
|
||||
|
|
@ -280,3 +281,8 @@ export const isGitSettingsModalOpenSelector = (state: AppState) =>
|
|||
|
||||
export const activeGitSettingsModalTabSelector = (state: AppState) =>
|
||||
state.ui.gitSync.activeGitSettingsModalTab;
|
||||
|
||||
export const isGitPersistBranchEnabledSelector = createSelector(
|
||||
selectFeatureFlags,
|
||||
(featureFlags) => featureFlags.release_git_persist_branch_enabled ?? false,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export const STORAGE_KEYS: {
|
|||
CODE_WIDGET_NAVIGATION_USED: "CODE_WIDGET_NAVIGATION_USED",
|
||||
OVERRIDDEN_FEATURE_FLAGS: "OVERRIDDEN_FEATURE_FLAGS",
|
||||
ACTION_TEST_PAYLOAD: "ACTION_TEST_PAYLOAD",
|
||||
LATEST_GIT_BRANCH: "LATEST_GIT_BRANCH",
|
||||
};
|
||||
|
||||
const store = localforage.createInstance({
|
||||
|
|
@ -1087,3 +1088,52 @@ export const storeActionTestPayload = async (payload: {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const setLatestGitBranchInLocal = async (
|
||||
userEmail: string,
|
||||
baseApplicationId: string,
|
||||
branch: string,
|
||||
) => {
|
||||
try {
|
||||
const storedBranches: Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
> = (await store.getItem(STORAGE_KEYS.LATEST_GIT_BRANCH)) ?? {};
|
||||
const userBranches = storedBranches?.[userEmail] ?? {};
|
||||
const newBranches = {
|
||||
...(storedBranches ?? {}),
|
||||
[userEmail]: {
|
||||
...userBranches,
|
||||
[baseApplicationId]: branch,
|
||||
},
|
||||
};
|
||||
|
||||
await store.setItem(STORAGE_KEYS.LATEST_GIT_BRANCH, newBranches);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("An error occurred while setting LATEST_GIT_BRANCH");
|
||||
log.error(error);
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLatestGitBranchFromLocal = async (
|
||||
userEmail: string,
|
||||
baseApplicationId: string,
|
||||
) => {
|
||||
try {
|
||||
const storedBranches: Record<string, Record<string, string>> | null =
|
||||
await store.getItem(STORAGE_KEYS.LATEST_GIT_BRANCH);
|
||||
const userBranches = storedBranches?.[userEmail] ?? {};
|
||||
const branch = userBranches?.[baseApplicationId] ?? null;
|
||||
|
||||
return branch;
|
||||
} catch (error) {
|
||||
log.error("An error occurred while fetching LATEST_GIT_BRANCH");
|
||||
log.error(error);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user