refactor(editor): Remove the restApi mixin (#6065)

*  Removing the `makeApiRequest` method from `restAPI` mixin, removing the mixing from the App component
*  Removing `restApi` mixin
* 👕 Fixing lint errors
* ✔️ Fixing execution list unit tests and merge bug in workflowRun mixin
* 🐛 Added missing useStore
This commit is contained in:
Milorad FIlipović 2023-04-24 10:50:49 +02:00 committed by GitHub
parent 4bd55f7a1e
commit 59db96771e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 905 additions and 734 deletions

View file

@ -37,7 +37,6 @@ import { showMessage } from '@/mixins/showMessage';
import { userHelpers } from '@/mixins/userHelpers';
import { loadLanguage } from './plugins/i18n';
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
import { restApi } from '@/mixins/restApi';
import { mapStores } from 'pinia';
import { useUIStore } from './stores/ui';
import { useSettingsStore } from './stores/settings';
@ -49,7 +48,7 @@ import { useHistoryHelper } from '@/composables/useHistoryHelper';
import { newVersions } from '@/mixins/newVersions';
import { useRoute } from 'vue-router/composables';
export default mixins(newVersions, showMessage, userHelpers, restApi).extend({
export default mixins(newVersions, showMessage, userHelpers).extend({
name: 'App',
components: {
LoadingView,

View file

@ -139,43 +139,6 @@ export interface IExternalHooks {
run(eventName: string, metadata?: IDataObject): Promise<void>;
}
/**
* @deprecated Do not add methods to this interface.
*/
export interface IRestApi {
getActiveWorkflows(): Promise<string[]>;
getActivationError(id: string): Promise<IActivationError | undefined>;
getCurrentExecutions(filter: ExecutionsQueryFilter): Promise<IExecutionsCurrentSummaryExtended[]>;
getPastExecutions(
filter: ExecutionsQueryFilter,
limit: number,
lastId?: string,
firstId?: string,
): Promise<IExecutionsListResponse>;
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>;
getCredentialTranslation(credentialType: string): Promise<object>;
removeTestWebhook(workflowId: string): Promise<boolean>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb>;
updateWorkflow(id: string, data: IWorkflowDataUpdate, forceSave?: boolean): Promise<IWorkflowDb>;
deleteWorkflow(name: string): Promise<void>;
getWorkflow(id: string): Promise<IWorkflowDb>;
getWorkflows(filter?: object): Promise<IWorkflowShortResponse[]>;
getWorkflowFromUrl(url: string): Promise<IWorkflowDb>;
getExecution(id: string): Promise<IExecutionResponse | undefined>;
deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void>;
retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
getTimezones(): Promise<IDataObject>;
getBinaryUrl(
dataPath: string,
mode: 'view' | 'download',
fileName?: string,
mimeType?: string,
): string;
getExecutionEvents(id: string): Promise<IAbstractEventMessage[]>;
}
export interface INodeTranslationHeaders {
data: {
[key: string]: {

View file

@ -26,11 +26,10 @@ import BinaryDataDisplayEmbed from '@/components/BinaryDataDisplayEmbed.vue';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import mixins from 'vue-typed-mixins';
import { restApi } from '@/mixins/restApi';
import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows';
export default mixins(nodeHelpers, restApi).extend({
export default mixins(nodeHelpers).extend({
name: 'BinaryDataDisplay',
components: {
BinaryDataDisplayEmbed,

View file

@ -19,13 +19,14 @@
</template>
<script lang="ts">
import mixins from 'vue-typed-mixins';
import { restApi } from '@/mixins/restApi';
import { IBinaryData, jsonParse } from 'n8n-workflow';
import type { PropType } from 'vue';
import VueJsonPretty from 'vue-json-pretty';
import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores';
import Vue from 'vue';
export default mixins(restApi).extend({
export default Vue.extend({
name: 'BinaryDataDisplayEmbed',
components: {
VueJsonPretty,
@ -44,6 +45,9 @@ export default mixins(restApi).extend({
jsonData: '',
};
},
computed: {
...mapStores(useWorkflowsStore),
},
async mounted() {
const { id, data, fileName, fileType, mimeType } = (this.binaryData || {}) as IBinaryData;
const isJSONData = fileType === 'json';
@ -56,7 +60,7 @@ export default mixins(restApi).extend({
}
} else {
try {
const binaryUrl = this.restApi().getBinaryUrl(id, 'view', fileName, mimeType);
const binaryUrl = this.workflowsStore.getBinaryUrl(id, 'view', fileName, mimeType);
if (isJSONData) {
this.jsonData = await (await fetch(binaryUrl)).json();
} else {

View file

@ -135,9 +135,7 @@ import Banner from '../Banner.vue';
import CopyInput from '../CopyInput.vue';
import CredentialInputs from './CredentialInputs.vue';
import OauthButton from './OauthButton.vue';
import { restApi } from '@/mixins/restApi';
import { addCredentialTranslation } from '@/plugins/i18n';
import mixins from 'vue-typed-mixins';
import { BUILTIN_CREDENTIALS_DOCS_URL, DOCS_DOMAIN, EnterpriseEditionFeature } from '@/constants';
import { IPermissions } from '@/permissions';
import { mapStores } from 'pinia';
@ -147,12 +145,12 @@ import { useRootStore } from '@/stores/n8nRootStore';
import { useNDVStore } from '@/stores/ndv';
import { useCredentialsStore } from '@/stores/credentials';
import { useNodeTypesStore } from '@/stores/nodeTypes';
import { ICredentialsResponse, IUpdateInformation, NodeAuthenticationOption } from '@/Interface';
import ParameterInputFull from '@/components/ParameterInputFull.vue';
import { ICredentialsResponse } from '@/Interface';
import AuthTypeSelector from '@/components/CredentialEdit/AuthTypeSelector.vue';
import GoogleAuthButton from './GoogleAuthButton.vue';
import Vue from 'vue';
export default mixins(restApi).extend({
export default Vue.extend({
name: 'CredentialConfig',
components: {
AuthTypeSelector,
@ -160,7 +158,6 @@ export default mixins(restApi).extend({
CopyInput,
CredentialInputs,
OauthButton,
ParameterInputFull,
GoogleAuthButton,
},
props: {
@ -226,7 +223,9 @@ export default mixins(restApi).extend({
if (this.$locale.exists(key)) return;
const credTranslation = await this.restApi().getCredentialTranslation(this.credentialType.name);
const credTranslation = await this.credentialsStore.getCredentialTranslation(
this.credentialType.name,
);
addCredentialTranslation(
{ [this.credentialType.name]: credTranslation },

View file

@ -56,7 +56,6 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
import { showMessage } from '@/mixins/showMessage';
import TagsDropdown from '@/components/TagsDropdown.vue';
import Modal from './Modal.vue';
import { restApi } from '@/mixins/restApi';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/settings';
import { useWorkflowsStore } from '@/stores/workflows';
@ -64,8 +63,9 @@ import { IWorkflowDataUpdate } from '@/Interface';
import { getWorkflowPermissions, IPermissions } from '@/permissions';
import { useUsersStore } from '@/stores/users';
import { createEventBus } from '@/event-bus';
import { useCredentialsStore } from '@/stores';
export default mixins(showMessage, workflowHelpers, restApi).extend({
export default mixins(showMessage, workflowHelpers).extend({
components: { TagsDropdown, Modal },
name: 'DuplicateWorkflow',
props: ['modalName', 'isActive', 'data'],
@ -87,7 +87,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
this.$nextTick(() => this.focusOnNameInput());
},
computed: {
...mapStores(useUsersStore, useSettingsStore, useWorkflowsStore),
...mapStores(useCredentialsStore, useUsersStore, useSettingsStore, useWorkflowsStore),
workflowPermissions(): IPermissions {
const isEmptyWorkflow = this.data.id === PLACEHOLDER_EMPTY_WORKFLOW_ID;
const isCurrentWorkflowEmpty =
@ -150,7 +150,7 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({
let workflowToUpdate: IWorkflowDataUpdate | undefined;
if (currentWorkflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) {
const { createdAt, updatedAt, usedCredentials, ...workflow } =
await this.restApi().getWorkflow(this.data.id);
await this.workflowsStore.fetchWorkflow(this.data.id);
workflowToUpdate = workflow;
this.removeForeignCredentialsFromWorkflow(

View file

@ -271,7 +271,6 @@ import WorkflowActivator from '@/components/WorkflowActivator.vue';
import ExecutionFilter from '@/components/ExecutionFilter.vue';
import { externalHooks } from '@/mixins/externalHooks';
import { VIEWS, WAIT_TIME_UNLIMITED } from '@/constants';
import { restApi } from '@/mixins/restApi';
import { genericHelpers } from '@/mixins/genericHelpers';
import { executionHelpers } from '@/mixins/executionsHelpers';
import { showMessage } from '@/mixins/showMessage';
@ -292,8 +291,7 @@ import { useWorkflowsStore } from '@/stores/workflows';
import { isEmpty, setPageTitle } from '@/utils';
import { executionFilterToQueryFilter } from '@/utils/executionUtils';
export default mixins(externalHooks, genericHelpers, executionHelpers, restApi, showMessage).extend(
{
export default mixins(externalHooks, genericHelpers, executionHelpers, showMessage).extend({
name: 'ExecutionsList',
components: {
ExecutionTime,
@ -457,7 +455,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
sendData.filters = this.workflowFilterPast;
try {
await this.restApi().deleteExecutions(sendData);
await this.workflowsStore.deleteExecutions(sendData);
} catch (error) {
this.isDataLoading = false;
this.$showError(
@ -511,7 +509,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
},
async loadActiveExecutions(): Promise<void> {
const activeExecutions = isEmpty(this.workflowFilterCurrent.metadata)
? await this.restApi().getCurrentExecutions(this.workflowFilterCurrent)
? await this.workflowsStore.getCurrentExecutions(this.workflowFilterCurrent)
: [];
for (const activeExecution of activeExecutions) {
if (activeExecution.workflowId && !activeExecution.workflowName) {
@ -530,11 +528,11 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
// iF you use firstId, filtering id >= 504 you won't
// ever get ids 500, 501, 502 and 503 when they finish
const pastExecutionsPromise: Promise<IExecutionsListResponse> =
this.restApi().getPastExecutions(filter, this.requestItemsPerRequest);
this.workflowsStore.getPastExecutions(filter, this.requestItemsPerRequest);
const currentExecutionsPromise: Promise<IExecutionsCurrentSummaryExtended[]> = isEmpty(
filter.metadata,
)
? this.restApi().getCurrentExecutions({})
? this.workflowsStore.getCurrentExecutions({})
: Promise.resolve([]);
const results = await Promise.all([pastExecutionsPromise, currentExecutionsPromise]);
@ -624,7 +622,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
this.finishedExecutionsCountEstimated = false;
return;
}
const data = await this.restApi().getPastExecutions(
const data = await this.workflowsStore.getPastExecutions(
this.workflowFilterPast,
this.requestItemsPerRequest,
);
@ -655,7 +653,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
let data: IExecutionsListResponse;
try {
data = await this.restApi().getPastExecutions(
data = await this.workflowsStore.getPastExecutions(
filter,
this.requestItemsPerRequest,
lastId,
@ -683,7 +681,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
},
async loadWorkflows() {
try {
const workflows = await this.restApi().getWorkflows();
const workflows = await this.workflowsStore.fetchAllWorkflows();
workflows.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
@ -712,7 +710,10 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
this.isDataLoading = true;
try {
const retrySuccessful = await this.restApi().retryExecution(execution.id, loadWorkflow);
const retrySuccessful = await this.workflowsStore.retryExecution(
execution.id,
loadWorkflow,
);
if (retrySuccessful) {
this.$showMessage({
@ -742,10 +743,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
try {
await Promise.all([this.loadActiveExecutions(), this.loadFinishedExecutions()]);
} catch (error) {
this.$showError(
error,
this.$locale.baseText('executionsList.showError.refreshData.title'),
);
this.$showError(error, this.$locale.baseText('executionsList.showError.refreshData.title'));
}
this.isDataLoading = false;
@ -839,7 +837,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
// can show the user in the UI that it is in progress
this.stoppingExecutions.push(activeExecutionId);
await this.restApi().stopCurrentExecution(activeExecutionId);
await this.workflowsStore.stopCurrentExecution(activeExecutionId);
// Remove it from the list of currently stopping executions
const index = this.stoppingExecutions.indexOf(activeExecutionId);
@ -873,7 +871,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
async deleteExecution(execution: IExecutionsSummary) {
this.isDataLoading = true;
try {
await this.restApi().deleteExecutions({ ids: [execution.id] });
await this.workflowsStore.deleteExecutions({ ids: [execution.id] });
await this.refreshData();
if (this.allVisibleSelected) {
@ -909,8 +907,7 @@ export default mixins(externalHooks, genericHelpers, executionHelpers, restApi,
}
},
},
},
);
});
</script>
<style module lang="scss">

View file

@ -86,10 +86,9 @@ import mixins from 'vue-typed-mixins';
import { executionHelpers, IExecutionUIData } from '@/mixins/executionsHelpers';
import { VIEWS } from '@/constants';
import { showMessage } from '@/mixins/showMessage';
import { restApi } from '@/mixins/restApi';
import ExecutionTime from '@/components/ExecutionTime.vue';
export default mixins(executionHelpers, showMessage, restApi).extend({
export default mixins(executionHelpers, showMessage).extend({
name: 'execution-card',
components: {
ExecutionTime,

View file

@ -127,7 +127,6 @@
<script lang="ts">
import mixins from 'vue-typed-mixins';
import { restApi } from '@/mixins/restApi';
import { showMessage } from '@/mixins/showMessage';
import WorkflowPreview from '@/components/WorkflowPreview.vue';
import { executionHelpers, IExecutionUIData } from '@/mixins/executionsHelpers';
@ -135,11 +134,10 @@ import { VIEWS } from '@/constants';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { Dropdown as ElDropdown } from 'element-ui';
import { IAbstractEventMessage } from 'n8n-workflow';
type RetryDropdownRef = InstanceType<typeof ElDropdown> & { hide: () => void };
export default mixins(restApi, showMessage, executionHelpers).extend({
export default mixins(showMessage, executionHelpers).extend({
name: 'execution-preview',
components: {
ElDropdown,

View file

@ -49,7 +49,6 @@ import {
NodeHelpers,
} from 'n8n-workflow';
import mixins from 'vue-typed-mixins';
import { restApi } from '@/mixins/restApi';
import { showMessage } from '@/mixins/showMessage';
import { v4 as uuid } from 'uuid';
import { Route } from 'vue-router';
@ -71,13 +70,7 @@ const MAX_LOADING_ATTEMPTS = 5;
// Number of executions fetched on each page
const LOAD_MORE_PAGE_SIZE = 100;
export default mixins(
restApi,
showMessage,
executionHelpers,
debounceHelper,
workflowHelpers,
).extend({
export default mixins(showMessage, executionHelpers, debounceHelper, workflowHelpers).extend({
name: 'executions-list',
components: {
ExecutionsSidebar,
@ -225,7 +218,7 @@ export default mixins(
let data: IExecutionsListResponse;
try {
data = await this.restApi().getPastExecutions(this.requestFilter, limit, lastId);
data = await this.workflowsStore.getPastExecutions(this.requestFilter, limit, lastId);
} catch (error) {
this.loadingMore = false;
this.$showError(error, this.$locale.baseText('executionsList.showError.loadMore.title'));
@ -260,7 +253,7 @@ export default mixins(
this.executions[executionIndex - 1] ||
this.executions[0];
await this.restApi().deleteExecutions({ ids: [this.$route.params.executionId] });
await this.workflowsStore.deleteExecutions({ ids: [this.$route.params.executionId] });
if (this.temporaryExecution?.id === this.$route.params.executionId) {
this.temporaryExecution = null;
}
@ -300,7 +293,7 @@ export default mixins(
const activeExecutionId = this.$route.params.executionId;
try {
await this.restApi().stopCurrentExecution(activeExecutionId);
await this.workflowsStore.stopCurrentExecution(activeExecutionId);
this.$showMessage({
title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'),
@ -498,7 +491,7 @@ export default mixins(
let data: IWorkflowDb | undefined;
try {
data = await this.restApi().getWorkflow(workflowId);
data = await this.workflowsStore.fetchWorkflow(workflowId);
} catch (error) {
this.$showError(error, this.$locale.baseText('nodeView.showError.openWorkflow.title'));
return;
@ -644,7 +637,7 @@ export default mixins(
}
},
async loadActiveWorkflows(): Promise<void> {
this.workflowsStore.activeWorkflows = await this.restApi().getActiveWorkflows();
await this.workflowsStore.fetchActiveWorkflows();
},
async onRetryExecution(payload: { execution: IExecutionsSummary; command: string }) {
const loadWorkflow = payload.command === 'current-workflow';
@ -665,7 +658,10 @@ export default mixins(
},
async retryExecution(execution: IExecutionsSummary, loadWorkflow?: boolean) {
try {
const retrySuccessful = await this.restApi().retryExecution(execution.id, loadWorkflow);
const retrySuccessful = await this.workflowsStore.retryExecution(
execution.id,
loadWorkflow,
);
if (retrySuccessful === true) {
this.$showMessage({

View file

@ -509,7 +509,7 @@ export default mixins(workflowHelpers).extend({
}
try {
await this.restApi().deleteWorkflow(this.currentWorkflowId);
await this.workflowsStore.deleteWorkflowAPI(this.currentWorkflowId);
} catch (error) {
this.$showError(
error,

View file

@ -97,7 +97,6 @@ import GiftNotificationIcon from './GiftNotificationIcon.vue';
import WorkflowSettings from '@/components/WorkflowSettings.vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import { restApi } from '@/mixins/restApi';
import { showMessage } from '@/mixins/showMessage';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { workflowRun } from '@/mixins/workflowRun';
@ -118,7 +117,6 @@ import { isNavigationFailure } from 'vue-router';
export default mixins(
genericHelpers,
restApi,
showMessage,
workflowHelpers,
workflowRun,

View file

@ -98,7 +98,6 @@
<script lang="ts">
import { PropType } from 'vue';
import { restApi } from '@/mixins/restApi';
import {
ICredentialsResponse,
INodeUi,
@ -142,7 +141,7 @@ interface CredentialDropdownOption extends ICredentialsResponse {
typeDisplayName: string;
}
export default mixins(genericHelpers, nodeHelpers, restApi, showMessage).extend({
export default mixins(genericHelpers, nodeHelpers, showMessage).extend({
name: 'NodeCredentials',
props: {
readonly: {

View file

@ -171,7 +171,7 @@ export default mixins(workflowRun, pinData).extend({
methods: {
async stopWaitingForWebhook() {
try {
await this.restApi().removeTestWebhook(this.workflowsStore.workflowId);
await this.workflowsStore.removeTestWebhook(this.workflowsStore.workflowId);
} catch (error) {
this.$showError(error, this.$locale.baseText('ndv.execute.stopWaitingForWebhook.error'));
return;

View file

@ -1251,7 +1251,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten
const { id, data, fileName, fileExtension, mimeType } = this.binaryData[index][key];
if (id) {
const url = this.restApi().getBinaryUrl(id, 'download', fileName, mimeType);
const url = this.workflowsStore.getBinaryUrl(id, 'download', fileName, mimeType);
saveAs(url, [fileName, fileExtension].join('.'));
return;
} else {

View file

@ -86,7 +86,7 @@ const workflowName = ref('');
onMounted(async () => {
const currentSettings = getCurrentSettings();
try {
const { name } = await workflowStore.fetchWorkflow(
const { name } = await workflowStore.fetchAndSetWorkflow(
currentSettings?.firstSuccessfulWorkflowId ?? '',
);
workflowName.value = name;

View file

@ -105,7 +105,7 @@ export default mixins(showMessage, workflowActivate).extend({
async displayActivationError() {
let errorMessage: string;
try {
const errorData = await this.restApi().getActivationError(this.workflowId);
const errorData = await this.workflowsStore.getActivationError(this.workflowId);
if (errorData === undefined) {
errorMessage = this.$locale.baseText(

View file

@ -73,7 +73,6 @@ import {
import { showMessage } from '@/mixins/showMessage';
import { getWorkflowPermissions, IPermissions } from '@/permissions';
import dateformat from 'dateformat';
import { restApi } from '@/mixins/restApi';
import WorkflowActivator from '@/components/WorkflowActivator.vue';
import Vue from 'vue';
import { mapStores } from 'pinia';
@ -91,7 +90,7 @@ export const WORKFLOW_LIST_ITEM_ACTIONS = {
DELETE: 'delete',
};
export default mixins(showMessage, restApi).extend({
export default mixins(showMessage).extend({
data() {
return {
EnterpriseEditionFeature,
@ -234,7 +233,7 @@ export default mixins(showMessage, restApi).extend({
}
try {
await this.restApi().deleteWorkflow(this.data.id);
await this.workflowsStore.deleteWorkflowAPI(this.data.id);
this.workflowsStore.deleteWorkflow(this.data.id);
} catch (error) {
this.$showError(

View file

@ -327,7 +327,6 @@
import Vue from 'vue';
import { externalHooks } from '@/mixins/externalHooks';
import { restApi } from '@/mixins/restApi';
import { genericHelpers } from '@/mixins/genericHelpers';
import { showMessage } from '@/mixins/showMessage';
import {
@ -357,7 +356,7 @@ import useWorkflowsEEStore from '@/stores/workflows.ee';
import { useUsersStore } from '@/stores/users';
import { createEventBus } from '@/event-bus';
export default mixins(externalHooks, genericHelpers, restApi, showMessage).extend({
export default mixins(externalHooks, genericHelpers, showMessage).extend({
name: 'WorkflowSettings',
components: {
Modal,
@ -703,7 +702,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten
return;
}
const timezones = await this.restApi().getTimezones();
const timezones = await this.settingsStore.getTimezones();
let defaultTimezoneValue = timezones[this.defaultValues.timezone] as string | undefined;
if (defaultTimezoneValue === undefined) {
@ -724,7 +723,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten
}
},
async loadWorkflows() {
const workflows = await this.restApi().getWorkflows();
const workflows = await this.workflowsStore.fetchAllWorkflows();
workflows.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
@ -789,7 +788,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten
data.versionId = this.workflowsStore.workflowVersionId;
try {
const workflow = await this.restApi().updateWorkflow(this.$route.params.name, data);
const workflow = await this.workflowsStore.updateWorkflow(this.$route.params.name, data);
this.workflowsStore.setWorkflowVersionId(workflow.versionId);
} catch (error) {
this.$showError(

View file

@ -448,7 +448,7 @@ export default mixins(showMessage).extend({
this.workflow.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID &&
!this.workflow.sharedWith?.length // Sharing info already loaded
) {
await this.workflowsStore.fetchWorkflow(this.workflow.id);
await this.workflowsStore.fetchAndSetWorkflow(this.workflow.id);
}
}

View file

@ -12,17 +12,21 @@ import { genericHelpers } from '@/mixins/genericHelpers';
import { executionHelpers } from '@/mixins/executionsHelpers';
import { showMessage } from '@/mixins/showMessage';
import { i18nInstance } from '@/plugins/i18n';
import type { IWorkflowShortResponse } from '@/Interface';
import type { IWorkflowDb } from '@/Interface';
import type { IExecutionsSummary } from 'n8n-workflow';
import { waitAllPromises } from '@/__tests__/utils';
import { useWorkflowsStore } from '@/stores';
const workflowDataFactory = (): IWorkflowShortResponse => ({
const workflowDataFactory = (): IWorkflowDb => ({
createdAt: faker.date.past().toDateString(),
updatedAt: faker.date.past().toDateString(),
id: faker.datatype.uuid(),
name: faker.datatype.string(),
active: faker.datatype.boolean(),
tags: [],
nodes: [],
connections: {},
versionId: faker.datatype.number().toString(),
});
const executionDataFactory = (): IExecutionsSummary => ({
@ -45,20 +49,6 @@ const executionsData = Array.from({ length: 2 }, () => ({
estimated: false,
}));
let getPastExecutionsSpy = vi.fn().mockResolvedValue({ count: 0, results: [], estimated: false });
const mockRestApiMixin = defineComponent({
methods: {
restApi() {
return {
getWorkflows: vi.fn().mockResolvedValue(workflowsData),
getCurrentExecutions: vi.fn().mockResolvedValue([]),
getPastExecutions: getPastExecutionsSpy,
};
},
},
});
const renderOptions = {
pinia: createTestingPinia({
initialState: {
@ -83,7 +73,7 @@ const renderOptions = {
}),
i18n: i18nInstance,
stubs: ['font-awesome-icon'],
mixins: [externalHooks, genericHelpers, executionHelpers, showMessage, mockRestApiMixin],
mixins: [externalHooks, genericHelpers, executionHelpers, showMessage],
};
function TelemetryPlugin(vue: typeof Vue): void {
@ -113,7 +103,22 @@ Vue.use(TelemetryPlugin);
Vue.use(PiniaVuePlugin);
describe('ExecutionsList.vue', () => {
const workflowsStore: ReturnType<typeof useWorkflowsStore> = useWorkflowsStore();
beforeEach(() => {
vi.spyOn(workflowsStore, 'fetchAllWorkflows').mockResolvedValue(workflowsData);
vi.spyOn(workflowsStore, 'getCurrentExecutions').mockResolvedValue([]);
});
afterEach(() => {
vi.clearAllMocks();
});
it('should render empty list', async () => {
vi.spyOn(workflowsStore, 'getPastExecutions').mockResolvedValueOnce({
count: 0,
results: [],
estimated: false,
});
const { queryAllByTestId, queryByTestId, getByTestId } = await renderComponent();
await userEvent.click(getByTestId('execution-auto-refresh-checkbox'));
@ -124,8 +129,8 @@ describe('ExecutionsList.vue', () => {
});
it('should handle selection flow when loading more items', async () => {
getPastExecutionsSpy = vi
.fn()
const storeSpy = vi
.spyOn(workflowsStore, 'getPastExecutions')
.mockResolvedValueOnce(executionsData[0])
.mockResolvedValueOnce(executionsData[1]);
@ -134,7 +139,7 @@ describe('ExecutionsList.vue', () => {
await userEvent.click(getByTestId('select-visible-executions-checkbox'));
expect(getPastExecutionsSpy).toHaveBeenCalledTimes(1);
expect(storeSpy).toHaveBeenCalledTimes(1);
expect(
getAllByTestId('select-execution-checkbox').filter((el) =>
el.contains(el.querySelector(':checked')),
@ -145,7 +150,7 @@ describe('ExecutionsList.vue', () => {
await userEvent.click(getByTestId('load-more-button'));
expect(getPastExecutionsSpy).toHaveBeenCalledTimes(2);
expect(storeSpy).toHaveBeenCalledTimes(2);
expect(getAllByTestId('select-execution-checkbox').length).toBe(20);
expect(
getAllByTestId('select-execution-checkbox').filter((el) =>

View file

@ -32,11 +32,8 @@ import {
IUser,
} from '@/Interface';
import { restApi } from '@/mixins/restApi';
import { get } from 'lodash-es';
import mixins from 'vue-typed-mixins';
import { isObjectLiteral } from '@/utils';
import { getCredentialPermissions } from '@/permissions';
import { mapStores } from 'pinia';
@ -45,8 +42,9 @@ import { useUsersStore } from '@/stores/users';
import { useWorkflowsStore } from '@/stores/workflows';
import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useCredentialsStore } from '@/stores/credentials';
import Vue from 'vue';
export const nodeHelpers = mixins(restApi).extend({
export const nodeHelpers = Vue.extend({
computed: {
...mapStores(
useCredentialsStore,

View file

@ -44,7 +44,6 @@ import {
} from '../Interface';
import { externalHooks } from '@/mixins/externalHooks';
import { restApi } from '@/mixins/restApi';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { showMessage } from '@/mixins/showMessage';
@ -325,7 +324,7 @@ function executeData(
return executeData;
}
export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showMessage).extend({
export const workflowHelpers = mixins(externalHooks, nodeHelpers, showMessage).extend({
computed: {
...mapStores(
useNodeTypesStore,
@ -664,7 +663,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM
if (isCurrentWorkflow) {
data = await this.getWorkflowDataToSave();
} else {
const { versionId } = await this.restApi().getWorkflow(workflowId);
const { versionId } = await this.workflowsStore.fetchWorkflow(workflowId);
data.versionId = versionId;
}
@ -672,7 +671,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM
data.active = active;
}
const workflow = await this.restApi().updateWorkflow(workflowId, data);
const workflow = await this.workflowsStore.updateWorkflow(workflowId, data);
this.workflowsStore.setWorkflowVersionId(workflow.versionId);
if (isCurrentWorkflow) {
@ -714,7 +713,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM
workflowDataRequest.versionId = this.workflowsStore.workflowVersionId;
const workflowData = await this.restApi().updateWorkflow(
const workflowData = await this.workflowsStore.updateWorkflow(
currentWorkflow,
workflowDataRequest,
forceSave,
@ -831,7 +830,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM
if (tags) {
workflowDataRequest.tags = tags;
}
const workflowData = await this.restApi().createNewWorkflow(workflowDataRequest);
const workflowData = await this.workflowsStore.createNewWorkflow(workflowDataRequest);
this.workflowsStore.addWorkflow(workflowData);
@ -944,7 +943,7 @@ export const workflowHelpers = mixins(externalHooks, nodeHelpers, restApi, showM
async dataHasChanged(id: string) {
const currentData = await this.getWorkflowDataToSave();
const data: IWorkflowDb = await this.restApi().getWorkflow(id);
const data: IWorkflowDb = await this.workflowsStore.fetchWorkflow(id);
if (data !== undefined) {
const x = {

View file

@ -9,7 +9,6 @@ import {
} from 'n8n-workflow';
import { externalHooks } from '@/mixins/externalHooks';
import { restApi } from '@/mixins/restApi';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { showMessage } from '@/mixins/showMessage';
@ -20,7 +19,7 @@ import { useUIStore } from '@/stores/ui';
import { useWorkflowsStore } from '@/stores/workflows';
import { useRootStore } from '@/stores/n8nRootStore';
export const workflowRun = mixins(externalHooks, restApi, workflowHelpers, showMessage).extend({
export const workflowRun = mixins(externalHooks, workflowHelpers, showMessage).extend({
setup() {
return {
...useTitleChange(),
@ -45,7 +44,7 @@ export const workflowRun = mixins(externalHooks, restApi, workflowHelpers, showM
let response: IExecutionPushResponse;
try {
response = await this.restApi().runWorkflow(runData);
response = await this.workflowsStore.runWorkflow(runData);
} catch (error) {
this.uiStore.removeActiveAction('workflowRunning');
throw error;

View file

@ -12,7 +12,7 @@ import {
updateCredential,
} from '@/api/credentials';
import { setCredentialSharedWith } from '@/api/credentials.ee';
import { getAppNameFromCredType } from '@/utils';
import { getAppNameFromCredType, makeRestApiRequest } from '@/utils';
import { EnterpriseEditionFeature, STORES } from '@/constants';
import {
ICredentialMap,
@ -376,5 +376,15 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
),
);
},
async getCredentialTranslation(credentialType: string): Promise<object> {
const rootStore = useRootStore();
return await makeRestApiRequest(
rootStore.getRestApiContext,
'GET',
'/credential-translation',
{ credentialType },
);
},
},
});

View file

@ -35,6 +35,7 @@ import { useRootStore } from './n8nRootStore';
import { useUIStore } from './ui';
import { useUsersStore } from './users';
import { useVersionsStore } from './versions';
import { makeRestApiRequest } from '@/utils';
export const useSettingsStore = defineStore(STORES.SETTINGS, {
state: (): ISettingsState => ({
@ -340,5 +341,9 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
setSaveManualExecutions(saveManualExecutions: boolean) {
Vue.set(this, 'saveManualExecutions', saveManualExecutions);
},
async getTimezones(): Promise<IDataObject> {
const rootStore = useRootStore();
return makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/options/timezones');
},
},
});

View file

@ -10,16 +10,23 @@ import {
} from '@/constants';
import {
ExecutionsQueryFilter,
IActivationError,
IExecutionDeleteFilter,
IExecutionPushResponse,
IExecutionResponse,
IExecutionsCurrentSummaryExtended,
IExecutionsListResponse,
IExecutionsStopData,
INewWorkflowData,
INodeUi,
INodeUpdatePropertiesInformation,
IPushDataExecutionFinished,
IPushDataNodeExecuteAfter,
IPushDataUnsavedExecutionFinished,
IStartRunData,
IUpdateInformation,
IUsedCredential,
IWorkflowDataUpdate,
IWorkflowDb,
IWorkflowsMap,
WorkflowsState,
@ -27,6 +34,7 @@ import {
import { defineStore } from 'pinia';
import {
deepCopy,
IAbstractEventMessage,
IConnection,
IConnections,
IDataObject,
@ -69,6 +77,8 @@ import {
stringSizeInBytes,
isObjectLiteral,
isEmpty,
makeRestApiRequest,
unflattenExecutionData,
} from '@/utils';
import { useNDVStore } from './ndv';
import { useNodeTypesStore } from './nodeTypes';
@ -345,6 +355,19 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
return this.getWorkflow(nodes, connections, copyData);
},
// Returns a workflow from a given URL
async getWorkflowFromUrl(url: string): Promise<IWorkflowDb> {
const rootStore = useRootStore();
return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/workflows/from-url', {
url,
});
},
async getActivationError(id: string): Promise<IActivationError | undefined> {
const rootStore = useRootStore();
return makeRestApiRequest(rootStore.getRestApiContext, 'GET', `/active/error/${id}`);
},
async fetchAllWorkflows(): Promise<IWorkflowDb[]> {
const rootStore = useRootStore();
const workflows = await getWorkflows(rootStore.getRestApiContext);
@ -352,7 +375,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
return workflows;
},
async fetchWorkflow(id: string): Promise<IWorkflowDb> {
async fetchAndSetWorkflow(id: string): Promise<IWorkflowDb> {
const rootStore = useRootStore();
const workflow = await getWorkflow(rootStore.getRestApiContext, id);
this.addWorkflow(workflow);
@ -1025,6 +1048,144 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
Vue.set(this, 'activeExecutions', newActiveExecutions);
},
async retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean> {
let sendData;
if (loadWorkflow === true) {
sendData = {
loadWorkflow: true,
};
}
const rootStore = useRootStore();
return await makeRestApiRequest(
rootStore.getRestApiContext,
'POST',
`/executions/${id}/retry`,
sendData,
);
},
// Deletes executions
async deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void> {
const rootStore = useRootStore();
return await makeRestApiRequest(
rootStore.getRestApiContext,
'POST',
'/executions/delete',
sendData as unknown as IDataObject,
);
},
// TODO: For sure needs some kind of default filter like last day, with max 10 results, ...
async getPastExecutions(
filter: IDataObject,
limit: number,
lastId?: string,
firstId?: string,
): Promise<IExecutionsListResponse> {
let sendData = {};
if (filter) {
sendData = {
filter,
firstId,
lastId,
limit,
};
}
const rootStore = useRootStore();
return makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/executions', sendData);
},
async getCurrentExecutions(filter: IDataObject): Promise<IExecutionsCurrentSummaryExtended[]> {
let sendData = {};
if (filter) {
sendData = {
filter,
};
}
const rootStore = useRootStore();
return await makeRestApiRequest(
rootStore.getRestApiContext,
'GET',
'/executions-current',
sendData,
);
},
async getExecution(id: string): Promise<IExecutionResponse | undefined> {
const rootStore = useRootStore();
const response = await makeRestApiRequest(
rootStore.getRestApiContext,
'GET',
`/executions/${id}`,
);
return response && unflattenExecutionData(response);
},
async fetchWorkflow(id: string): Promise<IWorkflowDb> {
const rootStore = useRootStore();
return makeRestApiRequest(rootStore.getRestApiContext, 'GET', `/workflows/${id}`);
},
// Creates a new workflow
async createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb> {
const rootStore = useRootStore();
return makeRestApiRequest(
rootStore.getRestApiContext,
'POST',
'/workflows',
sendData as unknown as IDataObject,
);
},
// Deletes a workflow
async deleteWorkflowAPI(name: string): Promise<void> {
const rootStore = useRootStore();
return makeRestApiRequest(rootStore.getRestApiContext, 'DELETE', `/workflows/${name}`);
},
// Updates an existing workflow
async updateWorkflow(
id: string,
data: IWorkflowDataUpdate,
forceSave = false,
): Promise<IWorkflowDb> {
const rootStore = useRootStore();
return makeRestApiRequest(
rootStore.getRestApiContext,
'PATCH',
`/workflows/${id}${forceSave ? '?forceSave=true' : ''}`,
data as unknown as IDataObject,
);
},
async runWorkflow(startRunData: IStartRunData): Promise<IExecutionPushResponse> {
const rootStore = useRootStore();
return await makeRestApiRequest(
rootStore.getRestApiContext,
'POST',
'/workflows/run',
startRunData as unknown as IDataObject,
);
},
async removeTestWebhook(workflowId: string): Promise<boolean> {
const rootStore = useRootStore();
return await makeRestApiRequest(
rootStore.getRestApiContext,
'DELETE',
`/test-webhook/${workflowId}`,
);
},
async stopCurrentExecution(executionId: string): Promise<IExecutionsStopData> {
const rootStore = useRootStore();
return await makeRestApiRequest(
rootStore.getRestApiContext,
'POST',
`/executions-current/${executionId}/stop`,
);
},
async loadCurrentWorkflowExecutions(
requestFilter: ExecutionsQueryFilter,
): Promise<IExecutionsSummary[]> {
@ -1047,13 +1208,16 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
throw error;
}
},
async fetchExecutionDataById(executionId: string): Promise<IExecutionResponse | null> {
const rootStore = useRootStore();
return await getExecutionData(rootStore.getRestApiContext, executionId);
},
deleteExecution(execution: IExecutionsSummary): void {
this.currentWorkflowExecutions.splice(this.currentWorkflowExecutions.indexOf(execution), 1);
},
addToCurrentExecutions(executions: IExecutionsSummary[]): void {
executions.forEach((execution) => {
const exists = this.currentWorkflowExecutions.find((ex) => ex.id === execution.id);
@ -1062,6 +1226,23 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
}
});
},
// Returns all the available timezones
async getExecutionEvents(id: string): Promise<IAbstractEventMessage[]> {
const rootStore = useRootStore();
return makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/eventbus/execution/' + id);
},
// Binary data
async getBinaryUrl(dataPath, mode, fileName, mimeType): string {
const rootStore = useRootStore();
let restUrl = rootStore.getRestUrl;
if (restUrl.startsWith('/')) restUrl = window.location.origin + restUrl;
const url = new URL(`${restUrl}/data/${dataPath}`);
url.searchParams.append('mode', mode);
if (fileName) url.searchParams.append('fileName', fileName);
if (mimeType) url.searchParams.append('mimeType', mimeType);
return url.toString();
},
setNodePristine(nodeName: string, isPristine: boolean): void {
Vue.set(this.nodeMetadata[nodeName], 'pristine', isPristine);
},

View file

@ -1,6 +1,12 @@
import axios, { AxiosRequestConfig, Method } from 'axios';
import { IDataObject } from 'n8n-workflow';
import type { IRestApiContext } from '@/Interface';
import type {
IExecutionFlattedResponse,
IExecutionResponse,
IRestApiContext,
IWorkflowDb,
} from '@/Interface';
import { parse } from 'flatted';
export const NO_NETWORK_ERROR_CODE = 999;
@ -127,3 +133,27 @@ export async function post(
) {
return await request({ method: 'POST', baseURL, endpoint, headers, data: params });
}
/**
* Unflattens the Execution data.
*
* @param {IExecutionFlattedResponse} fullExecutionData The data to unflatten
*/
export function unflattenExecutionData(
fullExecutionData: IExecutionFlattedResponse,
): IExecutionResponse {
// Unflatten the data
const returnData: IExecutionResponse = {
...fullExecutionData,
workflowData: fullExecutionData.workflowData as IWorkflowDb,
data: parse(fullExecutionData.data),
};
returnData.finished = returnData.finished ? returnData.finished : false;
if (fullExecutionData.id) {
returnData.id = fullExecutionData.id;
}
return returnData;
}

View file

@ -205,7 +205,6 @@ import { copyPaste } from '@/mixins/copyPaste';
import { externalHooks } from '@/mixins/externalHooks';
import { genericHelpers } from '@/mixins/genericHelpers';
import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow';
import { restApi } from '@/mixins/restApi';
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
import useCanvasMouseSelect from '@/composables/useCanvasMouseSelect';
import { showMessage } from '@/mixins/showMessage';
@ -322,7 +321,6 @@ export default mixins(
externalHooks,
genericHelpers,
moveNodeWorkflow,
restApi,
showMessage,
workflowHelpers,
workflowRun,
@ -783,7 +781,7 @@ export default mixins(
this.resetWorkspace();
let data: IExecutionResponse | undefined;
try {
data = await this.restApi().getExecution(executionId);
data = await this.workflowsStore.getExecution(executionId);
} catch (error) {
this.$showError(error, this.$locale.baseText('nodeView.showError.openExecution.title'));
return;
@ -1403,14 +1401,14 @@ export default mixins(
try {
this.stopExecutionInProgress = true;
await this.restApi().stopCurrentExecution(executionId);
await this.workflowsStore.stopCurrentExecution(executionId);
this.$showMessage({
title: this.$locale.baseText('nodeView.showMessage.stopExecutionTry.title'),
type: 'success',
});
} catch (error) {
// Execution stop might fail when the execution has already finished. Let's treat this here.
const execution = await this.restApi().getExecution(executionId);
const execution = await this.workflowsStore.getExecution(executionId);
if (execution === undefined) {
// execution finished but was not saved (e.g. due to low connectivity)
@ -1476,7 +1474,7 @@ export default mixins(
async stopWaitingForWebhook() {
try {
await this.restApi().removeTestWebhook(this.workflowsStore.workflowId);
await this.workflowsStore.removeTestWebhook(this.workflowsStore.workflowId);
} catch (error) {
this.$showError(
error,
@ -1549,7 +1547,7 @@ export default mixins(
this.startLoading();
try {
workflowData = await this.restApi().getWorkflowFromUrl(url);
workflowData = await this.workflowsStore.getWorkflowFromUrl(url);
} catch (error) {
this.stopLoading();
this.$showError(
@ -2586,7 +2584,7 @@ export default mixins(
if (workflowId !== null) {
let workflow: IWorkflowDb | undefined = undefined;
try {
workflow = await this.restApi().getWorkflow(workflowId);
workflow = await this.workflowsStore.fetchWorkflow(workflowId);
} catch (error) {
this.$showError(error, this.$locale.baseText('openWorkflow.workflowNotFoundError'));
@ -3586,8 +3584,7 @@ export default mixins(
return Promise.resolve();
},
async loadActiveWorkflows(): Promise<void> {
const activeWorkflows = await this.restApi().getActiveWorkflows();
this.workflowsStore.activeWorkflows = activeWorkflows;
await this.workflowsStore.fetchActiveWorkflows();
},
async loadNodeTypes(): Promise<void> {
await this.nodeTypesStore.getNodeTypes();

View file

@ -14,16 +14,14 @@ import { showMessage } from '@/mixins/showMessage';
import mixins from 'vue-typed-mixins';
import { IFormBoxConfig } from '@/Interface';
import { VIEWS, ASSUMPTION_EXPERIMENT } from '@/constants';
import { restApi } from '@/mixins/restApi';
import { VIEWS } from '@/constants';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useSettingsStore } from '@/stores/settings';
import { useUsersStore } from '@/stores/users';
import { useCredentialsStore } from '@/stores/credentials';
import { usePostHog } from '@/stores/posthog';
export default mixins(showMessage, restApi).extend({
export default mixins(showMessage).extend({
name: 'SetupView',
components: {
AuthView,