diff --git a/packages/design-system/src/components/N8nBadge/Badge.vue b/packages/design-system/src/components/N8nBadge/Badge.vue index 605c78243c..7c6aed8b2f 100644 --- a/packages/design-system/src/components/N8nBadge/Badge.vue +++ b/packages/design-system/src/components/N8nBadge/Badge.vue @@ -16,7 +16,10 @@ export default defineComponent({ theme: { type: String, default: 'default', - validator: (value: string) => ['default', 'primary', 'secondary', 'tertiary'].includes(value), + validator: (value: string) => + ['default', 'success', 'warning', 'danger', 'primary', 'secondary', 'tertiary'].includes( + value, + ), }, size: { type: String, @@ -49,6 +52,27 @@ export default defineComponent({ border-color: var(--color-text-light); } +.success { + composes: badge; + border-radius: var(--border-radius-base); + color: var(--color-success); + border-color: var(--color-success); +} + +.warning { + composes: badge; + border-radius: var(--border-radius-base); + color: var(--color-warning); + border-color: var(--color-warning); +} + +.danger { + composes: badge; + border-radius: var(--border-radius-base); + color: var(--color-danger); + border-color: var(--color-danger); +} + .primary { composes: badge; padding: var(--spacing-5xs) var(--spacing-3xs); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 859a2ca104..35aa7a24d0 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1480,6 +1480,7 @@ export interface SourceControlAggregatedFile { name: string; status: string; type: string; + updatedAt?: string; } export declare namespace Cloud { diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 5d8903d604..ae5bdba064 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -133,6 +133,7 @@ import { MAX_WORKFLOW_NAME_LENGTH, MODAL_CONFIRM, PLACEHOLDER_EMPTY_WORKFLOW_ID, + SOURCE_CONTROL_PUSH_MODAL_KEY, VIEWS, WORKFLOW_MENU_ACTIONS, WORKFLOW_SETTINGS_MODAL_KEY, @@ -151,7 +152,7 @@ import BreakpointsObserver from '@/components/BreakpointsObserver.vue'; import type { IUser, IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare } from '@/Interface'; import { saveAs } from 'file-saver'; -import { useTitleChange, useToast, useMessage } from '@/composables'; +import { useTitleChange, useToast, useMessage, useLoadingService } from '@/composables'; import type { MessageBoxInputData } from 'element-ui/types/message-box'; import { useUIStore, @@ -161,6 +162,7 @@ import { useTagsStore, useUsersStore, useUsageStore, + useSourceControlStore, } from '@/stores'; import type { IPermissions } from '@/permissions'; import { getWorkflowPermissions } from '@/permissions'; @@ -197,7 +199,10 @@ export default defineComponent({ }, }, setup() { + const loadingService = useLoadingService(); + return { + loadingService, ...useTitleChange(), ...useToast(), ...useMessage(), @@ -211,6 +216,7 @@ export default defineComponent({ tagsEditBus: createEventBus(), MAX_WORKFLOW_NAME_LENGTH, tagsSaving: false, + eventBus: createEventBus(), EnterpriseEditionFeature, }; }, @@ -224,6 +230,7 @@ export default defineComponent({ useWorkflowsStore, useUsersStore, useCloudPlanStore, + useSourceControlStore, ), currentUser(): IUser | null { return this.usersStore.currentUser; @@ -305,6 +312,15 @@ export default defineComponent({ ); } + actions.push({ + id: WORKFLOW_MENU_ACTIONS.PUSH, + label: this.$locale.baseText('menuActions.push'), + disabled: + !this.sourceControlStore.isEnterpriseSourceControlEnabled || + !this.onWorkflowPage || + this.onExecutionsTab, + }); + actions.push({ id: WORKFLOW_MENU_ACTIONS.SETTINGS, label: this.$locale.baseText('generic.settings'), @@ -514,6 +530,30 @@ export default defineComponent({ (this.$refs.importFile as HTMLInputElement).click(); break; } + case WORKFLOW_MENU_ACTIONS.PUSH: { + this.loadingService.startLoading(); + try { + await this.onSaveButtonClick(); + + const status = await this.sourceControlStore.getAggregatedStatus(); + const workflowStatus = status.filter( + (s) => + (s.id === this.currentWorkflowId && s.type === 'workflow') || s.type !== 'workflow', + ); + + this.uiStore.openModalWithData({ + name: SOURCE_CONTROL_PUSH_MODAL_KEY, + data: { eventBus: this.eventBus, status: workflowStatus }, + }); + } catch (error) { + this.showError(error, this.$locale.baseText('error')); + } finally { + this.loadingService.stopLoading(); + this.loadingService.setLoadingText(this.$locale.baseText('genericHelpers.loading')); + } + + break; + } case WORKFLOW_MENU_ACTIONS.SETTINGS: { this.uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY); break; diff --git a/packages/editor-ui/src/components/MainSidebarSourceControl.vue b/packages/editor-ui/src/components/MainSidebarSourceControl.vue index 052baa09dd..8a8c9016df 100644 --- a/packages/editor-ui/src/components/MainSidebarSourceControl.vue +++ b/packages/editor-ui/src/components/MainSidebarSourceControl.vue @@ -4,12 +4,16 @@ import { useRouter } from 'vue-router/composables'; import { createEventBus } from 'n8n-design-system/utils'; import { useI18n, useLoadingService, useMessage, useToast } from '@/composables'; import { useUIStore, useSourceControlStore } from '@/stores'; -import { SOURCE_CONTROL_PUSH_MODAL_KEY, VIEWS } from '@/constants'; +import { SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, VIEWS } from '@/constants'; const props = defineProps<{ isCollapsed: boolean; }>(); +const responseStatuses = { + CONFLICT: 409, +}; + const router = useRouter(); const loadingService = useLoadingService(); const uiStore = useUIStore(); @@ -47,28 +51,17 @@ async function pushWorkfolder() { async function pullWorkfolder() { loadingService.startLoading(); loadingService.setLoadingText(i18n.baseText('settings.sourceControl.loading.pull')); + try { await sourceControlStore.pullWorkfolder(false); } catch (error) { const errorResponse = error.response; - if (errorResponse?.status === 409) { - const confirm = await message.confirm( - i18n.baseText('settings.sourceControl.modals.pull.description'), - i18n.baseText('settings.sourceControl.modals.pull.title'), - { - confirmButtonText: i18n.baseText('settings.sourceControl.modals.pull.buttons.save'), - cancelButtonText: i18n.baseText('settings.sourceControl.modals.pull.buttons.cancel'), - }, - ); - - try { - if (confirm === 'confirm') { - await sourceControlStore.pullWorkfolder(true); - } - } catch (error) { - toast.showError(error, 'Error'); - } + if (errorResponse?.status === responseStatuses.CONFLICT) { + uiStore.openModalWithData({ + name: SOURCE_CONTROL_PULL_MODAL_KEY, + data: { eventBus, status: errorResponse.data.data }, + }); } else { toast.showError(error, 'Error'); } diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index d0fccd5a52..3746992e6b 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -117,6 +117,12 @@ + + + + @@ -146,6 +152,7 @@ import { LOG_STREAM_MODAL_KEY, ASK_AI_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, + SOURCE_CONTROL_PULL_MODAL_KEY, } from '@/constants'; import AboutModal from './AboutModal.vue'; @@ -172,6 +179,7 @@ import ImportCurlModal from './ImportCurlModal.vue'; import WorkflowShareModal from './WorkflowShareModal.ee.vue'; import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue'; import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue'; +import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue'; export default defineComponent({ name: 'Modals', @@ -200,6 +208,7 @@ export default defineComponent({ ImportCurlModal, EventDestinationSettingsModal, SourceControlPushModal, + SourceControlPullModal, }, data: () => ({ COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, @@ -225,6 +234,7 @@ export default defineComponent({ IMPORT_CURL_MODAL_KEY, LOG_STREAM_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, + SOURCE_CONTROL_PULL_MODAL_KEY, }), }); diff --git a/packages/editor-ui/src/components/SourceControlPullModal.ee.vue b/packages/editor-ui/src/components/SourceControlPullModal.ee.vue new file mode 100644 index 0000000000..bc58765f8c --- /dev/null +++ b/packages/editor-ui/src/components/SourceControlPullModal.ee.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/editor-ui/src/components/SourceControlPushModal.ee.vue b/packages/editor-ui/src/components/SourceControlPushModal.ee.vue index e27eee84ab..bc5c5fa3fa 100644 --- a/packages/editor-ui/src/components/SourceControlPushModal.ee.vue +++ b/packages/editor-ui/src/components/SourceControlPushModal.ee.vue @@ -9,6 +9,7 @@ import { useI18n, useLoadingService, useToast } from '@/composables'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useUIStore } from '@/stores'; import { useRoute } from 'vue-router/composables'; +import dateformat from 'dateformat'; const props = defineProps({ data: { @@ -17,6 +18,8 @@ const props = defineProps({ }, }); +const defaultStagedFileTypes = ['tags', 'variables', 'credential']; + const loadingService = useLoadingService(); const uiStore = useUIStore(); const toast = useToast(); @@ -31,10 +34,71 @@ const commitMessage = ref(''); const loading = ref(true); const context = ref<'workflow' | 'workflows' | 'credentials' | string>(''); +const statusToBadgeThemeMap = { + created: 'success', + deleted: 'danger', + modified: 'warning', + renamed: 'warning', +}; + const isSubmitDisabled = computed(() => { return !commitMessage.value || Object.values(staged.value).every((value) => !value); }); +const workflowId = computed(() => { + if (context.value === 'workflow') { + return route.params.name as string; + } + + return ''; +}); + +const sortedFiles = computed(() => { + const statusPriority = { + deleted: 1, + modified: 2, + renamed: 3, + created: 4, + }; + + return [...files.value].sort((a, b) => { + if (context.value === 'workflow') { + if (a.id === workflowId.value) { + return -1; + } else if (b.id === workflowId.value) { + return 1; + } + } + + if (statusPriority[a.status] < statusPriority[b.status]) { + return -1; + } else if (statusPriority[a.status] > statusPriority[b.status]) { + return 1; + } + + return a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0; + }); +}); + +const selectAll = computed(() => { + return files.value.every((file) => staged.value[file.file]); +}); + +const workflowFiles = computed(() => { + return files.value.filter((file) => file.type === 'workflow'); +}); + +const stagedWorkflowFiles = computed(() => { + return workflowFiles.value.filter((workflow) => staged.value[workflow.file]); +}); + +const selectAllIndeterminate = computed(() => { + return ( + stagedWorkflowFiles.value.length > 0 && + stagedWorkflowFiles.value.length < workflowFiles.value.length + ); +}); + onMounted(async () => { context.value = getContext(); try { @@ -46,6 +110,22 @@ onMounted(async () => { } }); +function onToggleSelectAll() { + if (selectAll.value) { + files.value.forEach((file) => { + if (!defaultStagedFileTypes.includes(file.type)) { + staged.value[file.file] = false; + } + }); + } else { + files.value.forEach((file) => { + if (!defaultStagedFileTypes.includes(file.type)) { + staged.value[file.file] = true; + } + }); + } +} + function getContext() { if (route.fullPath.startsWith('/workflows')) { return 'workflows'; @@ -62,20 +142,24 @@ function getContext() { } function getStagedFilesByContext(files: SourceControlAggregatedFile[]): Record { - const stagedFiles: SourceControlAggregatedFile[] = []; - if (context.value === 'workflows') { - stagedFiles.push(...files.filter((file) => file.file.startsWith('workflows'))); - } else if (context.value === 'credentials') { - stagedFiles.push(...files.filter((file) => file.file.startsWith('credentials'))); - } else if (context.value === 'workflow') { - const workflowId = route.params.name as string; - stagedFiles.push(...files.filter((file) => file.type === 'workflow' && file.id === workflowId)); - } - - return stagedFiles.reduce>((acc, file) => { - acc[file.file] = true; + const stagedFiles = files.reduce((acc, file) => { + acc[file.file] = false; return acc; }, {}); + + files.forEach((file) => { + if (defaultStagedFileTypes.includes(file.type)) { + stagedFiles[file.file] = true; + } + + if (context.value === 'workflow' && file.type === 'workflow' && file.id === workflowId.value) { + stagedFiles[file.file] = true; + } else if (context.value === 'workflows' && file.type === 'workflow') { + stagedFiles[file.file] = true; + } + }); + + return stagedFiles; } function setStagedStatus(file: SourceControlAggregatedFile, status: boolean) { @@ -89,6 +173,20 @@ function close() { uiStore.closeModal(SOURCE_CONTROL_PUSH_MODAL_KEY); } +function renderUpdatedAt(file: SourceControlAggregatedFile) { + const currentYear = new Date().getFullYear(); + + return i18n.baseText('settings.sourceControl.lastUpdated', { + interpolate: { + date: dateformat( + file.updatedAt, + `d mmm${file.updatedAt.startsWith(currentYear) ? '' : ', yyyy'}`, + ), + time: dateformat(file.updatedAt, 'HH:MM'), + }, + }); +} + async function commitAndPush() { const fileNames = files.value.filter((file) => staged.value[file.file]).map((file) => file.file); @@ -135,12 +233,24 @@ async function commitAndPush() { -
- - {{ i18n.baseText('settings.sourceControl.modals.push.filesToCommit') }} - +
+
+ + + {{ i18n.baseText('settings.sourceControl.modals.push.workflowsToCommit') }} + + + ({{ stagedWorkflowFiles.length }}/{{ workflowFiles.length }}) + + +
- - - Workflow - Credential - Id: {{ file.id }} - - +
+ + Deleted Workflow: + Deleted Credential: + {{ file.id }} + + {{ file.name }} - - - - {{ file.status }} - + +
+ + {{ renderUpdatedAt(file) }} + +
+
+ + {{ i18n.baseText('settings.sourceControl.modals.push.overrideVersionInGit') }} + +
+
+
+ + Current workflow + + + {{ file.status }} + +
@@ -228,22 +353,22 @@ async function commitAndPush() { &:last-child { margin-bottom: 0; } +} - .listItemBody { - display: flex; - flex-direction: row; - align-items: center; +.listItemBody { + display: flex; + flex-direction: row; + align-items: center; +} - .listItemCheckbox { - display: inline-flex !important; - margin-bottom: 0 !important; - margin-right: var(--spacing-2xs); - } +.listItemCheckbox { + display: inline-flex !important; + margin-bottom: 0 !important; + margin-right: var(--spacing-2xs) !important; +} - .listItemStatus { - margin-left: var(--spacing-2xs); - } - } +.listItemStatus { + margin-left: auto; } .footer { diff --git a/packages/editor-ui/src/components/__tests__/MainSidebarSourceControl.test.ts b/packages/editor-ui/src/components/__tests__/MainSidebarSourceControl.test.ts index b80bd9821f..950b75d733 100644 --- a/packages/editor-ui/src/components/__tests__/MainSidebarSourceControl.test.ts +++ b/packages/editor-ui/src/components/__tests__/MainSidebarSourceControl.test.ts @@ -4,15 +4,16 @@ import userEvent from '@testing-library/user-event'; import { PiniaVuePlugin } from 'pinia'; import { createTestingPinia } from '@pinia/testing'; import { merge } from 'lodash-es'; -import { STORES } from '@/constants'; +import { SOURCE_CONTROL_PULL_MODAL_KEY, STORES } from '@/constants'; import { i18nInstance } from '@/plugins/i18n'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue'; -import { useUsersStore, useSourceControlStore } from '@/stores'; +import { useUsersStore, useSourceControlStore, useUIStore } from '@/stores'; let pinia: ReturnType; let sourceControlStore: ReturnType; let usersStore: ReturnType; +let uiStore: ReturnType; const renderComponent = (renderOptions: Parameters[1] = {}) => { return render( @@ -42,6 +43,7 @@ describe('MainSidebarSourceControl', () => { }); sourceControlStore = useSourceControlStore(); + uiStore = useUIStore(); usersStore = useUsersStore(); }); @@ -89,13 +91,25 @@ describe('MainSidebarSourceControl', () => { }); it('should show confirm if pull response http status code is 409', async () => { + const status = {}; vi.spyOn(sourceControlStore, 'pullWorkfolder').mockRejectedValueOnce({ - response: { status: 409 }, + response: { status: 409, data: { data: status } }, }); + const openModalSpy = vi.spyOn(uiStore, 'openModalWithData'); + const { getAllByRole, getByRole } = renderComponent({ props: { isCollapsed: false } }); await userEvent.click(getAllByRole('button')[0]); - await waitFor(() => expect(getByRole('dialog')).toBeInTheDocument()); + await waitFor(() => + expect(openModalSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: SOURCE_CONTROL_PULL_MODAL_KEY, + data: expect.objectContaining({ + status, + }), + }), + ), + ); }); }); }); diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index d21c2b423a..9086e185f3 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -49,6 +49,7 @@ export const IMPORT_CURL_MODAL_KEY = 'importCurl'; export const LOG_STREAM_MODAL_KEY = 'settingsLogStream'; export const SOURCE_CONTROL_PUSH_MODAL_KEY = 'sourceControlPush'; +export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull'; export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = { UNINSTALL: 'uninstall', @@ -429,6 +430,7 @@ export const enum WORKFLOW_MENU_ACTIONS { DOWNLOAD = 'download', IMPORT_FROM_URL = 'import-from-url', IMPORT_FROM_FILE = 'import-from-file', + PUSH = 'push', SETTINGS = 'settings', DELETE = 'delete', } diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 3b2c1558ca..8c8ffe4c7f 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -630,6 +630,7 @@ "mainSidebar.executions": "All executions", "menuActions.duplicate": "Duplicate", "menuActions.download": "Download", + "menuActions.push": "Push to Git", "menuActions.importFromUrl": "Import from URL...", "menuActions.importFromFile": "Import from File...", "menuActions.delete": "Delete", @@ -1328,10 +1329,11 @@ "settings.usageAndPlan.desktop.description": "Cloud plans allow you to collaborate with teammates. Plus you don’t need to leave this app open all the time for your workflows to run.", "settings.sourceControl.title": "Source Control", "settings.sourceControl.actionBox.title": "Available on Enterprise plan", - "settings.sourceControl.actionBox.description": "Use Source Control to connect your instance to an external Git repository to backup and track changes made to your workflows, variables, and credentials. With Source Control you can also sync instances across multiple environments (development, production...).", + "settings.sourceControl.actionBox.description": "Use multiple instances for different environments (dev, prod, etc.), deploying between them via a Git repository.", + "settings.sourceControl.actionBox.description.link": "More info", "settings.sourceControl.actionBox.buttonText": "See plans", - "settings.sourceControl.description": "Source Control allows you to connect your n8n instance to a Git branch of a repository. You can connect your branches to multiples n8n instances to create a multi environments setup. {link}", - "settings.sourceControl.description.link": "Learn how to set up Source Control and Environments in n8n.", + "settings.sourceControl.description": "Use multiple instances for different environments (dev, prod, etc.), deploying between them via a Git repository. {link}", + "settings.sourceControl.description.link": "More info", "settings.sourceControl.gitConfig": "Git configuration", "settings.sourceControl.repoUrl": "Git repository URL (SSH)", "settings.sourceControl.repoUrlPlaceholder": "e.g. git@github.com:my-team/my-repository", @@ -1371,14 +1373,18 @@ "settings.sourceControl.modals.push.description.learnMore": "Learn more", "settings.sourceControl.modals.push.description.learnMore.url": "https://docs.n8n.io/source-control/using/", "settings.sourceControl.modals.push.filesToCommit": "Files to commit", + "settings.sourceControl.modals.push.workflowsToCommit": "Workflows to commit", "settings.sourceControl.modals.push.everythingIsUpToDate": "Everything is up to date", + "settings.sourceControl.modals.push.overrideVersionInGit": "This will override the version in Git", "settings.sourceControl.modals.push.commitMessage": "Commit message", "settings.sourceControl.modals.push.commitMessage.placeholder": "e.g. My commit", "settings.sourceControl.modals.push.buttons.cancel": "Cancel", "settings.sourceControl.modals.push.buttons.save": "Commit and Push", "settings.sourceControl.modals.push.success.title": "Pushed successfully", "settings.sourceControl.modals.push.success.description": "The files you selected were committed and pushed to the remote repository", - "settings.sourceControl.modals.pull.title": "Override local changes", + "settings.sourceControl.pull.success.title": "Pulled successfully", + "settings.sourceControl.pull.success.description": "Make sure you fill out the details of any new credentials or variables", + "settings.sourceControl.modals.pull.title": "Override local changes?", "settings.sourceControl.modals.pull.description": "Some remote changes are going to override some of your local changes. Are you sure you want to continue?", "settings.sourceControl.modals.pull.buttons.cancel": "@:_reusableBaseText.cancel", "settings.sourceControl.modals.pull.buttons.save": "Pull and override", @@ -1398,6 +1404,7 @@ "settings.sourceControl.toast.disconnected.error": "Error disconnecting from Git", "settings.sourceControl.loading.pull": "Pulling from remote", "settings.sourceControl.loading.push": "Pushing to remote", + "settings.sourceControl.lastUpdated": "Last updated {date} at {time}", "settings.sourceControl.saved.title": "Settings successfully saved", "settings.sourceControl.refreshBranches.tooltip": "Reload branches list", "settings.sourceControl.refreshBranches.success": "Branches successfully refreshed", diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index fa96e4ae22..af2a00c9d9 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -31,6 +31,7 @@ import { WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_SHARE_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, + SOURCE_CONTROL_PULL_MODAL_KEY, } from '@/constants'; import type { CurlToJSONResponse, @@ -136,6 +137,9 @@ export const useUIStore = defineStore(STORES.UI, { [SOURCE_CONTROL_PUSH_MODAL_KEY]: { open: false, }, + [SOURCE_CONTROL_PULL_MODAL_KEY]: { + open: false, + }, }, modalStack: [], sidebarMenuCollapsed: true, diff --git a/packages/editor-ui/src/views/SettingsSourceControl.vue b/packages/editor-ui/src/views/SettingsSourceControl.vue index d5b32ee330..3967dff8df 100644 --- a/packages/editor-ui/src/views/SettingsSourceControl.vue +++ b/packages/editor-ui/src/views/SettingsSourceControl.vue @@ -379,6 +379,12 @@ const refreshBranches = async () => { +