feat: Add various source control improvements (#6533)
* feat: update source control notice wording * feat: update source control paywall state * fix: remove source control git repository ssh input hint * feat: hide tags, variables, and credentials from push modal * feat: add status colors and current workflow marking and sorting * feat: add select all workflows to push modal * fix: push everything besides current workflow with push workflow action * feat: add source control pull modal * feat: add updatedAt integration * fix: add time to last updated * fix: fix sorting, taking deleted into account * fix: update 409 pull workflow test * fix: add status priority sorting * fix: fix linting issue
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -117,6 +117,12 @@
|
||||
<SourceControlPushModal :modalName="modalName" :data="data" />
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="SOURCE_CONTROL_PULL_MODAL_KEY">
|
||||
<template #default="{ modalName, data }">
|
||||
<SourceControlPullModal :modalName="modalName" :data="data" />
|
||||
</template>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import Modal from './Modal.vue';
|
||||
import { SOURCE_CONTROL_PULL_MODAL_KEY } from '@/constants';
|
||||
import type { PropType } from 'vue';
|
||||
import type { EventBus } from 'n8n-design-system/utils';
|
||||
import type { SourceControlStatus } from '@/Interface';
|
||||
import { useI18n, useLoadingService, useToast } from '@/composables';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUIStore } from '@/stores';
|
||||
import { useRoute } from 'vue-router/composables';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object as PropType<{ eventBus: EventBus; status: SourceControlStatus }>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const defaultStagedFileTypes = ['tags', 'variables', 'credential'];
|
||||
|
||||
const loadingService = useLoadingService();
|
||||
const uiStore = useUIStore();
|
||||
const toast = useToast();
|
||||
const { i18n } = useI18n();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const route = useRoute();
|
||||
|
||||
function close() {
|
||||
uiStore.closeModal(SOURCE_CONTROL_PULL_MODAL_KEY);
|
||||
}
|
||||
|
||||
async function pullWorkfolder() {
|
||||
loadingService.startLoading(i18n.baseText('settings.sourceControl.loading.pull'));
|
||||
close();
|
||||
|
||||
try {
|
||||
await sourceControlStore.pullWorkfolder(true);
|
||||
toast.showMessage({
|
||||
message: i18n.baseText('settings.sourceControl.pull.success.description'),
|
||||
title: i18n.baseText('settings.sourceControl.pull.success.title'),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
toast.showError(error, 'Error');
|
||||
} finally {
|
||||
loadingService.stopLoading();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
width="500px"
|
||||
:title="i18n.baseText('settings.sourceControl.modals.pull.title')"
|
||||
:eventBus="data.eventBus"
|
||||
:name="SOURCE_CONTROL_PULL_MODAL_KEY"
|
||||
>
|
||||
<template #content>
|
||||
<div :class="$style.container">
|
||||
<n8n-text>
|
||||
{{ i18n.baseText('settings.sourceControl.modals.pull.description') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<n8n-button type="tertiary" class="mr-2xs" @click="close">
|
||||
{{ i18n.baseText('settings.sourceControl.modals.pull.buttons.cancel') }}
|
||||
</n8n-button>
|
||||
<n8n-button type="primary" @click="pullWorkfolder">
|
||||
{{ i18n.baseText('settings.sourceControl.modals.pull.buttons.save') }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.container > * {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -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<string, boolean> {
|
||||
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<Record<string, boolean>>((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() {
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
|
||||
<div v-if="files.length > 0">
|
||||
<n8n-text bold tag="p" class="mt-l mb-2xs">
|
||||
{{ i18n.baseText('settings.sourceControl.modals.push.filesToCommit') }}
|
||||
</n8n-text>
|
||||
<div v-if="workflowFiles.length > 0">
|
||||
<div class="mt-l mb-2xs">
|
||||
<n8n-checkbox
|
||||
:indeterminate="selectAllIndeterminate"
|
||||
:value="selectAll"
|
||||
@input="onToggleSelectAll"
|
||||
>
|
||||
<n8n-text bold tag="strong">
|
||||
{{ i18n.baseText('settings.sourceControl.modals.push.workflowsToCommit') }}
|
||||
</n8n-text>
|
||||
<n8n-text tag="strong" v-show="workflowFiles.length > 0">
|
||||
({{ stagedWorkflowFiles.length }}/{{ workflowFiles.length }})
|
||||
</n8n-text>
|
||||
</n8n-checkbox>
|
||||
</div>
|
||||
<n8n-card
|
||||
v-for="file in files"
|
||||
v-for="file in sortedFiles"
|
||||
v-show="!defaultStagedFileTypes.includes(file.type)"
|
||||
:key="file.file"
|
||||
:class="$style.listItem"
|
||||
@click="setStagedStatus(file, !staged[file.file])"
|
||||
@@ -151,19 +261,34 @@ async function commitAndPush() {
|
||||
:class="$style.listItemCheckbox"
|
||||
@input="setStagedStatus(file, !staged[file.file])"
|
||||
/>
|
||||
<n8n-text bold>
|
||||
<span v-if="file.status === 'deleted'">
|
||||
<span v-if="file.type === 'workflow'"> Workflow </span>
|
||||
<span v-if="file.type === 'credential'"> Credential </span>
|
||||
Id: {{ file.id }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<div>
|
||||
<n8n-text v-if="file.status === 'deleted'" color="text-light">
|
||||
<span v-if="file.type === 'workflow'"> Deleted Workflow: </span>
|
||||
<span v-if="file.type === 'credential'"> Deleted Credential: </span>
|
||||
<strong>{{ file.id }}</strong>
|
||||
</n8n-text>
|
||||
<n8n-text bold v-else>
|
||||
{{ file.name }}
|
||||
</span>
|
||||
</n8n-text>
|
||||
<n8n-badge :class="$style.listItemStatus">
|
||||
{{ file.status }}
|
||||
</n8n-badge>
|
||||
</n8n-text>
|
||||
<div v-if="file.updatedAt">
|
||||
<n8n-text color="text-light" size="small">
|
||||
{{ renderUpdatedAt(file) }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<div v-if="file.conflict">
|
||||
<n8n-text color="danger" size="small">
|
||||
{{ i18n.baseText('settings.sourceControl.modals.push.overrideVersionInGit') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.listItemStatus">
|
||||
<n8n-badge class="mr-2xs" v-if="workflowId === file.id && file.type === 'workflow'">
|
||||
Current workflow
|
||||
</n8n-badge>
|
||||
<n8n-badge :theme="statusToBadgeThemeMap[file.status] || 'default'">
|
||||
{{ file.status }}
|
||||
</n8n-badge>
|
||||
</div>
|
||||
</div>
|
||||
</n8n-card>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<typeof createTestingPinia>;
|
||||
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
|
||||
const renderComponent = (renderOptions: Parameters<typeof render>[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,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user