feat: Version control mvp (#6271)

* implement basic git service

* cleanup connected prop

* add skeleton of git functions

* initial import/export setup

* split out export service

* refactor and improve export

* begin import

* more commands and basic import

* clean up imports with transactions

* work folder import functions

* reintroduce versionid

* add missing import to pull workfolder

* add get-status endpoint

* add cleanup to disconnect

* add initRepo options

* add more checks and cleanup

* minor cleanup

* refactor prefs

* fix server.ts

* fix sending deleted files

* rename files to ee

* add variable override and fix critical cred import bug

* fix mkdir race condition

* make initRepo default to true

* fix front back integration

* improve connect flow

* add comment to generated ssh key

* fix(editor): use useToast composable

* fix buttons size

* commenting out repo init for now

* fix(editor): update UI logic

* fix(editor): remove console.log

* fix(editor): remove unused ref

* adjust endpoints for improved UI

* fix(editor): add push and pull buttons

* keep or not ssh key

* switching file name to id

* fix(editor): add success messages, fix save button

* fixed faulty diff preventing pull

* fix build

* fix(editor): adding loader to VC components

* removing duplicate exports

* improve conflict finding on push pull

* manage pull conflict

* alternate push pull

* fix pull confirmation

* fix rm and credential export/import

* switch to alternative pull implementation

* fix initial commit

* fix(editor): subscribing to VC store action to refresh lists

* fix(editor): wrap VC store actions with try

* feat: add fine-grained file selection for push action

* fix: close modal after successful push

* fix(editor): VC preferences validation

* fix confirm

* fix: update endpoint to /get-status

* feat: update pull modal override changes message

* fix missing wf error

* undo

* removing connect endpoint

* fix(editor): add button titles

* fix(editor): cleaning up store action

* add version-control/set-read-only protection

* fix(editor): adding set branch readonly

* fix(editor): remove Push button if branch set to readonly

* fix(editor): fix some styles

* fix(editor): remove duplicate and delete actions in WF list when branch is readonly

* fix: load status before opening selective push modal

* fix(editor): extend readonly logic

* add cleanup after failed initRepo

* fix deleted files crashing get-status

* fix n8n-checkbox in staging dialog

* fix(editor): fix loading

* fix(editor): resize buttons

* fix(editor): fix translation

* fix(editor): fix copy text size

* fix(editor): fix copy text size

* fix(editor): add disconnection confirmation

* fix(editor): add disconnection confirmation

* fix(editor): set large buttons

* add public api Pull endpoint

* feat: add refresh ssh key

* return prefs when new keys are generated

* fix(editor): adding readOnly mode to main header

* fix(editor): adding readOnly mode to workflow settings

* improve credential owner import

* add middleware to endpoints

* improve public api error/doc

* do not create branch if one already exists

* update wordings for connect toasts

* fix(editor): updating and separating readonly modes

* fix(editor): fix readonly mode in WF list

* fix(editor): disable elements dragging on canvas in readonly mode (WIP: not working when NodeView page is loaded first)

* fix(editor): fix canvas draggables in readonly env

* fix(editor): remove unused variables

* fix(editor): hide actions in node connections when readonly

* fix(editor): hide actions in node connections when readonly

* fix(editor): disable Save button when readonly

* fix(editor): disable Save settings if no branch is selected

* fix(editor): lint fix

* fix(editor): update snapshots

* fix(editor): replace Loading... text

* fix(editor): reset Loading... text

* reset branchname on disconnect

* fix(editor): adding some translations

* fix(editor): fix unit test

* fix(editor): fix loading

* fix(editor): set settings saved message

* fix(editor): update connection flag

* fix branchName not returning after connect

* temporary (but still breaking) fix for postgres

* fix(editor): adding tooltip to Push/Pull buttons when they're collapsed

* fix(editor): enabled activator in readonly mode

* fix test

* fix(editor): disabling new item addition for workflows in readonly mode

* fix(editor): modify Pull/Push button tooltips

* do not commit empty variables file

---------

Co-authored-by: Michael Auerswald <michael.auerswald@gmail.com>
Co-authored-by: Romain Minaud <romain.minaud@gmail.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
This commit is contained in:
Csaba Tuncsik
2023-05-31 15:01:57 +02:00
committed by GitHub
parent 04cfa548af
commit 1b321416c0
75 changed files with 3720 additions and 460 deletions

View File

@@ -57,11 +57,12 @@ import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useVersionControlStore } from '@/stores/versionControl.store';
type IResourcesListLayoutInstance = Vue & { sendFiltersTelemetry: (source: string) => void };
export default defineComponent({
name: 'SettingsPersonalView',
name: 'CredentialsView',
components: {
ResourcesListLayout,
CredentialCard,
@@ -74,10 +75,17 @@ export default defineComponent({
sharedWith: '',
type: '',
},
versionControlStoreUnsubscribe: () => {},
};
},
computed: {
...mapStores(useCredentialsStore, useNodeTypesStore, useUIStore, useUsersStore),
...mapStores(
useCredentialsStore,
useNodeTypesStore,
useUIStore,
useUsersStore,
useVersionControlStore,
),
allCredentials(): ICredentialsResponse[] {
return this.credentialsStore.allCredentials;
},
@@ -141,6 +149,18 @@ export default defineComponent({
this.sendFiltersTelemetry('type');
},
},
mounted() {
this.versionControlStoreUnsubscribe = this.versionControlStore.$onAction(({ name, after }) => {
if (name === 'pullWorkfolder' && after) {
after(() => {
void this.initialize();
});
}
});
},
beforeUnmount() {
this.versionControlStoreUnsubscribe();
},
});
</script>

View File

@@ -54,7 +54,7 @@
@run="onNodeRun"
:key="`${nodeData.id}_node`"
:name="nodeData.name"
:isReadOnly="isReadOnly"
:isReadOnly="isReadOnly || readOnlyEnv"
:instance="instance"
:isActive="!!activeNode && activeNode.name === nodeData.name"
:hideActions="pullConnActive"
@@ -76,7 +76,7 @@
@removeNode="(name) => removeNode(name, true)"
:key="`${nodeData.id}_sticky`"
:name="nodeData.name"
:isReadOnly="isReadOnly"
:isReadOnly="isReadOnly || readOnlyEnv"
:instance="instance"
:isActive="!!activeNode && activeNode.name === nodeData.name"
:nodeViewScale="nodeViewScale"
@@ -87,7 +87,7 @@
</div>
</div>
<node-details-view
:readOnly="isReadOnly"
:readOnly="isReadOnly || readOnlyEnv"
:renaming="renamingActive"
:isProductionExecutionPreview="isProductionExecutionPreview"
@valueChanged="valueChanged"
@@ -95,7 +95,7 @@
@saveKeyboardShortcut="onSaveKeyboardShortcut"
/>
<node-creation
v-if="!isReadOnly"
v-if="!isReadOnly && !readOnlyEnv"
:create-node-active="createNodeActive"
:node-view-scale="nodeViewScale"
@toggleNodeCreator="onToggleNodeCreator"
@@ -262,26 +262,29 @@ import type {
} from '@/Interface';
import { debounceHelper } from '@/mixins/debounce';
import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import type { Route, RawLocation } from 'vue-router';
import { dataPinningEventBus, nodeViewEventBus } from '@/event-bus';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useSegment } from '@/stores/segment.store';
import { useTemplatesStore } from '@/stores/templates.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useTagsStore } from '@/stores/tags.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useCanvasStore } from '@/stores/canvas.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import { useEnvironmentsStore } from '@/stores';
import {
useEnvironmentsStore,
useWorkflowsEEStore,
useCanvasStore,
useNodeCreatorStore,
useTagsStore,
useCredentialsStore,
useNodeTypesStore,
useTemplatesStore,
useSegment,
useNDVStore,
useRootStore,
useWorkflowsStore,
useUsersStore,
useSettingsStore,
useUIStore,
useHistoryStore,
useVersionControlStore,
} from '@/stores';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { getAccountAge, getConnectionInfo, getNodeViewTab } from '@/utils';
import { useHistoryStore } from '@/stores/history.store';
import {
AddConnectionCommand,
AddNodeCommand,
@@ -480,7 +483,11 @@ export default defineComponent({
useEnvironmentsStore,
useWorkflowsEEStore,
useHistoryStore,
useVersionControlStore,
),
readOnlyEnv(): boolean {
return this.versionControlStore.preferences.branchReadOnly;
},
nativelyNumberSuffixedDefaults(): string[] {
return this.rootStore.nativelyNumberSuffixedDefaults;
},
@@ -497,7 +504,9 @@ export default defineComponent({
return this.$route.name === VIEWS.DEMO;
},
showCanvasAddButton(): boolean {
return this.loadingService === null && !this.containsTrigger && !this.isDemo;
return (
this.loadingService === null && !this.containsTrigger && !this.isDemo && !this.readOnlyEnv
);
},
lastSelectedNode(): INodeUi | null {
return this.uiStore.getLastSelectedNode;
@@ -2195,6 +2204,7 @@ export default defineComponent({
if (
this.isReadOnly ||
this.readOnlyEnv ||
this.enterTimer ||
!connection ||
connection === this.activeConnection
@@ -2223,7 +2233,13 @@ export default defineComponent({
this.enterTimer = undefined;
}
if (this.isReadOnly || !connection || this.activeConnection?.id !== connection.id) return;
if (
this.isReadOnly ||
this.readOnlyEnv ||
!connection ||
this.activeConnection?.id !== connection.id
)
return;
this.exitTimer = setTimeout(() => {
this.exitTimer = undefined;

View File

@@ -1,40 +1,167 @@
<script lang="ts" setup>
import { i18n as locale } from '@/plugins/i18n';
import { computed, reactive, onBeforeMount, ref } from 'vue';
import type { Rule, RuleGroup } from 'n8n-design-system/types';
import { MODAL_CONFIRM, VALID_EMAIL_REGEX } from '@/constants';
import { useVersionControlStore } from '@/stores/versionControl.store';
import { useUIStore } from '@/stores/ui.store';
import { useMessage } from '@/composables';
import { useToast, useMessage, useLoadingService, useI18n } from '@/composables';
import CopyInput from '@/components/CopyInput.vue';
const { i18n: locale } = useI18n();
const versionControlStore = useVersionControlStore();
const uiStore = useUIStore();
const toast = useToast();
const message = useMessage();
const loadingService = useLoadingService();
const onContinue = () => {
void versionControlStore.initSsh({
name: versionControlStore.state.authorName,
email: versionControlStore.state.authorEmail,
remoteRepository: versionControlStore.state.repositoryUrl,
});
const isConnected = ref(false);
const onConnect = async () => {
loadingService.startLoading();
try {
await versionControlStore.savePreferences({
authorName: versionControlStore.preferences.authorName,
authorEmail: versionControlStore.preferences.authorEmail,
repositoryUrl: versionControlStore.preferences.repositoryUrl,
});
await versionControlStore.getBranches();
isConnected.value = true;
toast.showMessage({
title: locale.baseText('settings.versionControl.toast.connected.title'),
message: locale.baseText('settings.versionControl.toast.connected.message'),
type: 'success',
});
} catch (error) {
toast.showError(error, locale.baseText('settings.versionControl.toast.connected.error'));
}
loadingService.stopLoading();
};
const onConnect = () => {
void versionControlStore.initRepository();
const onDisconnect = async () => {
try {
const confirmation = await message.confirm(
locale.baseText('settings.versionControl.modals.disconnect.message'),
locale.baseText('settings.versionControl.modals.disconnect.title'),
{
confirmButtonText: locale.baseText('settings.versionControl.modals.disconnect.confirm'),
cancelButtonText: locale.baseText('settings.versionControl.modals.disconnect.cancel'),
},
);
if (confirmation === MODAL_CONFIRM) {
loadingService.startLoading();
await versionControlStore.disconnect(true);
isConnected.value = false;
toast.showMessage({
title: locale.baseText('settings.versionControl.toast.disconnected.title'),
message: locale.baseText('settings.versionControl.toast.disconnected.message'),
type: 'success',
});
}
} catch (error) {
toast.showError(error, locale.baseText('settings.versionControl.toast.disconnected.error'));
}
loadingService.stopLoading();
};
const onSave = () => {
void versionControlStore.savePreferences(versionControlStore.preferences);
const onSave = async () => {
loadingService.startLoading();
try {
await Promise.all([
versionControlStore.setBranch(versionControlStore.preferences.branchName),
versionControlStore.setBranchReadonly(versionControlStore.preferences.branchReadOnly),
]);
toast.showMessage({
title: locale.baseText('settings.versionControl.saved.title'),
type: 'success',
});
} catch (error) {
toast.showError(error, 'Error setting branch');
}
loadingService.stopLoading();
};
const onSelect = async (b: string) => {
if (b === versionControlStore.preferences.currentBranch) {
if (b === versionControlStore.preferences.branchName) {
return;
}
versionControlStore.preferences.currentBranch = b;
versionControlStore.preferences.branchName = b;
};
const goToUpgrade = () => {
uiStore.goToUpgrade('version-control', 'upgrade-version-control');
};
onBeforeMount(async () => {
if (versionControlStore.preferences.connected) {
isConnected.value = true;
void versionControlStore.getBranches();
}
});
const formValidationStatus = reactive<Record<string, boolean>>({
repoUrl: false,
authorName: false,
authorEmail: false,
});
function onValidate(key: string, value: boolean) {
formValidationStatus[key] = value;
}
const repoUrlValidationRules: Array<Rule | RuleGroup> = [
{ name: 'REQUIRED' },
{
name: 'MATCH_REGEX',
config: {
regex: /(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|\#[-\d\w._]+?)$/,
message: locale.baseText('settings.versionControl.repoUrlInvalid'),
},
},
];
const authorNameValidationRules: Array<Rule | RuleGroup> = [{ name: 'REQUIRED' }];
const authorEmailValidationRules: Array<Rule | RuleGroup> = [
{ name: 'REQUIRED' },
{
name: 'MATCH_REGEX',
config: {
regex: VALID_EMAIL_REGEX,
message: locale.baseText('settings.versionControl.authorEmailInvalid'),
},
},
];
const validForConnection = computed(
() =>
formValidationStatus.repoUrl &&
formValidationStatus.authorName &&
formValidationStatus.authorEmail,
);
async function refreshSshKey() {
try {
const confirmation = await message.confirm(
locale.baseText('settings.versionControl.modals.refreshSshKey.message'),
locale.baseText('settings.versionControl.modals.refreshSshKey.title'),
{
confirmButtonText: locale.baseText('settings.versionControl.modals.refreshSshKey.confirm'),
cancelButtonText: locale.baseText('settings.versionControl.modals.refreshSshKey.cancel'),
},
);
if (confirmation === MODAL_CONFIRM) {
await versionControlStore.generateKeyPair();
toast.showMessage({
title: locale.baseText('settings.versionControl.refreshSshKey.successful.title'),
type: 'success',
});
}
} catch (error) {
toast.showError(error, locale.baseText('settings.versionControl.refreshSshKey.error.title'));
}
}
</script>
<template>
@@ -60,11 +187,29 @@ const goToUpgrade = () => {
}}</n8n-heading>
<div :class="$style.group">
<label for="repoUrl">{{ locale.baseText('settings.versionControl.repoUrl') }}</label>
<n8n-input
id="repoUrl"
:placeholder="locale.baseText('settings.versionControl.repoUrlPlaceholder')"
v-model="versionControlStore.preferences.repositoryUrl"
/>
<div :class="$style.groupFlex">
<n8n-form-input
label
class="ml-0"
id="repoUrl"
name="repoUrl"
validateOnBlur
:validationRules="repoUrlValidationRules"
:disabled="isConnected"
:placeholder="locale.baseText('settings.versionControl.repoUrlPlaceholder')"
v-model="versionControlStore.preferences.repositoryUrl"
@validate="(value) => onValidate('repoUrl', value)"
/>
<n8n-button
class="ml-2xs"
type="tertiary"
v-if="isConnected"
@click="onDisconnect"
size="large"
icon="trash"
>{{ locale.baseText('settings.versionControl.button.disconnect') }}</n8n-button
>
</div>
<small>{{ locale.baseText('settings.versionControl.repoUrlDescription') }}</small>
</div>
<div :class="[$style.group, $style.groupFlex]">
@@ -72,49 +217,71 @@ const goToUpgrade = () => {
<label for="authorName">{{
locale.baseText('settings.versionControl.authorName')
}}</label>
<n8n-input id="authorName" v-model="versionControlStore.preferences.authorName" />
<n8n-form-input
label
id="authorName"
name="authorName"
validateOnBlur
:validationRules="authorNameValidationRules"
v-model="versionControlStore.preferences.authorName"
@validate="(value) => onValidate('authorName', value)"
/>
</div>
<div>
<label for="authorEmail">{{
locale.baseText('settings.versionControl.authorEmail')
}}</label>
<n8n-input id="authorEmail" v-model="versionControlStore.preferences.authorEmail" />
<n8n-form-input
label
type="email"
id="authorEmail"
name="authorEmail"
validateOnBlur
:validationRules="authorEmailValidationRules"
v-model="versionControlStore.preferences.authorEmail"
@validate="(value) => onValidate('authorEmail', value)"
/>
</div>
</div>
<n8n-button
v-if="!versionControlStore.preferences.publicKey"
@click="onContinue"
size="large"
class="mt-2xs"
>{{ locale.baseText('settings.versionControl.button.continue') }}</n8n-button
>
<div v-if="versionControlStore.preferences.publicKey" :class="$style.group">
<label>{{ locale.baseText('settings.versionControl.sshKey') }}</label>
<CopyInput
:value="versionControlStore.preferences.publicKey"
:copy-button-text="locale.baseText('generic.clickToCopy')"
/>
<div :class="{ [$style.sshInput]: !isConnected }">
<CopyInput
collapse
size="medium"
:value="versionControlStore.preferences.publicKey"
:copy-button-text="locale.baseText('generic.clickToCopy')"
/>
<n8n-button
v-if="!isConnected"
size="large"
type="tertiary"
icon="sync"
class="ml-s"
@click="refreshSshKey"
>
{{ locale.baseText('settings.versionControl.refreshSshKey') }}
</n8n-button>
</div>
<n8n-notice type="info" class="mt-s">
<i18n path="settings.versionControl.sshKeyDescription">
<template #link>
<a href="#" target="_blank">
{{ locale.baseText('settings.versionControl.sshKeyDescriptionLink') }}
</a>
<a href="#" target="_blank">{{
locale.baseText('settings.versionControl.sshKeyDescriptionLink')
}}</a>
</template>
</i18n>
</n8n-notice>
</div>
<n8n-button
v-if="
versionControlStore.preferences.publicKey &&
!versionControlStore.preferences.branches.length
"
v-if="!isConnected"
@click="onConnect"
size="large"
:disabled="!validForConnection"
:class="$style.connect"
>{{ locale.baseText('settings.versionControl.button.connect') }}</n8n-button
>
<div v-if="versionControlStore.preferences.branches.length">
<div v-if="isConnected">
<div :class="$style.group">
<hr />
<n8n-heading size="xlarge" tag="h2" class="mb-s">{{
@@ -122,7 +289,7 @@ const goToUpgrade = () => {
}}</n8n-heading>
<label>{{ locale.baseText('settings.versionControl.branches') }}</label>
<n8n-select
:value="versionControlStore.preferences.currentBranch"
:value="versionControlStore.preferences.branchName"
class="mb-s"
size="medium"
filterable
@@ -151,20 +318,17 @@ const goToUpgrade = () => {
</i18n>
</n8n-checkbox>
</div>
<div :class="$style.group">
<!-- <div :class="$style.group">
<label>{{ locale.baseText('settings.versionControl.color') }}</label>
<div>
<n8n-color-picker size="small" v-model="versionControlStore.preferences.branchColor" />
</div>
</div>
</div> -->
<div :class="[$style.group, 'pt-s']">
<n8n-button
v-if="
versionControlStore.preferences.publicKey &&
versionControlStore.preferences.currentBranch
"
@click="onSave"
size="large"
:disabled="!versionControlStore.preferences.branchName"
>{{ locale.baseText('settings.versionControl.button.save') }}</n8n-button
>
</div>
@@ -188,6 +352,8 @@ const goToUpgrade = () => {
<style lang="scss" module>
.group {
padding: 0 0 var(--spacing-s);
width: 100%;
display: block;
label {
display: inline-block;
@@ -211,6 +377,7 @@ const goToUpgrade = () => {
.groupFlex {
display: flex;
align-items: flex-start;
> div {
flex: 1;
@@ -233,6 +400,20 @@ const goToUpgrade = () => {
margin: var(--spacing-2xl) 0 0;
}
.sshInput {
width: 100%;
display: flex;
align-items: center;
> div {
width: calc(100% - 144px - var(--spacing-s));
}
> button {
height: 42px;
}
}
hr {
margin: 0 0 var(--spacing-xl);
border: 1px solid var(--color-foreground-light);

View File

@@ -1,6 +1,12 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useEnvironmentsStore, useUIStore, useSettingsStore, useUsersStore } from '@/stores';
import { computed, ref, onBeforeMount, onBeforeUnmount } from 'vue';
import {
useEnvironmentsStore,
useUIStore,
useSettingsStore,
useUsersStore,
useVersionControlStore,
} from '@/stores';
import { useI18n, useTelemetry, useToast, useUpgradeLink, useMessage } from '@/composables';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
@@ -22,6 +28,8 @@ const uiStore = useUIStore();
const telemetry = useTelemetry();
const { i18n } = useI18n();
const message = useMessage();
const versionControlStore = useVersionControlStore();
let versionControlStoreUnsubscribe = () => {};
const layoutRef = ref<InstanceType<typeof ResourcesListLayout> | null>(null);
@@ -207,6 +215,20 @@ function goToUpgrade() {
function displayName(resource: EnvironmentVariable) {
return resource.key;
}
onBeforeMount(() => {
versionControlStoreUnsubscribe = versionControlStore.$onAction(({ name, after }) => {
if (name === 'pullWorkfolder' && after) {
after(() => {
void initialize();
});
}
});
});
onBeforeUnmount(() => {
versionControlStoreUnsubscribe();
});
</script>
<template>

View File

@@ -9,6 +9,7 @@
:show-aside="allWorkflows.length > 0"
:shareable="isShareable"
:initialize="initialize"
:disabled="readOnlyEnv"
@click:add="addWorkflow"
@update:filters="filters = $event"
>
@@ -19,9 +20,10 @@
:data="data"
@expand:tags="updateItemSize(data)"
@click:tag="onClickTag"
:readOnly="readOnlyEnv"
/>
</template>
<template #empty>
<template v-if="!readOnlyEnv" #empty>
<div class="text-center mt-s">
<n8n-heading tag="h2" size="xlarge" class="mb-2xs">
{{
@@ -103,6 +105,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useVersionControlStore } from '@/stores/versionControl.store';
type IResourcesListLayoutInstance = Vue & { sendFiltersTelemetry: (source: string) => void };
@@ -128,6 +131,7 @@ const WorkflowsView = defineComponent({
status: StatusFilter.ALL,
tags: [] as string[],
},
versionControlStoreUnsubscribe: () => {},
};
},
computed: {
@@ -137,6 +141,7 @@ const WorkflowsView = defineComponent({
useUsersStore,
useWorkflowsStore,
useCredentialsStore,
useVersionControlStore,
),
currentUser(): IUser {
return this.usersStore.currentUser || ({} as IUser);
@@ -163,6 +168,9 @@ const WorkflowsView = defineComponent({
},
];
},
readOnlyEnv(): boolean {
return this.versionControlStore.preferences.branchReadOnly;
},
},
methods: {
addWorkflow() {
@@ -220,6 +228,17 @@ const WorkflowsView = defineComponent({
},
mounted() {
void this.usersStore.showPersonalizationSurvey();
this.versionControlStoreUnsubscribe = this.versionControlStore.$onAction(({ name, after }) => {
if (name === 'pullWorkfolder' && after) {
after(() => {
void this.initialize();
});
}
});
},
beforeUnmount() {
this.versionControlStoreUnsubscribe();
},
});

View File

@@ -26,7 +26,7 @@ const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
},
);
describe('SettingsSso', () => {
describe('SettingsVersionControl', () => {
beforeEach(() => {
pinia = createTestingPinia({
initialState: {