mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Merge branch 'main' into rebase/lts-main-5-mgs
This commit is contained in:
commit
eaa9635eac
149 changed files with 13347 additions and 8666 deletions
6
.github/workflows/merging-pr.yml
vendored
6
.github/workflows/merging-pr.yml
vendored
|
|
@ -8,7 +8,8 @@ jobs:
|
|||
merge-submodules:
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
(github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'lts-3.16')
|
||||
(github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'lts-3.16') &&
|
||||
!startsWith(github.event.pull_request.head.ref, 'release/')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
@ -46,7 +47,8 @@ jobs:
|
|||
needs: merge-submodules
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
(github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'lts-3.16')
|
||||
(github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'lts-3.16') &&
|
||||
!startsWith(github.event.pull_request.head.ref, 'release/')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
|
|||
395
.github/workflows/release-automation.yml
vendored
Normal file
395
.github/workflows/release-automation.yml
vendored
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
name: Release Automation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
# Uncomment the line below to also trigger on lts-3.16 branch
|
||||
# branches: [main, lts-3.16]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
jobs:
|
||||
release-automation:
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
startsWith(github.event.pull_request.head.ref, 'release/') &&
|
||||
contains(github.event.pull_request.title, '|') &&
|
||||
contains(github.event.pull_request.title, '[') &&
|
||||
contains(github.event.pull_request.title, ']')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.extract-version.outputs.version }}
|
||||
tag: ${{ steps.create-tag.outputs.tag }}
|
||||
release_notes: ${{ steps.generate-notes.outputs.release_notes }}
|
||||
|
||||
steps:
|
||||
- name: Extract Branch Name and Base Branch
|
||||
run: |
|
||||
echo "BRANCH_NAME=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV
|
||||
echo "BASE_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Merge PR in ee-server (if exists)
|
||||
run: |
|
||||
PR=$(gh pr list -R ToolJet/ee-server --head "$BRANCH_NAME" --base "$BASE_BRANCH" --state open --json number -q '.[0].number')
|
||||
if [ -n "$PR" ]; then
|
||||
echo "Found ee-server PR: #$PR targeting $BASE_BRANCH"
|
||||
gh pr merge -R ToolJet/ee-server "$PR" --merge --admin
|
||||
else
|
||||
echo "No open ee-server PR for branch $BRANCH_NAME targeting $BASE_BRANCH"
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Merge PR in ee-frontend (if exists)
|
||||
run: |
|
||||
PR=$(gh pr list -R ToolJet/ee-frontend --head "$BRANCH_NAME" --base "$BASE_BRANCH" --state open --json number -q '.[0].number')
|
||||
if [ -n "$PR" ]; then
|
||||
echo "Found ee-frontend PR: #$PR targeting $BASE_BRANCH"
|
||||
gh pr merge -R ToolJet/ee-frontend "$PR" --merge --admin
|
||||
else
|
||||
echo "No open ee-frontend PR for branch $BRANCH_NAME targeting $BASE_BRANCH"
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Wait for submodule merges to complete
|
||||
run: |
|
||||
echo "Waiting for submodule merges to complete..."
|
||||
sleep 30
|
||||
|
||||
- name: Extract Version from PR Title
|
||||
id: extract-version
|
||||
run: |
|
||||
TITLE="${{ github.event.pull_request.title }}"
|
||||
echo "PR Title: $TITLE"
|
||||
|
||||
# Extract the complete tag from text after | character
|
||||
if [[ "$TITLE" =~ \|.*\[([^]]+)\] ]]; then
|
||||
TAG="${BASH_REMATCH[1]}"
|
||||
echo "Extracted tag: $TAG"
|
||||
echo "version=$TAG" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ No valid tag format found in PR title"
|
||||
echo "Expected format: 'some text | [tag]'"
|
||||
echo "Examples: 'platform release | [v3.16-beta.1]' or 'hotfix | [v3.16.18-lts]'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout Repository
|
||||
if: steps.extract-version.outputs.version != ''
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "adish.madhu@gmail.com"
|
||||
|
||||
- name: Create Version Bump Branch
|
||||
id: create-branch
|
||||
run: |
|
||||
TAG="${{ steps.extract-version.outputs.version }}"
|
||||
# Create branch name using tag without special characters
|
||||
CLEAN_TAG=$(echo "$TAG" | sed 's/[^a-zA-Z0-9.-]//g')
|
||||
BRANCH_NAME="version-bump-$CLEAN_TAG-$(date +%s)"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
|
||||
- name: Update Version Files
|
||||
run: |
|
||||
TAG="${{ steps.extract-version.outputs.version }}"
|
||||
# Remove 'v' prefix for .version files if present
|
||||
VERSION="${TAG#v}"
|
||||
|
||||
# Update all three .version files
|
||||
echo "$VERSION" > .version
|
||||
echo "$VERSION" > server/.version
|
||||
echo "$VERSION" > frontend/.version
|
||||
|
||||
echo "Updated all .version files to: $VERSION (from tag: $TAG)"
|
||||
echo "Files updated:"
|
||||
echo " - ./.version"
|
||||
echo " - ./server/.version"
|
||||
echo " - ./frontend/.version"
|
||||
|
||||
|
||||
- name: Update Submodules to Latest
|
||||
run: |
|
||||
echo "Updating submodules to latest commits..."
|
||||
|
||||
# Determine which branch to use for submodules
|
||||
BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
|
||||
# For main branch, use main in submodules
|
||||
# For lts-3.16 branch, use lts-3.16 in submodules (when enabled)
|
||||
if [ "$BASE_BRANCH" = "lts-3.16" ]; then
|
||||
SUBMODULE_BRANCH="lts-3.16"
|
||||
else
|
||||
SUBMODULE_BRANCH="main"
|
||||
fi
|
||||
|
||||
echo "Base branch: $BASE_BRANCH"
|
||||
echo "Using submodule branch: $SUBMODULE_BRANCH"
|
||||
|
||||
# Get latest commit SHAs from submodule repositories
|
||||
echo "Fetching latest commit from ToolJet/ee-frontend:$SUBMODULE_BRANCH"
|
||||
FRONTEND_SHA=$(gh api repos/ToolJet/ee-frontend/branches/$SUBMODULE_BRANCH --jq '.commit.sha' || gh api repos/ToolJet/ee-frontend/branches/main --jq '.commit.sha')
|
||||
echo "Frontend SHA: $FRONTEND_SHA"
|
||||
|
||||
echo "Fetching latest commit from ToolJet/ee-server:$SUBMODULE_BRANCH"
|
||||
SERVER_SHA=$(gh api repos/ToolJet/ee-server/branches/$SUBMODULE_BRANCH --jq '.commit.sha' || gh api repos/ToolJet/ee-server/branches/main --jq '.commit.sha')
|
||||
echo "Server SHA: $SERVER_SHA"
|
||||
|
||||
# Update submodule pointers to specific commit SHAs
|
||||
cd frontend/ee
|
||||
git fetch origin
|
||||
git checkout "$FRONTEND_SHA"
|
||||
cd ../..
|
||||
|
||||
cd server/ee
|
||||
git fetch origin
|
||||
git checkout "$SERVER_SHA"
|
||||
cd ../..
|
||||
|
||||
# Check if there are any changes
|
||||
if ! git diff --quiet .version server/.version frontend/.version frontend/ee server/ee; then
|
||||
echo "Changes detected in version files and/or submodules"
|
||||
else
|
||||
echo "No changes detected"
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Commit Changes
|
||||
run: |
|
||||
TAG="${{ steps.extract-version.outputs.version }}"
|
||||
VERSION="${TAG#v}" # Remove 'v' prefix for display
|
||||
git add .version server/.version frontend/.version frontend/ee server/ee
|
||||
|
||||
# Only commit if there are changes
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "Version bump to $TAG
|
||||
|
||||
- Update .version files to $VERSION
|
||||
- Update submodules to latest commit SHA
|
||||
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
|
||||
- name: Create Version Bump PR
|
||||
id: create-pr
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ steps.create-branch.outputs.branch_name }}
|
||||
title: "Version bump to ${{ steps.extract-version.outputs.version }}"
|
||||
body: |
|
||||
## Version Bump to ${{ steps.extract-version.outputs.version }}
|
||||
|
||||
This PR updates the version files and submodules for the release version ${{ steps.extract-version.outputs.version }}.
|
||||
|
||||
### Changes:
|
||||
- ✅ Updated `.version` files (root, server, frontend) to `${{ steps.extract-version.outputs.version }}`
|
||||
- ✅ Updated submodules to latest commits
|
||||
|
||||
### Auto-generated
|
||||
This PR was automatically created by the release automation workflow.
|
||||
|
||||
base: ${{ github.event.pull_request.base.ref }}
|
||||
# When lts-3.16 is enabled, this will use the correct base branch
|
||||
|
||||
- name: Auto-merge Version Bump PR
|
||||
if: steps.create-pr.outputs.pull-request-number != ''
|
||||
run: |
|
||||
PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}"
|
||||
echo "Auto-merging version bump PR #$PR_NUMBER"
|
||||
|
||||
# Wait a moment for PR to be fully created
|
||||
sleep 10
|
||||
|
||||
# Merge the PR
|
||||
gh pr merge "$PR_NUMBER" --squash --admin
|
||||
|
||||
# Wait for merge to complete
|
||||
sleep 10
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout Updated Base Branch
|
||||
run: |
|
||||
BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
git fetch origin "$BASE_BRANCH"
|
||||
git checkout "$BASE_BRANCH"
|
||||
git pull origin "$BASE_BRANCH"
|
||||
|
||||
- name: Create Git Tag
|
||||
id: create-tag
|
||||
run: |
|
||||
TAG="${{ steps.extract-version.outputs.version }}"
|
||||
BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
|
||||
# Determine correct branch for tag creation based on tag type
|
||||
if [[ "$TAG" == *"lts"* ]]; then
|
||||
TARGET_BRANCH="lts-3.16"
|
||||
echo "LTS tag detected - creating tag from lts-3.16 branch"
|
||||
elif [[ "$TAG" == *"beta"* ]]; then
|
||||
TARGET_BRANCH="main"
|
||||
echo "Beta tag detected - creating tag from main branch"
|
||||
else
|
||||
# Default to the base branch of the PR
|
||||
TARGET_BRANCH="$BASE_BRANCH"
|
||||
echo "Using PR base branch: $TARGET_BRANCH"
|
||||
fi
|
||||
|
||||
echo "Creating tag: $TAG from branch: $TARGET_BRANCH in base repo and submodules"
|
||||
|
||||
# Ensure we're on the correct branch and it's up to date
|
||||
git fetch origin "$TARGET_BRANCH"
|
||||
git checkout "$TARGET_BRANCH"
|
||||
git pull origin "$TARGET_BRANCH"
|
||||
|
||||
# Create and push the tag in the base repository
|
||||
echo "Creating tag in base repository..."
|
||||
git tag -a "$TAG" -m "Release $TAG"
|
||||
git push origin "$TAG"
|
||||
|
||||
# Create tags in submodules
|
||||
echo "Creating tag in ee-frontend submodule..."
|
||||
cd frontend/ee
|
||||
git fetch origin
|
||||
if [[ "$TAG" == *"lts"* ]]; then
|
||||
git checkout lts-3.16 2>/dev/null || git checkout main
|
||||
else
|
||||
git checkout main
|
||||
fi
|
||||
git pull origin HEAD
|
||||
git tag -a "$TAG" -m "Release $TAG"
|
||||
git push origin "$TAG"
|
||||
cd ../..
|
||||
|
||||
echo "Creating tag in ee-server submodule..."
|
||||
cd server/ee
|
||||
git fetch origin
|
||||
if [[ "$TAG" == *"lts"* ]]; then
|
||||
git checkout lts-3.16 2>/dev/null || git checkout main
|
||||
else
|
||||
git checkout main
|
||||
fi
|
||||
git pull origin HEAD
|
||||
git tag -a "$TAG" -m "Release $TAG"
|
||||
git push origin "$TAG"
|
||||
cd ../..
|
||||
|
||||
echo "Tags created successfully in all repositories"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate Release Notes
|
||||
id: generate-notes
|
||||
run: |
|
||||
TAG="${{ steps.extract-version.outputs.version }}"
|
||||
|
||||
# Get the previous tag
|
||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "")
|
||||
|
||||
echo "Current tag: $TAG"
|
||||
echo "Previous tag: $PREVIOUS_TAG"
|
||||
|
||||
# Generate release notes using GitHub's auto-generation
|
||||
BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
if [ -n "$PREVIOUS_TAG" ]; then
|
||||
NOTES=$(gh api --method POST /repos/ToolJet/ToolJet/releases/generate-notes \
|
||||
-f tag_name="$TAG" \
|
||||
-f previous_tag_name="$PREVIOUS_TAG" \
|
||||
-f target_commitish="$BASE_BRANCH" \
|
||||
--jq '.body')
|
||||
else
|
||||
NOTES="Initial release version $TAG"
|
||||
fi
|
||||
|
||||
# Set multiline output
|
||||
{
|
||||
echo 'release_notes<<EOF'
|
||||
echo "$NOTES"
|
||||
echo 'EOF'
|
||||
} >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Send Slack Notification
|
||||
if: steps.generate-notes.outputs.release_notes != ''
|
||||
run: |
|
||||
TAG="${{ steps.extract-version.outputs.version }}"
|
||||
REPO_URL="https://github.com/ToolJet/ToolJet/releases/tag/$TAG"
|
||||
|
||||
# Create simplified Slack message
|
||||
SLACK_MESSAGE="🚀 New Release: $TAG
|
||||
|
||||
Tag: $TAG
|
||||
|
||||
A new release has been created and is ready for testing.
|
||||
|
||||
Release: $REPO_URL
|
||||
|
||||
Please test thoroughly before promotion."
|
||||
|
||||
# Send to Slack (using webhook URL from secrets)
|
||||
if [ -n "${{ secrets.SLACK_WEBHOOK_URL }}" ]; then
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data "{\"text\":\"$SLACK_MESSAGE\"}" \
|
||||
"${{ secrets.SLACK_WEBHOOK_URL }}"
|
||||
echo "✅ Slack notification sent successfully"
|
||||
else
|
||||
echo "⚠️ SLACK_WEBHOOK_URL secret not configured - skipping notification"
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: steps.create-tag.outputs.tag != ''
|
||||
run: |
|
||||
TAG="${{ steps.create-tag.outputs.tag }}"
|
||||
|
||||
# Determine if this is a pre-release based on tag content
|
||||
if [[ "$TAG" == *"beta"* ]]; then
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
NOTES_PREFIX="Pre-release version $TAG for testing purposes."
|
||||
elif [[ "$TAG" == *"lts"* ]]; then
|
||||
PRERELEASE_FLAG=""
|
||||
NOTES_PREFIX="LTS release version $TAG."
|
||||
else
|
||||
PRERELEASE_FLAG="--prerelease"
|
||||
NOTES_PREFIX="Release version $TAG."
|
||||
fi
|
||||
|
||||
gh release create "$TAG" \
|
||||
--title "Release $TAG" \
|
||||
--generate-notes \
|
||||
$PRERELEASE_FLAG \
|
||||
--notes "$NOTES_PREFIX Please test thoroughly before promotion."
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## 🎉 Release Automation Complete!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### ✅ Completed Tasks:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Extracted version: \`${{ steps.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Updated .version file" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Updated submodules to latest commits" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Created and merged version bump PR" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Created git tag: \`${{ steps.create-tag.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Generated release notes" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Sent Slack notification" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Created GitHub release" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🔗 Links:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [Tag](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.create-tag.outputs.tag }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [Release](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.create-tag.outputs.tag }})" >> $GITHUB_STEP_SUMMARY
|
||||
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
3.20.119-lts
|
||||
3.21.5-beta
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ RUN git checkout ${BRANCH_NAME}
|
|||
|
||||
RUN git submodule update --init --recursive
|
||||
|
||||
# Checkout the same branch in submodules if it exists, otherwise fallback to lts-3.16
|
||||
# Checkout the same branch in submodules if it exists, otherwise fallback to main
|
||||
RUN git submodule foreach " \
|
||||
if git show-ref --verify --quiet refs/heads/${BRANCH_NAME} || \
|
||||
git ls-remote --exit-code --heads origin ${BRANCH_NAME}; then \
|
||||
git checkout ${BRANCH_NAME}; \
|
||||
else \
|
||||
echo 'Branch ${BRANCH_NAME} not found in submodule \$name, falling back to lts-3.16'; \
|
||||
git checkout lts-3.16; \
|
||||
echo 'Branch ${BRANCH_NAME} not found in submodule \$name, falling back to main'; \
|
||||
git checkout main; \
|
||||
fi"
|
||||
|
||||
# Scripts for building
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ export const commonText = {
|
|||
cancelButton: "Cancel",
|
||||
folderCreatedToast: "Folder created.",
|
||||
createFolder: "Create folder",
|
||||
AddedToFolderToast: "Added to folder.",
|
||||
AddedToFolderToast: "Application added to folder successfully!",
|
||||
appCreatedToast: "App created successfully!",
|
||||
appRenamedToast: "App name has been updated!",
|
||||
appRemovedFromFolderMessage:
|
||||
"The app will be removed from this folder, do you want to continue?",
|
||||
appRemovedFromFolderTaost: "Removed from folder.",
|
||||
appRemovedFromFolderTaost: "Application removed from folder successfully!",
|
||||
modalYesButton: "Yes",
|
||||
emptyFolderText: "This folder is empty",
|
||||
allApplicationsLink: "All applications",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ WHERE table_schema = 'public'
|
|||
AND table_type = 'BASE TABLE';`,
|
||||
postgresResponseNodeQuery: "return postgresql1.data",
|
||||
postgresExpectedValue: "server_side_pagination",
|
||||
|
||||
|
||||
restApiUrl: "http://9.234.17.31:8000/delay/10s",
|
||||
restApiResponseNodeQuery: "return restapi1.data",
|
||||
restApiExpectedValue: "<!DOCTYPE html>",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,284 @@
|
|||
import { fake } from "Fixtures/fake";
|
||||
import { commonSelectors } from "Selectors/common";
|
||||
import { postgreSqlSelector } from "Selectors/postgreSql";
|
||||
import { postgreSqlText } from "Texts/postgreSql";
|
||||
import { deleteWorkflowAndDS } from "Support/utils/dataSource";
|
||||
import { dataSourceSelector } from "Selectors/dataSource";
|
||||
import { harperDbText } from "Texts/harperDb";
|
||||
import { workflowsText } from "Texts/workflows";
|
||||
import { workflowSelector } from "Selectors/workflows";
|
||||
import {
|
||||
fillDataSourceTextField,
|
||||
selectAndAddDataSource,
|
||||
} from "Support/utils/postgreSql";
|
||||
|
||||
import {
|
||||
dataSourceNode,
|
||||
verifyTextInResponseOutput,
|
||||
connectNodeToResponse,
|
||||
createWorkflowApp,
|
||||
fillStartNodeInput,
|
||||
deleteWorkflow,
|
||||
backToWorkFlows,
|
||||
} from "Support/utils/workFlows";
|
||||
|
||||
const data = {};
|
||||
|
||||
describe("Workflows with Datasource", () => {
|
||||
beforeEach(() => {
|
||||
cy.apiLogin();
|
||||
cy.visit("/");
|
||||
data.wfName = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
data.dataSourceName = fake.lastName
|
||||
.toLowerCase()
|
||||
.replaceAll("[^A-Za-z]", "");
|
||||
});
|
||||
|
||||
it("Creating workflows with runjs and validating execution", () => {
|
||||
cy.createWorkflowApp(data.wfName);
|
||||
cy.fillStartNodeInput();
|
||||
cy.dataSourceNode("Run JavaScript code");
|
||||
|
||||
cy.get(workflowSelector.nodeName(workflowsText.runjs)).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get(workflowSelector.inputField(workflowsText.runjsInputField))
|
||||
.click({ force: true })
|
||||
.realType("return startTrigger.params", { delay: 50 });
|
||||
|
||||
cy.get("body").click(50, 50);
|
||||
cy.wait(500);
|
||||
|
||||
cy.connectNodeToResponse(workflowsText.runjs, "return runjs1.data");
|
||||
cy.verifyTextInResponseOutput("your value");
|
||||
cy.deleteWorkflow(data.wfName);
|
||||
});
|
||||
|
||||
it("Creating workflows with postgres and validating execution", () => {
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-manual-pgsql`,
|
||||
"postgresql",
|
||||
[
|
||||
{ key: "connection_type", value: "manual", encrypted: false },
|
||||
{ key: "host", value: `${Cypress.env("pg_host")}`, encrypted: false },
|
||||
{ key: "port", value: 5432, encrypted: false },
|
||||
{ key: "ssl_enabled", value: false, encrypted: false },
|
||||
{ key: "database", value: "postgres", encrypted: false },
|
||||
{ key: "ssl_certificate", value: "none", encrypted: false },
|
||||
{
|
||||
key: "username",
|
||||
value: `${Cypress.env("pg_user")}`,
|
||||
encrypted: false,
|
||||
},
|
||||
{
|
||||
key: "password",
|
||||
value: `${Cypress.env("pg_password")}`,
|
||||
encrypted: false,
|
||||
},
|
||||
{ key: "ca_cert", value: null, encrypted: true },
|
||||
{ key: "client_key", value: null, encrypted: true },
|
||||
{ key: "client_cert", value: null, encrypted: true },
|
||||
{ key: "root_cert", value: null, encrypted: true },
|
||||
{ key: "connection_string", value: null, encrypted: true },
|
||||
]
|
||||
);
|
||||
cy.get(
|
||||
dataSourceSelector.dataSourceNameButton(
|
||||
`cypress-${data.dataSourceName}-manual-pgsql`
|
||||
)
|
||||
)
|
||||
.should("be.visible")
|
||||
.click();
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get(postgreSqlSelector.textConnectionVerified, {
|
||||
timeout: 10000,
|
||||
}).should("have.text", postgreSqlText.labelConnectionVerified);
|
||||
cy.reload();
|
||||
|
||||
cy.createWorkflowApp(data.wfName);
|
||||
cy.fillStartNodeInput();
|
||||
cy.dataSourceNode(`cypress-${data.dataSourceName}-manual-pgsql`);
|
||||
cy.get(workflowSelector.nodeName(workflowsText.postgresql)).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get(workflowSelector.inputField(workflowsText.pgsqlQueryInputField))
|
||||
.click({ force: true })
|
||||
.clearAndTypeOnCodeMirror("")
|
||||
.realType(
|
||||
`SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE';`,
|
||||
{ delay: 50 }
|
||||
);
|
||||
|
||||
cy.get("body").click(50, 50);
|
||||
cy.wait(500);
|
||||
|
||||
cy.connectNodeToResponse(
|
||||
workflowsText.postgresql,
|
||||
"return postgresql1.data"
|
||||
);
|
||||
cy.verifyTextInResponseOutput("employees");
|
||||
|
||||
deleteWorkflowAndDS(
|
||||
data.wfName,
|
||||
`cypress-${data.dataSourceName}-manual-pgsql`
|
||||
);
|
||||
});
|
||||
|
||||
it("Creating workflows with rest-api and validating execution", () => {
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.apiCreateGDS(
|
||||
`${Cypress.env("server_host")}/api/data-sources`,
|
||||
`cypress-${data.dataSourceName}-restapi`,
|
||||
"restapi",
|
||||
[
|
||||
{ key: "url", value: "https://httpbin.org" },
|
||||
{ key: "auth_type", value: "basic" },
|
||||
{ key: "grant_type", value: "authorization_code" },
|
||||
{ key: "add_token_to", value: "header" },
|
||||
{ key: "header_prefix", value: "Bearer " },
|
||||
{ key: "access_token_url", value: "" },
|
||||
{ key: "client_id", value: "" },
|
||||
{
|
||||
key: "client_secret",
|
||||
encrypted: true,
|
||||
credential_id: "b044a293-82b4-4381-84fd-d173c86a6a0c",
|
||||
},
|
||||
{ key: "audience", value: "" },
|
||||
{ key: "scopes", value: "read, write" },
|
||||
{ key: "username", value: "user", encrypted: false },
|
||||
{ key: "password", value: "pass", encrypted: true },
|
||||
{
|
||||
key: "bearer_token",
|
||||
encrypted: true,
|
||||
credential_id: "21caf3cb-dbde-43c7-9f42-77feffb63062",
|
||||
},
|
||||
{ key: "auth_url", value: "" },
|
||||
{ key: "client_auth", value: "header" },
|
||||
{ key: "headers", value: [["", ""]] },
|
||||
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
|
||||
{ key: "custom_auth_params", value: [["", ""]] },
|
||||
{
|
||||
key: "access_token_custom_headers",
|
||||
value: [["", ""]],
|
||||
encrypted: false,
|
||||
},
|
||||
{ key: "multiple_auth_enabled", value: false, encrypted: false },
|
||||
{ key: "ssl_certificate", value: "none", encrypted: false },
|
||||
{ key: "retry_network_errors", value: true, encrypted: false },
|
||||
{ key: "url_parameters", value: [["", ""]], encrypted: false },
|
||||
{ key: "tokenData", encrypted: false },
|
||||
]
|
||||
);
|
||||
|
||||
cy.createWorkflowApp(data.wfName);
|
||||
cy.fillStartNodeInput();
|
||||
cy.dataSourceNode(`cypress-${data.dataSourceName}-restapi`);
|
||||
cy.get(workflowSelector.nodeName(workflowsText.restapi)).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get(workflowSelector.inputField(workflowsText.restapiUrlInputField))
|
||||
.eq(0)
|
||||
.click({ force: true })
|
||||
.clearAndTypeOnCodeMirror("")
|
||||
.realType(`http://9.234.17.31:8000/delay/10s`, { delay: 50 });
|
||||
|
||||
cy.get("body").click(50, 50);
|
||||
cy.wait(500);
|
||||
|
||||
cy.connectNodeToResponse(workflowsText.restapi, "return restapi1.data");
|
||||
cy.verifyTextInResponseOutput("<!DOCTYPE html>");
|
||||
deleteWorkflowAndDS(data.wfName, `cypress-${data.dataSourceName}-restapi`);
|
||||
});
|
||||
|
||||
it("Creating workflows with harperdb and validating execution", () => {
|
||||
const Host = Cypress.env("harperdb_host");
|
||||
const Port = Cypress.env("harperdb_port");
|
||||
const Username = Cypress.env("harperdb_username");
|
||||
const Password = Cypress.env("harperdb_password");
|
||||
|
||||
cy.get(commonSelectors.globalDataSourceIcon).click();
|
||||
cy.installMarketplacePlugin("HarperDB");
|
||||
|
||||
selectAndAddDataSource(
|
||||
"databases",
|
||||
harperDbText.harperDb,
|
||||
data.dataSourceName
|
||||
);
|
||||
|
||||
fillDataSourceTextField(
|
||||
harperDbText.hostLabel,
|
||||
harperDbText.hostInputPlaceholder,
|
||||
Host
|
||||
);
|
||||
|
||||
fillDataSourceTextField(
|
||||
harperDbText.portLabel,
|
||||
harperDbText.portPlaceholder,
|
||||
Port
|
||||
);
|
||||
|
||||
fillDataSourceTextField(
|
||||
harperDbText.userNameLabel,
|
||||
harperDbText.userNamePlaceholder,
|
||||
Username
|
||||
);
|
||||
|
||||
fillDataSourceTextField(
|
||||
harperDbText.passwordlabel,
|
||||
harperDbText.passwordPlaceholder,
|
||||
Password
|
||||
);
|
||||
|
||||
cy.get(postgreSqlSelector.buttonTestConnection).click();
|
||||
cy.get(postgreSqlSelector.textConnectionVerified, {
|
||||
timeout: 10000,
|
||||
}).should("have.text", postgreSqlText.labelConnectionVerified);
|
||||
|
||||
cy.get(postgreSqlSelector.buttonSave)
|
||||
.verifyVisibleElement("have.text", postgreSqlText.buttonTextSave)
|
||||
.click();
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
postgreSqlText.toastDSSaved
|
||||
);
|
||||
|
||||
cy.createWorkflowApp(data.wfName);
|
||||
cy.fillStartNodeInput();
|
||||
cy.dataSourceNode(`cypress-${data.dataSourceName}-harperdb`);
|
||||
cy.get(workflowSelector.nodeName(workflowsText.harperdb)).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get('[data-cy$="-select-dropdown"]').click();
|
||||
|
||||
cy.get(".react-select__menu")
|
||||
.should("be.visible")
|
||||
.within(() => {
|
||||
cy.contains(/sql/i).click();
|
||||
});
|
||||
|
||||
cy.get(workflowSelector.inputField(workflowsText.harperdbInputField))
|
||||
.click({ force: true })
|
||||
|
||||
.click()
|
||||
.clearAndTypeOnCodeMirror("")
|
||||
.realType(`SELECT * FROM tooljet_harper.tooljet_table;`, { delay: 50 });
|
||||
|
||||
cy.get("body").click(50, 50);
|
||||
cy.wait(500);
|
||||
|
||||
cy.connectNodeToResponse(workflowsText.harperdb, "return harperdb1.data");
|
||||
cy.verifyTextInResponseOutput("Test Record 3");
|
||||
|
||||
deleteWorkflowAndDS(data.wfName, `cypress-${data.dataSourceName}-harperdb`);
|
||||
});
|
||||
});
|
||||
|
|
@ -77,6 +77,11 @@ export const deleteAppandDatasourceAfterExecution = (
|
|||
deleteDatasource(datasourceName);
|
||||
};
|
||||
|
||||
export const deleteWorkflowAndDS = (appName, datasourceName) => {
|
||||
cy.deleteWorkflow(appName);
|
||||
deleteDatasource(datasourceName);
|
||||
};
|
||||
|
||||
export const closeDSModal = () => {
|
||||
cy.get("body").then(($body) => {
|
||||
cy.wait(500);
|
||||
|
|
|
|||
|
|
@ -137,4 +137,4 @@ export const verifyTextInResponseOutputLimited = (expectedText, limit = 5) => {
|
|||
`Expected some value to include "${expectedText}", but got:\n\n${texts.join("\n")}`
|
||||
).to.be.true;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,23 +2,71 @@ FROM node:22.15.1 AS builder
|
|||
# Fix for JS heap limit allocation issue
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
RUN mkdir -p /app
|
||||
# Build nsjail for Python sandboxing
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
bison \
|
||||
flex \
|
||||
gcc \
|
||||
g++ \
|
||||
libprotobuf-dev \
|
||||
libnl-route-3-dev \
|
||||
libtool \
|
||||
make \
|
||||
pkg-config \
|
||||
protobuf-compiler \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build-nsjail
|
||||
RUN git clone --depth 1 --branch 3.4 https://github.com/google/nsjail.git && \
|
||||
cd nsjail && \
|
||||
make && \
|
||||
strip nsjail
|
||||
|
||||
# Build Python runtime with pre-installed packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-venv \
|
||||
python3-pip \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python3 -m venv /opt/python-runtime
|
||||
|
||||
RUN /opt/python-runtime/bin/pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
/opt/python-runtime/bin/pip install --no-cache-dir \
|
||||
numpy==1.26.4 \
|
||||
pandas==2.2.1 \
|
||||
requests==2.31.0 \
|
||||
httpx==0.27.0 \
|
||||
python-dateutil==2.9.0 \
|
||||
pytz==2024.1 \
|
||||
pydantic==2.6.4 \
|
||||
typing-extensions==4.10.0
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
# Set GitHub token and branch as build arguments
|
||||
# Set GitHub token, branch and repository URL as build arguments
|
||||
ARG CUSTOM_GITHUB_TOKEN
|
||||
ARG BRANCH_NAME
|
||||
ARG REPO_URL=https://github.com/ToolJet/ToolJet.git
|
||||
|
||||
# Clone and checkout the frontend repository
|
||||
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
|
||||
|
||||
RUN git config --global http.version HTTP/1.1
|
||||
RUN git config --global http.postBuffer 524288000
|
||||
RUN git clone https://github.com/ToolJet/ToolJet.git .
|
||||
RUN git clone ${REPO_URL} .
|
||||
|
||||
# The branch name needs to be changed the branch with modularisation in CE repo
|
||||
RUN git checkout ${BRANCH_NAME}
|
||||
RUN if git show-ref --verify --quiet refs/heads/${BRANCH_NAME} || \
|
||||
git ls-remote --exit-code --heads origin ${BRANCH_NAME}; then \
|
||||
git checkout ${BRANCH_NAME}; \
|
||||
else \
|
||||
echo "Branch ${BRANCH_NAME} not found, falling back to main"; \
|
||||
git checkout main; \
|
||||
fi
|
||||
|
||||
# Handle submodules - try normal submodule update first, if it fails clone directly from base repo
|
||||
RUN if git submodule update --init --recursive; then \
|
||||
|
|
@ -107,7 +155,10 @@ COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
|
|||
ENV NODE_ENV=production
|
||||
ENV TOOLJET_EDITION=ee
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
RUN apt-get update && apt-get install -y freetds-dev libaio1 wget supervisor
|
||||
# Install Redis 7.x from official Redis repository for BullMQ compatibility
|
||||
RUN curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg \
|
||||
&& echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb bullseye main" | tee /etc/apt/sources.list.d/redis.list \
|
||||
&& apt-get update && apt-get install -y freetds-dev libaio1 wget supervisor redis-server libprotobuf23 libnl-route-3-200 python3 python3-pip
|
||||
|
||||
# Install Instantclient Basic Light Oracle and Dependencies
|
||||
WORKDIR /opt/oracle
|
||||
|
|
@ -121,6 +172,15 @@ RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketpla
|
|||
# Set the Instant Client library paths
|
||||
ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}"
|
||||
|
||||
# Copy nsjail and Python runtime from builder
|
||||
COPY --from=builder /build-nsjail/nsjail/nsjail /usr/local/bin/nsjail
|
||||
RUN chmod 755 /usr/local/bin/nsjail
|
||||
|
||||
COPY --from=builder /opt/python-runtime /opt/python-runtime
|
||||
|
||||
# Create nsjail config directory and Python execution temp directory
|
||||
RUN mkdir -p /etc/nsjail /tmp/python-exec && chmod 1777 /tmp/python-exec
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# copy npm scripts
|
||||
|
|
@ -141,6 +201,8 @@ COPY --from=builder /app/server/node_modules ./app/server/node_modules
|
|||
COPY --from=builder /app/server/templates ./app/server/templates
|
||||
COPY --from=builder /app/server/scripts ./app/server/scripts
|
||||
COPY --from=builder /app/server/dist ./app/server/dist
|
||||
# Copy nsjail configuration for Python sandboxing
|
||||
COPY --from=builder /app/server/ee/workflows/nsjail/python-execution.cfg /etc/nsjail/python-execution.cfg
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
@ -166,7 +228,18 @@ RUN rm -rf /var/lib/postgresql/13/main && \
|
|||
# Initialize PostgreSQL
|
||||
RUN su - postgres -c "/usr/lib/postgresql/13/bin/initdb -D /var/lib/postgresql/13/main"
|
||||
|
||||
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
|
||||
# Configure Redis for BullMQ
|
||||
RUN mkdir -p /etc/redis /var/lib/redis /var/log/redis && \
|
||||
chown -R redis:redis /var/lib/redis /var/log/redis && \
|
||||
chmod 755 /var/lib/redis /var/log/redis
|
||||
|
||||
# Copy Redis configuration
|
||||
COPY ./docker/LTS/ee/redis.conf /etc/redis/redis.conf
|
||||
RUN chown redis:redis /etc/redis/redis.conf && \
|
||||
chmod 644 /etc/redis/redis.conf
|
||||
|
||||
# Configure Supervisor to manage PostgREST and ToolJet
|
||||
# Note: PostgreSQL and Redis are started directly in preview.sh
|
||||
RUN echo "[supervisord] \n" \
|
||||
"nodaemon=true \n" \
|
||||
"user=root \n" \
|
||||
|
|
@ -175,6 +248,10 @@ RUN echo "[supervisord] \n" \
|
|||
"command=/bin/postgrest \n" \
|
||||
"autostart=true \n" \
|
||||
"autorestart=true \n" \
|
||||
"stderr_logfile=/dev/stdout \n" \
|
||||
"stderr_logfile_maxbytes=0 \n" \
|
||||
"stdout_logfile=/dev/stdout \n" \
|
||||
"stdout_logfile_maxbytes=0 \n" \
|
||||
"\n" \
|
||||
"[program:tooljet] \n" \
|
||||
"user=root \n" \
|
||||
|
|
@ -205,6 +282,10 @@ ENV TOOLJET_HOST=http://localhost \
|
|||
PGRST_DB_URI=postgres://postgres:postgres@localhost/tooljet_db \
|
||||
PGRST_JWT_SECRET=r9iMKoe5CRMgvJBBtp4HrqN7QiPpUToj \
|
||||
PGRST_DB_PRE_CONFIG=postgrest.pre_config \
|
||||
REDIS_HOST=localhost \
|
||||
REDIS_PORT=6379 \
|
||||
REDIS_DB=0 \
|
||||
REDIS_TLS_ENABLED=false \
|
||||
ORM_LOGGING=true \
|
||||
DEPLOYMENT_PLATFORM=docker:local \
|
||||
HOME=/home/appuser \
|
||||
|
|
|
|||
|
|
@ -5,6 +5,50 @@ ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|||
|
||||
RUN npm i -g npm@10.9.2 && npm cache clean --force
|
||||
|
||||
# Build nsjail for Python sandboxing
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
bison \
|
||||
flex \
|
||||
gcc \
|
||||
g++ \
|
||||
libprotobuf-dev \
|
||||
libnl-route-3-dev \
|
||||
libtool \
|
||||
make \
|
||||
pkg-config \
|
||||
protobuf-compiler \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build-nsjail
|
||||
RUN git clone --depth 1 --branch 3.4 https://github.com/google/nsjail.git && \
|
||||
cd nsjail && \
|
||||
make && \
|
||||
strip nsjail
|
||||
|
||||
# Build Python runtime with pre-installed packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3.11 \
|
||||
python3.11-venv \
|
||||
python3-pip \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create isolated Python environment
|
||||
RUN python3.11 -m venv /opt/python-runtime
|
||||
|
||||
# Upgrade pip and install common packages
|
||||
RUN /opt/python-runtime/bin/pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
/opt/python-runtime/bin/pip install --no-cache-dir \
|
||||
numpy==1.26.4 \
|
||||
pandas==2.2.1 \
|
||||
requests==2.31.0 \
|
||||
httpx==0.27.0 \
|
||||
python-dateutil==2.9.0 \
|
||||
pytz==2024.1 \
|
||||
pydantic==2.6.4 \
|
||||
typing-extensions==4.10.0
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
|
|
@ -39,9 +83,10 @@ COPY ./package.json ./package.json
|
|||
|
||||
# Build plugins
|
||||
COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/
|
||||
RUN npm --prefix plugins ci --omit=dev
|
||||
RUN npm --prefix plugins install
|
||||
COPY ./plugins/ ./plugins/
|
||||
RUN NODE_ENV=production npm --prefix plugins run build && npm --prefix plugins prune --omit=dev
|
||||
RUN NODE_ENV=production npm --prefix plugins run build
|
||||
RUN npm --prefix plugins prune --production
|
||||
|
||||
ENV TOOLJET_EDITION=ee
|
||||
|
||||
|
|
@ -78,19 +123,25 @@ FROM debian:12-slim
|
|||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
wget \
|
||||
gnupg \
|
||||
unzip \
|
||||
ca-certificates \
|
||||
xz-utils \
|
||||
tar \
|
||||
postgresql-client \
|
||||
redis \
|
||||
libaio1 \
|
||||
git \
|
||||
openssh-client \
|
||||
freetds-dev \
|
||||
curl \
|
||||
wget \
|
||||
gnupg \
|
||||
unzip \
|
||||
ca-certificates \
|
||||
xz-utils \
|
||||
tar \
|
||||
postgresql-client \
|
||||
redis \
|
||||
libaio1 \
|
||||
git \
|
||||
openssh-client \
|
||||
freetds-dev \
|
||||
python3.11 \
|
||||
python3.11-venv \
|
||||
libprotobuf32 \
|
||||
libnl-route-3-200 \
|
||||
procps \
|
||||
libcap2-bin \
|
||||
&& apt-get upgrade -y -o Dpkg::Options::="--force-confold" \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
|
@ -102,7 +153,7 @@ RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \
|
|||
&& echo 'export PATH="/usr/local/lib/nodejs/bin:$PATH"' >> /etc/profile.d/nodejs.sh \
|
||||
&& /bin/bash -c "source /etc/profile.d/nodejs.sh" \
|
||||
&& rm node-v22.15.1-linux-x64.tar.xz
|
||||
ENV PATH=/usr/local/lib/nodejs/bin:$PATH
|
||||
ENV PATH=/usr/local/lib/nodejs/bin:/opt/python-runtime/bin:$PATH
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV TOOLJET_EDITION=ee
|
||||
|
|
@ -130,6 +181,24 @@ RUN mkdir -p /app
|
|||
|
||||
RUN useradd --create-home --home-dir /home/appuser appuser
|
||||
|
||||
# Copy nsjail and Python runtime from builder
|
||||
COPY --from=builder /build-nsjail/nsjail/nsjail /usr/local/bin/nsjail
|
||||
RUN chmod 4755 /usr/local/bin/nsjail
|
||||
|
||||
# Copy Python runtime with pre-installed packages
|
||||
COPY --from=builder /opt/python-runtime /opt/python-runtime
|
||||
|
||||
# Copy nsjail configuration file
|
||||
RUN mkdir -p /etc/nsjail
|
||||
COPY --from=builder /app/server/ee/workflows/nsjail/python-execution.cfg /etc/nsjail/python-execution.cfg
|
||||
|
||||
# Create Python execution directories
|
||||
RUN mkdir -p \
|
||||
/tmp/python-execution \
|
||||
/tmp/python-bundles \
|
||||
&& chmod 1777 /tmp/python-execution \
|
||||
&& chmod 1777 /tmp/python-bundles
|
||||
|
||||
# Use the PostgREST binary from the builder stage
|
||||
COPY --from=builder --chown=appuser:0 /postgrest /usr/local/bin/postgrest
|
||||
|
||||
|
|
@ -156,6 +225,8 @@ COPY --from=builder --chown=appuser:0 /app/server/dist ./app/server/dist
|
|||
COPY --from=builder --chown=appuser:0 /app/server/ee/ai/assets ./app/server/ee/ai/assets
|
||||
COPY ./docker/pre-release/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh
|
||||
|
||||
# Set group write permissions for frontend build files to support RedHat arbitrary user assignment
|
||||
RUN chmod -R g+w /app/frontend/build
|
||||
|
||||
# Create directory /home/appuser and set ownership to appuser
|
||||
RUN mkdir -p /home/appuser \
|
||||
|
|
@ -164,6 +235,11 @@ RUN mkdir -p /home/appuser \
|
|||
&& chmod -R g=u /home/appuser \
|
||||
&& npm cache clean --force
|
||||
|
||||
# Create gitsync directory with proper permissions for RedHat/OpenShift arbitrary UID support
|
||||
RUN mkdir -p /app/server/tooljet/gitsync \
|
||||
&& chown -R appuser:0 /app/server/tooljet \
|
||||
&& chmod -R 2770 /app/server/tooljet/gitsync
|
||||
|
||||
# Create rsyslog directory for audit logs with proper permissions
|
||||
RUN mkdir -p /home/appuser/rsyslog \
|
||||
&& chown -R appuser:0 /home/appuser/rsyslog \
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.20.119-lts
|
||||
3.21.5-beta
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 3537889338925f24f704a34d233515abf44fd9bc
|
||||
Subproject commit 16fc80f3d2e50307cedcf10133d4c7e1a82752f3
|
||||
|
|
@ -110,8 +110,8 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
|
|||
currentMode === 'view'
|
||||
? computeViewerBackgroundColor(isAppDarkMode, canvasBgColor)
|
||||
: !isAppDarkMode
|
||||
? '#EBEBEF'
|
||||
: '#2F3C4C';
|
||||
? '#EBEBEF'
|
||||
: '#2F3C4C';
|
||||
|
||||
if (isModuleMode) {
|
||||
return {
|
||||
|
|
@ -274,4 +274,4 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
@ -36,9 +36,9 @@ export const ConfigHandle = ({
|
|||
subContainerIndex,
|
||||
isDynamicHeightEnabled,
|
||||
}) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const { moduleId, isModuleEditor } = useModuleContext();
|
||||
const isModulesEnabled = useStore((state) => state.license.featureAccess?.modulesEnabled, shallow);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const componentName = useStore((state) => state.getComponentDefinition(id, moduleId)?.component?.name || '', shallow);
|
||||
const isMultipleComponentsSelected = useStore(
|
||||
(state) => (findHighestLevelofSelection(state?.selectedComponents)?.length > 1 ? true : false),
|
||||
|
|
@ -86,7 +86,7 @@ export const ConfigHandle = ({
|
|||
const deleteComponents = () => {
|
||||
const selectedComponents = getSelectedComponents();
|
||||
if (selectedComponents.length > 0) {
|
||||
setWidgetDeleteConfirmation(true);
|
||||
setWidgetDeleteConfirmation(true, isModuleEditor);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ export const DeleteWidgetConfirmation = ({ darkMode }) => {
|
|||
const showWidgetDeleteConfirmation = useStore((state) => state.showWidgetDeleteConfirmation, shallow);
|
||||
const setWidgetDeleteConfirmation = useStore((state) => state.setWidgetDeleteConfirmation, shallow);
|
||||
const deleteComponents = useStore((state) => state.deleteComponents, shallow);
|
||||
const deleteTargetIsModuleEditor = useStore((state) => state.deleteTargetIsModuleEditor, shallow);
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
deleteComponents();
|
||||
deleteComponents(undefined, 'canvas', { isModuleEditor: deleteTargetIsModuleEditor });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export default function Grid({ gridWidth, currentLayout, mainCanvasWidth }) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [noOfBoxs, triggerCanvasUpdater, menuPosition, hideLogo, hideHeader, isPageMenuHidden]);
|
||||
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
|
||||
const handleResizeStop = useCallback(
|
||||
(boxList) => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth,
|
|||
const handleRedo = useStore((state) => state.handleRedo);
|
||||
const setWidgetDeleteConfirmation = useStore((state) => state.setWidgetDeleteConfirmation);
|
||||
const moveComponentPosition = useStore((state) => state.moveComponentPosition, shallow);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState, shallow);
|
||||
const clearSelectedComponents = useStore((state) => state.clearSelectedComponents, shallow);
|
||||
const getSelectedComponents = useStore((state) => state.getSelectedComponents, shallow);
|
||||
|
|
@ -53,7 +53,7 @@ export const HotkeyProvider = ({ children, mode, currentLayout, canvasMaxWidth,
|
|||
const deleteComponents = () => {
|
||||
const selectedComponents = getSelectedComponents();
|
||||
if (selectedComponents.length > 0) {
|
||||
setWidgetDeleteConfirmation(true);
|
||||
setWidgetDeleteConfirmation(true, isModuleEditor);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import BulkIcon from '@/_ui/Icon/BulkIcons';
|
|||
import { getSubpath } from '@/_helpers/routes';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const NoComponentCanvasContainer = () => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const sampleDataSource = useStore((state) => state.sampleDataSource, shallow);
|
||||
const createDataQuery = useStore((state) => state.dataQuery.createDataQuery, shallow);
|
||||
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData, shallow);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const expandQueryPaneIfNeeded = useStore((state) => state.queryPanel.expandQueryPaneIfNeeded);
|
||||
|
||||
const queryBoxText = sampleDataSource
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React, { useRef, useEffect } from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import DataSourceSelect from '@/AppBuilder/QueryManager/Components/DataSourceSelect';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { FileCode2 } from 'lucide-react';
|
||||
|
||||
const AddQueryBtn = ({ darkMode, disabled: _disabled, onQueryCreate, showMenu, setShowMenu }) => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const selectRef = useRef();
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const disabled = _disabled || shouldFreeze;
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ const EditorInput = ({
|
|||
<div
|
||||
ref={currentEditorHeightRef}
|
||||
className={`cm-codehinter ${darkMode && 'cm-codehinter-dark-themed'} ${disabled ? 'disabled-cursor' : ''}`}
|
||||
data-cy={`${cyLabel.replace(/_/g, '-')}-input-field`}
|
||||
data-cy={`${cyLabel}-input-field`}
|
||||
>
|
||||
{/* sticky element to position the preview box correctly on top without flowing out of container */}
|
||||
{usePortalEditor && (
|
||||
|
|
|
|||
717
frontend/src/AppBuilder/Header/BranchDropdown.jsx
Normal file
717
frontend/src/AppBuilder/Header/BranchDropdown.jsx
Normal file
|
|
@ -0,0 +1,717 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Overlay, Popover } from 'react-bootstrap';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import '@/_styles/branch-dropdown.scss';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { CreateBranchModal } from './CreateBranchModal';
|
||||
import { SwitchBranchModal } from './SwitchBranchModal';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { gitSyncService } from '@/_services';
|
||||
import OverflowTooltip from '@/_components/OverflowTooltip';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
export function BranchDropdown({ appId, organizationId }) {
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [expandedBranches, setExpandedBranches] = useState(new Set());
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showSwitchModal, setShowSwitchModal] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('open'); // 'open' or 'closed'
|
||||
const [lastCommit, setLastCommit] = useState(null);
|
||||
const [isLoadingCommit, setIsLoadingCommit] = useState(false);
|
||||
const [hasFetchedPRs, setHasFetchedPRs] = useState(false); // Track if PRs have been fetched
|
||||
const [hasFetchedBranchInfo, setHasFetchedBranchInfo] = useState(false); // Track if branch info has been fetched
|
||||
const [isLoadingPRs, setIsLoadingPRs] = useState(false); // Track PR loading state
|
||||
const dropdownRef = useRef(null);
|
||||
const buttonRef = useRef(null);
|
||||
const popoverRef = useRef(null);
|
||||
|
||||
// Helper function to get relative time
|
||||
const getRelativeTime = (dateString) => {
|
||||
if (!dateString) return null;
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) return 'Updated today';
|
||||
if (diffDays === 1) return 'Updated yesterday';
|
||||
if (diffDays < 7) return `Updated ${diffDays} days ago`;
|
||||
if (diffDays < 30) return `Updated ${Math.floor(diffDays / 7)} weeks ago`;
|
||||
return `Updated ${Math.floor(diffDays / 30)} months ago`;
|
||||
};
|
||||
|
||||
// Helper function to format commit date (e.g., "25 Sept, 8:45am")
|
||||
const formatCommitDate = (dateString) => {
|
||||
if (!dateString) return 'Unknown date';
|
||||
const date = new Date(dateString);
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'];
|
||||
const day = date.getDate();
|
||||
const month = months[date.getMonth()];
|
||||
let hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const ampm = hours >= 12 ? 'pm' : 'am';
|
||||
hours = hours % 12 || 12;
|
||||
const minutesStr = minutes < 10 ? `0${minutes}` : minutes;
|
||||
return `${day} ${month}, ${hours}:${minutesStr}${ampm}`;
|
||||
};
|
||||
|
||||
// Helper function to check if branch is locked (will be used for branch switching UI later)
|
||||
const _isBranchLocked = (branch) => {
|
||||
return branch.is_merged || branch.isMerged || branch.is_released || branch.isReleased;
|
||||
};
|
||||
|
||||
// Helper function to build PR creation URL
|
||||
const buildPRCreationURL = () => {
|
||||
const defaultBranchName = orgGit?.git_https?.github_branch || orgGit?.git_ssh?.github_branch || 'main';
|
||||
const sourceBranch = currentBranchName;
|
||||
|
||||
// Get repository URL from orgGit (check https_url, ssh_url, or repository fields)
|
||||
const repoUrl =
|
||||
orgGit?.git_https?.https_url ||
|
||||
orgGit?.git_https?.repository ||
|
||||
orgGit?.git_ssh?.ssh_url ||
|
||||
orgGit?.git_ssh?.repository;
|
||||
|
||||
if (!repoUrl) {
|
||||
console.error('No repository URL found in orgGit:', orgGit);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract owner and repo name from URL
|
||||
// Handles: https://github.com/owner/repo.git, git@github.com:owner/repo.git, etc.
|
||||
// Updated regex to handle dots in repo names (e.g., git-sync-2.0-repo.git)
|
||||
const githubMatch = repoUrl.match(/github\.com[:/]([^/]+)\/(.+?)(\.git)?$/);
|
||||
const gitlabMatch = repoUrl.match(/gitlab\.com[:/]([^/]+)\/(.+?)(\.git)?$/);
|
||||
const bitbucketMatch = repoUrl.match(/bitbucket\.org[:/]([^/]+)\/(.+?)(\.git)?$/);
|
||||
|
||||
if (githubMatch) {
|
||||
const [, owner, repo] = githubMatch;
|
||||
return `https://github.com/${owner}/${repo}/compare/${defaultBranchName}...${sourceBranch}?expand=1`;
|
||||
} else if (gitlabMatch) {
|
||||
const [, owner, repo] = gitlabMatch;
|
||||
return `https://gitlab.com/${owner}/${repo}/-/merge_requests/new?merge_request[source_branch]=${sourceBranch}&merge_request[target_branch]=${defaultBranchName}`;
|
||||
} else if (bitbucketMatch) {
|
||||
const [, owner, repo] = bitbucketMatch;
|
||||
return `https://bitbucket.org/${owner}/${repo}/pull-requests/new?source=${sourceBranch}&dest=${defaultBranchName}`;
|
||||
}
|
||||
|
||||
console.error('Could not parse repository URL:', repoUrl);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Handle Create PR action
|
||||
const _handleCreatePR = () => {
|
||||
const prUrl = buildPRCreationURL();
|
||||
if (prUrl) {
|
||||
window.open(prUrl, '_blank', 'noopener,noreferrer');
|
||||
setShowDropdown(false);
|
||||
} else {
|
||||
toast.error('Unable to determine repository URL for PR creation');
|
||||
}
|
||||
};
|
||||
|
||||
// Zustand state
|
||||
const {
|
||||
currentBranch,
|
||||
allBranches,
|
||||
pullRequests,
|
||||
branchingEnabled,
|
||||
fetchBranches,
|
||||
fetchPullRequests,
|
||||
fetchDevelopmentVersions,
|
||||
switchBranch,
|
||||
switchToDefaultBranch,
|
||||
setCurrentBranch,
|
||||
orgGit,
|
||||
selectedVersion,
|
||||
} = useStore((state) => ({
|
||||
currentBranch: state.currentBranch,
|
||||
allBranches: state.allBranches,
|
||||
pullRequests: state.pullRequests,
|
||||
branchingEnabled: state.branchingEnabled,
|
||||
fetchBranches: state.fetchBranches,
|
||||
fetchPullRequests: state.fetchPullRequests,
|
||||
fetchDevelopmentVersions: state.fetchDevelopmentVersions,
|
||||
switchBranch: state.switchBranch,
|
||||
switchToDefaultBranch: state.switchToDefaultBranch,
|
||||
setCurrentBranch: state.setCurrentBranch,
|
||||
orgGit: state.orgGit,
|
||||
selectedVersion: state.selectedVersion,
|
||||
}));
|
||||
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true' || false;
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setShowDropdown(false);
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Reset commit state when dropdown closes
|
||||
useEffect(() => {
|
||||
if (!showDropdown) {
|
||||
setLastCommit(null);
|
||||
setIsLoadingCommit(false);
|
||||
setHasFetchedPRs(false); // Reset PR fetch state when dropdown closes
|
||||
setHasFetchedBranchInfo(false); // Reset branch info fetch state when dropdown closes
|
||||
}
|
||||
}, [showDropdown]);
|
||||
|
||||
// Fetch branches and PRs on mount and when dropdown opens
|
||||
useEffect(() => {
|
||||
if (branchingEnabled && appId && organizationId) {
|
||||
handleRefresh();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [branchingEnabled, appId, organizationId]);
|
||||
|
||||
// Manual fetch last commit function
|
||||
const fetchLastCommit = async () => {
|
||||
const currentBranchName = selectedVersion?.name || currentBranch?.name;
|
||||
const defaultBranchName = orgGit?.git_https?.github_branch || orgGit?.git_ssh?.github_branch || 'main';
|
||||
const isOnDefaultBranch = currentBranchName === defaultBranchName;
|
||||
|
||||
// Only fetch commit if on non-default branch
|
||||
if (!isOnDefaultBranch && currentBranchName && appId && organizationId) {
|
||||
setIsLoadingCommit(true);
|
||||
try {
|
||||
const data = await gitSyncService.checkForUpdates(appId, currentBranchName);
|
||||
const latestCommit = data?.meta_data?.latest_commit?.[0];
|
||||
|
||||
if (latestCommit) {
|
||||
setLastCommit({
|
||||
message: latestCommit.message || latestCommit.commitMessage,
|
||||
author: latestCommit.author || latestCommit.author_name,
|
||||
date: latestCommit.date || latestCommit.committed_date,
|
||||
});
|
||||
toast.success('Branch info fetched successfully');
|
||||
} else {
|
||||
setLastCommit(null);
|
||||
toast.info('No commits found for this branch');
|
||||
}
|
||||
setIsLoadingCommit(false);
|
||||
setHasFetchedBranchInfo(true); // Mark branch info as fetched
|
||||
} catch (error) {
|
||||
console.error('Error fetching last commit:', error);
|
||||
setLastCommit(null);
|
||||
setIsLoadingCommit(false);
|
||||
setHasFetchedBranchInfo(true); // Mark as fetched even on error to hide button
|
||||
// toast.error('Failed to fetch branch info');
|
||||
}
|
||||
} else {
|
||||
setLastCommit(null);
|
||||
setIsLoadingCommit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (!appId || !organizationId) return;
|
||||
|
||||
setIsLoadingPRs(true);
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchBranches(appId, organizationId),
|
||||
fetchPullRequests(appId, organizationId),
|
||||
fetchDevelopmentVersions(appId), // Fetch development versions for branch switching
|
||||
]);
|
||||
setHasFetchedPRs(true); // Mark PRs as fetched
|
||||
} catch (error) {
|
||||
console.error('Error refreshing branches/PRs:', error);
|
||||
toast.error('Failed to refresh branches');
|
||||
} finally {
|
||||
setIsLoadingPRs(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _handleBranchClick = async (branch) => {
|
||||
if (branch.name === currentBranch?.name) {
|
||||
setShowDropdown(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if this is the default branch (main/master/etc from config)
|
||||
const defaultBranchName = orgGit?.git_https?.github_branch || orgGit?.git_ssh?.github_branch || 'main';
|
||||
const isDefaultBranch = branch.name === defaultBranchName;
|
||||
|
||||
if (isDefaultBranch) {
|
||||
// Switch to default branch (finds active draft or latest version)
|
||||
const result = await switchToDefaultBranch(appId, branch.name);
|
||||
if (result.success) {
|
||||
setCurrentBranch(branch);
|
||||
if (result.isDraft) {
|
||||
toast.success(`Switched to ${branch.name} - Working on draft version`);
|
||||
} else {
|
||||
toast.success(`Switched to ${branch.name}`);
|
||||
}
|
||||
setShowDropdown(false);
|
||||
} else {
|
||||
toast.error(`Failed to switch to default branch: ${result.error}`);
|
||||
}
|
||||
} else {
|
||||
// Switch to feature branch
|
||||
const result = await switchBranch(appId, branch.name);
|
||||
if (result.success) {
|
||||
setCurrentBranch(branch);
|
||||
setShowDropdown(false);
|
||||
} else {
|
||||
toast.error(`Failed to switch branch: ${result.error}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching branch:', error);
|
||||
toast.error(error.message || 'Failed to switch branch');
|
||||
}
|
||||
};
|
||||
|
||||
const _toggleBranchExpand = (branchName) => {
|
||||
const newExpanded = new Set(expandedBranches);
|
||||
if (newExpanded.has(branchName)) {
|
||||
newExpanded.delete(branchName);
|
||||
} else {
|
||||
newExpanded.add(branchName);
|
||||
}
|
||||
setExpandedBranches(newExpanded);
|
||||
};
|
||||
|
||||
const _getPRForBranch = (branchName) => {
|
||||
return pullRequests.find((pr) => pr.source_branch === branchName || pr.sourceBranch === branchName);
|
||||
};
|
||||
|
||||
// Check if current branch is the default branch
|
||||
const defaultBranchName = orgGit?.git_https?.github_branch || orgGit?.git_ssh?.github_branch || 'main';
|
||||
// Use selectedVersion.name as the current branch (ToolJet's version/branch name)
|
||||
const currentBranchName = selectedVersion?.name || currentBranch?.name;
|
||||
|
||||
// Determine if on default branch:
|
||||
// - If versionType is 'version', we're on a regular version (show default branch UI)
|
||||
// - If versionType is 'branch', we're on a feature branch (show branch commit UI)
|
||||
const isOnDefaultBranch = selectedVersion?.versionType === 'version' || selectedVersion?.versionType !== 'branch';
|
||||
|
||||
// Display name: show default branch name when on a version, otherwise show current branch name
|
||||
const displayBranchName = isOnDefaultBranch ? defaultBranchName : currentBranchName;
|
||||
|
||||
// Filter PRs based on active tab
|
||||
// Check both 'state' and 'status' fields to support different API responses
|
||||
const openPRs = pullRequests.filter(
|
||||
(pr) => pr.state?.toLowerCase() === 'open' || pr.status?.toLowerCase() === 'open'
|
||||
);
|
||||
const closedPRs = pullRequests.filter(
|
||||
(pr) =>
|
||||
pr.state?.toLowerCase() === 'closed' ||
|
||||
pr.status?.toLowerCase() === 'closed' ||
|
||||
(pr.state?.toLowerCase() !== 'open' && pr.status?.toLowerCase() !== 'open')
|
||||
);
|
||||
const displayPRs = activeTab === 'open' ? openPRs : closedPRs;
|
||||
|
||||
// Format PR date
|
||||
const formatPRDate = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
});
|
||||
};
|
||||
|
||||
if (!branchingEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderPopover = (overlayProps) => (
|
||||
<Popover
|
||||
id="branch-dropdown-popover"
|
||||
className={cx('branch-dropdown-popover', { 'dark-theme theme-dark': darkMode })}
|
||||
ref={popoverRef}
|
||||
{...overlayProps}
|
||||
style={{
|
||||
...overlayProps?.style,
|
||||
minWidth: '320px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border-weak)',
|
||||
boxShadow: '0px 0px 1px rgba(48, 50, 51, 0.05), 0px 1px 1px rgba(48, 50, 51, 0.1)',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<Popover.Body style={{ padding: 0 }}>
|
||||
<div className={`${darkMode ? 'theme-dark' : ''}`} data-cy="branch-dropdown-popover">
|
||||
{/* Current Branch Header */}
|
||||
<div className={`branch-dropdown-current-branch ${!isOnDefaultBranch ? 'with-border' : ''}`}>
|
||||
{isOnDefaultBranch ? (
|
||||
<>
|
||||
<div className="branch-icon-container">
|
||||
<SolidIcon name="lockclosed" width="16" fill="var(--indigo9)" />
|
||||
</div>
|
||||
<div className="branch-info">
|
||||
<div className="branch-name-title">{displayBranchName || 'No branch selected'}</div>
|
||||
<div className="branch-metadata">
|
||||
<span className="metadata-text">Default branch</span>
|
||||
{(currentBranch?.updatedAt || currentBranch?.updated_at) && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="metadata-text">
|
||||
{getRelativeTime(currentBranch.updatedAt || currentBranch.updated_at)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="branch-icon-container-feature">
|
||||
<SolidIcon name="gitbranch" width="16" fill="var(--indigo9)" />
|
||||
</div>
|
||||
<div className="branch-info">
|
||||
<div className="branch-name-title">{displayBranchName || 'No branch selected'}</div>
|
||||
<div className="branch-metadata-feature">
|
||||
<span className="metadata-text">
|
||||
Created by {currentBranch?.created_by || currentBranch?.author || 'Unknown'}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span className="metadata-text">
|
||||
{getRelativeTime(
|
||||
selectedVersion?.createdAt ||
|
||||
selectedVersion?.created_at ||
|
||||
currentBranch?.createdAt ||
|
||||
currentBranch?.created_at
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
{isOnDefaultBranch ? (
|
||||
<>
|
||||
{/* Fetch PRs Button - Shown at top for default branch, hides after fetching */}
|
||||
{!hasFetchedPRs && (
|
||||
<div className="fetch-prs-section">
|
||||
<button
|
||||
className={`fetch-prs-btn ${isLoadingPRs ? 'loading' : ''}`}
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoadingPRs}
|
||||
data-cy="fetch-prs-btn"
|
||||
>
|
||||
{isLoadingPRs ? (
|
||||
<>
|
||||
<div className="spinner-small"></div>
|
||||
<span>Loading...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SolidIcon name="refresh" width="14" />
|
||||
<span>Fetch PRs</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PR Tabs and List - Only shown after fetching */}
|
||||
{hasFetchedPRs && (
|
||||
<>
|
||||
{/* PR Tabs */}
|
||||
<div className="pr-tabs">
|
||||
<button
|
||||
className={`pr-tab ${activeTab === 'open' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('open')}
|
||||
>
|
||||
Open PR
|
||||
</button>
|
||||
<button
|
||||
className={`pr-tab ${activeTab === 'closed' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('closed')}
|
||||
>
|
||||
Closed PR
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* PR List */}
|
||||
<div className="pr-list-container">
|
||||
{displayPRs.length === 0 ? (
|
||||
<div className="empty-pr-state-box">
|
||||
<AlertTriangle width="18" height="18" />
|
||||
<div className="empty-pr-content">
|
||||
<div className="empty-pr-title">
|
||||
{activeTab === 'open' ? 'There are no open PRs' : 'There are no closed PRs'}
|
||||
</div>
|
||||
<div className="empty-pr-description">
|
||||
{activeTab === 'open'
|
||||
? 'Create a pull request to contribute your changes'
|
||||
: 'Merge a pull request to contribute your changes'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
displayPRs.map((pr) => (
|
||||
<div key={pr.id} className="pr-item" data-cy={`pr-item-${pr.id}`}>
|
||||
<div className="pr-icon">
|
||||
<SolidIcon name="gitmerge" width="20" fill="var(--slate11)" />
|
||||
</div>
|
||||
<div className="pr-content">
|
||||
<OverflowTooltip
|
||||
className="pr-title"
|
||||
childrenClassName="pr-title"
|
||||
placement="top"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{pr.title || 'Untitled PR'}
|
||||
</OverflowTooltip>
|
||||
<div className="pr-metadata">
|
||||
from {pr.source_branch || pr.sourceBranch} | {formatPRDate(pr.created_at || pr.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Fetch Branch Info Button - Only show when not fetched yet */}
|
||||
{!hasFetchedBranchInfo && (
|
||||
<div className="fetch-branch-info-section">
|
||||
<button
|
||||
className="fetch-branch-info-btn"
|
||||
onClick={fetchLastCommit}
|
||||
disabled={isLoadingCommit}
|
||||
data-cy="fetch-branch-info-btn"
|
||||
>
|
||||
<SolidIcon name="refresh" width="14" />
|
||||
<span>{isLoadingCommit ? 'Fetching...' : 'Fetch branch info'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Latest Commit Section & Empty State - Only show after fetching */}
|
||||
{hasFetchedBranchInfo && (
|
||||
<>
|
||||
{/* Latest Commit Section - for non-default branches with commits */}
|
||||
{lastCommit && !isLoadingCommit && (
|
||||
<div className="latest-commit-section">
|
||||
{/* <div className="latest-commit-header">
|
||||
<span className="section-label">LATEST COMMIT</span>
|
||||
</div> */}
|
||||
<div className="commit-info">
|
||||
<div className="commit-icon">
|
||||
<SolidIcon name="commit" width="20" />
|
||||
</div>
|
||||
<div className="commit-content">
|
||||
<div className="commit-title">{lastCommit.message || 'No message'}</div>
|
||||
<div className="commit-metadata">
|
||||
By {lastCommit.author || 'Unknown'} | {formatCommitDate(lastCommit.date)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state - no commits yet */}
|
||||
{!lastCommit && !isLoadingCommit && (
|
||||
<div className="no-commits-empty-state">
|
||||
<AlertTriangle width="18" height="18" />
|
||||
<div className="empty-state-content">
|
||||
<div className="empty-state-title">There are no commits yet</div>
|
||||
<div className="empty-state-description">
|
||||
Commit your changes to create a pull request to contribute them
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading state for commit */}
|
||||
{isLoadingCommit && (
|
||||
<div className="loading-commit-state">
|
||||
<div className="spinner"></div>
|
||||
<span>Loading commit info...</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Footer actions */}
|
||||
<div className="branch-dropdown-footer">
|
||||
{/* Default branch footer: Create branch + Switch branch */}
|
||||
{isOnDefaultBranch ? (
|
||||
<>
|
||||
<button
|
||||
className="create-branch-btn"
|
||||
onClick={() => {
|
||||
setShowDropdown(false);
|
||||
setShowCreateModal(true);
|
||||
}}
|
||||
data-cy="create-branch-btn"
|
||||
>
|
||||
<SolidIcon name="plus" width="14" fill="var(--indigo9)" />
|
||||
<span>Create new branch</span>
|
||||
</button>
|
||||
{console.log('BranchDropdown - allBranches:', allBranches, 'length:', allBranches.length) ||
|
||||
(true && allBranches.length > 0 && (
|
||||
<button
|
||||
className="switch-branch-btn"
|
||||
onClick={() => {
|
||||
setShowDropdown(false);
|
||||
setShowSwitchModal(true);
|
||||
}}
|
||||
data-cy="switch-branch-btn"
|
||||
>
|
||||
<SolidIcon name="refresh" width="14" />
|
||||
<span>Switch branch</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Feature branch footer: Create PR + Switch branch */}
|
||||
{/* Always show Create PR button when on sub-branch */}
|
||||
<button className="create-pr-btn" onClick={_handleCreatePR} data-cy="create-pr-btn">
|
||||
<SolidIcon name="gitmerge" width="14" fill="var(--indigo9)" />
|
||||
<span>Create pull request</span>
|
||||
</button>
|
||||
{allBranches.length > 0 && (
|
||||
<button
|
||||
className="switch-branch-btn"
|
||||
onClick={() => {
|
||||
setShowDropdown(false);
|
||||
setShowSwitchModal(true);
|
||||
}}
|
||||
data-cy="switch-branch-btn"
|
||||
>
|
||||
<SolidIcon name="refresh" width="14" />
|
||||
<span>Switch branch</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`branch-dropdown-container ${showDropdown ? 'selected' : ''} ${darkMode ? 'dark-theme' : ''}`}
|
||||
ref={buttonRef}
|
||||
data-cy="branch-dropdown-container"
|
||||
>
|
||||
<button
|
||||
className="branch-dropdown-button"
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
data-cy="branch-dropdown-header"
|
||||
>
|
||||
<SolidIcon name="gitbranch" width="16" fill="var(--slate12)" />
|
||||
<span className="branch-name" data-cy="current-branch-name">
|
||||
{displayBranchName || 'Select branch'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Overlay
|
||||
show={showDropdown}
|
||||
target={buttonRef.current}
|
||||
placement="bottom-end"
|
||||
rootClose
|
||||
onHide={() => setShowDropdown(false)}
|
||||
popperConfig={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: 'viewport',
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
fallbackPlacements: ['bottom-start', 'top-end', 'top-start'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 4],
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{({ placement: _placement, arrowProps: _arrowProps, show: _show, popper: _popper, ...props }) => (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 1050,
|
||||
}}
|
||||
>
|
||||
{renderPopover(props)}
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
|
||||
{/* Create Branch Modal */}
|
||||
{showCreateModal && (
|
||||
<CreateBranchModal
|
||||
appId={appId}
|
||||
organizationId={organizationId}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onSuccess={(newBranch) => {
|
||||
// Optionally switch to the new branch after creation
|
||||
if (newBranch) {
|
||||
setCurrentBranch(newBranch);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Switch Branch Modal */}
|
||||
{showSwitchModal && (
|
||||
<SwitchBranchModal
|
||||
show={showSwitchModal}
|
||||
onClose={() => setShowSwitchModal(false)}
|
||||
appId={appId}
|
||||
organizationId={organizationId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tooltip for PR details */}
|
||||
{/* Tooltip for PR details */}
|
||||
<Tooltip
|
||||
id="branch-dropdown-tooltip"
|
||||
className="branch-pr-tooltip"
|
||||
style={{
|
||||
backgroundColor: 'var(--slate12)',
|
||||
color: 'var(--slate1)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
maxWidth: '300px',
|
||||
whiteSpace: 'pre-line',
|
||||
zIndex: 10000,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
396
frontend/src/AppBuilder/Header/CreateBranchModal.jsx
Normal file
396
frontend/src/AppBuilder/Header/CreateBranchModal.jsx
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useVersionManagerStore } from '@/_stores/versionManagerStore';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { DraftVersionWarningModal } from './DraftVersionWarningModal';
|
||||
import { Alert } from '@/_ui/Alert';
|
||||
import AlertDialog from '@/_ui/AlertDialog';
|
||||
import cx from 'classnames';
|
||||
import '@/_styles/create-branch-modal.scss';
|
||||
|
||||
export function CreateBranchModal({ onClose, onSuccess, appId, organizationId }) {
|
||||
const [branchName, setBranchName] = useState('');
|
||||
const [createFrom, setCreateFrom] = useState('');
|
||||
const [autoCommit, setAutoCommit] = useState(true);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [validationError, setValidationError] = useState('');
|
||||
const [showDraftWarning, setShowDraftWarning] = useState(false);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const {
|
||||
allBranches,
|
||||
isDraftVersionActive,
|
||||
createBranch,
|
||||
switchBranch,
|
||||
fetchBranches,
|
||||
lazyLoadAppVersions,
|
||||
fetchDevelopmentVersions,
|
||||
editingVersion,
|
||||
currentBranch,
|
||||
releasedVersionId,
|
||||
} = useStore((state) => ({
|
||||
allBranches: state.allBranches || [],
|
||||
isDraftVersionActive: state.isDraftVersionActive,
|
||||
createBranch: state.createBranch,
|
||||
switchBranch: state.switchBranch,
|
||||
fetchBranches: state.fetchBranches,
|
||||
lazyLoadAppVersions: state.lazyLoadAppVersions,
|
||||
fetchDevelopmentVersions: state.fetchDevelopmentVersions,
|
||||
editingVersion: state.editingVersion,
|
||||
currentBranch: state.currentBranch,
|
||||
releasedVersionId: state.releasedVersionId,
|
||||
}));
|
||||
|
||||
// Get versions from versionManagerStore
|
||||
const { versions, fetchVersions } = useVersionManagerStore((state) => ({
|
||||
versions: state.versions || [],
|
||||
fetchVersions: state.fetchVersions,
|
||||
}));
|
||||
|
||||
// Load versions when modal opens - always refresh to get latest versions
|
||||
useEffect(() => {
|
||||
if (appId) {
|
||||
fetchVersions(appId);
|
||||
}
|
||||
}, [appId, fetchVersions]);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Get status badge for version
|
||||
const getVersionStatusBadge = (version) => {
|
||||
const status = version.status || '';
|
||||
// Use releasedVersionId to determine if version is released (same pattern as VersionDropdownItem)
|
||||
const isReleased = version.id === releasedVersionId;
|
||||
|
||||
if (status === 'DRAFT') {
|
||||
return { label: 'Draft', className: 'status-badge-draft' };
|
||||
} else if (isReleased) {
|
||||
return { label: 'Released', className: 'status-badge-released' };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Get parent version name for "Created from" text
|
||||
const getCreatedFromText = (version) => {
|
||||
if (version.parentVersionId) {
|
||||
// Look up the parent version to get its name
|
||||
const parentVersion = versions.find((v) => v.id === version.parentVersionId);
|
||||
const parentName = parentVersion?.name || `v${version.parentVersionId}`;
|
||||
return `Created from ${parentName}`;
|
||||
}
|
||||
return version.description || '';
|
||||
};
|
||||
|
||||
const selectedVersion = versions.find((v) => v.id === createFrom);
|
||||
const selectedBadge = selectedVersion ? getVersionStatusBadge(selectedVersion) : null;
|
||||
|
||||
const validateBranchName = (name) => {
|
||||
if (!name || name.trim().length === 0) {
|
||||
return 'Branch name is required';
|
||||
}
|
||||
|
||||
// No spaces allowed
|
||||
if (/\s/.test(name)) {
|
||||
return 'Branch name cannot contain spaces';
|
||||
}
|
||||
|
||||
// Only alphanumeric, hyphens, and underscores
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||
return 'Branch name can only contain letters, numbers, hyphens, and underscores';
|
||||
}
|
||||
|
||||
// Check for uniqueness
|
||||
const existingBranch = allBranches.find((b) => b.name?.toLowerCase() === name.toLowerCase());
|
||||
if (existingBranch) {
|
||||
return 'A branch with this name already exists';
|
||||
}
|
||||
|
||||
// Reserved names
|
||||
const reservedNames = ['main', 'master', 'head', 'origin'];
|
||||
if (reservedNames.includes(name.toLowerCase())) {
|
||||
return 'This branch name is reserved';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleBranchNameChange = (e) => {
|
||||
const newName = e.target.value;
|
||||
setBranchName(newName);
|
||||
|
||||
// Clear validation error when user starts typing
|
||||
if (validationError) {
|
||||
setValidationError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateBranch = async () => {
|
||||
// Validate branch name
|
||||
const error = validateBranchName(branchName);
|
||||
if (error) {
|
||||
setValidationError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
|
||||
// Determine the base branch - use current branch name or default to 'main'
|
||||
const baseBranchName = currentBranch?.name || 'main';
|
||||
|
||||
const branchData = {
|
||||
branchName: branchName.trim(),
|
||||
versionFromId: createFrom,
|
||||
baseBranch: baseBranchName,
|
||||
autoCommit: autoCommit,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await createBranch(appId, organizationId, branchData);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(`Branch "${branchName}" created successfully`);
|
||||
|
||||
// Refresh branches list and versions BEFORE switching
|
||||
// This ensures the new branch version is available when we call switchBranch
|
||||
await Promise.all([
|
||||
fetchBranches(appId, organizationId),
|
||||
lazyLoadAppVersions(appId),
|
||||
// Also fetch development versions since the new branch is in Development environment
|
||||
fetchDevelopmentVersions(appId),
|
||||
]);
|
||||
|
||||
console.log('CreateBranchModal - versions refreshed, now switching to branch:', branchName.trim());
|
||||
|
||||
// Switch to the newly created branch (similar to version creation)
|
||||
try {
|
||||
const switchResult = await switchBranch(appId, branchName.trim());
|
||||
console.log('CreateBranchModal - switchBranch result:', switchResult);
|
||||
} catch (switchError) {
|
||||
console.error('Error switching to new branch:', switchError);
|
||||
toast.error('Branch created but failed to switch to it');
|
||||
}
|
||||
|
||||
onSuccess?.(result.data);
|
||||
onClose();
|
||||
} else {
|
||||
// Handle specific errors
|
||||
if (result.error === 'DRAFT_EXISTS') {
|
||||
setShowDraftWarning(true);
|
||||
} else {
|
||||
setValidationError(result.error || 'Failed to create branch');
|
||||
toast.error(result.error || 'Failed to create branch');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating branch:', error);
|
||||
setValidationError('An unexpected error occurred');
|
||||
toast.error('Failed to create branch');
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !isCreating && !isDropdownOpen) {
|
||||
handleCreateBranch();
|
||||
} else if (e.key === 'Escape' && isDropdownOpen) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Set default "Create from" on mount
|
||||
useEffect(() => {
|
||||
if (versions.length > 0 && !createFrom) {
|
||||
// Filter to only version-type versions (exclude branches)
|
||||
const versionTypeVersions = versions.filter((v) => {
|
||||
const versionType = v.versionType || v.version_type;
|
||||
return versionType === 'version';
|
||||
});
|
||||
|
||||
if (versionTypeVersions.length === 0) {
|
||||
return; // No valid versions to select from
|
||||
}
|
||||
|
||||
// If editingVersion is a version-type, use it; otherwise use first valid version
|
||||
if (editingVersion?.id) {
|
||||
const editingVersionType = editingVersion.versionType || editingVersion.version_type;
|
||||
if (editingVersionType === 'version') {
|
||||
setCreateFrom(editingVersion.id);
|
||||
} else {
|
||||
// Current editing version is a branch, use first version-type version
|
||||
setCreateFrom(versionTypeVersions[0].id);
|
||||
}
|
||||
} else {
|
||||
setCreateFrom(versionTypeVersions[0].id);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [versions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertDialog
|
||||
show={true}
|
||||
closeModal={onClose}
|
||||
title="Create branch"
|
||||
checkForBackground={true}
|
||||
customClassName="create-branch-modal"
|
||||
>
|
||||
<div className="create-branch-modal-body">
|
||||
{/* Draft warning message */}
|
||||
{isDraftVersionActive && (
|
||||
<div className="draft-warning-message">
|
||||
<SolidIcon name="information" width="16" />
|
||||
<span>A draft version exists. Commit or discard it before creating a new branch.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create from dropdown */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="create-from-select" className="form-label">
|
||||
Create from version
|
||||
</label>
|
||||
<div className="custom-dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={cx('custom-dropdown-trigger', { 'is-open': isDropdownOpen })}
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
disabled={isCreating}
|
||||
>
|
||||
<div className="custom-dropdown-value">
|
||||
{selectedVersion ? (
|
||||
<>
|
||||
<span className="version-name">{selectedVersion.name}</span>
|
||||
{selectedBadge && (
|
||||
<span className={cx('status-badge', selectedBadge.className)}>{selectedBadge.label}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="version-name">Select version...</span>
|
||||
)}
|
||||
</div>
|
||||
<SolidIcon name="cheverondown" width="16" />
|
||||
</button>
|
||||
{isDropdownOpen && (
|
||||
<div className="custom-dropdown-menu">
|
||||
{versions
|
||||
.filter((version) => {
|
||||
// Only show versions with versionType === 'version' (exclude branch-type versions)
|
||||
const versionType = version.versionType || version.version_type;
|
||||
return versionType === 'version';
|
||||
})
|
||||
.map((version) => {
|
||||
const badge = getVersionStatusBadge(version);
|
||||
const createdFrom = getCreatedFromText(version);
|
||||
const isSelected = version.id === createFrom;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={version.id}
|
||||
className={cx('dropdown-item', { 'is-selected': isSelected })}
|
||||
onClick={() => {
|
||||
setCreateFrom(version.id);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="check-icon">
|
||||
<SolidIcon name="tick" width="16" />
|
||||
</div>
|
||||
)}
|
||||
{!isSelected && <div className="check-icon-placeholder" />}
|
||||
<div className="item-content">
|
||||
<div className="item-header">
|
||||
<span className="item-name">{version.name}</span>
|
||||
{badge && <span className={cx('status-badge', badge.className)}>{badge.label}</span>}
|
||||
</div>
|
||||
{createdFrom && <div className="item-description">{createdFrom}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Branch name input */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="branch-name-input" className="form-label">
|
||||
Branch name
|
||||
</label>
|
||||
<input
|
||||
id="branch-name-input"
|
||||
type="text"
|
||||
className={`branch-modal-form-input ${validationError ? 'form-input-error' : ''}`}
|
||||
placeholder="Enter branch name"
|
||||
value={branchName}
|
||||
onChange={handleBranchNameChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isCreating}
|
||||
autoFocus
|
||||
/>
|
||||
{validationError && <div className="form-error-message">{validationError}</div>}
|
||||
<div className="form-helper-text">Branch name must be unique and max 50 characters</div>
|
||||
</div>
|
||||
|
||||
{/* Auto-commit checkbox */}
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-checkbox"
|
||||
checked={autoCommit}
|
||||
onChange={(e) => setAutoCommit(e.target.checked)}
|
||||
disabled={true}
|
||||
/>
|
||||
<span className="checkbox-text">
|
||||
Commit changes
|
||||
<span className="checkbox-helper">
|
||||
Branch will always be created in git to ensure sync with ToolJet
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Info message about branch creation */}
|
||||
<Alert placeSvgTop={true} svg="warning-icon" cls="create-branch-info">
|
||||
Branch can only be created from master
|
||||
</Alert>
|
||||
|
||||
{/* Footer buttons */}
|
||||
<div className="col d-flex justify-content-end gap-2 mt-3">
|
||||
<ButtonSolid variant="tertiary" onClick={onClose} disabled={isCreating} size="md">
|
||||
Cancel
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
variant="primary"
|
||||
onClick={handleCreateBranch}
|
||||
disabled={isCreating || isDraftVersionActive || !branchName.trim()}
|
||||
isLoading={isCreating}
|
||||
size="md"
|
||||
>
|
||||
{'Create branch'}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Draft Version Warning Modal */}
|
||||
{showDraftWarning && <DraftVersionWarningModal onClose={() => setShowDraftWarning(false)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,20 +9,14 @@ import useStore from '@/AppBuilder/_stores/store';
|
|||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import '../../_styles/version-modal.scss';
|
||||
import { useVersionManagerStore } from '@/_stores/versionManagerStore';
|
||||
|
||||
const CreateDraftVersionModal = ({
|
||||
showCreateAppVersion,
|
||||
setShowCreateAppVersion,
|
||||
handleCommitEnableChange,
|
||||
canCommit,
|
||||
orgGit,
|
||||
fetchingOrgGit,
|
||||
handleCommitOnVersionCreation = () => { },
|
||||
}) => {
|
||||
const CreateDraftVersionModal = ({ showCreateAppVersion, setShowCreateAppVersion, fetchingOrgGit }) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const [isCreatingVersion, setIsCreatingVersion] = useState(false);
|
||||
const [versionName, setVersionName] = useState('');
|
||||
const [isGitSyncEnabled, setIsGitSyncEnabled] = useState(false);
|
||||
const refreshVersions = useVersionManagerStore((state) => state.refreshVersions);
|
||||
const {
|
||||
createNewVersionAction,
|
||||
changeEditorVersionAction,
|
||||
|
|
@ -30,6 +24,9 @@ const CreateDraftVersionModal = ({
|
|||
developmentVersions,
|
||||
appId,
|
||||
selectedVersion,
|
||||
selectedEnvironment,
|
||||
orgGit,
|
||||
appGit,
|
||||
} = useStore(
|
||||
(state) => ({
|
||||
createNewVersionAction: state.createNewVersionAction,
|
||||
|
|
@ -42,6 +39,8 @@ const CreateDraftVersionModal = ({
|
|||
appId: state.appStore.modules[moduleId].app.appId,
|
||||
currentVersionId: state.currentVersionId,
|
||||
selectedVersion: state.selectedVersion,
|
||||
appGit: state.appGit,
|
||||
orgGit: state.orgGit,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
|
@ -50,12 +49,11 @@ const CreateDraftVersionModal = ({
|
|||
const savedVersions = developmentVersions.filter((version) => version.status !== 'DRAFT');
|
||||
useEffect(() => {
|
||||
const gitSyncEnabled =
|
||||
orgGit?.git_ssh?.is_enabled ||
|
||||
orgGit?.git_https?.is_enabled ||
|
||||
orgGit?.git_lab?.is_enabled;
|
||||
appGit?.org_git?.git_ssh?.is_enabled ||
|
||||
appGit?.org_git?.git_https?.is_enabled ||
|
||||
appGit?.org_git?.git_lab?.is_enabled;
|
||||
setIsGitSyncEnabled(gitSyncEnabled);
|
||||
}, [orgGit]);
|
||||
|
||||
}, [appGit]);
|
||||
const [selectedVersionForCreation, setSelectedVersionForCreation] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -112,8 +110,8 @@ const CreateDraftVersionModal = ({
|
|||
savedVersions.length > 0
|
||||
? savedVersions.map((version) => ({ label: version.name, value: version.id }))
|
||||
: selectedVersion && selectedVersion.status !== 'DRAFT'
|
||||
? [{ label: selectedVersion.name, value: selectedVersion.id }]
|
||||
: [];
|
||||
? [{ label: selectedVersion.name, value: selectedVersion.id }]
|
||||
: [];
|
||||
|
||||
const createVersion = () => {
|
||||
if (versionName.trim().length > 25) {
|
||||
|
|
@ -145,14 +143,13 @@ const CreateDraftVersionModal = ({
|
|||
setShowCreateAppVersion(false);
|
||||
// Refresh development versions to update the list with the new draft
|
||||
fetchDevelopmentVersions(appId);
|
||||
refreshVersions(appId, selectedEnvironment?.id);
|
||||
// Use changeEditorVersionAction to properly switch to the new draft version
|
||||
// This will update selectedVersion with all fields including status
|
||||
changeEditorVersionAction(
|
||||
appId,
|
||||
newVersion.id,
|
||||
(data) => {
|
||||
handleCommitOnVersionCreation(data);
|
||||
},
|
||||
() => {},
|
||||
(error) => {
|
||||
console.error('Error switching to new draft version:', error);
|
||||
toast.error('Draft created but failed to switch to it');
|
||||
|
|
@ -261,28 +258,6 @@ const CreateDraftVersionModal = ({
|
|||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
{isGitSyncEnabled && (
|
||||
<div className="commit-changes mb-3">
|
||||
<div>
|
||||
<input
|
||||
className="form-check-input"
|
||||
checked={canCommit}
|
||||
type="checkbox"
|
||||
onChange={handleCommitEnableChange}
|
||||
data-cy="git-commit-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="tj-text tj-text-xsm" data-cy="commit-changes-label">
|
||||
Commit changes
|
||||
</div>
|
||||
<div className="tj-text-xxsm" data-cy="commit-helper-text">
|
||||
This will commit the creation of the new version to the git repo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="create-draft-version-footer">
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@ const CreateVersionModal = ({
|
|||
canCommit,
|
||||
orgGit,
|
||||
fetchingOrgGit,
|
||||
handleCommitOnVersionCreation = () => { },
|
||||
handleCommitOnVersionCreation,
|
||||
versionId,
|
||||
onVersionCreated,
|
||||
isBranchingEnabled,
|
||||
}) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const setResolvedGlobals = useStore((state) => state.setResolvedGlobals, shallow);
|
||||
|
|
@ -28,6 +29,8 @@ const CreateVersionModal = ({
|
|||
const [versionName, setVersionName] = useState('');
|
||||
const [versionDescription, setVersionDescription] = useState('');
|
||||
const isGitSyncEnabled = orgGit?.git_ssh?.is_enabled || orgGit?.git_https?.is_enabled || orgGit?.git_lab?.is_enabled;
|
||||
const { current_organization_id } = authenticationService.currentSessionValue;
|
||||
|
||||
const {
|
||||
changeEditorVersionAction,
|
||||
environmentChangedAction,
|
||||
|
|
@ -39,6 +42,7 @@ const CreateVersionModal = ({
|
|||
currentEnvironment,
|
||||
environments,
|
||||
setIsEditorFreezed,
|
||||
appGit,
|
||||
} = useStore(
|
||||
(state) => ({
|
||||
changeEditorVersionAction: state.changeEditorVersionAction,
|
||||
|
|
@ -55,6 +59,7 @@ const CreateVersionModal = ({
|
|||
currentEnvironment: state.selectedEnvironment,
|
||||
environments: state.environments,
|
||||
setIsEditorFreezed: state.setIsEditorFreezed,
|
||||
appGit: state.appGit,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
|
@ -146,12 +151,77 @@ const CreateVersionModal = ({
|
|||
setIsCreatingVersion(true);
|
||||
|
||||
try {
|
||||
if (isGitSyncEnabled && isBranchingEnabled) {
|
||||
if (!appGit?.git_app_name || !appGit?.id) {
|
||||
toast.error(
|
||||
"Empty apps can't be versioned. Build your app first and then save your work through version control."
|
||||
);
|
||||
setIsCreatingVersion(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tagCheck = await gitSyncService.checkTagExists(appId, versionName.trim());
|
||||
if (tagCheck.exists) {
|
||||
toast.error(
|
||||
`Cannot save: Tag '${tagCheck.tagName}' already exists. ` +
|
||||
`Please rename your version to a unique name before saving.`
|
||||
);
|
||||
setIsCreatingVersion(false);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// If check fails, log warning but allow save to proceed
|
||||
// (tag creation will fail separately if there's an issue)
|
||||
console.warn('Tag existence check failed, proceeding with save', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Only call git-related APIs if git sync is enabled
|
||||
await appVersionService.save(appId, selectedVersionForCreation.id, {
|
||||
name: versionName,
|
||||
description: versionDescription,
|
||||
description: versionDescription || undefined,
|
||||
// need to add commit changes logic here
|
||||
status: 'PUBLISHED',
|
||||
});
|
||||
// if (isGitSyncEnabled) {
|
||||
// // The backend's version-rename-commit event is suppressed when the status is
|
||||
// // also changing to PUBLISHED (save-version flow), so there's no competing push.
|
||||
// // We always handle the commit here with the correct "Version Created" message.
|
||||
// const updatedVersionData = {
|
||||
// ...selectedVersionForCreation,
|
||||
// name: versionName,
|
||||
// description: versionDescription,
|
||||
// };
|
||||
// handleCommitOnVersionCreation(updatedVersionData, selectedVersion)
|
||||
// .then((commitDone) => {
|
||||
// if (!commitDone) return;
|
||||
// if (isBranchingEnabled) {
|
||||
// return gitSyncService.createGitTag(
|
||||
// appId,
|
||||
// selectedVersionForCreation.id,
|
||||
// versionDescription || `Version ${versionName.trim()} created`
|
||||
// );
|
||||
// }
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error('Commit or tag failed:', error);
|
||||
// toast.error(error?.data?.message || 'Commit or tag failed');
|
||||
// });
|
||||
// }
|
||||
|
||||
if (isGitSyncEnabled && isBranchingEnabled) {
|
||||
gitSyncService
|
||||
.createGitTag(
|
||||
appId,
|
||||
selectedVersionForCreation.id,
|
||||
versionDescription || `Version ${versionName.trim()} created`
|
||||
)
|
||||
.catch((error) => {
|
||||
toast.error(error?.data?.message || 'Tag creation failed');
|
||||
});
|
||||
}
|
||||
|
||||
toast.success('Version Created successfully');
|
||||
setVersionName('');
|
||||
setVersionDescription('');
|
||||
|
|
@ -194,10 +264,7 @@ const CreateVersionModal = ({
|
|||
changeEditorVersionAction(
|
||||
appId,
|
||||
newVersionData.editing_version.id,
|
||||
() => {
|
||||
console.log('Successfully switched environment and version');
|
||||
handleCommitOnVersionCreation(newVersionData, selectedVersion);
|
||||
},
|
||||
() => {},
|
||||
(error) => {
|
||||
console.error('Error switching to newly created version:', error);
|
||||
toast.error('Version created but failed to switch to it');
|
||||
|
|
@ -210,9 +277,7 @@ const CreateVersionModal = ({
|
|||
await changeEditorVersionAction(
|
||||
appId,
|
||||
newVersionData.editing_version.id,
|
||||
() => {
|
||||
handleCommitOnVersionCreation(newVersionData, selectedVersion);
|
||||
},
|
||||
() => {},
|
||||
(error) => {
|
||||
console.error('Error switching to newly created version:', error);
|
||||
toast.error('Version created but failed to switch to it');
|
||||
|
|
@ -229,8 +294,7 @@ const CreateVersionModal = ({
|
|||
toast.error('Version name already exists.');
|
||||
} else if (error?.error) {
|
||||
toast.error(error?.error);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
toast.error('Error while creating version. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -325,7 +389,8 @@ const CreateVersionModal = ({
|
|||
</div>
|
||||
</div> */}
|
||||
|
||||
{isGitSyncEnabled && (
|
||||
{/* Disabling autoCommit */}
|
||||
{/* {isGitSyncEnabled && (
|
||||
<div className="commit-changes mt-3">
|
||||
<div>
|
||||
<input
|
||||
|
|
@ -333,6 +398,7 @@ const CreateVersionModal = ({
|
|||
checked={canCommit}
|
||||
type="checkbox"
|
||||
onChange={handleCommitEnableChange}
|
||||
disabled={isBranchingEnabled}
|
||||
data-cy="git-commit-input"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -345,7 +411,8 @@ const CreateVersionModal = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<div className="mt-3">
|
||||
<Alert placeSvgTop={true} svg="warning-icon" className="create-version-alert">
|
||||
<div
|
||||
|
|
|
|||
66
frontend/src/AppBuilder/Header/DraftVersionWarningModal.jsx
Normal file
66
frontend/src/AppBuilder/Header/DraftVersionWarningModal.jsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import React from 'react';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import '@/_styles/draft-version-warning-modal.scss';
|
||||
|
||||
export function DraftVersionWarningModal({ onClose }) {
|
||||
const handleOverlayClick = (e) => {
|
||||
if (e.target.classList.contains('draft-warning-modal-overlay')) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="draft-warning-modal-overlay" onClick={handleOverlayClick}>
|
||||
<div className="draft-warning-modal">
|
||||
<div className="draft-warning-modal-header">
|
||||
<div className="warning-icon-container">
|
||||
<SolidIcon name="warningtriangle" width="24" />
|
||||
</div>
|
||||
<button className="close-button" onClick={onClose} aria-label="Close modal">
|
||||
<SolidIcon name="remove" width="16" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="draft-warning-modal-body">
|
||||
<h3 className="warning-title">Draft Version Exists</h3>
|
||||
<p className="warning-message">
|
||||
You cannot create a new branch while a draft version exists. Please commit or discard the current draft
|
||||
version before creating a new branch.
|
||||
</p>
|
||||
|
||||
<div className="warning-info-box">
|
||||
<SolidIcon name="information" width="16" />
|
||||
<div className="info-text">
|
||||
<strong>What's a draft version?</strong>
|
||||
<p>
|
||||
A draft version contains uncommitted changes. Only one draft version can exist at a time to prevent
|
||||
conflicts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="draft-warning-modal-footer">
|
||||
<ButtonSolid variant="primary" onClick={onClose} className="close-action-button">
|
||||
Close
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,16 +10,39 @@ import { PenLine } from 'lucide-react';
|
|||
|
||||
function EditAppName() {
|
||||
const { moduleId } = useModuleContext();
|
||||
const [appId, appName, setAppName, appCreationMode] = useStore(
|
||||
const [appId, appName, setAppName, appCreationMode, selectedVersion, orgGit, appGit] = useStore(
|
||||
(state) => [
|
||||
state.appStore.modules[moduleId].app.appId,
|
||||
state.appStore.modules[moduleId].app.appName,
|
||||
state.setAppName,
|
||||
state.appStore.modules[moduleId].app.creationMode,
|
||||
state.selectedVersion,
|
||||
state.orgGit,
|
||||
state.appGit,
|
||||
],
|
||||
shallow
|
||||
);
|
||||
|
||||
const isDraftVersion = selectedVersion?.status === 'DRAFT';
|
||||
const isGitSyncEnabled = orgGit?.git_ssh?.is_enabled || orgGit?.git_https?.is_enabled || orgGit?.git_lab?.is_enabled;
|
||||
const isAppCommittedToGit = !!appGit?.id;
|
||||
const isOnDefaultBranch = selectedVersion?.versionType !== 'branch';
|
||||
const isRenameDisabled = !isGitSyncEnabled
|
||||
? false
|
||||
: !isAppCommittedToGit
|
||||
? !isDraftVersion
|
||||
: !isDraftVersion || isOnDefaultBranch;
|
||||
|
||||
const getDisabledTooltipMessage = () => {
|
||||
if (isGitSyncEnabled && isAppCommittedToGit && isOnDefaultBranch) {
|
||||
return "Renaming isn't allowed on master. Switch branch to update name.";
|
||||
}
|
||||
if (!isDraftVersion && isGitSyncEnabled) {
|
||||
return 'Renaming of app is only allowed on draft versions';
|
||||
}
|
||||
return appName;
|
||||
};
|
||||
|
||||
const [showRenameModal, setShowRenameModal] = useState(false);
|
||||
|
||||
const handleRenameApp = async (newAppName, appId) => {
|
||||
|
|
@ -31,7 +54,7 @@ function EditAppName() {
|
|||
return true;
|
||||
}
|
||||
try {
|
||||
await appsService.saveApp(appId, { name: sanitizedName });
|
||||
await appsService.saveApp(appId, { name: sanitizedName, editingVersionId: selectedVersion?.id });
|
||||
setAppName(sanitizedName);
|
||||
toast.success('App name has been updated!');
|
||||
return true;
|
||||
|
|
@ -48,12 +71,21 @@ function EditAppName() {
|
|||
return (
|
||||
<>
|
||||
<div className="tw-h-full tw-flex tw-items-start tw-justify-start">
|
||||
<ToolTip message={appName} placement="bottom" isVisible={appCreationMode !== 'GIT'}>
|
||||
<ToolTip
|
||||
message={getDisabledTooltipMessage()}
|
||||
placement="bottom"
|
||||
isVisible={appCreationMode !== 'GIT' || isRenameDisabled}
|
||||
>
|
||||
<button
|
||||
className="edit-app-name-button tw-h-8 tw-min-w-[100px] tw-rounded-lg tw-pr-1 tw-w-auto tw-font-medium tw-cursor-pointer tw-outline-none tw-bg-transparent tw-border tw-border-transparent hover:tw-border-border-strong tw-shadow-none tw-group tw-transition-all tw-duration-300 tw-flex tw-items-center tw-relative tw-justify-start"
|
||||
className="edit-app-name-button tw-h-8 tw-min-w-[100px] tw-rounded-lg tw-pr-1 tw-w-auto tw-font-medium tw-outline-none tw-bg-transparent tw-border tw-border-transparent tw-shadow-none tw-group tw-transition-all tw-duration-300 tw-flex tw-items-center tw-relative tw-justify-start"
|
||||
style={{
|
||||
cursor: isRenameDisabled ? 'not-allowed' : 'pointer',
|
||||
opacity: isRenameDisabled ? 0.6 : 1,
|
||||
}}
|
||||
type="button"
|
||||
data-cy="edit-app-name-button"
|
||||
onClick={() => setShowRenameModal(true)}
|
||||
onClick={() => !isRenameDisabled && setShowRenameModal(true)}
|
||||
disabled={isRenameDisabled}
|
||||
>
|
||||
<span
|
||||
className="tw-font-title-large tw-truncate tw-w-full tw-block tw-text-start group-hover:tw-w-[calc(100%-24px)] tw-text-[var(--slate12)]"
|
||||
|
|
@ -61,9 +93,11 @@ function EditAppName() {
|
|||
>
|
||||
{appName}
|
||||
</span>
|
||||
<span className="tw-absolute tw-right-0.5 tw-top-1 tw-text-icon-default tw-hidden group-hover:tw-block tw-w-7 tw-h-7 tw-items-center tw-justify-center">
|
||||
<PenLine width="16" height="16" name="pencil" />
|
||||
</span>
|
||||
{!isRenameDisabled && (
|
||||
<span className="tw-absolute tw-right-0.5 tw-top-1 tw-text-icon-default tw-hidden group-hover:tw-block tw-w-7 tw-h-7 tw-items-center tw-justify-center">
|
||||
<PenLine width="16" height="16" name="pencil" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,21 +6,24 @@ import LogoNavDropdown from '@/modules/Appbuilder/components/LogoNavDropdown';
|
|||
import HeaderActions from './HeaderActions';
|
||||
import { VersionManagerDropdown, VersionManagerErrorBoundary } from './VersionManager';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import RightTopHeaderButtons from './RightTopHeaderButtons/RightTopHeaderButtons';
|
||||
|
||||
import RightTopHeaderButtons, { PreviewAndShareIcons } from './RightTopHeaderButtons/RightTopHeaderButtons';
|
||||
import { ModuleEditorBanner } from '@/modules/Modules/components';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { BranchDropdown } from './BranchDropdown';
|
||||
import './styles/style.scss';
|
||||
|
||||
import SaveIndicator from './SaveIndicator';
|
||||
|
||||
export const EditorHeader = ({ darkMode }) => {
|
||||
const { moduleId, isModuleEditor } = useModuleContext();
|
||||
const { isSaving, saveError, isVersionReleased } = useStore(
|
||||
const { isSaving, saveError, isVersionReleased, appId, organizationId, selectedVersion } = useStore(
|
||||
(state) => ({
|
||||
isSaving: state.appStore.modules[moduleId].app.isSaving,
|
||||
saveError: state.appStore.modules[moduleId].app.saveError,
|
||||
isVersionReleased: state.isVersionReleased,
|
||||
appId: state.appStore.modules[moduleId].app.appId,
|
||||
organizationId: state.appStore.modules[moduleId].app.organizationId,
|
||||
selectedVersion: state.selectedVersion,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
|
@ -51,7 +54,7 @@ export const EditorHeader = ({ darkMode }) => {
|
|||
<LogoNavDropdown darkMode={darkMode} />
|
||||
</h1>
|
||||
<div className="d-flex flex-row tw-mr-1">
|
||||
{isModuleEditor && <ModuleEditorBanner />}
|
||||
{isModuleEditor && <ModuleEditorBanner showBeta={true} />}
|
||||
<EditAppName />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -78,9 +81,14 @@ export const EditorHeader = ({ darkMode }) => {
|
|||
<div className="d-flex version-manager-container p-0 align-items-center gap-0">
|
||||
{!isModuleEditor && (
|
||||
<>
|
||||
<VersionManagerErrorBoundary>
|
||||
<VersionManagerDropdown darkMode={darkMode} />
|
||||
</VersionManagerErrorBoundary>
|
||||
<PreviewAndShareIcons />
|
||||
{<BranchDropdown appId={appId} organizationId={organizationId} />}
|
||||
{/* Hide version dropdown when on a feature branch */}
|
||||
{selectedVersion?.versionType !== 'branch' && (
|
||||
<VersionManagerErrorBoundary>
|
||||
<VersionManagerDropdown darkMode={darkMode} />
|
||||
</VersionManagerErrorBoundary>
|
||||
)}
|
||||
<RightTopHeaderButtons isModuleEditor={isModuleEditor} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import React from 'react';
|
|||
import cx from 'classnames';
|
||||
import Branch from '@assets/images/icons/branch.svg';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const FreezeVersionInfo = ({
|
||||
info = 'App cannot be edited after promotion. Please create a new version from Development to make any changes.',
|
||||
hide = false,
|
||||
}) => {
|
||||
const isViewOnly = useStore((state) => state.getShouldFreeze());
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const isViewOnly = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const isAiOperationInProgress = useStore((state) => state?.ai?.isLoading);
|
||||
|
||||
if (!isViewOnly || hide || isAiOperationInProgress) return null;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import { Link } from 'react-router-dom';
|
|||
import { useAppPreviewLink } from '@/_hooks/useAppPreviewLink';
|
||||
import { ToggleLayoutButtons } from './ToggleLayoutButtons';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const HeaderActions = function HeaderActions ({ darkMode, showFullWidth, showPreviewBtn = true }) {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const {
|
||||
currentLayout,
|
||||
canUndo,
|
||||
|
|
@ -25,8 +27,8 @@ const HeaderActions = function HeaderActions ({ darkMode, showFullWidth, showPre
|
|||
} = useStore(
|
||||
(state) => ({
|
||||
currentLayout: state.currentLayout,
|
||||
canUndo: state.canUndo && !(state.isEditorFreezed || state.isVersionReleased),
|
||||
canRedo: state.canRedo && !(state.isEditorFreezed || state.isVersionReleased),
|
||||
canUndo: state.canUndo && !state.getShouldFreeze(false, isModuleEditor),
|
||||
canRedo: state.canRedo && !state.getShouldFreeze(false, isModuleEditor),
|
||||
toggleCurrentLayout: state.toggleCurrentLayout,
|
||||
showToggleLayoutBtn: state.showToggleLayoutBtn,
|
||||
showUndoRedoBtn: state.showUndoRedoBtn,
|
||||
|
|
|
|||
96
frontend/src/AppBuilder/Header/LifecycleCTAButton.jsx
Normal file
96
frontend/src/AppBuilder/Header/LifecycleCTAButton.jsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import React from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { Button } from '@/components/ui/Button/Button';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
/**
|
||||
* LifecycleCTAButton - Dynamic button that shows git operations based on branch type
|
||||
*
|
||||
* States:
|
||||
* - Default Branch: "Pull commit" - Opens git sync modal with pull/push tabs
|
||||
* - Feature Branch: "Commit" - Opens git sync modal to commit changes
|
||||
*/
|
||||
const LifecycleCTAButton = () => {
|
||||
const { moduleId } = useModuleContext();
|
||||
|
||||
const { selectedVersion, toggleGitSyncModal, creationMode, featureAccess, isEditorFreezed, isGitSyncConfigured } =
|
||||
useStore(
|
||||
(state) => ({
|
||||
selectedVersion: state.selectedVersion,
|
||||
toggleGitSyncModal: state.toggleGitSyncModal,
|
||||
creationMode: state.appStore.modules[moduleId]?.app?.creationMode,
|
||||
featureAccess: state?.license?.featureAccess,
|
||||
isEditorFreezed: state.isEditorFreezed,
|
||||
isGitSyncConfigured: state.isGitSyncConfigured,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
||||
const isGitSyncEnabled = featureAccess?.gitSync;
|
||||
|
||||
// If git sync is not available in the plan or license is expired, hide completely
|
||||
if (!isGitSyncEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine if we're on default branch or feature branch
|
||||
// - versionType === 'version' means default branch
|
||||
// - versionType === 'branch' means feature branch
|
||||
const isOnDefaultBranch = selectedVersion?.versionType === 'version' || selectedVersion?.versionType !== 'branch';
|
||||
|
||||
// Determine button state based on git configuration and branch type
|
||||
const getButtonConfig = () => {
|
||||
if (!isGitSyncConfigured) {
|
||||
// Git is in the plan but not configured in the workspace
|
||||
return {
|
||||
label: 'Configure Git',
|
||||
icon: 'commit',
|
||||
variant: 'secondary',
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (isOnDefaultBranch) {
|
||||
// Default branch - show "Pull commit" button
|
||||
return {
|
||||
label: 'Pull commit',
|
||||
icon: 'commit',
|
||||
variant: 'secondary',
|
||||
disabled: false,
|
||||
};
|
||||
} else {
|
||||
// Feature branch - show "Commit" button
|
||||
return {
|
||||
label: 'Commit',
|
||||
icon: 'commit',
|
||||
variant: 'secondary',
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getButtonConfig();
|
||||
const handleClick = () => {
|
||||
// Open the git sync modal (which has pull/push tabs)
|
||||
toggleGitSyncModal(creationMode);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="lifecycle-cta-button">
|
||||
<Button
|
||||
variant={config.variant}
|
||||
onClick={handleClick}
|
||||
disabled={config.disabled}
|
||||
data-tooltip-id="editor-header-tooltip"
|
||||
data-tooltip-content={config.tooltip}
|
||||
>
|
||||
<SolidIcon fill="var(--icon-accent)" viewBox="0 0 16 16" name={config.icon} width="16" />
|
||||
<span>{config.label}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LifecycleCTAButton;
|
||||
53
frontend/src/AppBuilder/Header/LockedBranchBanner.jsx
Normal file
53
frontend/src/AppBuilder/Header/LockedBranchBanner.jsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import '@/_styles/locked-branch-banner.scss';
|
||||
|
||||
/**
|
||||
* LockedBranchBanner - Displays a full-width warning banner when viewing a read-only branch
|
||||
* Shows below the editor navigation bar when current branch is locked (merged/released)
|
||||
*
|
||||
* @param {boolean} isVisible - Whether to show the banner
|
||||
* @param {string} branchName - Name of the locked branch
|
||||
* @param {string} reason - Reason why branch is locked (e.g., "merged", "released")
|
||||
*/
|
||||
const LockedBranchBanner = ({ isVisible = false, branchName = '', reason = 'merged' }) => {
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reasonText =
|
||||
reason === 'released'
|
||||
? 'This branch has been released and is now read-only'
|
||||
: reason === 'main_config_branch'
|
||||
? `${branchName} is locked. Create a branch to make edits.`
|
||||
: 'This branch has been merged and is now read-only';
|
||||
|
||||
return (
|
||||
<div className="locked-branch-banner">
|
||||
<div className="locked-branch-banner-content">
|
||||
<svg
|
||||
className="locked-branch-banner-icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.6667 7.33333H12V5.33333C12 3.49238 10.5076 2 8.66667 2C6.82572 2 5.33333 3.49238 5.33333 5.33333V7.33333H4.66667C3.93029 7.33333 3.33333 7.93029 3.33333 8.66667V12.6667C3.33333 13.403 3.93029 14 4.66667 14H12.6667C13.403 14 14 13.403 14 12.6667V8.66667C14 7.93029 13.403 7.33333 12.6667 7.33333ZM6.66667 5.33333C6.66667 4.22876 7.56209 3.33333 8.66667 3.33333C9.77124 3.33333 10.6667 4.22876 10.6667 5.33333V7.33333H6.66667V5.33333Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<div className="locked-branch-banner-text">
|
||||
<span className="locked-branch-banner-message">{reasonText}</span>
|
||||
{branchName && (
|
||||
<span className="locked-branch-banner-branch">
|
||||
Branch: <strong>{branchName}</strong>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LockedBranchBanner;
|
||||
|
|
@ -6,24 +6,34 @@ import { shallow } from 'zustand/shallow';
|
|||
import queryString from 'query-string';
|
||||
import { isEmpty } from 'lodash';
|
||||
import GitSyncManager from '../GitSyncManager';
|
||||
import LifecycleCTAButton from '../LifecycleCTAButton';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import PromoteReleaseButton from '@/modules/Appbuilder/components/PromoteReleaseButton';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const RightTopHeaderButtons = ({ isModuleEditor }) => {
|
||||
const { selectedVersion, selectedEnvironment } = useStore((state) => ({
|
||||
selectedVersion: state.selectedVersion,
|
||||
selectedEnvironment: state.selectedEnvironment,
|
||||
}));
|
||||
|
||||
const isNotPromotedOrReleased = selectedEnvironment?.name === 'development' && !selectedVersion?.isReleased;
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-end navbar-right-section">
|
||||
<div className=" release-buttons">
|
||||
<GitSyncManager />
|
||||
<div className="tw-hidden navbar-seperator" />
|
||||
<PreviewAndShareIcons />
|
||||
{/* <PreviewAndShareIcons /> */}
|
||||
{isNotPromotedOrReleased && <LifecycleCTAButton />}
|
||||
{/* need to review if we need this or not */}
|
||||
{/* {!isModuleEditor && <PromoteReleaseButton />} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PreviewAndShareIcons = () => {
|
||||
export const PreviewAndShareIcons = () => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const {
|
||||
featureAccess,
|
||||
|
|
@ -73,7 +83,7 @@ const PreviewAndShareIcons = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="preview-share-wrap navbar-nav flex-row tw-mr-1">
|
||||
<div className="preview-share-wrap navbar-nav flex-row">
|
||||
<div className="nav-item">
|
||||
{appId && (
|
||||
<ManageAppUsers
|
||||
|
|
|
|||
269
frontend/src/AppBuilder/Header/SwitchBranchModal.jsx
Normal file
269
frontend/src/AppBuilder/Header/SwitchBranchModal.jsx
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import AlertDialog from '@/_ui/AlertDialog';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { CreateBranchModal } from './CreateBranchModal';
|
||||
import '@/_styles/switch-branch-modal.scss';
|
||||
|
||||
export function SwitchBranchModal({ show, onClose, appId, organizationId }) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
|
||||
const {
|
||||
allBranches,
|
||||
selectedVersion,
|
||||
currentBranch,
|
||||
fetchBranches,
|
||||
switchBranch,
|
||||
switchToDefaultBranch,
|
||||
setCurrentBranch,
|
||||
orgGit,
|
||||
lazyLoadAppVersions,
|
||||
fetchDevelopmentVersions,
|
||||
appVersions,
|
||||
} = useStore((state) => ({
|
||||
allBranches: state.allBranches,
|
||||
selectedVersion: state.selectedVersion,
|
||||
currentBranch: state.currentBranch,
|
||||
fetchBranches: state.fetchBranches,
|
||||
switchBranch: state.switchBranch,
|
||||
switchToDefaultBranch: state.switchToDefaultBranch,
|
||||
setCurrentBranch: state.setCurrentBranch,
|
||||
orgGit: state.orgGit,
|
||||
lazyLoadAppVersions: state.lazyLoadAppVersions,
|
||||
fetchDevelopmentVersions: state.fetchDevelopmentVersions,
|
||||
appVersions: state.appVersions,
|
||||
}));
|
||||
|
||||
const defaultBranchName = orgGit?.git_https?.github_branch || orgGit?.git_ssh?.github_branch || 'main';
|
||||
// Determine current branch name: use selectedVersion.name for branches, or default branch name for versions
|
||||
const currentBranchName =
|
||||
selectedVersion?.versionType === 'branch' || selectedVersion?.version_type === 'branch'
|
||||
? selectedVersion?.name
|
||||
: selectedVersion?.versionType === 'version' || selectedVersion?.version_type === 'version'
|
||||
? defaultBranchName
|
||||
: currentBranch?.name || defaultBranchName;
|
||||
|
||||
useEffect(() => {
|
||||
if (show && appId && organizationId) {
|
||||
setIsLoading(true);
|
||||
// Fetch branches, versions, and development versions for proper branch switching
|
||||
Promise.all([
|
||||
fetchBranches(appId, organizationId),
|
||||
lazyLoadAppVersions(appId),
|
||||
fetchDevelopmentVersions(appId),
|
||||
]).finally(() => setIsLoading(false));
|
||||
}
|
||||
}, [show, appId, organizationId, fetchBranches, lazyLoadAppVersions, fetchDevelopmentVersions]);
|
||||
|
||||
// Filter branches: exclude branches that are version names (versionType === 'version')
|
||||
const filteredBranches = allBranches.filter((branch) => {
|
||||
// Apply search filter
|
||||
if (!branch.name.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this branch name corresponds to a version with versionType === 'version'
|
||||
// If so, exclude it (it's a version name, not an actual branch)
|
||||
const isVersionName = appVersions?.some(
|
||||
(v) => v.name === branch.name && (v.versionType === 'version' || v.version_type === 'version')
|
||||
);
|
||||
|
||||
// Show the branch only if it's NOT a version name
|
||||
return !isVersionName;
|
||||
});
|
||||
|
||||
const handleBranchClick = async (branch) => {
|
||||
if (branch.name === currentBranchName) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine if this is the default branch
|
||||
const defaultBranchName = orgGit?.git_https?.github_branch || orgGit?.git_ssh?.github_branch || 'main';
|
||||
const isDefaultBranch = branch.name === defaultBranchName;
|
||||
|
||||
if (isDefaultBranch) {
|
||||
// Switch to default branch (finds active draft or latest version)
|
||||
const result = await switchToDefaultBranch(appId, branch.name);
|
||||
if (result.success) {
|
||||
setCurrentBranch(branch);
|
||||
if (result.isDraft) {
|
||||
toast.success(`Switched to ${branch.name} - Working on draft version`);
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
} else {
|
||||
// Switch to feature branch
|
||||
await switchBranch(appId, branch.name);
|
||||
setCurrentBranch(branch);
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching branch:', error);
|
||||
const errorMessage = error?.error || error?.message || 'Failed to switch branch';
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const getRelativeTime = (dateString) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) return 'today';
|
||||
if (diffDays === 1) return 'yesterday';
|
||||
if (diffDays < 7) return `${diffDays} days ago`;
|
||||
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
|
||||
return `${Math.floor(diffDays / 30)} months ago`;
|
||||
};
|
||||
|
||||
const handleViewInGitRepo = () => {
|
||||
// Get repository URL from orgGit (check https_url, ssh_url, or repository fields)
|
||||
const repoUrl =
|
||||
orgGit?.git_https?.https_url ||
|
||||
orgGit?.git_https?.repository ||
|
||||
orgGit?.git_ssh?.ssh_url ||
|
||||
orgGit?.git_ssh?.repository;
|
||||
|
||||
if (!repoUrl) {
|
||||
console.error('No repository URL found in orgGit:', orgGit);
|
||||
toast.error('Git repository URL not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract owner and repo name from URL and construct web URL
|
||||
// Handles: https://github.com/owner/repo.git, git@github.com:owner/repo.git, etc.
|
||||
const githubMatch = repoUrl.match(/github\.com[:/]([^/]+)\/(.+?)(\.git)?$/);
|
||||
const gitlabMatch = repoUrl.match(/gitlab\.com[:/]([^/]+)\/(.+?)(\.git)?$/);
|
||||
const bitbucketMatch = repoUrl.match(/bitbucket\.org[:/]([^/]+)\/(.+?)(\.git)?$/);
|
||||
|
||||
let webUrl = null;
|
||||
if (githubMatch) {
|
||||
const [, owner, repo] = githubMatch;
|
||||
webUrl = `https://github.com/${owner}/${repo}`;
|
||||
} else if (gitlabMatch) {
|
||||
const [, owner, repo] = gitlabMatch;
|
||||
webUrl = `https://gitlab.com/${owner}/${repo}`;
|
||||
} else if (bitbucketMatch) {
|
||||
const [, owner, repo] = bitbucketMatch;
|
||||
webUrl = `https://bitbucket.org/${owner}/${repo}`;
|
||||
} else {
|
||||
// Fallback: try to clean up the URL
|
||||
webUrl = repoUrl
|
||||
.replace(/\.git$/, '')
|
||||
.replace(/^git@/, 'https://')
|
||||
.replace(/:([^/])/, '/$1');
|
||||
}
|
||||
|
||||
if (webUrl) {
|
||||
window.open(webUrl, '_blank');
|
||||
} else {
|
||||
toast.error('Could not parse repository URL');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
show={show}
|
||||
closeModal={onClose}
|
||||
title="Switch branch"
|
||||
checkForBackground={true}
|
||||
customClassName="switch-branch-modal"
|
||||
>
|
||||
<div className="switch-branch-modal-content">
|
||||
{/* Search Section */}
|
||||
<div className="search-section">
|
||||
<label className="section-label">ALL OPEN BRANCHES</label>
|
||||
<div className="search-input-wrapper">
|
||||
<SolidIcon name="search" width="16" fill="var(--slate11)" />
|
||||
<input
|
||||
type="text"
|
||||
className="search-input"
|
||||
placeholder="Search.."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
data-cy="branch-search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Branch List */}
|
||||
<div className="branch-list-section">
|
||||
{isLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner"></div>
|
||||
<span>Loading branches...</span>
|
||||
</div>
|
||||
) : filteredBranches.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>No branches found</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredBranches.map((branch) => {
|
||||
const isCurrentBranch = branch.name === currentBranchName;
|
||||
return (
|
||||
<div
|
||||
key={branch.id || branch.name}
|
||||
className={`branch-list-item ${isCurrentBranch ? 'active' : ''}`}
|
||||
onClick={() => handleBranchClick(branch)}
|
||||
data-cy={`branch-list-item-${branch.name}`}
|
||||
>
|
||||
<div className="branch-checkbox">
|
||||
{isCurrentBranch && <SolidIcon name="check2" width="16" fill="var(--indigo9)" />}
|
||||
</div>
|
||||
<div className="branch-list-content">
|
||||
<div className="branch-list-name">{branch.name}</div>
|
||||
<div className="branch-list-meta">
|
||||
Created by {branch.author || branch.created_by || 'default'},{' '}
|
||||
{getRelativeTime(branch.createdAt || branch.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="modal-footer-actions">
|
||||
<button className="footer-btn secondary" onClick={handleViewInGitRepo} data-cy="view-in-git-repo-btn">
|
||||
<span>View in git repo</span>
|
||||
<SolidIcon name="newtab" width="14" fill="var(--icon-default)" />
|
||||
</button>
|
||||
<button
|
||||
className="footer-btn accent"
|
||||
onClick={() => {
|
||||
setShowCreateModal(true);
|
||||
}}
|
||||
data-cy="create-branch-from-modal-btn"
|
||||
>
|
||||
<SolidIcon name="plusicon" width="14" fill="var(--indigo9)" />
|
||||
<span>Create new branch</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Branch Modal */}
|
||||
{showCreateModal && (
|
||||
<CreateBranchModal
|
||||
appId={appId}
|
||||
organizationId={organizationId}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onSuccess={(newBranch) => {
|
||||
// Optionally switch to the new branch after creation
|
||||
if (newBranch) {
|
||||
setCurrentBranch(newBranch);
|
||||
onClose(); // Close the switch branch modal too
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,11 +4,16 @@ import { ToolTip } from '@/_components/ToolTip';
|
|||
import { Button } from '@/components/ui/Button/Button';
|
||||
import './style.scss';
|
||||
|
||||
const CreateDraftButton = ({ onClick, disabled = false, darkMode = false }) => {
|
||||
const CreateDraftButton = ({
|
||||
onClick,
|
||||
disabled = false,
|
||||
darkMode = false,
|
||||
disabledTooltip = 'Draft version can only be created from saved versions.',
|
||||
}) => {
|
||||
return (
|
||||
<div className={cx('create-draft-button', { 'dark-theme theme-dark': darkMode })} style={{ padding: '8px' }}>
|
||||
<ToolTip
|
||||
message={'Draft version can only be created from saved versions.'}
|
||||
message={disabledTooltip}
|
||||
tooltipClassName="create-draft-button-tooltip"
|
||||
placement="left"
|
||||
show={disabled}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const VersionManagerDropdown = ({ darkMode = false, ...props }) => {
|
|||
developmentVersions,
|
||||
setSelectedVersion,
|
||||
fetchDevelopmentVersions,
|
||||
orgGit,
|
||||
} = useStore(
|
||||
(state) => ({
|
||||
appId: state.appId ?? state.appStore.modules[moduleId]?.app?.appId,
|
||||
|
|
@ -48,6 +49,7 @@ const VersionManagerDropdown = ({ darkMode = false, ...props }) => {
|
|||
developmentVersions: state.developmentVersions,
|
||||
fetchDevelopmentVersions: state.fetchDevelopmentVersions,
|
||||
setSelectedVersion: state.setSelectedVersion,
|
||||
orgGit: state.orgGit,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
|
|
@ -122,9 +124,27 @@ const VersionManagerDropdown = ({ darkMode = false, ...props }) => {
|
|||
const hasPublished = versions.some((v) => v.status === 'PUBLISHED');
|
||||
|
||||
// Check if there's only one draft and no other saved versions
|
||||
const draftVersions = developmentVersions.filter((v) => v.status === 'DRAFT');
|
||||
// draftVersions are versions of type 'version' (not branches)
|
||||
const draftVersions = developmentVersions.filter((v) => v.versionType === 'version' && v.status === 'DRAFT');
|
||||
const savedVersions = developmentVersions.filter((v) => v.status !== 'DRAFT');
|
||||
const shouldDisableCreateDraft = draftVersions.length > 0 && savedVersions.length === 0;
|
||||
const isGitSyncEnabled = orgGit?.git_ssh?.is_enabled || orgGit?.git_https?.is_enabled || orgGit?.git_lab?.is_enabled;
|
||||
|
||||
// Disable create draft logic:
|
||||
// - Git sync enabled: disable if any draft already exists
|
||||
// - Git sync disabled: disable if no published versions AND a draft exists (need published version to create from)
|
||||
const shouldDisableCreateDraft = isGitSyncEnabled
|
||||
? draftVersions.length > 0
|
||||
: savedVersions.length === 0 && draftVersions.length > 0;
|
||||
|
||||
// Determine tooltip message based on why create draft is disabled
|
||||
let createDraftDisabledTooltip = '';
|
||||
if (shouldDisableCreateDraft) {
|
||||
if (isGitSyncEnabled) {
|
||||
createDraftDisabledTooltip = 'Draft version already exists.';
|
||||
} else if (savedVersions.length === 0) {
|
||||
createDraftDisabledTooltip = 'Draft version can only be created from saved versions.';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to close dropdown and reset UI state
|
||||
const closeDropdown = () => {
|
||||
|
|
@ -255,6 +275,9 @@ const VersionManagerDropdown = ({ darkMode = false, ...props }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// Count only actual versions, not sub-branches
|
||||
const versionOnlyCount = versions.filter((v) => v.versionType === 'version').length;
|
||||
|
||||
const renderPopover = (overlayProps) => (
|
||||
<Popover
|
||||
id="version-manager-popover"
|
||||
|
|
@ -282,7 +305,7 @@ const VersionManagerDropdown = ({ darkMode = false, ...props }) => {
|
|||
</div>
|
||||
|
||||
{/* Search Field - Only show if more than 5 versions */}
|
||||
{versions.length > 5 && (
|
||||
{versionOnlyCount > 5 && (
|
||||
<div>
|
||||
<VersionSearchField value={searchQuery} onChange={handleSearchChange} />
|
||||
</div>
|
||||
|
|
@ -352,7 +375,12 @@ const VersionManagerDropdown = ({ darkMode = false, ...props }) => {
|
|||
{/* Divider */}
|
||||
<div style={{ height: '1px', backgroundColor: 'var(--border-weak)' }} />
|
||||
|
||||
<CreateDraftButton onClick={handleCreateDraft} disabled={shouldDisableCreateDraft} darkMode={darkMode} />
|
||||
<CreateDraftButton
|
||||
onClick={handleCreateDraft}
|
||||
disabled={shouldDisableCreateDraft}
|
||||
disabledTooltip={createDraftDisabledTooltip}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ const VersionSearchField = ({ value, onChange, placeholder = 'Search versions by
|
|||
<div
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid var(--border-default)',
|
||||
borderRadius: '6px',
|
||||
padding: '7px 12px',
|
||||
|
|
|
|||
|
|
@ -1,51 +1,51 @@
|
|||
.environment-toggle {
|
||||
padding: 8px;
|
||||
padding: 8px;
|
||||
|
||||
.environment-toggle__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--slate3);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
justify-content: space-around;
|
||||
gap: 4px;
|
||||
.environment-toggle__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--slate3);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
justify-content: space-around;
|
||||
gap: 4px;
|
||||
|
||||
.environment-toggle__btn {
|
||||
padding: 6px 12px !important;
|
||||
border: none;
|
||||
border-radius: 4px !important;
|
||||
background-color: transparent;
|
||||
color: var(--text-placeholder);
|
||||
font-weight: 400;
|
||||
transition: all 0.12s ease;
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none !important;
|
||||
.environment-toggle__btn {
|
||||
padding: 6px 12px !important;
|
||||
border: none;
|
||||
border-radius: 4px !important;
|
||||
background-color: transparent;
|
||||
color: var(--text-placeholder);
|
||||
font-weight: 400;
|
||||
transition: all 0.12s ease;
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none !important;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background-color: var(--interactive-hover);
|
||||
color: var(--text-default);
|
||||
}
|
||||
&:hover:not(.disabled) {
|
||||
background-color: var(--interactive-hover);
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--background-surface-layer-01);
|
||||
color: var(--text-default);
|
||||
font-weight: 500;
|
||||
box-shadow: var(--elevation-100-box-shadow);
|
||||
}
|
||||
&.selected {
|
||||
background-color: var(--background-surface-layer-01);
|
||||
color: var(--text-default);
|
||||
font-weight: 500;
|
||||
box-shadow: var(--elevation-100-box-shadow);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--text-disabled);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--text-disabled);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.environment-toggle__btn.btn {
|
||||
min-width: 0;
|
||||
}
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
}
|
||||
|
||||
.versions-list {
|
||||
|
||||
// Custom scrollbar styling
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
|
@ -299,6 +300,7 @@
|
|||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
|
@ -306,10 +308,12 @@
|
|||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
@ -323,13 +327,16 @@
|
|||
.version-item-skeleton {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
|
||||
> div {
|
||||
>div {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.create-draft-button-tooltip{
|
||||
display: flex;
|
||||
.tooltip-inner{
|
||||
width: 323px !important;
|
||||
|
||||
.create-draft-button-tooltip {
|
||||
display: flex;
|
||||
|
||||
.tooltip-inner {
|
||||
max-width: 323px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -82,4 +82,4 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,14 @@ import { toast } from 'react-hot-toast';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { copyToClipboard } from './utils';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
const useCallbackActions = () => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const deleteComponents = useStore((state) => state.deleteComponents, shallow);
|
||||
const setSelectedComponents = useStore((state) => state.setSelectedComponents, shallow);
|
||||
const currentPageComponents = useStore((state) => state?.getCurrentPageComponents(), shallow);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const runQuery = useStore((state) => state.queryPanel.runQuery);
|
||||
const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll);
|
||||
const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery, shallow);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ import { EventManager } from '@/AppBuilder/RightSideBar/Inspector/EventManager';
|
|||
import NotificationBanner from '@/_components/NotificationBanner';
|
||||
import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
|
||||
import CodeHinter from '@/AppBuilder/CodeEditor';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () => null }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const dataSources = useStore((state) => state.dataSources);
|
||||
const globalDataSources = useStore((state) => state.globalDataSources);
|
||||
const sampleDataSource = useStore((state) => state.sampleDataSource);
|
||||
|
|
@ -50,7 +52,7 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () =
|
|||
const ElementToRender = selectedDataSource?.plugin_id ? source : allSources[sourcecomponentName];
|
||||
const defaultOptions = useRef({});
|
||||
|
||||
const isFreezed = useStore((state) => state.getShouldFreeze());
|
||||
const isFreezed = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
|
||||
useEffect(() => {
|
||||
setDataSourceMeta(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import posthogHelper from '@/modules/common/helpers/posthogHelper';
|
|||
import { useAppDataStore } from '@/_stores/appDataStore';
|
||||
|
||||
export const QueryManagerHeader = forwardRef(({ darkMode, setActiveTab, activeTab }, ref) => {
|
||||
const { moduleId } = useModuleContext();
|
||||
const { moduleId, isModuleEditor } = useModuleContext();
|
||||
const updateQuerySuggestions = useStore((state) => state.queryPanel.updateQuerySuggestions);
|
||||
const previewQuery = useStore((state) => state.queryPanel.previewQuery);
|
||||
const renameQuery = useStore((state) => state.dataQuery.renameQuery);
|
||||
|
|
@ -27,7 +27,7 @@ export const QueryManagerHeader = forwardRef(({ darkMode, setActiveTab, activeTa
|
|||
const showCreateQuery = useStore((state) => state.queryPanel.showCreateQuery);
|
||||
const setShowCreateQuery = useStore((state) => state.queryPanel.setShowCreateQuery);
|
||||
const queryName = selectedQuery?.name ?? '';
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedQuery?.name) {
|
||||
|
|
@ -154,7 +154,8 @@ export const QueryManagerHeader = forwardRef(({ darkMode, setActiveTab, activeTa
|
|||
});
|
||||
|
||||
const NameInput = ({ onInput, value, darkMode, isDiabled, selectedQuery }) => {
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const isFocused = useStore((state) => state.queryPanel.nameInputFocused, shallow);
|
||||
const setIsFocused = useStore((state) => state.queryPanel.setNameInputFocused, shallow);
|
||||
const [name, setName] = useState(value);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import DataSourceIcon from '../QueryManager/Components/DataSourceIcon';
|
|||
import { isQueryRunnable, decodeEntities } from '@/_helpers/utils';
|
||||
import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
//TODO: Remove this
|
||||
import { Confirm } from '@/AppBuilder/Viewer/Confirm';
|
||||
// TODO: enable delete query confirmation popup
|
||||
|
|
@ -16,6 +17,7 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
|
|||
import { QueryRenameInput } from './QueryRenameInput';
|
||||
|
||||
export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const queryNameEleRef = useRef(null);
|
||||
|
||||
const isQuerySelected = useStore((state) => state.queryPanel.isQuerySelected(dataQuery.id), shallow);
|
||||
|
|
@ -26,7 +28,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
|||
const renameQuery = useStore((state) => state.dataQuery.renameQuery);
|
||||
const deleteDataQueries = useStore((state) => state.dataQuery.deleteDataQueries);
|
||||
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
|
||||
const renamingQueryId = useStore((state) => state.queryPanel.renamingQueryId);
|
||||
const deletingQueryId = useStore((state) => state.queryPanel.deletingQueryId);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import DataSourceSelect from '../QueryManager/Components/DataSourceSelect';
|
|||
import { OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import FolderEmpty from '@/_ui/Icon/solidIcons/FolderEmpty';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { appPermissionService } from '@/_services';
|
||||
|
|
@ -45,7 +46,8 @@ export const QueryDataPane = ({ darkMode }) => {
|
|||
const showQueryPermissionModal = useStore((state) => state.queryPanel.showQueryPermissionModal);
|
||||
const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
|
||||
const setQueries = useStore((state) => state.dataQuery.setQueries);
|
||||
const isFreezed = useStore((state) => state.getShouldFreeze());
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const isFreezed = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
|
||||
useEffect(() => {
|
||||
setQueryPanelSearchTerm(searchTermForFilters);
|
||||
|
|
@ -241,9 +243,10 @@ const EmptyDataSource = () => (
|
|||
);
|
||||
|
||||
const AddDataSourceButton = ({ darkMode, disabled: _disabled }) => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const [showMenu, setShowMenu] = useShowPopover(false, '#query-add-ds-popover', '#query-add-ds-popover-btn');
|
||||
const selectRef = useRef();
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
// const { isVersionReleased, isEditorFreezed } = useStore(
|
||||
// (state) => ({
|
||||
// isVersionReleased: state.isVersionReleased,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export const ComponentsManagerTab = ({ darkMode, isModuleEditor }) => {
|
|||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [moduleError, setModuleError] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('components');
|
||||
const _shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const _shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const isAutoMobileLayout = useStore((state) => state.currentLayout === 'mobile' && state.getIsAutoMobileLayout());
|
||||
const shouldFreeze = _shouldFreeze || isAutoMobileLayout;
|
||||
const edition = fetchEdition();
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import { Navigation } from './Components/Navigation';
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Button } from '@/components/ui/Button/Button';
|
||||
import { TreeSelect } from './Components/TreeSelect/TreeSelect.jsx';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
import '../ComponentManagerTab/styles.scss';
|
||||
|
||||
const INSPECTOR_HEADER_OPTIONS = [
|
||||
|
|
@ -157,10 +158,11 @@ export const Inspector = ({
|
|||
selectedComponentId,
|
||||
handleRightSidebarToggle,
|
||||
}) => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const allComponents = useStore((state) => state.getCurrentPageComponents());
|
||||
const setComponentProperty = useStore((state) => state.setComponentProperty, shallow);
|
||||
const setComponentName = useStore((state) => state.setComponentName, shallow);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const clearSelectedComponents = useStore((state) => state.clearSelectedComponents, shallow);
|
||||
const isVersionReleased = useStore((state) => state.isVersionReleased);
|
||||
const setWidgetDeleteConfirmation = useStore((state) => state.setWidgetDeleteConfirmation);
|
||||
|
|
@ -448,7 +450,7 @@ export const Inspector = ({
|
|||
setTimeout(() => setInputFocus(), 0);
|
||||
}
|
||||
if (value === 'delete') {
|
||||
setWidgetDeleteConfirmation(true);
|
||||
setWidgetDeleteConfirmation(true, isModuleEditor);
|
||||
}
|
||||
if (value === 'permission') {
|
||||
if (!hasAppPermissionComponent) return;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { RIGHT_SIDE_BAR_TAB } from './rightSidebarConstants';
|
|||
|
||||
export const RightSideBar = ({ darkMode }) => {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze());
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const activeTab = useStore((state) => state.activeRightSideBarTab);
|
||||
const isRightSidebarOpen = useStore((state) => state.isRightSidebarOpen);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ export const createAppVersionSlice = (set, get) => ({
|
|||
|
||||
setAppVersionPromoted: (value) => set(() => ({ isAppVersionPromoted: value }), false, 'setAppVersionPromoted'),
|
||||
|
||||
getShouldFreeze: (skipIsEditorFreezedCheck = false) => {
|
||||
getShouldFreeze: (skipIsEditorFreezedCheck = false, isModuleEditor = false) => {
|
||||
if (isModuleEditor) return false;
|
||||
return (
|
||||
get().isVersionReleased ||
|
||||
(!skipIsEditorFreezedCheck && get().isEditorFreezed) ||
|
||||
|
|
|
|||
565
frontend/src/AppBuilder/_stores/slices/branchSlice.js
Normal file
565
frontend/src/AppBuilder/_stores/slices/branchSlice.js
Normal file
|
|
@ -0,0 +1,565 @@
|
|||
import { gitSyncService } from '@/_services';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
const initialState = {
|
||||
currentBranch: null,
|
||||
allBranches: [],
|
||||
pullRequests: [],
|
||||
branchingEnabled: false,
|
||||
isDraftVersionActive: false,
|
||||
isLoadingBranches: false,
|
||||
isLoadingPRs: false,
|
||||
branchError: null,
|
||||
};
|
||||
|
||||
export const createBranchSlice = (set, get) => ({
|
||||
...initialState,
|
||||
|
||||
/**
|
||||
* Fetch all branches for the current app
|
||||
* @param {string} appId - Application ID
|
||||
* @param {string} organizationId - Organization ID
|
||||
*/
|
||||
fetchBranches: async (appId, organizationId) => {
|
||||
set(() => ({ isLoadingBranches: true, branchError: null }), false, 'fetchBranches:start');
|
||||
|
||||
try {
|
||||
const response = await gitSyncService.getAllBranches(appId, organizationId);
|
||||
const branches = response?.branches || [];
|
||||
|
||||
// Only set default branch if current version is a branch type
|
||||
// If selectedVersion is a regular version (not a branch), keep currentBranch as null
|
||||
const selectedVersion = useStore.getState().selectedVersion;
|
||||
const isOnBranch = selectedVersion?.versionType === 'branch' || selectedVersion?.version_type === 'branch';
|
||||
|
||||
let defaultBranch = get().currentBranch;
|
||||
if (isOnBranch) {
|
||||
const matchingBranch = branches.find((b) => b.name === selectedVersion.name);
|
||||
if (matchingBranch) {
|
||||
defaultBranch = matchingBranch;
|
||||
} else if (!defaultBranch && branches.length) {
|
||||
defaultBranch =
|
||||
branches.find((b) => b.name === 'main') || branches.find((b) => b.name === 'master') || branches[0];
|
||||
}
|
||||
}
|
||||
|
||||
set(
|
||||
() => ({
|
||||
allBranches: branches,
|
||||
isLoadingBranches: false,
|
||||
currentBranch: isOnBranch ? defaultBranch || null : null,
|
||||
}),
|
||||
false,
|
||||
'fetchBranches:success'
|
||||
);
|
||||
|
||||
return { success: true, branches };
|
||||
} catch (error) {
|
||||
console.error('Error fetching branches:', error);
|
||||
set(
|
||||
() => ({
|
||||
isLoadingBranches: false,
|
||||
branchError: error.message || 'Failed to fetch branches',
|
||||
}),
|
||||
false,
|
||||
'fetchBranches:error'
|
||||
);
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new branch
|
||||
* @param {string} appId - Application ID
|
||||
* @param {string} organizationId - Organization ID
|
||||
* @param {object} branchData - { branchName, versionFromId, baseBranch, autoCommit }
|
||||
*/
|
||||
createBranch: async (appId, organizationId, branchData) => {
|
||||
set(() => ({ branchError: null }), false, 'createBranch:start');
|
||||
|
||||
// Check for draft version before creating
|
||||
const isDraft = get().checkDraftStatus();
|
||||
if (isDraft && !branchData.force) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'DRAFT_EXISTS',
|
||||
message: 'You can only have one draft version at a time. Please save or release your current draft first.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await gitSyncService.createBranch(appId, organizationId, {
|
||||
branchName: branchData.branchName,
|
||||
versionFromId: branchData.versionFromId,
|
||||
baseBranch: branchData.baseBranch || 'main', // Default to 'main' if not provided
|
||||
autoCommit: branchData.autoCommit || false,
|
||||
});
|
||||
|
||||
// Refresh branches list after creation
|
||||
await get().fetchBranches(appId, organizationId);
|
||||
|
||||
// Update current branch if successful
|
||||
if (response?.branch) {
|
||||
set(
|
||||
() => ({
|
||||
currentBranch: response.branch,
|
||||
}),
|
||||
false,
|
||||
'createBranch:success'
|
||||
);
|
||||
}
|
||||
|
||||
return { success: true, branch: response?.branch };
|
||||
} catch (error) {
|
||||
const apiErrorMessage = error?.data?.message || error?.error || error?.message || 'Failed to create branch';
|
||||
|
||||
set(
|
||||
() => ({
|
||||
branchError: apiErrorMessage,
|
||||
}),
|
||||
false,
|
||||
'createBranch:error'
|
||||
);
|
||||
|
||||
return { success: false, error: apiErrorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch to a different branch (changes the editing version to the branch version)
|
||||
* Branches are represented as versions with versionType === 'branch'
|
||||
* IMPORTANT: Branches always work in Development environment, so we switch to Development first
|
||||
* @param {string} appId - Application ID
|
||||
* @param {string} branchName - Target branch name
|
||||
*/
|
||||
switchBranch: async (appId, branchName) => {
|
||||
set(() => ({ branchError: null }), false, 'switchBranch:start');
|
||||
|
||||
try {
|
||||
const state = get();
|
||||
|
||||
// Get Development environment - branches ALWAYS work in Development
|
||||
const developmentEnv = state.environments?.find((env) => env.name === 'Development' || env.priority === 1);
|
||||
if (!developmentEnv) {
|
||||
throw new Error('Development environment not found');
|
||||
}
|
||||
|
||||
// Get development versions to find the branch version
|
||||
const developmentVersions = state.developmentVersions || [];
|
||||
const branchVersion = developmentVersions.find(
|
||||
(version) =>
|
||||
(version.versionType === 'branch' || version.version_type === 'branch') && version.name === branchName
|
||||
);
|
||||
|
||||
if (!branchVersion) {
|
||||
// Check if branch exists in allBranches but not as a version
|
||||
const branchExists = state.allBranches.find((b) => b.name === branchName);
|
||||
if (branchExists) {
|
||||
throw new Error(
|
||||
`Branch "${branchName}" exists in Git but has no corresponding version. You may need to create this branch in ToolJet first.`
|
||||
);
|
||||
}
|
||||
|
||||
const availableBranches = developmentVersions
|
||||
.filter((v) => v.versionType === 'branch' || v.version_type === 'branch')
|
||||
.map((v) => v.name)
|
||||
.join(', ');
|
||||
throw new Error(
|
||||
`Branch version not found: ${branchName}. Available branch versions: ${availableBranches || 'none'}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if already on this branch version AND in Development environment
|
||||
const alreadyOnVersion = state.selectedVersion?.id === branchVersion.id;
|
||||
const alreadyInDevelopment = state.selectedEnvironment?.id === developmentEnv.id;
|
||||
|
||||
if (alreadyOnVersion && alreadyInDevelopment) {
|
||||
return { success: true, data: state.selectedVersion };
|
||||
}
|
||||
|
||||
// Update current branch first
|
||||
const targetBranch = state.allBranches.find((b) => b.name === branchName) || { name: branchName };
|
||||
set(
|
||||
(state) => ({
|
||||
...state,
|
||||
currentBranch: targetBranch,
|
||||
}),
|
||||
false,
|
||||
'switchBranch:updating-branch'
|
||||
);
|
||||
|
||||
// Switch to branch version (and Development environment if needed)
|
||||
return new Promise((resolve, reject) => {
|
||||
// If not in Development environment, switch to it first
|
||||
if (!alreadyInDevelopment) {
|
||||
state.environmentChangedAction(developmentEnv, () => {
|
||||
// After environment switch, change to the branch version
|
||||
state.changeEditorVersionAction(
|
||||
appId,
|
||||
branchVersion.id,
|
||||
(data) => {
|
||||
state.setCurrentVersionId(branchVersion.id);
|
||||
state.setSelectedVersion(branchVersion);
|
||||
resolve({ success: true, data });
|
||||
},
|
||||
(error) => {
|
||||
console.error('switchBranch - error after environment change:', error);
|
||||
set(
|
||||
() => ({ branchError: error.message || 'Failed to switch to branch' }),
|
||||
false,
|
||||
'switchBranch:error'
|
||||
);
|
||||
reject({ success: false, error: error.message });
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Already in Development, just switch to branch version
|
||||
state.changeEditorVersionAction(
|
||||
appId,
|
||||
branchVersion.id,
|
||||
(data) => {
|
||||
state.setCurrentVersionId(branchVersion.id);
|
||||
state.setSelectedVersion(branchVersion);
|
||||
resolve({ success: true, data });
|
||||
},
|
||||
(error) => {
|
||||
console.error('switchBranch - error switching version:', error);
|
||||
set(() => ({ branchError: error.message || 'Failed to switch to branch' }), false, 'switchBranch:error');
|
||||
reject({ success: false, error: error.message });
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error switching branch:', error);
|
||||
set(
|
||||
() => ({
|
||||
branchError: error.message || 'Failed to switch branch',
|
||||
}),
|
||||
false,
|
||||
'switchBranch:error'
|
||||
);
|
||||
|
||||
throw error; // Re-throw to be caught by the modal
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch to the default branch (main/master/configured branch)
|
||||
*
|
||||
* This function mimics the behavior of the version dropdown's handleVersionSelect (line 144):
|
||||
* 1. Switch to Development environment first (if not already there)
|
||||
* 2. Determine which version to switch to based on PRD scenarios
|
||||
* 3. Call changeEditorVersionAction to load version data
|
||||
* 4. Update currentBranch and selectedVersion
|
||||
*
|
||||
* Version Selection Logic (Based on PRD Scenarios):
|
||||
* PRIORITY 1: DRAFT version (latest draft if multiple exist - v3 draft > v2.1 draft)
|
||||
* PRIORITY 2: RELEASED version (latest released - v2 > v1)
|
||||
* PRIORITY 3: PUBLISHED/Saved version (latest published)
|
||||
*
|
||||
* @param {string} appId - Application ID
|
||||
* @param {string} defaultBranchName - Name of the default branch (from git config)
|
||||
*/
|
||||
switchToDefaultBranch: async (appId, defaultBranchName) => {
|
||||
set(() => ({ branchError: null }), false, 'switchToDefaultBranch:start');
|
||||
|
||||
try {
|
||||
const state = get();
|
||||
|
||||
// Get feature branch names (exclude default branch) to help with filtering
|
||||
const featureBranchNames = state.allBranches.filter((b) => b.name !== defaultBranchName).map((b) => b.name);
|
||||
|
||||
// Branches always work in Development environment - ALWAYS use developmentVersions
|
||||
// This matches the PRD scenarios where all branch work happens in Development
|
||||
const developmentVersions = state.developmentVersions || [];
|
||||
|
||||
// Filter to get ONLY default branch versions (exclude branch-type versions)
|
||||
// A version is a default branch version if it does NOT have versionType === 'branch'
|
||||
// We rely on versionType field to distinguish branch versions from regular versions
|
||||
const defaultBranchVersions = developmentVersions.filter((v) => {
|
||||
const hasBranchType = v.versionType === 'branch' || v.version_type === 'branch';
|
||||
|
||||
// Only exclude if it has branch type - keep all versions with versionType === 'version'
|
||||
return !hasBranchType;
|
||||
});
|
||||
|
||||
// Version selection priority (PRD Scenarios 1-4)
|
||||
let targetVersion = null;
|
||||
|
||||
// PRIORITY 1: DRAFT version (Scenarios 1, 2, 3, 4)
|
||||
// Get LATEST draft (most recently created) if multiple exist
|
||||
const draftVersions = defaultBranchVersions
|
||||
.filter((v) => v.status === 'DRAFT' || v.isDraft || v.is_draft)
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.createdAt || a.created_at || 0);
|
||||
const dateB = new Date(b.createdAt || b.created_at || 0);
|
||||
return dateB - dateA; // Descending - latest first
|
||||
});
|
||||
|
||||
targetVersion = draftVersions[0];
|
||||
|
||||
// PRIORITY 2: RELEASED version (Scenario 2 - when no draft exists after v1 released)
|
||||
if (!targetVersion) {
|
||||
const releasedVersions = defaultBranchVersions
|
||||
.filter((v) => v.status === 'RELEASED' || v.isReleased || v.is_released)
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.createdAt || a.created_at || 0);
|
||||
const dateB = new Date(b.createdAt || b.created_at || 0);
|
||||
return dateB - dateA; // Latest released
|
||||
});
|
||||
|
||||
targetVersion = releasedVersions[0];
|
||||
}
|
||||
|
||||
// PRIORITY 3: PUBLISHED version (fallback)
|
||||
if (!targetVersion) {
|
||||
const publishedVersions = defaultBranchVersions
|
||||
.filter((v) => v.status === 'PUBLISHED' || v.isPublished || v.is_published)
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.createdAt || a.created_at || 0);
|
||||
const dateB = new Date(b.createdAt || b.created_at || 0);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
targetVersion = publishedVersions[0];
|
||||
}
|
||||
|
||||
// If no version found, error
|
||||
if (!targetVersion) {
|
||||
console.error('switchToDefaultBranch - no versions found!');
|
||||
console.error('switchToDefaultBranch - developmentVersions:', developmentVersions);
|
||||
console.error('switchToDefaultBranch - defaultBranchVersions:', defaultBranchVersions);
|
||||
throw new Error('No versions found for the default branch. Please create a version first.');
|
||||
}
|
||||
|
||||
// Get Development environment
|
||||
const developmentEnv = state.environments?.find((env) => env.name === 'Development' || env.priority === 1);
|
||||
if (!developmentEnv) {
|
||||
throw new Error('Development environment not found');
|
||||
}
|
||||
|
||||
// Check if already on this version AND in Development environment
|
||||
const alreadyOnVersion = state.selectedVersion?.id === targetVersion.id;
|
||||
const alreadyInDevelopment = state.selectedEnvironment?.id === developmentEnv.id;
|
||||
|
||||
if (alreadyOnVersion && alreadyInDevelopment) {
|
||||
const defaultBranch = state.allBranches.find((b) => b.name === defaultBranchName) || {
|
||||
name: defaultBranchName,
|
||||
};
|
||||
|
||||
set(
|
||||
(state) => ({
|
||||
...state,
|
||||
currentBranch: defaultBranch,
|
||||
}),
|
||||
false,
|
||||
'switchToDefaultBranch:already-on-version'
|
||||
);
|
||||
|
||||
return { success: true, data: state.selectedVersion, version: targetVersion };
|
||||
}
|
||||
|
||||
// Update current branch first
|
||||
const defaultBranch = state.allBranches.find((b) => b.name === defaultBranchName) || {
|
||||
name: defaultBranchName,
|
||||
};
|
||||
|
||||
set(
|
||||
(state) => ({
|
||||
...state,
|
||||
currentBranch: defaultBranch,
|
||||
}),
|
||||
false,
|
||||
'switchToDefaultBranch:updating-branch'
|
||||
);
|
||||
|
||||
// EXACTLY MATCH handleVersionSelect behavior (line 144 in VersionManagerDropdown.jsx)
|
||||
return new Promise((resolve, reject) => {
|
||||
// If not in Development environment, switch to it first (like handleVersionSelect does)
|
||||
if (!alreadyInDevelopment) {
|
||||
state.environmentChangedAction(developmentEnv, () => {
|
||||
// After environment switch, change the version
|
||||
state.changeEditorVersionAction(
|
||||
appId,
|
||||
targetVersion.id,
|
||||
(data) => {
|
||||
state.setCurrentVersionId(targetVersion.id);
|
||||
resolve({ success: true, data, version: targetVersion });
|
||||
},
|
||||
(error) => {
|
||||
console.error('switchToDefaultBranch - error after environment change:', error);
|
||||
set(
|
||||
() => ({ branchError: error.message || 'Failed to switch version' }),
|
||||
false,
|
||||
'switchToDefaultBranch:error'
|
||||
);
|
||||
reject({ success: false, error: error.message });
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Already in Development, just switch version (like handleVersionSelect does)
|
||||
state.changeEditorVersionAction(
|
||||
appId,
|
||||
targetVersion.id,
|
||||
(data) => {
|
||||
state.setCurrentVersionId(targetVersion.id);
|
||||
state.setSelectedVersion(targetVersion);
|
||||
resolve({ success: true, data, version: targetVersion });
|
||||
},
|
||||
(error) => {
|
||||
console.error('switchToDefaultBranch - error switching version:', error);
|
||||
set(
|
||||
() => ({ branchError: error.message || 'Failed to switch version' }),
|
||||
false,
|
||||
'switchToDefaultBranch:error'
|
||||
);
|
||||
reject({ success: false, error: error.message });
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error switching to default branch:', error);
|
||||
set(
|
||||
() => ({
|
||||
branchError: error.message || 'Failed to switch to default branch',
|
||||
}),
|
||||
false,
|
||||
'switchToDefaultBranch:error'
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch pull requests for the current app
|
||||
* @param {string} appId - Application ID
|
||||
*/
|
||||
fetchPullRequests: async (appId, organizationId) => {
|
||||
set(() => ({ isLoadingPRs: true, branchError: null }), false, 'fetchPullRequests:start');
|
||||
|
||||
try {
|
||||
const response = await gitSyncService.getPullRequests(appId, organizationId);
|
||||
const pullRequests = response?.pull_requests || [];
|
||||
|
||||
set(
|
||||
() => ({
|
||||
pullRequests,
|
||||
isLoadingPRs: false,
|
||||
}),
|
||||
false,
|
||||
'fetchPullRequests:success'
|
||||
);
|
||||
|
||||
return { success: true, pullRequests };
|
||||
} catch (error) {
|
||||
console.error('Error fetching pull requests:', error);
|
||||
set(
|
||||
() => ({
|
||||
isLoadingPRs: false,
|
||||
branchError: error.message || 'Failed to fetch pull requests',
|
||||
}),
|
||||
false,
|
||||
'fetchPullRequests:error'
|
||||
);
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a draft version is currently active
|
||||
* @returns {boolean} True if draft version exists
|
||||
*/
|
||||
checkDraftStatus: () => {
|
||||
const appVersions = get().appVersions || [];
|
||||
const hasDraft = appVersions.some((version) => version.isDraft || version.is_draft);
|
||||
|
||||
set(
|
||||
() => ({
|
||||
isDraftVersionActive: hasDraft,
|
||||
}),
|
||||
false,
|
||||
'checkDraftStatus'
|
||||
);
|
||||
|
||||
return hasDraft;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update branching enabled status
|
||||
* @param {boolean} enabled - Whether branching is enabled
|
||||
*/
|
||||
updateBranchingEnabled: (enabled) =>
|
||||
set(
|
||||
() => ({
|
||||
branchingEnabled: enabled,
|
||||
}),
|
||||
false,
|
||||
'updateBranchingEnabled'
|
||||
),
|
||||
|
||||
/**
|
||||
* Set current branch
|
||||
* @param {object} branch - Branch object
|
||||
*/
|
||||
setCurrentBranch: (branch) =>
|
||||
set(
|
||||
() => ({
|
||||
currentBranch: branch,
|
||||
}),
|
||||
false,
|
||||
'setCurrentBranch'
|
||||
),
|
||||
|
||||
/**
|
||||
* Clear branch error
|
||||
*/
|
||||
clearBranchError: () =>
|
||||
set(
|
||||
() => ({
|
||||
branchError: null,
|
||||
}),
|
||||
false,
|
||||
'clearBranchError'
|
||||
),
|
||||
|
||||
/**
|
||||
* Reset branch slice to initial state
|
||||
*/
|
||||
resetBranchSlice: () =>
|
||||
set(
|
||||
() => ({
|
||||
...initialState,
|
||||
}),
|
||||
false,
|
||||
'resetBranchSlice'
|
||||
),
|
||||
|
||||
/**
|
||||
* Get PR status for a specific branch
|
||||
* @param {string} branchName - Branch name
|
||||
* @returns {object|null} PR object or null
|
||||
*/
|
||||
getPRForBranch: (branchName) => {
|
||||
const pullRequests = get().pullRequests;
|
||||
return pullRequests.find((pr) => pr.source_branch === branchName || pr.sourceBranch === branchName) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if branch is readonly (merged or released)
|
||||
* @param {string} branchName - Branch name
|
||||
* @returns {boolean} True if branch is readonly
|
||||
*/
|
||||
isBranchReadonly: (branchName) => {
|
||||
const branch = get().allBranches.find((b) => b.name === branchName);
|
||||
if (!branch) return false;
|
||||
|
||||
return branch.is_merged || branch.isMerged || branch.is_released || branch.isReleased || false;
|
||||
},
|
||||
});
|
||||
|
|
@ -43,6 +43,7 @@ const initialState = {
|
|||
},
|
||||
selectedComponents: [],
|
||||
showWidgetDeleteConfirmation: false,
|
||||
deleteTargetIsModuleEditor: false,
|
||||
focusedParentId: null,
|
||||
modalsOpenOnCanvas: [],
|
||||
showComponentPermissionModal: false,
|
||||
|
|
@ -1203,7 +1204,7 @@ export const createComponentsSlice = (set, get) => ({
|
|||
deleteComponents: (
|
||||
selected,
|
||||
moduleId = 'canvas',
|
||||
{ skipUndoRedo = false, saveAfterAction = true, isCut = false, skipFormUpdate = false } = {}
|
||||
{ skipUndoRedo = false, saveAfterAction = true, isCut = false, skipFormUpdate = false, isModuleEditor = false } = {}
|
||||
) => {
|
||||
const {
|
||||
saveComponentChanges,
|
||||
|
|
@ -1222,7 +1223,7 @@ export const createComponentsSlice = (set, get) => ({
|
|||
getCurrentPageIndex,
|
||||
} = get();
|
||||
const isAppBeingEditedByAI = get().ai?.isLoading ?? false;
|
||||
const shouldFreeze = getShouldFreeze(isAppBeingEditedByAI);
|
||||
const shouldFreeze = getShouldFreeze(isAppBeingEditedByAI, isModuleEditor);
|
||||
const currentPageId = getCurrentPageId(moduleId);
|
||||
const appEvents = get().eventsSlice.getModuleEvents(moduleId);
|
||||
const componentNames = [];
|
||||
|
|
@ -2015,9 +2016,10 @@ export const createComponentsSlice = (set, get) => ({
|
|||
|
||||
await savePageChanges(app.appId, currentVersionId, currentPageId, { autoComputeLayout: false });
|
||||
},
|
||||
setWidgetDeleteConfirmation: (value) => {
|
||||
setWidgetDeleteConfirmation: (value, isModuleEditor = false) => {
|
||||
set((state) => {
|
||||
state.showWidgetDeleteConfirmation = value;
|
||||
if (value) state.deleteTargetIsModuleEditor = isModuleEditor;
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ const initialState = {
|
|||
developmentVersions: [],
|
||||
draftVersions: [],
|
||||
publishedVersions: [],
|
||||
draftVersions: [],
|
||||
publishedVersions: [],
|
||||
environmentLoadingState: 'completed',
|
||||
isPublicAccess: false,
|
||||
};
|
||||
|
|
@ -134,16 +136,25 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
|
|||
);
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
...state,
|
||||
selectedEnvironment,
|
||||
selectedVersion: response.editorVersion,
|
||||
appVersionEnvironment: response.appVersionEnvironment,
|
||||
shouldRenderPromoteButton: response.shouldRenderPromoteButton,
|
||||
shouldRenderReleaseButton: response.shouldRenderReleaseButton,
|
||||
environments: response.environments,
|
||||
versionsPromotedToEnvironment: [response.editorVersion],
|
||||
}));
|
||||
set((state) => {
|
||||
const stateUpdate = {
|
||||
...state,
|
||||
selectedEnvironment,
|
||||
selectedVersion: response.editorVersion,
|
||||
appVersionEnvironment: response.appVersionEnvironment,
|
||||
shouldRenderPromoteButton: response.shouldRenderPromoteButton,
|
||||
shouldRenderReleaseButton: response.shouldRenderReleaseButton,
|
||||
environments: response.environments,
|
||||
versionsPromotedToEnvironment: [response.editorVersion],
|
||||
};
|
||||
|
||||
// Clear currentBranch if initial version is not a branch
|
||||
const versionType = response.editorVersion?.versionType || response.editorVersion?.version_type;
|
||||
if (versionType !== 'branch') {
|
||||
stateUpdate.currentBranch = null;
|
||||
}
|
||||
return stateUpdate;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ DEBUG - Error while initializing the environment dropdown', error);
|
||||
}
|
||||
|
|
@ -223,7 +234,8 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
|
|||
selectedVersionId,
|
||||
versionDescription = '',
|
||||
onSuccess,
|
||||
onFailure
|
||||
onFailure,
|
||||
versionType = 'version'
|
||||
) => {
|
||||
try {
|
||||
const editorEnvironment = get().selectedEnvironment.id;
|
||||
|
|
@ -232,7 +244,8 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
|
|||
versionName,
|
||||
versionDescription,
|
||||
selectedVersionId,
|
||||
editorEnvironment
|
||||
editorEnvironment,
|
||||
versionType
|
||||
);
|
||||
const editorVersion = {
|
||||
id: newVersion.id,
|
||||
|
|
@ -336,6 +349,8 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
|
|||
name: data.editing_version.name,
|
||||
current_environment_id: data.editing_version.currentEnvironmentId,
|
||||
status: data.editing_version.status,
|
||||
// Preserve versionType from API response to distinguish between regular versions and branch versions
|
||||
versionType: data.editing_version.versionType || data.editing_version.version_type || 'version',
|
||||
};
|
||||
const appVersionEnvironment = get().environments.find(
|
||||
(environment) => environment.id === selectedVersion.current_environment_id
|
||||
|
|
@ -356,6 +371,12 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({
|
|||
useStore.getState()?.license?.featureAccess
|
||||
),
|
||||
};
|
||||
|
||||
// Clear currentBranch if switching to a regular version (not a branch)
|
||||
if (selectedVersion.versionType !== 'branch') {
|
||||
optionsToUpdate.currentBranch = null;
|
||||
}
|
||||
|
||||
set((state) => ({ ...state, ...optionsToUpdate }));
|
||||
onSuccess(data);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ export const createGitSyncSlice = (set, get) => ({
|
|||
const selectedEnvironment = useStore.getState()?.selectedEnvironment;
|
||||
const isEditorFreezed = useStore.getState()?.isEditorFreezed;
|
||||
|
||||
return featureAccess?.gitSync && selectedEnvironment?.priority === 1 && (creationMode === 'GIT' || !isEditorFreezed)
|
||||
return featureAccess?.gitSync && selectedEnvironment?.priority === 1
|
||||
? set((state) => ({ showGitSyncModal: !state.showGitSyncModal }), false, 'toggleGitSyncModal')
|
||||
: () => { };
|
||||
: () => {};
|
||||
},
|
||||
fetchAppGit: async (currentOrganizationId, currentAppVersionId) => {
|
||||
set((state) => ({ appLoading: true }), false, 'setAppLoading');
|
||||
|
|
@ -26,18 +26,23 @@ export const createGitSyncSlice = (set, get) => ({
|
|||
const data = await gitSyncService.getAppGitConfigs(currentOrganizationId, currentAppVersionId);
|
||||
const allowEditing = data?.app_git?.allow_editing ?? false;
|
||||
const orgGit = data?.app_git?.org_git;
|
||||
const isBranchingEnabled = orgGit?.is_branching_enabled ?? false;
|
||||
const appGit = data?.app_git;
|
||||
const isGitSyncConfigured = data?.app_git?.is_git_sync_configured
|
||||
set((state) => ({ isGitSyncConfigured }), false, 'isGitSyncConfigured')
|
||||
set((state) => ({ appGit }), false, 'setAppGit');
|
||||
const isGitSyncConfigured = data?.app_git?.is_git_sync_configured;
|
||||
// Update branchingEnabled in branchSlice
|
||||
get().updateBranchingEnabled?.(isBranchingEnabled);
|
||||
set((state) => ({ isGitSyncConfigured }), false, 'isGitSyncConfigured');
|
||||
set((state) => ({ orgGit }), false, 'setOrgGit');
|
||||
set((state) => ({ appGit }), false, 'setAppGit');
|
||||
set((state) => ({ allowEditing }), false, 'setAllowEditing');
|
||||
console.log('app git', appGit);
|
||||
return allowEditing;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch app git configs:', error);
|
||||
// Set allowEditing to false on error
|
||||
set((state) => ({ allowEditing: false }), false, 'setAllowEditing');
|
||||
// Also reset branching on error
|
||||
get().updateBranchingEnabled?.(false);
|
||||
return false;
|
||||
} finally {
|
||||
set((state) => ({ appLoading: false }), false, 'setAppLoading');
|
||||
|
|
@ -45,5 +50,5 @@ export const createGitSyncSlice = (set, get) => ({
|
|||
},
|
||||
setAppGit(appGit) {
|
||||
set((state) => ({ appGit: appGit }), false, 'setAppGit');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { createFormComponentSlice } from './slices/componentSlices/formComponent
|
|||
import { createInspectorSlice } from './slices/inspectorSlice';
|
||||
import { createModuleSlice } from './slices/moduleSlice';
|
||||
import { listViewComponentSlice } from './slices/componentSlices/listViewComponentSlice';
|
||||
import { createBranchSlice } from './slices/branchSlice';
|
||||
enableMapSet();
|
||||
|
||||
export default create(
|
||||
|
|
@ -71,6 +72,7 @@ export default create(
|
|||
// component slices
|
||||
...createFormComponentSlice(...state),
|
||||
...listViewComponentSlice(...state),
|
||||
...createBranchSlice(...state),
|
||||
})),
|
||||
{ name: 'App Builder Store', anonymousActionType: 'unknown' }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ class HomePageComponent extends React.Component {
|
|||
selectedAppRepo: null,
|
||||
importingApp: false,
|
||||
importingGitAppOperations: {},
|
||||
latestCommitData: null,
|
||||
tags: [],
|
||||
fetchingLatestCommitData: false,
|
||||
selectedVersionOption: null,
|
||||
featuresLoaded: false,
|
||||
showCreateAppModal: false,
|
||||
showCreateAppFromTemplateModal: false,
|
||||
|
|
@ -841,24 +845,55 @@ class HomePageComponent extends React.Component {
|
|||
};
|
||||
|
||||
importGitApp = () => {
|
||||
const { appsFromRepos, selectedAppRepo, orgGit, importedAppName } = this.state;
|
||||
const { appsFromRepos, selectedAppRepo, orgGit, importedAppName, selectedVersionOption, tags } = this.state;
|
||||
const appToImport = appsFromRepos[selectedAppRepo];
|
||||
const { git_app_name, git_version_id, git_version_name, last_commit_message, last_commit_user, lastpush_date } =
|
||||
appToImport;
|
||||
const {
|
||||
git_app_name,
|
||||
git_version_id,
|
||||
git_version_name,
|
||||
last_commit_message,
|
||||
last_commit_user,
|
||||
lastpush_date,
|
||||
app_co_relation_id,
|
||||
} = appToImport;
|
||||
|
||||
let commitHash = null;
|
||||
let commitMessage = last_commit_message;
|
||||
let commitUser = last_commit_user;
|
||||
let commitDate = lastpush_date;
|
||||
let gitVersionNameToUse = git_version_name;
|
||||
|
||||
// If a tag is selected (not latest), use tag's commit info
|
||||
if (selectedVersionOption && selectedVersionOption !== 'latest') {
|
||||
const selectedTag = tags.find((t) => t.name === selectedVersionOption);
|
||||
if (selectedTag) {
|
||||
commitHash = selectedTag.commit?.sha;
|
||||
commitMessage = selectedTag.message;
|
||||
commitUser = selectedTag.tagger?.name;
|
||||
commitDate = selectedTag.tagger?.date;
|
||||
gitVersionNameToUse = selectedTag.name.split('/').pop();
|
||||
} else {
|
||||
commitMessage = last_commit_message;
|
||||
commitUser = last_commit_user;
|
||||
commitDate = lastpush_date;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ importingApp: true });
|
||||
const body = {
|
||||
gitAppId: selectedAppRepo,
|
||||
gitAppName: git_app_name,
|
||||
gitVersionName: git_version_name,
|
||||
gitVersionName: gitVersionNameToUse,
|
||||
gitVersionId: git_version_id,
|
||||
lastCommitMessage: last_commit_message,
|
||||
lastCommitUser: last_commit_user,
|
||||
lastPushDate: new Date(lastpush_date),
|
||||
organizationGitId: orgGit?.id,
|
||||
appName: importedAppName?.trim().replace(/\s+/g, ' '),
|
||||
allowEditing: this.state.isAppImportEditable,
|
||||
...(commitHash && { commitHash, appCoRelationId: app_co_relation_id }),
|
||||
...(commitMessage && { lastCommitMessage: commitMessage }),
|
||||
...(commitUser && { lastCommitUser: commitUser }),
|
||||
...(commitDate && { lastPushDate: new Date(commitDate) }),
|
||||
};
|
||||
|
||||
gitSyncService
|
||||
.importGitApp(body)
|
||||
.then((data) => {
|
||||
|
|
@ -883,7 +918,7 @@ class HomePageComponent extends React.Component {
|
|||
folderService
|
||||
.addToFolder(appOperations.selectedApp.id, appOperations.selectedFolder)
|
||||
.then(() => {
|
||||
toast.success('Added to folder.');
|
||||
toast.success('Application added to folder successfully!');
|
||||
this.foldersChanged();
|
||||
this.setState({ appOperations: {}, showAddToFolderModal: false });
|
||||
posthogHelper.captureEvent('click_add_to_folder_button', {
|
||||
|
|
@ -910,7 +945,7 @@ class HomePageComponent extends React.Component {
|
|||
folderService
|
||||
.removeAppFromFolder(appOperations.selectedApp.id, appOperations.selectedFolder.id)
|
||||
.then(() => {
|
||||
toast.success('Removed from folder.');
|
||||
toast.success('Application removed from folder successfully!');
|
||||
|
||||
this.fetchApps(1, appOperations.selectedFolder.id);
|
||||
this.fetchFolders();
|
||||
|
|
@ -1008,7 +1043,13 @@ class HomePageComponent extends React.Component {
|
|||
|
||||
generateOptionsForRepository = () => {
|
||||
const { appsFromRepos } = this.state;
|
||||
return Object.keys(appsFromRepos).map((gitAppId) => ({
|
||||
|
||||
// Filter out non-app keys like 'has_latest_changes' and 'tags'
|
||||
const appIds = Object.keys(appsFromRepos).filter(
|
||||
(key) => key !== 'has_latest_changes' && key !== 'tags' && appsFromRepos[key]?.git_app_name
|
||||
);
|
||||
|
||||
return appIds.map((gitAppId) => ({
|
||||
name: appsFromRepos[gitAppId].git_app_name,
|
||||
value: gitAppId,
|
||||
}));
|
||||
|
|
@ -1154,7 +1195,7 @@ class HomePageComponent extends React.Component {
|
|||
this.setState({
|
||||
importingGitAppOperations: validationMessage,
|
||||
});
|
||||
return;
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
importedAppName: newAppName,
|
||||
|
|
@ -1162,6 +1203,44 @@ class HomePageComponent extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
handleAppRepoChange = async (newVal) => {
|
||||
const { appsFromRepos, orgGit } = this.state;
|
||||
const selectedApp = appsFromRepos[newVal];
|
||||
this.setState({
|
||||
selectedAppRepo: newVal,
|
||||
importedAppName: selectedApp?.git_app_name,
|
||||
});
|
||||
if (selectedApp?.app_name_exist === 'EXIST') {
|
||||
this.setState({
|
||||
importingGitAppOperations: { message: 'App name already exists' },
|
||||
fetchingLatestCommitData: true,
|
||||
latestCommitData: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
importingGitAppOperations: {},
|
||||
fetchingLatestCommitData: true,
|
||||
latestCommitData: null,
|
||||
selectedVersionOption: null,
|
||||
});
|
||||
}
|
||||
const selectedBranch = orgGit?.git_https?.github_branch || orgGit?.git_ssh?.git_branch || orgGit?.git_lab_branch;
|
||||
|
||||
try {
|
||||
const data = await gitSyncService.checkForUpdatesByAppName(selectedApp?.git_app_name, selectedBranch);
|
||||
this.setState({
|
||||
latestCommitData: data?.metaData,
|
||||
tags: data?.metaData.tags,
|
||||
fetchingLatestCommitData: false,
|
||||
selectedVersionOption: 'latest',
|
||||
});
|
||||
// TODO: Handle the response data
|
||||
} catch (error) {
|
||||
console.error('Failed to check for updates:', error);
|
||||
this.setState({ fetchingLatestCommitData: false });
|
||||
}
|
||||
};
|
||||
|
||||
// Helper functions for workflow limit checks
|
||||
hasWorkflowLimitReached = () => {
|
||||
const { workflowInstanceLevelLimit, workflowWorkspaceLevelLimit } = this.state;
|
||||
|
|
@ -1213,6 +1292,70 @@ class HomePageComponent extends React.Component {
|
|||
this.eraseAIOnboardingRelatedCookies();
|
||||
};
|
||||
|
||||
generateVersionOptions = () => {
|
||||
const { latestCommitData, tags } = this.state;
|
||||
const options = [];
|
||||
|
||||
if (latestCommitData?.latestCommit?.[0]) {
|
||||
options.push({
|
||||
label: 'Latest commit',
|
||||
value: 'latest',
|
||||
isLatest: true,
|
||||
isDraft: true,
|
||||
sha: latestCommitData?.latestCommit?.[0]?.commitId,
|
||||
});
|
||||
}
|
||||
|
||||
// Add tags - filter out tags that have the same SHA as the latest commit
|
||||
if (tags && tags.length > 0) {
|
||||
tags.forEach((tag) => {
|
||||
const [, version] = tag.name.split('/');
|
||||
options.push({
|
||||
label: version,
|
||||
value: tag.name,
|
||||
isLatest: false,
|
||||
isDraft: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
getSelectedVersionCommitInfo = () => {
|
||||
const { selectedVersionOption, latestCommitData, tags } = this.state;
|
||||
const isLatest = !selectedVersionOption || selectedVersionOption === 'latest';
|
||||
|
||||
if (isLatest) {
|
||||
return {
|
||||
message: latestCommitData?.latestCommit[0]?.message,
|
||||
author: latestCommitData?.latestCommit[0]?.author,
|
||||
date: latestCommitData?.latestCommit[0]?.date,
|
||||
versionName: latestCommitData?.gitVersionName,
|
||||
};
|
||||
}
|
||||
|
||||
const selectedTag = tags?.find((t) => t.name === selectedVersionOption);
|
||||
return {
|
||||
message: selectedTag?.message,
|
||||
author: selectedTag?.tagger?.name,
|
||||
date: selectedTag?.tagger?.date,
|
||||
versionName: selectedVersionOption?.split('/')?.pop(),
|
||||
};
|
||||
};
|
||||
|
||||
renderVersionOption = (option) => {
|
||||
return (
|
||||
<div className="version-option-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span className="version-option-name">{option.label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
handleVersionOptionChange = (newVal) => {
|
||||
this.setState({ selectedVersionOption: newVal });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
apps,
|
||||
|
|
@ -1242,7 +1385,10 @@ class HomePageComponent extends React.Component {
|
|||
fetchingAppsFromRepos,
|
||||
selectedAppRepo,
|
||||
appsFromRepos,
|
||||
latestCommitData,
|
||||
fetchingLatestCommitData,
|
||||
importingApp,
|
||||
selectedVersionOption,
|
||||
importingGitAppOperations,
|
||||
featuresLoaded,
|
||||
showCreateAppModal,
|
||||
|
|
@ -1351,7 +1497,6 @@ class HomePageComponent extends React.Component {
|
|||
const threshold = 3;
|
||||
const isLong = missingGroups.length > threshold;
|
||||
const displayedGroups = missingGroupsExpanded ? missingGroups : missingGroups.slice(0, threshold);
|
||||
|
||||
return (
|
||||
<Layout switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode}>
|
||||
<div className="wrapper home-page">
|
||||
|
|
@ -1536,18 +1681,7 @@ class HomePageComponent extends React.Component {
|
|||
<Select
|
||||
options={this.generateOptionsForRepository()}
|
||||
disabled={importingApp}
|
||||
onChange={(newVal) => {
|
||||
this.setState(
|
||||
{ selectedAppRepo: newVal, importedAppName: appsFromRepos[newVal]?.git_app_name },
|
||||
() => {
|
||||
if (appsFromRepos[newVal]?.app_name_exist === 'EXIST') {
|
||||
this.setState({ importingGitAppOperations: { message: 'App name already exists' } });
|
||||
} else {
|
||||
this.setState({ importingGitAppOperations: {} });
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
onChange={this.handleAppRepoChange}
|
||||
width={'100%'}
|
||||
value={selectedAppRepo}
|
||||
placeholder={'Select app from git repository...'}
|
||||
|
|
@ -1558,8 +1692,9 @@ class HomePageComponent extends React.Component {
|
|||
</div>
|
||||
{selectedAppRepo && (
|
||||
<div className="commit-info">
|
||||
<div className="form-group mb-3">
|
||||
<label className="mb-1 info-label mt-3 tj-text-xsm font-weight-500" data-cy="app-name-label">
|
||||
{/* APP NAME */}
|
||||
<div className="form-group">
|
||||
<label className="mb-1 info-label tj-text-xsm font-weight-500" data-cy="app-name-label">
|
||||
App name
|
||||
</label>
|
||||
<div className="tj-app-input">
|
||||
|
|
@ -1584,6 +1719,8 @@ class HomePageComponent extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* EDITABLE CHECKBOX */}
|
||||
<div className="application-editable-checkbox-container">
|
||||
<input
|
||||
className="form-check-input"
|
||||
|
|
@ -1601,22 +1738,55 @@ class HomePageComponent extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VERSION/TAG SELECT */}
|
||||
<div className="form-group">
|
||||
<label className="mb-1 info-label tj-text-xsm font-weight-500" data-cy="version-select-label">
|
||||
Select version to pull from
|
||||
</label>
|
||||
<div className="tj-app-input" data-cy="version-select">
|
||||
<Select
|
||||
options={this.generateVersionOptions()}
|
||||
disabled={importingApp || fetchingLatestCommitData}
|
||||
onChange={this.handleVersionOptionChange}
|
||||
width={'100%'}
|
||||
value={this.state.selectedVersionOption}
|
||||
placeholder={fetchingLatestCommitData ? 'Loading versions...' : 'Select version or tag...'}
|
||||
closeMenuOnSelect={true}
|
||||
customWrap={true}
|
||||
customOption={this.renderVersionOption}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LAST COMMIT */}
|
||||
<div className="form-group">
|
||||
<label className="mb-1 tj-text-xsm font-weight-500" data-cy="last-commit-label">
|
||||
Last commit
|
||||
</label>
|
||||
<div className="last-commit-info form-control">
|
||||
<div className="message-info">
|
||||
<div data-cy="las-commit-message">
|
||||
{appsFromRepos[selectedAppRepo]?.last_commit_message ?? 'No commits yet'}
|
||||
</div>
|
||||
<div data-cy="last-commit-version">{appsFromRepos[selectedAppRepo]?.git_version_name}</div>
|
||||
</div>
|
||||
<div className="author-info" data-cy="auther-info">
|
||||
{`Done by ${appsFromRepos[selectedAppRepo]?.last_commit_user} at ${moment(
|
||||
new Date(appsFromRepos[selectedAppRepo]?.lastpush_date)
|
||||
).format('DD MMM YYYY, h:mm a')}`}
|
||||
</div>
|
||||
{fetchingLatestCommitData ? (
|
||||
// need to add UI for loading state here -> Pending
|
||||
<div className="message-info">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="message-info">
|
||||
<div data-cy="last-commit-message">
|
||||
{this.getSelectedVersionCommitInfo().message || 'No commits yet'}
|
||||
</div>
|
||||
<div data-cy="last-commit-version">
|
||||
{this.getSelectedVersionCommitInfo().gitVersionName}
|
||||
</div>
|
||||
</div>
|
||||
{this.getSelectedVersionCommitInfo().author && this.getSelectedVersionCommitInfo().date && (
|
||||
<div className="author-info" data-cy="auther-info">
|
||||
{`Done by ${this.getSelectedVersionCommitInfo().author} at ${moment(
|
||||
new Date(this.getSelectedVersionCommitInfo().date)
|
||||
).format('DD MMM YYYY, h:mm a')}`}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2051,3 +2221,7 @@ const withStore = (Component) => (props) => {
|
|||
};
|
||||
|
||||
export const HomePage = withTranslation()(withStore(withRouter(HomePageComponent)));
|
||||
|
||||
// Need to fix latest commit thing ->
|
||||
// Call the same api with latest commit flag as true once the user selects a specific app from the list,
|
||||
// It should make that call once the endpoint is selected
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export function AppModal({
|
|||
appType,
|
||||
dependentPluginsDetail = [],
|
||||
dependentPlugins = [],
|
||||
modalType,
|
||||
// isAutoCommit = false,
|
||||
}) {
|
||||
if (!selectedAppName && templateDetails) {
|
||||
selectedAppName = templateDetails?.name || '';
|
||||
|
|
@ -218,27 +220,32 @@ export function AppModal({
|
|||
{`${appTypeName} name must be unique and max 50 characters`}
|
||||
</small>
|
||||
)}
|
||||
{orgGit?.is_enabled && appType != APP_TYPE.WORKFLOW && appType != APP_TYPE.MODULE && (
|
||||
<div className="commit-changes mt-3">
|
||||
<div>
|
||||
<input
|
||||
class="form-check-input"
|
||||
checked={commitEnabled}
|
||||
type="checkbox"
|
||||
onChange={handleCommitEnableChange}
|
||||
data-cy="git-commit-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="tj-text tj-text-xsm" data-cy="commit-changes-label">
|
||||
Commit changes
|
||||
{/* Disabling autoCommit */}
|
||||
{/* {orgGit?.is_enabled &&
|
||||
modalType !== 'create' &&
|
||||
appType != APP_TYPE.WORKFLOW &&
|
||||
appType != APP_TYPE.MODULE && (
|
||||
<div className="commit-changes mt-3">
|
||||
<div>
|
||||
<input
|
||||
class="form-check-input"
|
||||
checked={commitEnabled}
|
||||
type="checkbox"
|
||||
onChange={handleCommitEnableChange}
|
||||
// disabled={isAutoCommit}
|
||||
data-cy="git-commit-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="tj-text-xxsm" data-cy="commit-helper-text">
|
||||
This action commits the app's creation to the git repository
|
||||
<div>
|
||||
<div className="tj-text tj-text-xsm" data-cy="commit-changes-label">
|
||||
Commit changes
|
||||
</div>
|
||||
<div className="tj-text-xxsm" data-cy="commit-helper-text">
|
||||
This action commits the app's creation to the git repository
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
{dependentPlugins && dependentPlugins.length >= 1 && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
|
|||
import { SortableContext, arrayMove } from '@dnd-kit/sortable';
|
||||
import { SortableItem } from './components';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
|
||||
|
||||
export function SortableList({ items, onChange, renderItem }) {
|
||||
const { isModuleEditor } = useModuleContext();
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
|
|
@ -17,7 +19,7 @@ export function SortableList({ items, onChange, renderItem }) {
|
|||
// })
|
||||
);
|
||||
|
||||
const shouldFreeze = useStore((state) => state.isVersionReleased || state.isEditorFreezed);
|
||||
const shouldFreeze = useStore((state) => state.getShouldFreeze(false, isModuleEditor));
|
||||
const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -46,12 +46,13 @@ function getAppVersionData(appId, versionId, mode) {
|
|||
);
|
||||
}
|
||||
|
||||
function create(appId, versionName, versionDescription, versionFromId, currentEnvironmentId) {
|
||||
function create(appId, versionName, versionDescription, versionFromId, currentEnvironmentId, versionType = 'version') {
|
||||
const body = {
|
||||
versionName,
|
||||
versionDescription,
|
||||
versionFromId,
|
||||
environmentId: currentEnvironmentId,
|
||||
versionType,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
|
|
@ -150,10 +151,10 @@ function autoSaveApp(
|
|||
const body = !type
|
||||
? { ...diff }
|
||||
: bodyMappings[type]?.[operation] || {
|
||||
is_user_switched_version: isUserSwitchedVersion,
|
||||
pageId,
|
||||
diff,
|
||||
};
|
||||
is_user_switched_version: isUserSwitchedVersion,
|
||||
pageId,
|
||||
diff,
|
||||
};
|
||||
|
||||
if (type === 'components' && operation === 'delete' && isComponentCutProcess) {
|
||||
body['is_component_cut'] = true;
|
||||
|
|
|
|||
|
|
@ -13,12 +13,22 @@ export const gitSyncService = {
|
|||
gitPull,
|
||||
importGitApp,
|
||||
checkForUpdates,
|
||||
checkForUpdatesByAppName,
|
||||
confirmPullChanges,
|
||||
updateStatus,
|
||||
getGitStatus,
|
||||
saveProviderConfigs,
|
||||
updateAppEditState,
|
||||
getAppGitConfigs,
|
||||
// New branch management methods
|
||||
getAllBranches,
|
||||
createBranch,
|
||||
getPullRequests,
|
||||
switchBranch,
|
||||
updateGitConfigs,
|
||||
getGitConfigs,
|
||||
createGitTag,
|
||||
checkTagExists,
|
||||
};
|
||||
|
||||
function create(organizationId, gitUrl, gitType) {
|
||||
|
|
@ -38,11 +48,12 @@ function create(organizationId, gitUrl, gitType) {
|
|||
}
|
||||
|
||||
function updateConfig(organizationGitId, updateParam, gitType = '') {
|
||||
const { gitUrl, autoCommit, keyType } = updateParam;
|
||||
const { gitUrl, autoCommit, keyType, branchingEnabled } = updateParam;
|
||||
const body = {
|
||||
...(gitUrl && { gitUrl }),
|
||||
...(autoCommit != null && { autoCommit }),
|
||||
...(keyType && { keyType }),
|
||||
...(branchingEnabled && { branchingEnabled }),
|
||||
};
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
|
|
@ -108,6 +119,7 @@ function deleteConfig(organizationGitId, gitType) {
|
|||
}
|
||||
|
||||
function gitPush(body, appGitId, versionId) {
|
||||
// body can now include { commitMessage, sourceBranch } when branching is enabled
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
|
|
@ -134,13 +146,27 @@ function getAppConfig(organizationId, versionId) {
|
|||
return response;
|
||||
}
|
||||
|
||||
function checkForUpdates(appId) {
|
||||
function checkForUpdates(appId, branchName = '') {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/gitpull/app/${appId}`, requestOptions).then(handleResponse);
|
||||
return fetch(`${config.apiUrl}/app-git/gitpull/app/${appId}?branch=${branchName}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function checkForUpdatesByAppName(appName, branchName = '') {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
const params = new URLSearchParams();
|
||||
if (appName) params.append('appName', appName);
|
||||
if (branchName) params.append('branch', branchName);
|
||||
return fetch(`${config.apiUrl}/app-git/gitpull/app?${params.toString()}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function gitPull() {
|
||||
|
|
@ -221,4 +247,135 @@ function getAppGitConfigs(workspaceId, versionId) {
|
|||
|
||||
return fetch(`${config.apiUrl}/app-git/${workspaceId}/app/${versionId}/configs`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
// Branch Management API Methods
|
||||
|
||||
/**
|
||||
* Get all branches for an app
|
||||
* @param {string} appId - Application ID
|
||||
* @param {string} organizationId - Organization ID
|
||||
* @returns {Promise} Promise resolving to branches array
|
||||
*/
|
||||
function getAllBranches(appId, organizationId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/${organizationId}/app/${appId}/branches`, requestOptions).then((response) =>
|
||||
handleResponse(response, false, null, true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new branch
|
||||
* @param {string} appId - Application ID
|
||||
* @param {string} organizationId - Organization ID
|
||||
* @param {object} branchData - { branch_name, version_from_id, auto_commit }
|
||||
* @returns {Promise} Promise resolving to created branch
|
||||
*/
|
||||
function createBranch(appId, organizationId, branchData) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(branchData),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/${organizationId}/app/${appId}/branches`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pull requests for an app
|
||||
* @param {string} appId - Application ID
|
||||
* @returns {Promise} Promise resolving to pull requests array
|
||||
*/
|
||||
function getPullRequests(appId, organizationId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/${organizationId}/app/${appId}/pull-requests`, requestOptions).then(
|
||||
(response) => handleResponse(response, false, null, true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different branch (pull commits from branch)
|
||||
* @param {string} appId - Application ID
|
||||
* @param {string} branchName - Target branch name
|
||||
* @returns {Promise} Promise resolving to pull result
|
||||
*/
|
||||
function switchBranch(appId, branchName) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/gitpull/app/${appId}?branch=${branchName}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update git configurations (including branching enabled status)
|
||||
* @param {string} appId - Application ID
|
||||
* @param {object} configs - Configuration object { branching_enabled, ...otherConfigs }
|
||||
* @returns {Promise} Promise resolving to updated configs
|
||||
*/
|
||||
function updateGitConfigs(appId, configs) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(configs),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/${appId}/configs`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get git configurations for an app version
|
||||
* @param {string} organizationId - Organization ID
|
||||
* @param {string} versionId - Version ID
|
||||
* @returns {Promise} Promise resolving to git configs
|
||||
*/
|
||||
function getGitConfigs(organizationId, versionId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/${organizationId}/app/${versionId}/configs`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function createGitTag(appId, versionId, versionDescription) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ message: versionDescription }),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/${appId}/versions/${versionId}/tag`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a git tag already exists for the given app and version name.
|
||||
* This should be called BEFORE saving the version locally to ensure
|
||||
* local save and tag creation stay in sync.
|
||||
* @param {string} appId - Application ID
|
||||
* @param {string} versionName - Version name to check
|
||||
* @returns {Promise<{ exists: boolean, tag_name: string }>} Promise resolving to tag existence check
|
||||
*/
|
||||
function checkTagExists(appId, versionName) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
};
|
||||
return fetch(`${config.apiUrl}/app-git/${appId}/check-tag/${encodeURIComponent(versionName)}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
// Remove all app-git api's to separate service from here.
|
||||
|
|
|
|||
|
|
@ -81,10 +81,24 @@ export const useEnvironmentsAndVersionsStore = create(
|
|||
},
|
||||
setSelectedVersion: (selectedVersion) => set({ selectedVersion }),
|
||||
setEnvironmentAndVersionsInitStatus: (state) => set({ completedEnvironmentAndVersionsInit: state }),
|
||||
createNewVersionAction: async (appId, versionName, selectedVersionId, onSuccess, onFailure) => {
|
||||
createNewVersionAction: async (
|
||||
appId,
|
||||
versionName,
|
||||
selectedVersionId,
|
||||
onSuccess,
|
||||
onFailure,
|
||||
versionType = 'version'
|
||||
) => {
|
||||
try {
|
||||
const editorEnvironment = get().selectedEnvironment.id;
|
||||
const newVersion = await appVersionService.create(appId, versionName, selectedVersionId, editorEnvironment);
|
||||
const newVersion = await appVersionService.create(
|
||||
appId,
|
||||
versionName,
|
||||
'',
|
||||
selectedVersionId,
|
||||
editorEnvironment,
|
||||
versionType
|
||||
);
|
||||
const editorVersion = {
|
||||
id: newVersion.id,
|
||||
name: newVersion.name,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ export const useVersionManagerStore = create(
|
|||
const { versions, searchQuery } = get();
|
||||
|
||||
let filtered = versions;
|
||||
filtered = filtered.filter((v) => {
|
||||
const versionType = v.versionType || v.version_type;
|
||||
return versionType !== 'branch';
|
||||
});
|
||||
|
||||
// Filter by search query only
|
||||
if (searchQuery) {
|
||||
|
|
|
|||
1153
frontend/src/_styles/branch-dropdown.scss
Normal file
1153
frontend/src/_styles/branch-dropdown.scss
Normal file
File diff suppressed because it is too large
Load diff
574
frontend/src/_styles/create-branch-modal.scss
Normal file
574
frontend/src/_styles/create-branch-modal.scss
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
// Create Branch Modal Styles
|
||||
.create-branch-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.create-branch-modal {
|
||||
width: 404px;
|
||||
min-height: auto;
|
||||
background-color: var(--slate1);
|
||||
border: 1px solid var(--slate6);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: modalFadeIn 0.2s ease;
|
||||
|
||||
&.theme-dark {
|
||||
background-color: var(--slate2);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.create-branch-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--slate6);
|
||||
|
||||
.modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: var(--slate12);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s ease;
|
||||
color: var(--slate11);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slate4);
|
||||
color: var(--slate12);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-branch-modal-body {
|
||||
flex: 1;
|
||||
max-height: calc(80vh - 140px);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--slate7);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--slate3);
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
color: var(--slate12);
|
||||
margin-bottom: 2px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
color: var(--slate12);
|
||||
background-color: var(--slate2);
|
||||
border: 1px solid var(--slate6);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: var(--slate7);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--indigo9);
|
||||
box-shadow: 0 0 0 3px var(--indigo3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.branch-modal-form-input {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--slate12);
|
||||
background-color: var(--slate1);
|
||||
border: 1px solid var(--slate7);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
line-height: 20px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--slate9);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: var(--slate8);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--indigo9);
|
||||
box-shadow: 0 0 0 3px var(--indigo3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.form-input-error {
|
||||
border-color: var(--red9);
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 3px var(--red3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-error-message {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--red11);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.form-helper-text {
|
||||
margin-top: 2px;
|
||||
padding-left: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: var(--slate10);
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 4px 0;
|
||||
|
||||
&:hover {
|
||||
.form-checkbox:not(:disabled) {
|
||||
border-color: var(--slate8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
border: 1px solid var(--slate7);
|
||||
border-radius: 4px;
|
||||
background-color: var(--slate3);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
margin-top: 0;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: var(--slate8);
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: var(--indigo9);
|
||||
border-color: var(--indigo9);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--slate12);
|
||||
|
||||
.checkbox-helper {
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: var(--slate11);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.draft-warning-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--amber2);
|
||||
border: 1px solid var(--amber6);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--amber11);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: var(--amber12);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.info-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--blue2);
|
||||
border: 1px solid var(--blue6);
|
||||
border-radius: 6px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--blue11);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: var(--blue12);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.create-branch-info {
|
||||
background-color: var(--background-accent-weak);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--slate12);
|
||||
|
||||
img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.create-branch-modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid var(--slate6);
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cancel-button,
|
||||
button[variant='tertiary'] {
|
||||
min-width: auto;
|
||||
height: 32px;
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.create-button,
|
||||
button[variant='primary'] {
|
||||
min-width: auto;
|
||||
height: 32px;
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Dropdown Styles (matching Figma design)
|
||||
.custom-dropdown {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-dropdown-trigger {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 7px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--slate1);
|
||||
border: 1px solid var(--slate7);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 12px;
|
||||
color: var(--slate12);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: var(--slate8);
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
border-color: var(--indigo9);
|
||||
border-width: 1.5px;
|
||||
padding: 6.5px 9.5px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--slate11);
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.is-open svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-dropdown-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.version-name {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--slate12);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--slate1);
|
||||
border: 1px solid var(--slate6);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
padding: 8px;
|
||||
animation: dropdownFadeIn 0.15s ease;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--slate7);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--slate3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdownFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slate3);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: var(--slate2);
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--indigo9);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.check-icon-placeholder {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--slate12);
|
||||
line-height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-description {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
color: var(--slate10);
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 18px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-badge-draft {
|
||||
background-color: var(--amber3);
|
||||
color: var(--amber11);
|
||||
}
|
||||
|
||||
.status-badge-released {
|
||||
background-color: var(--green3);
|
||||
color: var(--green11);
|
||||
}
|
||||
|
||||
.status-badge-published {
|
||||
background-color: var(--blue3);
|
||||
color: var(--blue11);
|
||||
}
|
||||
|
||||
// Dark mode adjustments
|
||||
.theme-dark {
|
||||
.custom-dropdown-trigger {
|
||||
background-color: var(--slate2);
|
||||
}
|
||||
|
||||
.custom-dropdown-menu {
|
||||
background-color: var(--slate2);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
&:hover {
|
||||
background-color: var(--slate4);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: var(--slate3);
|
||||
}
|
||||
}
|
||||
}
|
||||
158
frontend/src/_styles/draft-version-warning-modal.scss
Normal file
158
frontend/src/_styles/draft-version-warning-modal.scss
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// Draft Version Warning Modal Styles
|
||||
.draft-warning-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.draft-warning-modal {
|
||||
width: 360px;
|
||||
min-height: 198px;
|
||||
background-color: var(--slate1);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: modalFadeIn 0.2s ease;
|
||||
|
||||
&.theme-dark {
|
||||
background-color: var(--slate2);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.draft-warning-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px 0;
|
||||
|
||||
.warning-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--amber3);
|
||||
|
||||
svg {
|
||||
color: var(--amber11);
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s ease;
|
||||
color: var(--slate11);
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slate4);
|
||||
color: var(--slate12);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.draft-warning-modal-body {
|
||||
flex: 1;
|
||||
padding: 16px 24px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.warning-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--slate12);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
font-size: 14px;
|
||||
color: var(--slate11);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.warning-info-box {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--blue2);
|
||||
border: 1px solid var(--blue6);
|
||||
border-radius: 6px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--blue11);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
strong {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--blue12);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: var(--blue11);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.draft-warning-modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--slate6);
|
||||
|
||||
.close-action-button {
|
||||
min-width: 80px;
|
||||
height: 32px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
78
frontend/src/_styles/locked-branch-banner.scss
Normal file
78
frontend/src/_styles/locked-branch-banner.scss
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// LockedBranchBanner - Full-width warning banner for read-only branches
|
||||
// Displays below the editor navigation when branch is locked
|
||||
|
||||
.locked-branch-banner {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: linear-gradient(90deg, #fef3c7 0%, #fde68a 100%);
|
||||
border-bottom: 1px solid #fbbf24;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
position: relative;
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 1200px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
flex-shrink: 0;
|
||||
color: #92400e;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&-message {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #92400e;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&-branch {
|
||||
font-size: 13px;
|
||||
color: #78350f;
|
||||
line-height: 20px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
color: #451a03;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
.dark-theme {
|
||||
.locked-branch-banner {
|
||||
background: linear-gradient(90deg, #451a03 0%, #78350f 100%);
|
||||
border-bottom-color: #92400e;
|
||||
|
||||
&-icon {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
&-message {
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
&-branch {
|
||||
color: #fef3c7;
|
||||
|
||||
strong {
|
||||
color: #fef3c7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -225,7 +225,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: content-box;
|
||||
|
||||
|
||||
&.no-preview-settings {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
|
@ -348,6 +348,7 @@
|
|||
align-items: center;
|
||||
justify-content: start;
|
||||
gap: 10px;
|
||||
|
||||
.page-name {
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-weight: 500;
|
||||
|
|
@ -604,56 +605,21 @@
|
|||
height: 61px !important; //1px to account for the border at navbar bottom
|
||||
|
||||
header {
|
||||
height: 100% !important;
|
||||
background-color: var(--nav-menu-bg) !important;
|
||||
border-bottom-color: var(--nav-menu-border);
|
||||
max-height: none;
|
||||
position: static;
|
||||
height: 100% !important;
|
||||
background-color: var(--nav-menu-bg) !important;
|
||||
border-bottom-color: var(--nav-menu-border);
|
||||
max-height: none;
|
||||
position: static;
|
||||
|
||||
.header-container {
|
||||
background-color: transparent;
|
||||
padding: 12px 8px !important;
|
||||
flex-wrap: nowrap !important;
|
||||
|
||||
.icon-btn {
|
||||
height: 36px !important;
|
||||
width: 36px !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
height: auto !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-page-menu-popup {
|
||||
.mobile-header {
|
||||
height: 61px !important; //1px to account for the border bottom
|
||||
|
||||
header {
|
||||
height: 100% !important;
|
||||
max-height: none;
|
||||
background-color: var(--nav-menu-bg) !important;
|
||||
border-bottom-color: var(--nav-menu-border);
|
||||
position: static;
|
||||
|
||||
.header-container {
|
||||
.header-container {
|
||||
background-color: transparent;
|
||||
padding: 12px 8px !important;
|
||||
flex-wrap: nowrap !important;
|
||||
|
||||
.icon-btn {
|
||||
height: 36px !important;
|
||||
width: 36px !important;
|
||||
padding: 10px !important;
|
||||
height: 36px !important;
|
||||
width: 36px !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
|
|
@ -662,15 +628,50 @@
|
|||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
min-width: 0;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-navigation-area {
|
||||
.mobile-page-menu-popup {
|
||||
.mobile-header {
|
||||
height: 61px !important; //1px to account for the border bottom
|
||||
|
||||
header {
|
||||
height: 100% !important;
|
||||
max-height: none;
|
||||
background-color: var(--nav-menu-bg) !important;
|
||||
border-bottom-color: var(--nav-menu-border);
|
||||
position: static;
|
||||
|
||||
.header-container {
|
||||
padding: 12px 8px !important;
|
||||
flex-wrap: nowrap !important;
|
||||
|
||||
.icon-btn {
|
||||
height: 36px !important;
|
||||
width: 36px !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
height: auto !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-navigation-area {
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
background-color: var(--nav-menu-bg);
|
||||
|
|
@ -678,137 +679,137 @@
|
|||
flex-direction: column;
|
||||
box-sizing: content-box;
|
||||
padding: 8px 0;
|
||||
|
||||
.pages-wrapper {
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tj-list-item {
|
||||
height: 36px;
|
||||
justify-content: start;
|
||||
padding: 8px 12px;
|
||||
gap: 10px;
|
||||
margin: 0px !important;
|
||||
color: var(--nav-item-label-color);
|
||||
border-radius: var(--nav-item-pill-radius);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hovered-nav-item-pill-bg);
|
||||
}
|
||||
|
||||
&.tj-list-item-selected {
|
||||
color: var(--selected-nav-item-label-color);
|
||||
background-color: var(--selected-nav-item-pill-bg);
|
||||
}
|
||||
|
||||
.page-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-icon {
|
||||
line-height: 10px !important;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border: none;
|
||||
|
||||
.accordion-body{
|
||||
padding: 0 0 0 32px !important;
|
||||
border-bottom: 0px !important;
|
||||
|
||||
.tj-list-item {
|
||||
margin-top: 6px !important;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-group-wrapper {
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
border-radius: var(--nav-item-pill-radius);
|
||||
color: var(--nav-item-label-color);
|
||||
text-align: left;
|
||||
|
||||
.group-info {
|
||||
.pages-wrapper {
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
gap: 10px;
|
||||
|
||||
.page-name {
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hovered-nav-item-pill-bg);
|
||||
}
|
||||
|
||||
&.page-group-selected[data-state="closed"] {
|
||||
color: var(--selected-nav-item-label-color);
|
||||
background-color: var(--selected-nav-item-pill-bg);
|
||||
}
|
||||
|
||||
.tj-list-item {
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
a.page-link {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a.page-link:hover {
|
||||
color: white;
|
||||
background-color: #4D72FA;
|
||||
}
|
||||
|
||||
a.page-link.active {
|
||||
color: white;
|
||||
background-color: #4D72FA;
|
||||
}
|
||||
}
|
||||
|
||||
.page-dark-mode-btn-wrapper {
|
||||
.tj-list-item {
|
||||
height: 36px;
|
||||
justify-content: start;
|
||||
padding: 8px 12px;
|
||||
gap: 10px;
|
||||
margin: 0px !important;
|
||||
color: var(--nav-item-label-color);
|
||||
border-radius: var(--nav-item-pill-radius);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hovered-nav-item-pill-bg);
|
||||
}
|
||||
|
||||
&.tj-list-item-selected {
|
||||
color: var(--selected-nav-item-label-color);
|
||||
background-color: var(--selected-nav-item-pill-bg);
|
||||
}
|
||||
|
||||
.page-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-icon {
|
||||
line-height: 10px !important;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border: none;
|
||||
|
||||
.accordion-body {
|
||||
padding: 0 0 0 32px !important;
|
||||
border-bottom: 0px !important;
|
||||
|
||||
.tj-list-item {
|
||||
margin-top: 6px !important;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-group-wrapper {
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
border-radius: var(--nav-item-pill-radius);
|
||||
color: var(--nav-item-label-color);
|
||||
text-align: left;
|
||||
|
||||
.group-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
gap: 10px;
|
||||
|
||||
.page-name {
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hovered-nav-item-pill-bg);
|
||||
}
|
||||
|
||||
&.page-group-selected[data-state="closed"] {
|
||||
color: var(--selected-nav-item-label-color);
|
||||
background-color: var(--selected-nav-item-pill-bg);
|
||||
}
|
||||
|
||||
.tj-list-item {
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
a.page-link {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a.page-link:hover {
|
||||
color: white;
|
||||
background-color: #4D72FA;
|
||||
}
|
||||
|
||||
a.page-link.active {
|
||||
color: white;
|
||||
background-color: #4D72FA;
|
||||
}
|
||||
}
|
||||
|
||||
.page-dark-mode-btn-wrapper {
|
||||
background-color: var(--nav-menu-bg);
|
||||
display: flex;
|
||||
border-top: 1px solid var(--nav-menu-border);
|
||||
height: 57px;
|
||||
padding: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.page-menu-scroll {
|
||||
.page-menu-scroll {
|
||||
scrollbar-color: var(--interactive-hover) transparent !important;
|
||||
scrollbar-width: thin !important;
|
||||
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
|
@ -816,8 +817,8 @@
|
|||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--interactive-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.viewer {
|
||||
|
|
@ -841,6 +842,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.offset-top-bar-navigation {
|
||||
#sidebar-page-navigation {
|
||||
.navigation-area {
|
||||
|
|
@ -954,10 +956,12 @@
|
|||
height: 100%;
|
||||
z-index: 1000;
|
||||
|
||||
&:hover, &.active {
|
||||
&:hover,
|
||||
&.active {
|
||||
.navigation-area.navigation-hover-trigger {
|
||||
box-shadow: 0 0 0 1px #4af !important;
|
||||
}
|
||||
|
||||
.mobile-nav-container.navigation-hover-trigger {
|
||||
border: 1px solid #4af;
|
||||
}
|
||||
|
|
|
|||
225
frontend/src/_styles/switch-branch-modal.scss
Normal file
225
frontend/src/_styles/switch-branch-modal.scss
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// Switch Branch Modal Styles
|
||||
.switch-branch-modal {
|
||||
.modal-dialog {
|
||||
max-width: 424px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 6px;
|
||||
background: var(--slate1);
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid var(--slate6);
|
||||
padding: 16px 24px 8px;
|
||||
|
||||
.modal-title {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: #11181c;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-branch-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
// Search Section
|
||||
.search-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
|
||||
.section-label {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
color: var(--slate11);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.24px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 7px 12px;
|
||||
background: var(--slate1);
|
||||
border: 1px solid var(--slate7);
|
||||
border-radius: 6px;
|
||||
height: 32px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: none;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--slate12);
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--slate11);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Branch List
|
||||
.branch-list-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
.branch-list-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 6px 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slate3);
|
||||
}
|
||||
|
||||
&.active {
|
||||
.branch-list-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.branch-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.branch-list-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.branch-list-name {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
color: var(--slate12);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.branch-list-meta {
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
color: var(--slate11);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 16px;
|
||||
color: var(--slate11);
|
||||
font-size: 13px;
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--slate6);
|
||||
border-top-color: var(--indigo9);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Footer
|
||||
.modal-footer-actions {
|
||||
border-top: 1px solid var(--slate6);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px 0 0;
|
||||
|
||||
.footer-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: 1px solid var(--slate6);
|
||||
background: var(--slate1);
|
||||
color: var(--slate12);
|
||||
|
||||
&:hover {
|
||||
background: var(--slate3);
|
||||
}
|
||||
|
||||
&.accent {
|
||||
border-color: var(--indigo7);
|
||||
|
||||
&:hover {
|
||||
background: var(--indigo2);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--indigo9);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
@ -14494,131 +14494,6 @@ tbody {
|
|||
}
|
||||
}
|
||||
|
||||
.git-sync-modal,
|
||||
.modal-base {
|
||||
|
||||
.create-commit-container,
|
||||
.commit-info,
|
||||
.pull-container,
|
||||
.pushpull-container {
|
||||
height: 260px !important;
|
||||
|
||||
.form-control {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--slate12);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
.tj-input-error-state {
|
||||
border: 1px solid var(--tomato9) !important;
|
||||
}
|
||||
|
||||
.tj-input-error {
|
||||
color: var(--tomato10) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: var(--slate10);
|
||||
}
|
||||
|
||||
.tj-input-error {
|
||||
color: var(--tomato10);
|
||||
}
|
||||
|
||||
.form-control.disabled {
|
||||
background-color: var(--slate3) !important;
|
||||
color: var(--slate9) !important;
|
||||
}
|
||||
|
||||
.last-commit-info {
|
||||
background: var(--slate3);
|
||||
|
||||
.message-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
font-size: 10px;
|
||||
color: var(--slate11);
|
||||
}
|
||||
}
|
||||
|
||||
.check-for-updates {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--indigo9);
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: var(--indigo9);
|
||||
}
|
||||
|
||||
rect {
|
||||
fill: none;
|
||||
}
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
height: unset !important;
|
||||
|
||||
.primary-spin-loader {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid var(--slate5);
|
||||
padding: 1rem;
|
||||
|
||||
.tj-btn-left-icon {
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
path {
|
||||
fill: var(--indigo1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tj-large-btn {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.loader-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-base {
|
||||
.tj-text-xxsm {
|
||||
color: var(--slate11);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid var(--slate5) !important;
|
||||
|
||||
.modal-title {
|
||||
color: var(--slate12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-gap-7 {
|
||||
gap: 7px;
|
||||
}
|
||||
|
|
@ -17891,22 +17766,6 @@ section.ai-message-prompt-input-wrapper {
|
|||
}
|
||||
}
|
||||
|
||||
.git-sync-modal {
|
||||
width: 400px !important;
|
||||
height: 484px !important;
|
||||
}
|
||||
|
||||
.dark-theme.git-sync-modal {
|
||||
.modal-header {
|
||||
border-bottom: 1px solid var(--slate5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.git-sync-modal .modal-header .modal-title .push-pull-tabs .tab-push.active,
|
||||
.git-sync-modal .modal-header .modal-title .push-pull-tabs .tab-pull.active {
|
||||
border-bottom: 2px solid var(--indigo9) !important;
|
||||
}
|
||||
|
||||
.custom_fc_frame {
|
||||
left: 40px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
background-color: #ffffff;
|
||||
color: var(--text-default);
|
||||
cursor: pointer;
|
||||
box-shadow: var(--elevation-100-box-shadow);
|
||||
box-shadow: var(--elevation-100-box-shadow);
|
||||
transition: box-shadow 0.12s ease, transform 0.08s ease;
|
||||
border-radius: 4px !important;
|
||||
outline: none;
|
||||
|
|
@ -293,7 +293,7 @@
|
|||
border-radius: 8px !important;
|
||||
padding: 0 !important;
|
||||
max-width: 300px !important;
|
||||
box-shadow: var(--elevation-400-box-shadow) !important;
|
||||
box-shadow: var(--elevation-400-box-shadow) !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
|
|
@ -309,14 +309,17 @@
|
|||
.tooltip.version-tooltip.bs-tooltip-start .tooltip-arrow::after {
|
||||
border-left-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.tooltip.version-tooltip.bs-tooltip-right .tooltip-arrow::before,
|
||||
.tooltip.version-tooltip.bs-tooltip-right .tooltip-arrow::after {
|
||||
border-right-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.tooltip.version-tooltip.bs-tooltip-top .tooltip-arrow::before,
|
||||
.tooltip.version-tooltip.bs-tooltip-top .tooltip-arrow::after {
|
||||
border-top-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.tooltip.version-tooltip.bs-tooltip-bottom .tooltip-arrow::before,
|
||||
.tooltip.version-tooltip.bs-tooltip-bottom .tooltip-arrow::after {
|
||||
border-bottom-color: #ffffff !important;
|
||||
|
|
|
|||
23
frontend/src/_ui/Icon/solidIcons/Check2.jsx
Normal file
23
frontend/src/_ui/Icon/solidIcons/Check2.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
const Check2 = ({ fill = '#3E63DD', width = '16', className = '', viewBox = '0 0 16 16', height }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height || width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.2902 2.89912C14.7297 3.25872 14.7945 3.90656 14.4349 4.34608L7.43009 12.9988C6.37251 14.4157 5.35903 14.0098 4.48418 13.2443L1.68515 10.7952C1.25776 10.4212 1.21445 9.77157 1.58842 9.34418C1.96238 8.91679 2.612 8.87348 3.03939 9.24745L5.83842 11.6966L12.8432 3.04381C13.2028 2.60428 13.8506 2.5395 14.2902 2.89912Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Check2;
|
||||
16
frontend/src/_ui/Icon/solidIcons/ChevronDownSmall.jsx
Normal file
16
frontend/src/_ui/Icon/solidIcons/ChevronDownSmall.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
const ChevronDownSmall = ({ fill = '#C1C8CD', width = '12', className = '', viewBox = '0 0 12 12' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke={fill} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ChevronDownSmall;
|
||||
16
frontend/src/_ui/Icon/solidIcons/CircleDot.jsx
Normal file
16
frontend/src/_ui/Icon/solidIcons/CircleDot.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
const CircleDot = ({ fill = '#C1C8CD', width = '14', className = '', viewBox = '0 0 16 16' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<circle cx="8" cy="8" r="3" fill={fill} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default CircleDot;
|
||||
22
frontend/src/_ui/Icon/solidIcons/Commit.jsx
Normal file
22
frontend/src/_ui/Icon/solidIcons/Commit.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
const Commit = ({ fill = 'var(--icon-default)', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M10 8C10 9.10457 9.10457 10 8 10C6.89543 10 6 9.10457 6 8M10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8M10 8H14M6 8H2"
|
||||
stroke={fill}
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Commit;
|
||||
28
frontend/src/_ui/Icon/solidIcons/ExternalLinkIcon.jsx
Normal file
28
frontend/src/_ui/Icon/solidIcons/ExternalLinkIcon.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
|
||||
const ExternalLinkIcon = ({ fill = '#C1C8CD', width = '16', className = '', viewBox = '0 0 16 16' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M13 3L13 7M13 3L9 3M13 3L8 8"
|
||||
stroke={fill}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13 9V13C13 13.5523 12.5523 14 12 14H3C2.44772 14 2 13.5523 2 13V4C2 3.44772 2.44772 3 3 3H7"
|
||||
stroke={fill}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ExternalLinkIcon;
|
||||
19
frontend/src/_ui/Icon/solidIcons/GitBranch.jsx
Normal file
19
frontend/src/_ui/Icon/solidIcons/GitBranch.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const GitBranch = ({ fill = '#C1C8CD', width = '16', className = '', viewBox = '0 0 16 16' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M14.6673 3.83325C14.6673 3.36263 14.5345 2.90156 14.2841 2.50309C14.0337 2.10461 13.6759 1.78491 13.2519 1.58075C12.8278 1.37658 12.3548 1.29626 11.8871 1.34901C11.4195 1.40175 10.9762 1.58543 10.6083 1.87892C10.2404 2.1724 9.96282 2.56377 9.80747 3.00801C9.65212 3.45226 9.62531 3.93132 9.73014 4.39012C9.83497 4.84892 10.0672 5.26881 10.4 5.6015C10.7329 5.93419 11.1529 6.16617 11.6118 6.27075V6.33325C11.6118 6.62794 11.4947 6.91055 11.2863 7.11893C11.078 7.3273 10.7954 7.44436 10.5007 7.44436H5.50066C5.11059 7.44528 4.72754 7.54823 4.38955 7.74297V6.27075C4.98882 6.13416 5.51678 5.7816 5.87257 5.28041C6.22836 4.77921 6.38704 4.16451 6.31832 3.55373C6.2496 2.94294 5.95828 2.37888 5.50001 1.96928C5.04173 1.55968 4.44863 1.33325 3.83399 1.33325C3.21935 1.33325 2.62625 1.55968 2.16798 1.96928C1.70971 2.37888 1.41839 2.94294 1.34967 3.55373C1.28094 4.16451 1.43963 4.77921 1.79542 5.28041C2.15121 5.7816 2.67917 6.13416 3.27844 6.27075V9.72909C2.67917 9.86567 2.15121 10.2182 1.79542 10.7194C1.43963 11.2206 1.28094 11.8353 1.34967 12.4461C1.41839 13.0569 1.70971 13.621 2.16798 14.0306C2.62625 14.4402 3.21935 14.6666 3.83399 14.6666C4.44863 14.6666 5.04173 14.4402 5.50001 14.0306C5.95828 13.621 6.2496 13.0569 6.31832 12.4461C6.38704 11.8353 6.22836 11.2206 5.87257 10.7194C5.51678 10.2182 4.98882 9.86567 4.38955 9.72909V9.66659C4.38955 9.3719 4.50661 9.08929 4.71499 8.88091C4.92336 8.67254 5.20598 8.55548 5.50066 8.55548H10.5007C11.0895 8.55364 11.6536 8.31893 12.07 7.90258C12.4863 7.48623 12.7211 6.92206 12.7229 6.33325V6.27075C13.2746 6.14372 13.7671 5.83367 14.1202 5.39107C14.4732 4.94848 14.6661 4.39941 14.6673 3.83325ZM2.4451 3.83325C2.4451 3.55855 2.52656 3.29002 2.67917 3.06162C2.83179 2.83322 3.0487 2.6552 3.30249 2.55008C3.55627 2.44496 3.83553 2.41745 4.10495 2.47104C4.37437 2.52463 4.62185 2.65691 4.81609 2.85115C5.01033 3.04539 5.14261 3.29287 5.1962 3.56229C5.24979 3.83171 5.22228 4.11097 5.11716 4.36475C5.01204 4.61854 4.83402 4.83545 4.60562 4.98807C4.37722 5.14068 4.10869 5.22214 3.83399 5.22214C3.4662 5.22031 3.11399 5.0734 2.85392 4.81332C2.59384 4.55325 2.44693 4.20104 2.4451 3.83325ZM5.22288 12.1666C5.22288 12.4413 5.14143 12.7098 4.98881 12.9382C4.8362 13.1666 4.61929 13.3446 4.3655 13.4498C4.11171 13.5549 3.83245 13.5824 3.56303 13.5288C3.29362 13.4752 3.04614 13.3429 2.8519 13.1487C2.65766 12.9544 2.52538 12.707 2.47179 12.4376C2.4182 12.1681 2.4457 11.8889 2.55083 11.6351C2.65595 11.3813 2.83397 11.1644 3.06237 11.0118C3.29077 10.8592 3.5593 10.7777 3.83399 10.7777C4.20179 10.7795 4.554 10.9264 4.81407 11.1865C5.07414 11.4466 5.22106 11.7988 5.22288 12.1666Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default GitBranch;
|
||||
21
frontend/src/_ui/Icon/solidIcons/GitMergeIcon.jsx
Normal file
21
frontend/src/_ui/Icon/solidIcons/GitMergeIcon.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
const GitMergeIcon = ({ fill = '#C1C8CD', width = '16', className = '', viewBox = '0 0 16 16' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 3C4 2.44772 4.44772 2 5 2C5.55228 2 6 2.44772 6 3C6 3.55228 5.55228 4 5 4C4.44772 4 4 3.55228 4 3ZM5 0C3.34315 0 2 1.34315 2 3C2 4.30622 2.83481 5.41746 4 5.82929V10.1707C2.83481 10.5825 2 11.6938 2 13C2 14.6569 3.34315 16 5 16C6.65685 16 8 14.6569 8 13C8 11.6938 7.16519 10.5825 6 10.1707V8C6 7.46957 6.21071 6.96086 6.58579 6.58579C6.96086 6.21071 7.46957 6 8 6H10C10.5304 6 11.0391 6.21071 11.4142 6.58579C11.7893 6.96086 12 7.46957 12 8V10.1707C10.8348 10.5825 10 11.6938 10 13C10 14.6569 11.3431 16 13 16C14.6569 16 16 14.6569 16 13C16 11.6938 15.1652 10.5825 14 10.1707V8C14 6.93913 13.5786 5.92172 12.8284 5.17157C12.0783 4.42143 11.0609 4 10 4H8C7.73478 4 7.47043 4.01728 7.21239 4.05058C6.85441 3.73088 6.44415 3.47326 6 3.31804V5.82929C7.16519 5.41746 8 4.30622 8 3C8 1.34315 6.65685 0 5 0ZM4 13C4 12.4477 4.44772 12 5 12C5.55228 12 6 12.4477 6 13C6 13.5523 5.55228 14 5 14C4.44772 14 4 13.5523 4 13ZM13 12C12.4477 12 12 12.4477 12 13C12 13.5523 12.4477 14 13 14C13.5523 14 14 13.5523 14 13C14 12.4477 13.5523 12 13 12Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default GitMergeIcon;
|
||||
19
frontend/src/_ui/Icon/solidIcons/LockClosed.jsx
Normal file
19
frontend/src/_ui/Icon/solidIcons/LockClosed.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const LockClosed = ({ fill = '#C1C8CD', width = '16', className = '', viewBox = '0 0 16 16' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M12.6667 7.33333H12V5.33333C12 3.49238 10.5076 2 8.66667 2C6.82572 2 5.33333 3.49238 5.33333 5.33333V7.33333H4.66667C3.93029 7.33333 3.33333 7.93029 3.33333 8.66667V12.6667C3.33333 13.403 3.93029 14 4.66667 14H12.6667C13.403 14 14 13.403 14 12.6667V8.66667C14 7.93029 13.403 7.33333 12.6667 7.33333ZM6.66667 5.33333C6.66667 4.22876 7.56209 3.33333 8.66667 3.33333C9.77124 3.33333 10.6667 4.22876 10.6667 5.33333V7.33333H6.66667V5.33333Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LockClosed;
|
||||
19
frontend/src/_ui/Icon/solidIcons/PlusIcon.jsx
Normal file
19
frontend/src/_ui/Icon/solidIcons/PlusIcon.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const PlusIcon = ({ fill = '#C1C8CD', width = '14', className = '', viewBox = '0 0 14 14' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M7 0C7.27614 0 7.5 0.223858 7.5 0.5V6.5H13.5C13.7761 6.5 14 6.72386 14 7C14 7.27614 13.7761 7.5 13.5 7.5H7.5V13.5C7.5 13.7761 7.27614 14 7 14C6.72386 14 6.5 13.7761 6.5 13.5V7.5H0.5C0.223858 7.5 0 7.27614 0 7C0 6.72386 0.223858 6.5 0.5 6.5H6.5V0.5C6.5 0.223858 6.72386 0 7 0Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default PlusIcon;
|
||||
21
frontend/src/_ui/Icon/solidIcons/Refresh.jsx
Normal file
21
frontend/src/_ui/Icon/solidIcons/Refresh.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
const Refresh = ({ fill = 'var(--icon-default)', width = '14', className = '', viewBox = '0 0 14 14' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill={fill}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.4051 1.21434C11.1716 1.1176 10.9028 1.17108 10.7241 1.34982L10.0479 2.02598C9.16089 1.48132 8.11662 1.16677 6.99934 1.16677C3.77768 1.16677 1.16602 3.77843 1.16602 7.00008C1.16602 7.46032 1.53911 7.83342 1.99935 7.83342C2.45958 7.83342 2.83268 7.46032 2.83268 7.00008C2.83268 4.69891 4.69815 2.83343 6.99934 2.83343C7.65305 2.83343 8.27137 2.98368 8.82193 3.25193L8.22404 3.84982C8.04529 4.02857 7.99182 4.29739 8.08856 4.53093C8.1853 4.76448 8.41319 4.91676 8.66598 4.91676H11.166C11.5111 4.91676 11.791 4.63693 11.791 4.29176V1.79176C11.791 1.53898 11.6387 1.31108 11.4051 1.21434ZM11.166 7.00008C11.166 6.53985 11.5391 6.16675 11.9993 6.16675C12.4596 6.16675 12.8326 6.53985 12.8326 7.00008C12.8326 10.2217 10.221 12.8334 6.99932 12.8334C5.88203 12.8334 4.83776 12.5188 3.95077 11.9742L3.27461 12.6503C3.09586 12.8291 2.82703 12.8826 2.59349 12.7858C2.35994 12.6891 2.20767 12.4612 2.20767 12.2084V9.70841C2.20767 9.36324 2.48749 9.08341 2.83266 9.08341H5.33266C5.58545 9.08341 5.81334 9.23569 5.91008 9.46924C6.00682 9.70274 5.95335 9.97158 5.7746 10.1503L5.17671 10.7482C5.72728 11.0165 6.3456 11.1667 6.99932 11.1667C9.3005 11.1667 11.166 9.30126 11.166 7.00008Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Refresh;
|
||||
33
frontend/src/_ui/Icon/solidIcons/RocketIcon.jsx
Normal file
33
frontend/src/_ui/Icon/solidIcons/RocketIcon.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
|
||||
const RocketIcon = ({ fill = '#C1C8CD', width = '16', className = '', viewBox = '0 0 16 16' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M10.5 1.5C10.5 1.5 13.5 1.5 14.5 2.5C15.5 3.5 14.5 6.5 14.5 6.5L11 10L6 9L7 4L10.5 1.5Z"
|
||||
fill={fill}
|
||||
stroke={fill}
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6 9L2.5 12.5C2 13 2 13.5 2 14C2 14.5 2.5 15 3 15C3.5 15 4 15 4.5 14.5L8 11L6 9Z"
|
||||
fill={fill}
|
||||
stroke={fill}
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="10.5" cy="5.5" r="1" fill={fill === '#C1C8CD' ? '#FFFFFF' : 'currentColor'} />
|
||||
<path d="M4 10L1.5 10.5L1 14L4.5 13.5L4 10Z" fill={fill} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default RocketIcon;
|
||||
|
|
@ -27,12 +27,15 @@ import CheveronDown from './CheveronDown.jsx';
|
|||
import CheveronLeft from './CheveronLeft.jsx';
|
||||
import CheveronRight from './CheveronRight.jsx';
|
||||
import CheveronUp from './CheveronUp.jsx';
|
||||
import ChevronDownSmall from './ChevronDownSmall.jsx';
|
||||
import CircleDot from './CircleDot.jsx';
|
||||
import ClearRectangle from './ClearRectangle.jsx';
|
||||
import CaretDown from './CaretDown.jsx';
|
||||
import CaretUp from './CaretUp.jsx';
|
||||
import Clock from './Clock.jsx';
|
||||
import CursorClick from './CursorClick.jsx';
|
||||
import LockGradient from './LockGradient.jsx';
|
||||
import LockClosed from './LockClosed.jsx';
|
||||
import DatasourceGradient from './DatasourceGradient.jsx';
|
||||
import CoinIcon from './CoinIcon.jsx';
|
||||
import Column from './Column.jsx';
|
||||
|
|
@ -52,6 +55,7 @@ import EnterpriseSmall from './EnterpriseSmall.jsx';
|
|||
import Eye from './Eye.jsx';
|
||||
import Eye1 from './Eye1.jsx';
|
||||
import EyeDisable from './EyeDisable.jsx';
|
||||
import ExternalLinkIcon from './ExternalLinkIcon.jsx';
|
||||
import Expand from './Expand.jsx';
|
||||
import File01 from './File01.jsx';
|
||||
import FileDownload from './FileDownload.jsx';
|
||||
|
|
@ -62,6 +66,8 @@ import Folder from './Folder.jsx';
|
|||
import FolderDownload from './FolderDownload.jsx';
|
||||
import FolderUpload from './FolderUpload.jsx';
|
||||
import GitSync from './GitSync.jsx';
|
||||
import GitBranch from './GitBranch.jsx';
|
||||
import GitMergeIcon from './GitMergeIcon.jsx';
|
||||
import FullOuterJoin from './FullOuterJoin.jsx';
|
||||
import Globe from './Globe.jsx';
|
||||
import Options from './Options.jsx';
|
||||
|
|
@ -98,6 +104,7 @@ import Page from './Page.jsx';
|
|||
import PageAdd from './PageAdd.jsx';
|
||||
import PageUpload from './PageUpload.jsx';
|
||||
import Pin from './Pin.jsx';
|
||||
import PlusIcon from './PlusIcon.jsx';
|
||||
import Unpin from './Unpin.jsx';
|
||||
import AlignRight from './AlignRight';
|
||||
import Play from './Play.jsx';
|
||||
|
|
@ -111,6 +118,7 @@ import Remove from './Remove.jsx';
|
|||
import Remove01 from './Remove01.jsx';
|
||||
import Remove03 from './Remove03.jsx';
|
||||
import RemoveRectangle from './RemoveRectangle.jsx';
|
||||
import Refresh from './Refresh.jsx';
|
||||
import RightArrow from './RightArrow.jsx';
|
||||
import RightOuterJoin from './RightOuterJoin.jsx';
|
||||
import Row from './Row.jsx';
|
||||
|
|
@ -170,6 +178,7 @@ import CloudInvalid from './CloudInvalid.jsx';
|
|||
import CloudValid from './CloudValid.jsx';
|
||||
import LayersVersion from './LayersVersion.jsx';
|
||||
import Comments from './Comments';
|
||||
import Commit from './Commit';
|
||||
import Inspect from './Inspect.jsx';
|
||||
import ArrowForwardUp from './ArrowForwardUp.jsx';
|
||||
import ArrowBackUp from './ArrowBackUp.jsx';
|
||||
|
|
@ -177,6 +186,7 @@ import CheveronLeftDouble from './CheveronLeftDouble.jsx';
|
|||
import CheveronRightDouble from './CheveronRightDouble.jsx';
|
||||
import Dot from './Dot.jsx';
|
||||
import Check from './Check.jsx';
|
||||
import Check2 from './Check2.jsx';
|
||||
import Editable from './Editable.jsx';
|
||||
import Save from './Save.jsx';
|
||||
import Cross from './Cross.jsx';
|
||||
|
|
@ -229,6 +239,7 @@ import AITag from './AITag.jsx';
|
|||
import SectionCollapse from './SectionCollapse.jsx';
|
||||
import SectionExpand from './SectionExpand.jsx';
|
||||
import Reset from './Reset.jsx';
|
||||
import RocketIcon from './RocketIcon.jsx';
|
||||
import Outbound from './Outbound.jsx';
|
||||
import AddPageGroupIcon from './AddPageGroup.jsx';
|
||||
import PageIcon from './PageIcon.jsx';
|
||||
|
|
@ -394,6 +405,10 @@ const Icon = (props) => {
|
|||
return <CheveronRightDouble {...props} />;
|
||||
case 'cheveronup':
|
||||
return <CheveronUp {...props} />;
|
||||
case 'chevrondownsmall':
|
||||
return <ChevronDownSmall {...props} />;
|
||||
case 'circledot':
|
||||
return <CircleDot {...props} />;
|
||||
case 'circularToggleDisabled':
|
||||
return <CircularToggleDisabled {...props} />;
|
||||
case 'circularToggleEnabled':
|
||||
|
|
@ -456,6 +471,8 @@ const Icon = (props) => {
|
|||
return <EnterpriseCrown {...props} />;
|
||||
case 'lockGradient':
|
||||
return <LockGradient {...props} />;
|
||||
case 'lockclosed':
|
||||
return <LockClosed {...props} />;
|
||||
case 'datasourceGradient':
|
||||
return <DatasourceGradient {...props} />;
|
||||
case 'enterbutton':
|
||||
|
|
@ -466,6 +483,8 @@ const Icon = (props) => {
|
|||
return <Eye1 {...props} />;
|
||||
case 'eyedisable':
|
||||
return <EyeDisable {...props} />;
|
||||
case 'externallink':
|
||||
return <ExternalLinkIcon {...props} />;
|
||||
case 'expand':
|
||||
return <Expand {...props} />;
|
||||
case 'file-code':
|
||||
|
|
@ -488,6 +507,10 @@ const Icon = (props) => {
|
|||
return <FolderUpload {...props} />;
|
||||
case 'gitsync':
|
||||
return <GitSync {...props} />;
|
||||
case 'gitbranch':
|
||||
return <GitBranch {...props} />;
|
||||
case 'gitmerge':
|
||||
return <GitMergeIcon {...props} />;
|
||||
case 'foreignkey':
|
||||
return <ForeignKey {...props} />;
|
||||
case 'fullouterjoin':
|
||||
|
|
@ -588,6 +611,8 @@ const Icon = (props) => {
|
|||
return <Play {...props} />;
|
||||
case 'plus':
|
||||
return <Plus {...props} />;
|
||||
case 'plusicon':
|
||||
return <PlusIcon {...props} />;
|
||||
case 'plus01':
|
||||
return <Plus01 {...props} />;
|
||||
case 'plusrectangle':
|
||||
|
|
@ -598,6 +623,8 @@ const Icon = (props) => {
|
|||
return <PostgreSQLIcon {...props} />;
|
||||
case 'reload':
|
||||
return <Reload {...props} />;
|
||||
case 'refresh':
|
||||
return <Refresh {...props} />;
|
||||
case 'read':
|
||||
return <Read {...props} />;
|
||||
case 'reloaderror':
|
||||
|
|
@ -618,6 +645,8 @@ const Icon = (props) => {
|
|||
return <Row {...props} />;
|
||||
case 'reset':
|
||||
return <Reset {...props} />;
|
||||
case 'rocket':
|
||||
return <RocketIcon {...props} />;
|
||||
case 'retry':
|
||||
return <Retry {...props} />;
|
||||
case 'sadrectangle':
|
||||
|
|
@ -646,6 +675,8 @@ const Icon = (props) => {
|
|||
return <ShiftButtonIcon {...props} />;
|
||||
case 'comments':
|
||||
return <Comments {...props} />;
|
||||
case 'commit':
|
||||
return <Commit {...props} />;
|
||||
case 'corners':
|
||||
return <Corners {...props} />;
|
||||
case 'share':
|
||||
|
|
@ -736,6 +767,8 @@ const Icon = (props) => {
|
|||
return <Dot {...props} />;
|
||||
case 'check':
|
||||
return <Check {...props} />;
|
||||
case 'check2':
|
||||
return <Check2 {...props} />;
|
||||
case 'editable':
|
||||
return <Editable {...props} />;
|
||||
case 'minimize':
|
||||
|
|
|
|||
|
|
@ -54,39 +54,47 @@ const PromoteConfirmationModal = React.memo(({ data, onClose, editingVersion })
|
|||
versionIdToPromote,
|
||||
async (response) => {
|
||||
toast.success(`${versionToPromote.name} has been promoted to ${data.target.name}!`);
|
||||
if (
|
||||
data?.current?.name == 'development' &&
|
||||
(creationMode !== 'GIT' || (creationMode === 'GIT' && allowAppEdit))
|
||||
) {
|
||||
try {
|
||||
const gitData = await gitSyncService.getAppConfig(current_organization_id, versionToPromote?.id);
|
||||
const appGit = gitData?.app_git;
|
||||
if (appGit && appGit?.org_git?.auto_commit) {
|
||||
const body = {
|
||||
gitAppName: appGit?.git_app_name,
|
||||
versionId: versionToPromote?.id,
|
||||
lastCommitMessage: ` ${versionToPromote.name} Version of app ${appGit?.git_app_name} promoted from development to staging`,
|
||||
gitVersionName: versionToPromote?.name,
|
||||
};
|
||||
await gitSyncService.gitPush(body, appGit?.id, versionToPromote?.id);
|
||||
toast.success('Changes committed successfully');
|
||||
}
|
||||
} catch (err) {
|
||||
const status = err?.statusCode;
|
||||
const error = err?.error;
|
||||
if (
|
||||
!(status === 404 && error === 'Git Configuration not found') &&
|
||||
!(error === 'No Git Provider is enabled for the workspace')
|
||||
) {
|
||||
toast.error(error, {
|
||||
style: {
|
||||
width: 'auto',
|
||||
maxWidth: '339px',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
setPromotingEnvironment(false);
|
||||
onClose();
|
||||
|
||||
{
|
||||
/* Can clean when autoCommit is removed completely */
|
||||
}
|
||||
// if (
|
||||
// data?.current?.name == 'development' &&
|
||||
// (creationMode !== 'GIT' || (creationMode === 'GIT' && allowAppEdit))
|
||||
// ) {
|
||||
// try {
|
||||
// const gitData = await gitSyncService.getAppConfig(current_organization_id, versionToPromote?.id);
|
||||
// const appGit = gitData?.app_git;
|
||||
// if (appGit && appGit?.org_git?.auto_commit) {
|
||||
// const body = {
|
||||
// gitAppName: appGit?.git_app_name,
|
||||
// versionId: versionToPromote?.id,
|
||||
// lastCommitMessage: ` ${versionToPromote.name} Version of app ${appGit?.git_app_name} promoted from development to staging`,
|
||||
// gitVersionName: versionToPromote?.name,
|
||||
// allowMasterPush: true,
|
||||
// };
|
||||
// await gitSyncService.gitPush(body, appGit?.id, versionToPromote?.id);
|
||||
// toast.success('Changes committed successfully');
|
||||
// }
|
||||
// } catch (err) {
|
||||
// const status = err?.statusCode;
|
||||
// const error = err?.error;
|
||||
// if (
|
||||
// !(status === 404 && error === 'Git Configuration not found') &&
|
||||
// !(error === 'No Git Provider is enabled for the workspace')
|
||||
// ) {
|
||||
// toast.error(error, {
|
||||
// style: {
|
||||
// width: 'auto',
|
||||
// maxWidth: '339px',
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// setSelectedEnvironment(response);
|
||||
// set env id here-----> state update
|
||||
// appEnvironmentChanged(response, true);
|
||||
|
|
|
|||
12927
plugins/package-lock.json
generated
12927
plugins/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1 +1 @@
|
|||
3.20.119-lts
|
||||
3.21.5-beta
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class NormalizeFolderAppsKeepFirstCreatedMappingPerApp1769151383974 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DELETE FROM folder_apps
|
||||
WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY app_id
|
||||
ORDER BY created_at ASC
|
||||
) AS rn
|
||||
FROM folder_apps
|
||||
) t
|
||||
WHERE t.rn > 1
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uniq_folder_apps_app_id
|
||||
ON folder_apps (app_id);
|
||||
`);
|
||||
}
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DROP INDEX IF EXISTS uniq_folder_apps_app_id;
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 69f565470789a69aeabbb5d4c54669ea7f43b4de
|
||||
Subproject commit 9d25317dba9c6063ff5d65d509c79a8f444ac4fe
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddCoRelationIdToAppEntities1743997765065 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE components ADD COLUMN IF NOT EXISTS "co_relation_id" uuid DEFAULT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE components DROP COLUMN IF EXISTS "co_relation_id"`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddBranchingAndSchemaVersionToOrgGitSync1761828716101 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Add isBranchingEnabled column to organization_git_sync table
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "organization_git_sync"
|
||||
ADD COLUMN IF NOT EXISTS "is_branching_enabled" boolean NOT NULL DEFAULT true`
|
||||
);
|
||||
// default value is set to true for now, this migration should be changed before releasing for lts and default should be false.
|
||||
|
||||
// Add schema_version column to organization_git_sync table
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "organization_git_sync"
|
||||
ADD COLUMN IF NOT EXISTS "schema_version" varchar NOT NULL DEFAULT '1.0.0'`
|
||||
);
|
||||
|
||||
}
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "organization_git_sync" DROP COLUMN IF EXISTS "is_branching_enabled"`
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "organization_git_sync" DROP COLUMN IF EXISTS "schema_version"`
|
||||
);
|
||||
}
|
||||
}
|
||||
24
server/migrations/1761828800001-AddVersionTypeColumn.ts
Normal file
24
server/migrations/1761828800001-AddVersionTypeColumn.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddVersionTypeColumn1761828800001 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TYPE app_version_type AS ENUM ('version', 'branch')
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "app_versions"
|
||||
ADD COLUMN "version_type" app_version_type NOT NULL DEFAULT 'version'
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "app_versions"
|
||||
DROP COLUMN IF EXISTS "version_type"
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DROP TYPE IF EXISTS app_version_type
|
||||
`);
|
||||
}
|
||||
}
|
||||
30
server/migrations/1762860937123-AddCreatedByToAppVersions.ts
Normal file
30
server/migrations/1762860937123-AddCreatedByToAppVersions.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from 'typeorm';
|
||||
|
||||
export class AddCreatedByToAppVersions1762860937123 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.addColumn(
|
||||
'app_versions',
|
||||
new TableColumn({
|
||||
name: 'created_by',
|
||||
type: 'uuid',
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'app_versions',
|
||||
new TableForeignKey({
|
||||
name: 'fk_app_versions_created_by',
|
||||
columnNames: ['created_by'],
|
||||
referencedColumnNames: ['id'],
|
||||
referencedTableName: 'users',
|
||||
onDelete: 'SET NULL',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropForeignKey('app_versions', 'fk_app_versions_created_by');
|
||||
await queryRunner.dropColumn('app_versions', 'created_by');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddCoRelationIdToAppEntities1763549159927 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const tables = [
|
||||
'pages',
|
||||
'data_queries',
|
||||
'data_sources',
|
||||
'data_source_options',
|
||||
'internal_tables',
|
||||
'app_versions',
|
||||
'event_handlers',
|
||||
'layouts'
|
||||
];
|
||||
|
||||
for (const table of tables) {
|
||||
await queryRunner.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS "co_relation_id" uuid DEFAULT NULL`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tables = [
|
||||
'pages',
|
||||
'data_queries',
|
||||
'data_sources',
|
||||
'data_source_options',
|
||||
'internal_tables',
|
||||
'app_versions',
|
||||
'event_handlers',
|
||||
'layouts'
|
||||
];
|
||||
|
||||
for (const table of tables) {
|
||||
await queryRunner.query(`ALTER TABLE "${table}" DROP COLUMN IF EXISTS "co_relation_id"`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
24
server/migrations/1765630548010-AddSourceTagToAppVersions.ts
Normal file
24
server/migrations/1765630548010-AddSourceTagToAppVersions.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class AddSourceTagToAppVersions1765630548010 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.addColumn(
|
||||
'app_versions',
|
||||
new TableColumn({
|
||||
name: 'source_tag',
|
||||
type: 'varchar',
|
||||
length: '256',
|
||||
isNullable: true,
|
||||
default: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropColumn('app_versions', 'source_tag');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds source_tag column to track version's sync state with GitHub tags:
|
||||
* - null: Version created locally, not synced → creates GitHub tag on save
|
||||
* - "{app_name}/{version_name}": Version synced with this tag (updated on every pull) → no tag created on save
|
||||
*/
|
||||
13
server/migrations/1768289065973-AddCoRelationIdToApps.ts
Normal file
13
server/migrations/1768289065973-AddCoRelationIdToApps.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddCoRelationIdToApps1768289065973 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS "co_relation_id" uuid DEFAULT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE apps DROP COLUMN IF EXISTS "co_relation_id"`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -26,17 +26,20 @@ export class OrganizationGitUpdateDto {
|
|||
@IsString()
|
||||
@IsIn(['ed25519', 'rsa'])
|
||||
keyType: 'ed25519' | 'rsa';
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
branchingEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class OrganizationGitHTTPSUpdateDto {
|
||||
export class OrganizationGitConfigUpdateDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
autoCommit: boolean;
|
||||
}
|
||||
export class OrganizationGitLabUpdateDto {
|
||||
autoCommit?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
autoCommit: boolean;
|
||||
branchingEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class OrganizationGitStatusUpdateDto {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ export class App extends BaseEntity {
|
|||
@Column({ name: 'app_generated_from_prompt', default: false })
|
||||
appGeneratedFromPrompt: boolean;
|
||||
|
||||
@Column({ name: 'co_relation_id', nullable: true })
|
||||
co_relation_id: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enumName: 'app_builder_mode',
|
||||
|
|
|
|||
|
|
@ -22,13 +22,21 @@ import { DataSource } from './data_source.entity';
|
|||
import { Page } from './page.entity';
|
||||
import { EventHandler } from './event_handler.entity';
|
||||
import { WorkflowSchedule } from './workflow_schedule.entity';
|
||||
import { User } from './user.entity';
|
||||
|
||||
export enum AppVersionType {
|
||||
VERSION = 'version',
|
||||
BRANCH = 'branch',
|
||||
}
|
||||
@Entity({ name: 'app_versions' })
|
||||
@Unique(['name', 'appId'])
|
||||
export class AppVersion extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'co_relation_id', nullable: true })
|
||||
co_relation_id: string;
|
||||
|
||||
@Column({ name: 'name' })
|
||||
name: string;
|
||||
|
||||
|
|
@ -47,6 +55,14 @@ export class AppVersion extends BaseEntity {
|
|||
@Column({ name: 'home_page_id' })
|
||||
homePageId: string;
|
||||
|
||||
@Column({
|
||||
name: 'version_type',
|
||||
type: 'enum',
|
||||
enum: AppVersionType,
|
||||
default: AppVersionType.VERSION,
|
||||
})
|
||||
versionType: AppVersionType;
|
||||
|
||||
@Column({ name: 'app_id' })
|
||||
appId: string;
|
||||
|
||||
|
|
@ -59,6 +75,14 @@ export class AppVersion extends BaseEntity {
|
|||
@Column({ name: 'parent_version_id', type: 'uuid', nullable: true })
|
||||
parentVersionId: string;
|
||||
|
||||
@Column({ name: 'created_by', type: 'uuid', nullable: true })
|
||||
createdBy: string;
|
||||
|
||||
@ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'created_by' })
|
||||
user: User;
|
||||
|
||||
// need to review if this should be a non-nullable field with default value as DRAFT status
|
||||
@Column({
|
||||
name: 'status',
|
||||
type: 'enum',
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ export class Component {
|
|||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'co_relation_id', nullable: true })
|
||||
co_relation_id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ export class DataQuery extends BaseEntity {
|
|||
@Column({ name: 'app_version_id' })
|
||||
appVersionId: string;
|
||||
|
||||
@Column({ name: 'co_relation_id', nullable: true })
|
||||
co_relation_id: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ export class DataSource extends BaseEntity {
|
|||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'co_relation_id', nullable: true })
|
||||
co_relation_id: string;
|
||||
|
||||
@Column({ name: 'name' })
|
||||
name: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ export class DataSourceOptions {
|
|||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'co_relation_id', nullable: true })
|
||||
co_relation_id: string;
|
||||
|
||||
@Column({ name: 'data_source_id' })
|
||||
dataSourceId: string;
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue