refactor: Migrate genericHelpers mixin to composable (#8220)

## Summary
- Moved out canvas loading handling to canvas store
- Tag editable routes via meta to remove router dependency from generic
helpers
- Replace all occurrences of `genericHelpers` mixin with composable and
audit usage
- Moved out `isRedirectSafe` and `getRedirectQueryParameter` out of
genericHelpers to remove dependency on router

Removing the router dependency is important, because `useRouter` and
`useRoute` compostables are only available if called from component
instance. So if composable is nested within another composable, we
wouldn't be able to use these. In this case we'd always need to inject
the router and pass it through several composables. That's why I moved
the `readonly` logic to router meta and `isRedirectSafe` and
`getRedirectQueryParameter` out as they were only used in a single
component.

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
oleg 2024-01-05 12:23:28 +01:00 committed by GitHub
parent f53c482939
commit 184ed8e17d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 199 additions and 221 deletions

View file

@ -17,14 +17,13 @@ import { BREAKPOINT_SM, BREAKPOINT_MD, BREAKPOINT_LG, BREAKPOINT_XL } from '@/co
* xl >= 1920
*/
import { genericHelpers } from '@/mixins/genericHelpers';
import { debounceHelper } from '@/mixins/debounce';
import { useUIStore } from '@/stores/ui.store';
import { getBannerRowHeight } from '@/utils/htmlUtils';
export default defineComponent({
name: 'BreakpointsObserver',
mixins: [genericHelpers, debounceHelper],
mixins: [debounceHelper],
props: ['valueXS', 'valueXL', 'valueLG', 'valueMD', 'valueSM', 'valueDefault'],
data() {
return {

View file

@ -12,11 +12,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { genericHelpers } from '@/mixins/genericHelpers';
export default defineComponent({
name: 'Card',
mixins: [genericHelpers],
props: {
loading: {
type: Boolean,

View file

@ -6,11 +6,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { genericHelpers } from '@/mixins/genericHelpers';
export default defineComponent({
name: 'ExecutionTime',
mixins: [genericHelpers],
props: ['startTime'],
data() {
return {
@ -24,7 +22,7 @@ export default defineComponent({
return '...';
}
const msPassed = this.nowTime - new Date(this.startTime).getTime();
return this.displayTimer(msPassed);
return this.$locale.displayTimer(msPassed);
},
},
mounted() {
@ -46,9 +44,3 @@ export default defineComponent({
},
});
</script>
<style lang="scss">
// .data-display-wrapper {
// }
</style>

View file

@ -107,7 +107,7 @@
v-else-if="execution.stoppedAt !== null && execution.stoppedAt !== undefined"
>
{{
displayTimer(
i18n.displayTimer(
new Date(execution.stoppedAt).getTime() -
new Date(execution.startedAt).getTime(),
true,
@ -288,7 +288,6 @@ import { mapStores } from 'pinia';
import ExecutionTime from '@/components/ExecutionTime.vue';
import ExecutionFilter from '@/components/ExecutionFilter.vue';
import { MODAL_CONFIRM, VIEWS, WAIT_TIME_UNLIMITED } from '@/constants';
import { genericHelpers } from '@/mixins/genericHelpers';
import { executionHelpers } from '@/mixins/executionsHelpers';
import { useToast } from '@/composables/useToast';
import { useMessage } from '@/composables/useMessage';
@ -318,7 +317,7 @@ export default defineComponent({
ExecutionTime,
ExecutionFilter,
},
mixins: [genericHelpers, executionHelpers],
mixins: [executionHelpers],
props: {
autoRefreshEnabled: {
type: Boolean,

View file

@ -50,7 +50,7 @@
<ExpressionEditorModalInput
ref="inputFieldExpression"
:model-value="modelValue"
:is-read-only="isReadOnlyRoute"
:is-read-only="isReadOnly"
:path="path"
:class="{ 'ph-no-capture': redactValues }"
data-test-id="expression-modal-input"
@ -87,8 +87,6 @@ import VariableSelector from '@/components/VariableSelector.vue';
import type { IVariableItemSelected } from '@/Interface';
import { genericHelpers } from '@/mixins/genericHelpers';
import { EXPRESSIONS_DOCS_URL } from '@/constants';
import { debounceHelper } from '@/mixins/debounce';
@ -106,10 +104,40 @@ export default defineComponent({
ExpressionEditorModalOutput,
VariableSelector,
},
mixins: [genericHelpers, debounceHelper],
props: ['dialogVisible', 'parameter', 'path', 'modelValue', 'eventSource', 'redactValues'],
mixins: [debounceHelper],
props: {
dialogVisible: {
type: Boolean,
default: false,
},
parameter: {
type: Object,
default: () => ({}),
},
path: {
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
eventSource: {
type: String,
default: '',
},
redactValues: {
type: Boolean,
default: false,
},
isReadOnly: {
type: Boolean,
default: false,
},
},
setup() {
const externalHooks = useExternalHooks();
return {
externalHooks,
};

View file

@ -183,8 +183,8 @@ import type { IPermissions } from '@/permissions';
import { getWorkflowPermissions } from '@/permissions';
import { createEventBus } from 'n8n-design-system/utils';
import { nodeViewEventBus } from '@/event-bus';
import { genericHelpers } from '@/mixins/genericHelpers';
import { hasPermission } from '@/rbac/permissions';
import { useCanvasStore } from '@/stores/canvas.store';
const hasChanged = (prev: string[], curr: string[]) => {
if (prev.length !== curr.length) {
@ -208,7 +208,7 @@ export default defineComponent({
BreakpointsObserver,
CollaborationPane,
},
mixins: [workflowHelpers, genericHelpers],
mixins: [workflowHelpers],
props: {
readOnly: {
type: Boolean,
@ -244,6 +244,7 @@ export default defineComponent({
useWorkflowsStore,
useUsersStore,
useSourceControlStore,
useCanvasStore,
),
currentUser(): IUser | null {
return this.usersStore.currentUser;
@ -586,7 +587,7 @@ export default defineComponent({
break;
}
case WORKFLOW_MENU_ACTIONS.PUSH: {
this.startLoading();
this.canvasStore.startLoading();
try {
await this.onSaveButtonClick();
@ -610,7 +611,7 @@ export default defineComponent({
this.showError(error, this.$locale.baseText('error'));
}
} finally {
this.stopLoading();
this.canvasStore.stopLoading();
}
break;

View file

@ -104,11 +104,7 @@
import type { CloudPlanAndUsageData, IExecutionResponse, IMenuItem, IVersion } from '@/Interface';
import GiftNotificationIcon from './GiftNotificationIcon.vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import { useMessage } from '@/composables/useMessage';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { workflowRun } from '@/mixins/workflowRun';
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
import { userHelpers } from '@/mixins/userHelpers';
import { debounceHelper } from '@/mixins/debounce';
@ -135,15 +131,13 @@ export default defineComponent({
ExecutionsUsage,
MainSidebarSourceControl,
},
mixins: [genericHelpers, workflowHelpers, workflowRun, userHelpers, debounceHelper],
mixins: [userHelpers, debounceHelper],
setup(props, ctx) {
const externalHooks = useExternalHooks();
return {
externalHooks,
...useMessage(),
// eslint-disable-next-line @typescript-eslint/no-misused-promises
...workflowRun.setup?.(props, ctx),
};
},
data() {
@ -418,31 +412,31 @@ export default defineComponent({
async handleSelect(key: string) {
switch (key) {
case 'workflows': {
if (this.$router.currentRoute.name !== VIEWS.WORKFLOWS) {
if (this.$router.currentRoute.value.name !== VIEWS.WORKFLOWS) {
this.goToRoute({ name: VIEWS.WORKFLOWS });
}
break;
}
case 'templates': {
if (this.$router.currentRoute.name !== VIEWS.TEMPLATES) {
if (this.$router.currentRoute.value.name !== VIEWS.TEMPLATES) {
this.goToRoute({ name: VIEWS.TEMPLATES });
}
break;
}
case 'credentials': {
if (this.$router.currentRoute.name !== VIEWS.CREDENTIALS) {
if (this.$router.currentRoute.value.name !== VIEWS.CREDENTIALS) {
this.goToRoute({ name: VIEWS.CREDENTIALS });
}
break;
}
case 'variables': {
if (this.$router.currentRoute.name !== VIEWS.VARIABLES) {
if (this.$router.currentRoute.value.name !== VIEWS.VARIABLES) {
this.goToRoute({ name: VIEWS.VARIABLES });
}
break;
}
case 'executions': {
if (this.$router.currentRoute.name !== VIEWS.EXECUTIONS) {
if (this.$router.currentRoute.value.name !== VIEWS.EXECUTIONS) {
this.goToRoute({ name: VIEWS.EXECUTIONS });
}
break;
@ -451,7 +445,7 @@ export default defineComponent({
const defaultRoute = this.findFirstAccessibleSettingsRoute();
if (defaultRoute) {
const route = this.$router.resolve({ name: defaultRoute });
if (this.$router.currentRoute.name !== defaultRoute) {
if (this.$router.currentRoute.value.name !== defaultRoute) {
this.goToRoute(route.path);
}
}
@ -463,7 +457,7 @@ export default defineComponent({
break;
}
case 'cloud-admin': {
this.cloudPlanStore.redirectToDashboard();
void this.cloudPlanStore.redirectToDashboard();
break;
}
case 'quickstart':
@ -494,7 +488,7 @@ export default defineComponent({
let defaultSettingsRoute = null;
for (const route of settingsRoutes) {
if (this.canUserAccessRouteByName(route)) {
if (this.canUserAccessRouteByName(route.toString())) {
defaultSettingsRoute = route;
break;
}

View file

@ -15,7 +15,7 @@
color="text-dark"
data-test-id="credentials-label"
>
<div v-if="readonly || isReadOnlyRoute">
<div v-if="readonly">
<n8n-input
:model-value="getSelectedName(credentialTypeDescription.name)"
disabled
@ -117,7 +117,6 @@ import type {
INodeTypeDescription,
} from 'n8n-workflow';
import { genericHelpers } from '@/mixins/genericHelpers';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
@ -147,7 +146,6 @@ export default defineComponent({
components: {
TitledList,
},
mixins: [genericHelpers],
props: {
readonly: {
type: Boolean,

View file

@ -15,7 +15,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import NodeIcon from '@/components/NodeIcon.vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import type { ITemplatesNode } from '@/Interface';
import { filterTemplateNodes } from '@/utils/nodeTypesUtils';
@ -24,7 +23,6 @@ export default defineComponent({
components: {
NodeIcon,
},
mixins: [genericHelpers],
props: {
nodes: {
type: Array,

View file

@ -1,5 +1,6 @@
<template>
<RunData
ref="runData"
:node="node"
:run-index="runIndex"
:linked-runs="linkedRuns"
@ -10,7 +11,6 @@
:executing-message="$locale.baseText('ndv.output.executing')"
:session-id="sessionId"
:block-u-i="blockUI"
ref="runData"
:is-production-execution-preview="isProductionExecutionPreview"
:is-pane-active="isPaneActive"
pane-type="output"

View file

@ -42,8 +42,8 @@
/>
<ExpressionParameterInput
v-else-if="isValueExpression || forceShowExpression"
:model-value="expressionDisplayValue"
ref="inputField"
:model-value="expressionDisplayValue"
:title="displayTitle"
:is-read-only="isReadOnly"
:is-single-line="isSingleLine"

View file

@ -6,9 +6,9 @@
>
<ResourceLocatorDropdown
ref="dropdown"
v-on-click-outside="hideResourceDropdown"
:model-value="modelValue ? modelValue.value : ''"
:show="resourceDropdownVisible"
v-on-click-outside="hideResourceDropdown"
:filterable="isSearchable"
:filter-required="requiresSearchFilter"
:resources="currentQueryResults"

View file

@ -607,7 +607,6 @@ import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
import JsonEditor from '@/components/JsonEditor/JsonEditor.vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import type { PinDataSource } from '@/composables/usePinnedData';
import { usePinnedData } from '@/composables/usePinnedData';
import { dataPinningEventBus } from '@/event-bus';
@ -621,6 +620,7 @@ import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
import { isObject } from 'lodash-es';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useSourceControlStore } from '@/stores/sourceControl.store';
const RunDataTable = defineAsyncComponent(async () => import('@/components/RunDataTable.vue'));
const RunDataJson = defineAsyncComponent(async () => import('@/components/RunDataJson.vue'));
@ -644,7 +644,6 @@ export default defineComponent({
RunDataHtml,
RunDataSearch,
},
mixins: [genericHelpers],
props: {
node: {
type: Object as PropType<INodeUi>,
@ -759,7 +758,10 @@ export default defineComponent({
this.hidePinDataDiscoveryTooltip();
},
computed: {
...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore),
...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore, useSourceControlStore),
isReadOnlyRoute() {
return this.$route?.meta?.readOnlyCanvas === true;
},
activeNode(): INodeUi | null {
return this.ndvStore.activeNode;
},
@ -1402,7 +1404,7 @@ export default defineComponent({
}
},
async downloadJsonData() {
const fileName = this.node!.name.replace(/[^\w\d]/g, '_');
const fileName = this.node.name.replace(/[^\w\d]/g, '_');
const blob = new Blob([JSON.stringify(this.rawInputData, null, 2)], {
type: 'application/json',
});
@ -1413,7 +1415,7 @@ export default defineComponent({
this.binaryDataDisplayVisible = true;
this.binaryDataDisplayData = {
node: this.node!.name,
node: this.node.name,
runIndex: this.runIndex,
outputIndex: this.currentOutputIndex,
index,

View file

@ -41,7 +41,6 @@ import { mapStores, storeToRefs } from 'pinia';
import jp from 'jsonpath';
import type { INodeUi } from '@/Interface';
import type { IDataObject } from 'n8n-workflow';
import { genericHelpers } from '@/mixins/genericHelpers';
import { clearJsonKey, convertPath } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils';
import { useWorkflowsStore } from '@/stores/workflows.store';
@ -51,6 +50,8 @@ import { useToast } from '@/composables/useToast';
import { useI18n } from '@/composables/useI18n';
import { nonExistingJsonPath } from '@/constants';
import { useClipboard } from '@/composables/useClipboard';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { usePinnedData } from '@/composables/usePinnedData';
type JsonPathData = {
@ -60,7 +61,6 @@ type JsonPathData = {
export default defineComponent({
name: 'RunDataJsonActions',
mixins: [genericHelpers],
props: {
node: {
type: Object as PropType<INodeUi>,
@ -109,7 +109,10 @@ export default defineComponent({
};
},
computed: {
...mapStores(useNDVStore, useWorkflowsStore),
...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore, useSourceControlStore),
isReadOnlyRoute() {
return this.$route?.meta?.readOnlyCanvas === true;
},
activeNode(): INodeUi | null {
return this.ndvStore.activeNode;
},

View file

@ -48,7 +48,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import { filterTemplateNodes } from '@/utils/nodeTypesUtils';
import { abbreviateNumber } from '@/utils/typesUtils';
import NodeList from './NodeList.vue';
@ -60,7 +59,6 @@ export default defineComponent({
TimeAgo,
NodeList,
},
mixins: [genericHelpers],
props: {
lastItem: {
type: Boolean,

View file

@ -38,12 +38,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import type { ITemplatesCategory } from '@/Interface';
export default defineComponent({
name: 'TemplateFilters',
mixins: [genericHelpers],
props: {
sortOnPopulate: {
type: Boolean,

View file

@ -34,7 +34,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import TemplateCard from './TemplateCard.vue';
export default defineComponent({
@ -42,7 +41,6 @@ export default defineComponent({
components: {
TemplateCard,
},
mixins: [genericHelpers],
props: {
infiniteScrollEnabled: {
type: Boolean,

View file

@ -14,7 +14,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import Card from '@/components/CollectionWorkflowCard.vue';
import NodeList from '@/components/NodeList.vue';
@ -24,7 +23,6 @@ export default defineComponent({
Card,
NodeList,
},
mixins: [genericHelpers],
props: {
loading: {
type: Boolean,

View file

@ -44,8 +44,6 @@ import Card from '@/components/CollectionWorkflowCard.vue';
import TemplatesInfoCard from '@/components/TemplatesInfoCard.vue';
import { VueAgile } from 'vue-agile';
import { genericHelpers } from '@/mixins/genericHelpers';
type SliderRef = InstanceType<typeof VueAgile>;
export default defineComponent({
@ -55,7 +53,6 @@ export default defineComponent({
TemplatesInfoCard,
agile: VueAgile,
},
mixins: [genericHelpers],
props: {
collections: {
type: Array as PropType<ITemplatesCollection[]>,

View file

@ -22,7 +22,6 @@
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import { executionHelpers } from '@/mixins/executionsHelpers';
import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
@ -39,7 +38,7 @@ export default defineComponent({
name: 'WorkerList',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/naming-convention
components: { PushConnectionTracker, WorkerCard },
mixins: [pushConnection, genericHelpers, executionHelpers],
mixins: [pushConnection, executionHelpers],
props: {
autoRefreshEnabled: {
type: Boolean,

View file

@ -355,7 +355,6 @@
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { genericHelpers } from '@/mixins/genericHelpers';
import { useToast } from '@/composables/useToast';
import type {
ITimeoutHMS,
@ -383,13 +382,13 @@ import { createEventBus } from 'n8n-design-system/utils';
import type { IPermissions } from '@/permissions';
import { getWorkflowPermissions } from '@/permissions';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useSourceControlStore } from '@/stores/sourceControl.store';
export default defineComponent({
name: 'WorkflowSettings',
components: {
Modal,
},
mixins: [genericHelpers],
setup() {
const externalHooks = useExternalHooks();
@ -459,9 +458,13 @@ export default defineComponent({
useRootStore,
useUsersStore,
useSettingsStore,
useSourceControlStore,
useWorkflowsStore,
useWorkflowsEEStore,
),
readOnlyEnv(): boolean {
return this.sourceControlStore.preferences.branchReadOnly;
},
workflowName(): string {
return this.workflowsStore.workflowName;
},

View file

@ -5,18 +5,18 @@ import { BulkCommand, Command } from '@/models/history';
import { useHistoryStore } from '@/stores/history.store';
import { useUIStore } from '@/stores/ui.store';
import { onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue';
import { onMounted, onUnmounted, nextTick } from 'vue';
import { useDebounceHelper } from './useDebounce';
import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport';
import { getNodeViewTab } from '@/utils/canvasUtils';
import type { Route } from 'vue-router';
import { useTelemetry } from './useTelemetry';
const UNDO_REDO_DEBOUNCE_INTERVAL = 100;
const ELEMENT_UI_OVERLAY_SELECTOR = '.el-overlay';
export function useHistoryHelper(activeRoute: Route) {
const instance = getCurrentInstance();
const telemetry = instance?.proxy.$telemetry;
const telemetry = useTelemetry();
const ndvStore = useNDVStore();
const historyStore = useHistoryStore();
@ -85,9 +85,9 @@ export function useHistoryHelper(activeRoute: Route) {
function trackCommand(command: Undoable, type: 'undo' | 'redo'): void {
if (command instanceof Command) {
telemetry?.track(`User hit ${type}`, { commands_length: 1, commands: [command.name] });
telemetry.track(`User hit ${type}`, { commands_length: 1, commands: [command.name] });
} else if (command instanceof BulkCommand) {
telemetry?.track(`User hit ${type}`, {
telemetry.track(`User hit ${type}`, {
commands_length: command.commands.length,
commands: command.commands.map((c) => c.name),
});

View file

@ -1,4 +1,4 @@
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from '@/composables/useI18n';
import { ElLoading as Loading } from 'element-plus';
@ -36,8 +36,11 @@ export function useLoadingService() {
}
}
const isLoading = computed(() => loadingService.value !== null);
return {
loadingService,
isLoading,
startLoading,
setLoadingText,
stopLoading,

View file

@ -544,7 +544,6 @@ export function useNodeHelpers() {
return [];
}
// TODO: Is this problematic?
let data: ITaskDataConnections | undefined = taskData.data;
if (paneType === 'input' && taskData.inputOverride) {
data = taskData.inputOverride!;

View file

@ -458,6 +458,8 @@ export const enum VIEWS {
WORKER_VIEW = 'WorkerView',
}
export const EDITABLE_CANVAS_VIEWS = [VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW, VIEWS.EXECUTION_DEBUG];
export const enum FAKE_DOOR_FEATURES {
ENVIRONMENTS = 'environments',
LOGGING = 'logging',

View file

@ -2,9 +2,8 @@ import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { i18n as locale } from '@/plugins/i18n';
import { genericHelpers } from './genericHelpers';
import type { IExecutionsSummary } from 'n8n-workflow';
import { convertToDisplayDateComponents } from '@/utils/formatters/dateFormatter';
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
export interface IExecutionUIData {
name: string;
@ -14,7 +13,6 @@ export interface IExecutionUIData {
}
export const executionHelpers = defineComponent({
mixins: [genericHelpers],
computed: {
...mapStores(useWorkflowsStore),
executionId(): string {
@ -64,7 +62,7 @@ export const executionHelpers = defineComponent({
const stoppedAt = execution.stoppedAt
? new Date(execution.stoppedAt).getTime()
: Date.now();
status.runningTime = this.displayTimer(
status.runningTime = this.$locale.displayTimer(
stoppedAt - new Date(execution.startedAt).getTime(),
true,
);
@ -73,7 +71,7 @@ export const executionHelpers = defineComponent({
return status;
},
formatDate(fullDate: Date | string | number) {
const { date, time } = convertToDisplayDateComponents(fullDate);
const { date, time } = convertToDisplayDate(fullDate);
return locale.baseText('executionsList.started', { interpolate: { time, date } });
},
},

View file

@ -1,89 +0,0 @@
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { VIEWS } from '@/constants';
import { useToast } from '@/composables/useToast';
import { useSourceControlStore } from '@/stores/sourceControl.store';
export const genericHelpers = defineComponent({
setup() {
return {
...useToast(),
};
},
data() {
return {
loadingService: null as null | { close: () => void; text: string },
};
},
computed: {
...mapStores(useSourceControlStore),
isReadOnlyRoute(): boolean {
return ![
VIEWS.WORKFLOW,
VIEWS.NEW_WORKFLOW,
VIEWS.LOG_STREAMING_SETTINGS,
VIEWS.EXECUTION_DEBUG,
].includes(this.$route.name as VIEWS);
},
readOnlyEnv(): boolean {
return this.sourceControlStore.preferences.branchReadOnly;
},
},
methods: {
displayTimer(msPassed: number, showMs = false): string {
if (msPassed < 60000) {
if (!showMs) {
return `${Math.floor(msPassed / 1000)}${this.$locale.baseText(
'genericHelpers.secShort',
)}`;
}
return `${msPassed / 1000}${this.$locale.baseText('genericHelpers.secShort')}`;
}
const secondsPassed = Math.floor(msPassed / 1000);
const minutesPassed = Math.floor(secondsPassed / 60);
const secondsLeft = (secondsPassed - minutesPassed * 60).toString().padStart(2, '0');
return `${minutesPassed}:${secondsLeft}${this.$locale.baseText('genericHelpers.minShort')}`;
},
/**
* @note Loading helpers extracted as composable in useLoadingService
*/
startLoading(text?: string) {
if (this.loadingService !== null) {
return;
}
this.loadingService = this.$loading({
lock: true,
text: text || this.$locale.baseText('genericHelpers.loading'),
background: 'var(--color-dialog-overlay-background)',
});
},
setLoadingText(text: string) {
if (this.loadingService !== null) {
this.loadingService.text = text;
}
},
stopLoading() {
if (this.loadingService !== null) {
this.loadingService.close();
this.loadingService = null;
}
},
isRedirectSafe() {
const redirect = this.getRedirectQueryParameter();
return redirect.startsWith('/');
},
getRedirectQueryParameter() {
let redirect = '';
if (typeof this.$route.query.redirect === 'string') {
redirect = decodeURIComponent(this.$route.query.redirect);
}
return redirect;
},
},
});

View file

@ -48,7 +48,6 @@ import type {
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { genericHelpers } from '@/mixins/genericHelpers';
import { get, isEqual } from 'lodash-es';
@ -68,6 +67,8 @@ import { v4 as uuid } from 'uuid';
import { useSettingsStore } from '@/stores/settings.store';
import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useCanvasStore } from '@/stores/canvas.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
export function getParentMainInputNode(workflow: Workflow, node: INode): INode {
const nodeType = useNodeTypesStore().getNodeType(node.type);
@ -475,9 +476,9 @@ export function executeData(
}
export const workflowHelpers = defineComponent({
mixins: [genericHelpers],
setup() {
const nodeHelpers = useNodeHelpers();
return {
...useToast(),
...useMessage(),
@ -887,12 +888,13 @@ export const workflowHelpers = defineComponent({
redirect = true,
forceSave = false,
): Promise<boolean> {
if (this.readOnlyEnv) {
return;
const readOnlyEnv = useSourceControlStore().preferences.branchReadOnly;
if (readOnlyEnv) {
return false;
}
const isLoading = useCanvasStore().isLoading;
const currentWorkflow = id || this.$route.params.name;
const isLoading = this.loadingService !== null;
if (!currentWorkflow || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(currentWorkflow)) {
return this.saveAsNewWorkflow({ name, tags }, redirect);

View file

@ -80,6 +80,22 @@ export class I18nClass {
return this.i18n.te(key) ? this.i18n.t(key).toString() : fallback ?? '';
}
displayTimer(msPassed: number, showMs = false): string {
if (msPassed < 60000) {
if (!showMs) {
return `${Math.floor(msPassed / 1000)}${this.baseText('genericHelpers.secShort')}`;
}
return `${msPassed / 1000}${this.baseText('genericHelpers.secShort')}`;
}
const secondsPassed = Math.floor(msPassed / 1000);
const minutesPassed = Math.floor(secondsPassed / 60);
const secondsLeft = (secondsPassed - minutesPassed * 60).toString().padStart(2, '0');
return `${minutesPassed}:${secondsLeft}${this.baseText('genericHelpers.minShort')}`;
}
/**
* Render a string of header text (a node's name and description),
* used variously in the nodes panel, under the node icon, etc.

View file

@ -13,7 +13,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useTemplatesStore } from '@/stores/templates.store';
import { useUIStore } from '@/stores/ui.store';
import { useSSOStore } from '@/stores/sso.store';
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
import { EnterpriseEditionFeature, VIEWS, EDITABLE_CANVAS_VIEWS } from '@/constants';
import { useTelemetry } from '@/composables/useTelemetry';
import { middleware } from '@/rbac/middleware';
import type { RouteConfig, RouterMiddleware } from '@/types/router';
@ -755,6 +755,19 @@ export const routes = [
},
] as Array<RouteRecordRaw & RouteConfig>;
function withCanvasReadOnlyMeta(route: RouteRecordRaw) {
if (!route.meta) {
route.meta = {};
}
route.meta.readOnlyCanvas = !EDITABLE_CANVAS_VIEWS.includes((route?.name ?? '') as VIEWS);
if (route.children) {
route.children = route.children.map(withCanvasReadOnlyMeta);
}
return route;
}
const router = createRouter({
history: createWebHistory(import.meta.env.DEV ? '/' : window.BASE_PATH ?? '/'),
scrollBehavior(to: RouteLocationNormalized & RouteConfig, from, savedPosition) {
@ -764,7 +777,7 @@ const router = createRouter({
to.meta.setScrollPosition(0);
}
},
routes,
routes: routes.map(withCanvasReadOnlyMeta),
});
router.beforeEach(async (to: RouteLocationNormalized & RouteConfig, from, next) => {

View file

@ -40,6 +40,7 @@ import {
SIDEBAR_WIDTH_EXPANDED,
} from '@/utils/nodeViewUtils';
import type { PointXY } from '@jsplumb/util';
import { useLoadingService } from '@/composables/useLoadingService';
export const useCanvasStore = defineStore('canvas', () => {
const workflowStore = useWorkflowsStore();
@ -47,6 +48,7 @@ export const useCanvasStore = defineStore('canvas', () => {
const uiStore = useUIStore();
const historyStore = useHistoryStore();
const sourceControlStore = useSourceControlStore();
const loadingService = useLoadingService();
const jsPlumbInstanceRef = ref<BrowserJsPlumbInstance>();
const isDragging = ref<boolean>(false);
@ -295,6 +297,10 @@ export const useCanvasStore = defineStore('canvas', () => {
lastSelectedConnection,
newNodeInsertPosition,
jsPlumbInstance,
isLoading: loadingService.isLoading,
startLoading: loadingService.startLoading,
setLoadingText: loadingService.setLoadingText,
stopLoading: loadingService.stopLoading,
setRecenteredCanvasAddButtonPosition,
getNodesWithPlaceholderNode,
canvasPositionFromPagePosition,

View file

@ -46,6 +46,7 @@ export interface RouteConfig {
};
scrollOffset?: number;
setScrollPosition?: (position: number) => void;
readOnlyCanvas?: boolean;
};
}

View file

@ -10,3 +10,15 @@ export const convertToDisplayDateComponents = (
const [date, time] = formattedDate.split('#');
return { date, time };
};
export function convertToDisplayDate(fullDate: Date | string | number): {
date: string;
time: string;
} {
const mask = `d mmm${
new Date(fullDate).getFullYear() === new Date().getFullYear() ? '' : ', yyyy'
}#HH:MM:ss`;
const formattedDate = dateformat(fullDate, mask);
const [date, time] = formattedDate.split('#');
return { date, time };
}

View file

@ -69,7 +69,6 @@
</template>
<script lang="ts">
import { genericHelpers } from '@/mixins/genericHelpers';
import type { IFormInputs } from '@/Interface';
import Logo from '../components/Logo.vue';
import {
@ -92,7 +91,6 @@ export default defineComponent({
components: {
Logo,
},
mixins: [genericHelpers],
props: {
reportError: Boolean,
},

View file

@ -49,18 +49,18 @@
:name="nodeData.name"
:is-read-only="isReadOnlyRoute || readOnlyEnv"
:instance="instance"
@deselectAllNodes="deselectAllNodes"
:is-active="!!activeNode && activeNode.name === nodeData.name"
@deselectNode="nodeDeselectedByName"
:hide-actions="pullConnActive"
@nodeSelected="nodeSelectedByName"
:is-production-execution-preview="isProductionExecutionPreview"
@runWorkflow="onRunNode"
:workflow="currentWorkflowObject"
@moved="onNodeMoved"
:disable-pointer-events="!canOpenNDV"
@run="onNodeRun"
:hide-node-issues="hideNodeIssues"
@deselectAllNodes="deselectAllNodes"
@deselectNode="nodeDeselectedByName"
@nodeSelected="nodeSelectedByName"
@runWorkflow="onRunNode"
@moved="onNodeMoved"
@run="onNodeRun"
>
<template #custom-tooltip>
<span
@ -77,10 +77,10 @@
:instance="instance"
:is-active="!!activeNode && activeNode.name === stickyData.name"
:node-view-scale="nodeViewScale"
@deselectAllNodes="deselectAllNodes"
:grid-size="GRID_SIZE"
@deselectNode="nodeDeselectedByName"
:hide-actions="pullConnActive"
@deselectAllNodes="deselectAllNodes"
@deselectNode="nodeDeselectedByName"
@nodeSelected="nodeSelectedByName"
@removeNode="(name) => removeNode(name, true)"
/>
@ -239,7 +239,6 @@ import {
UPDATE_WEBHOOK_ID_NODE_TYPES,
TIME,
} from '@/constants';
import { genericHelpers } from '@/mixins/genericHelpers';
import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow';
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
@ -372,6 +371,7 @@ import { useViewStacks } from '@/components/Node/NodeCreator/composables/useView
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useClipboard } from '@/composables/useClipboard';
import { usePinnedData } from '@/composables/usePinnedData';
import { useSourceControlStore } from '@/stores/sourceControl.store';
interface AddNodeOptions {
position?: XYPosition;
@ -394,7 +394,7 @@ export default defineComponent({
CanvasControls,
ContextMenu,
},
mixins: [genericHelpers, moveNodeWorkflow, workflowHelpers, workflowRun, debounceHelper],
mixins: [moveNodeWorkflow, workflowHelpers, workflowRun, debounceHelper],
async beforeRouteLeave(to, from, next) {
if (
getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS ||
@ -504,14 +504,14 @@ export default defineComponent({
// When entering this tab:
if (currentTab === MAIN_HEADER_TABS.WORKFLOW || isOpeningTemplate) {
if (workflowChanged || nodeViewNotInitialized || isOpeningTemplate) {
this.startLoading();
this.canvasStore.startLoading();
if (nodeViewNotInitialized) {
const previousDirtyState = this.uiStore.stateIsDirty;
this.resetWorkspace();
this.uiStore.stateIsDirty = previousDirtyState;
}
await Promise.all([this.loadCredentials(), this.initView()]);
this.stopLoading();
this.canvasStore.stopLoading();
if (this.blankRedirect) {
this.blankRedirect = false;
}
@ -570,6 +570,7 @@ export default defineComponent({
useExternalSecretsStore,
useCollaborationStore,
usePushConnectionStore,
useSourceControlStore,
),
nativelyNumberSuffixedDefaults(): string[] {
return this.nodeTypesStore.nativelyNumberSuffixedDefaults;
@ -587,9 +588,7 @@ export default defineComponent({
return this.$route.name === VIEWS.DEMO;
},
showCanvasAddButton(): boolean {
return (
this.loadingService === null && !this.containsTrigger && !this.isDemo && !this.readOnlyEnv
);
return !this.isLoading && !this.containsTrigger && !this.isDemo && !this.readOnlyEnv;
},
lastSelectedNode(): INodeUi | null {
return this.uiStore.getLastSelectedNode;
@ -702,11 +701,17 @@ export default defineComponent({
return this.canvasStore.jsPlumbInstance;
},
isLoading(): boolean {
return this.loadingService !== null;
return this.canvasStore.isLoading;
},
currentWorkflowObject(): Workflow {
return this.workflowsStore.getCurrentWorkflow();
},
readOnlyEnv(): boolean {
return this.sourceControlStore.preferences.branchReadOnly;
},
isReadOnlyRoute() {
return this.$route?.meta?.readOnlyCanvas === true;
},
},
data() {
return {
@ -756,7 +761,7 @@ export default defineComponent({
this.clipboard.onPaste.value = this.onClipboardPasteEvent;
this.startLoading();
this.canvasStore.startLoading();
const loadPromises = [
this.loadActiveWorkflows(),
this.loadCredentials(),
@ -799,7 +804,7 @@ export default defineComponent({
this.$locale.baseText('nodeView.showError.mounted2.message') + ':',
);
}
this.stopLoading();
this.canvasStore.stopLoading();
setTimeout(() => {
void this.usersStore.showPersonalizationSurvey();
@ -1140,7 +1145,7 @@ export default defineComponent({
this.onToggleNodeCreator({ source, createNodeActive: true });
},
async openExecution(executionId: string) {
this.startLoading();
this.canvasStore.startLoading();
this.resetWorkspace();
let data: IExecutionResponse | undefined;
try {
@ -1239,7 +1244,7 @@ export default defineComponent({
duration: 0,
});
}
this.stopLoading();
this.canvasStore.stopLoading();
},
async importWorkflowExact(data: { workflow: IWorkflowDataUpdate }) {
if (!data.workflow.nodes || !data.workflow.connections) {
@ -1257,8 +1262,8 @@ export default defineComponent({
this.canvasStore.zoomToFit();
},
async openWorkflowTemplate(templateId: string) {
this.startLoading();
this.setLoadingText(this.$locale.baseText('nodeView.loadingTemplate'));
this.canvasStore.startLoading();
this.canvasStore.setLoadingText(this.$locale.baseText('nodeView.loadingTemplate'));
this.resetWorkspace();
this.workflowsStore.currentWorkflowExecutions = [];
@ -1297,10 +1302,10 @@ export default defineComponent({
templateName: data.name,
workflow: data.workflow,
});
this.stopLoading();
this.canvasStore.stopLoading();
},
async openWorkflow(workflow: IWorkflowDb) {
this.startLoading();
this.canvasStore.startLoading();
const selectedExecution = this.workflowsStore.activeWorkflowExecution;
@ -1354,7 +1359,7 @@ export default defineComponent({
} else {
this.workflowsStore.activeWorkflowExecution = selectedExecution;
}
this.stopLoading();
this.canvasStore.stopLoading();
this.collaborationStore.notifyWorkflowOpened(workflow.id);
},
touchTap(e: MouseEvent | TouchEvent) {
@ -1997,18 +2002,18 @@ export default defineComponent({
async getWorkflowDataFromUrl(url: string): Promise<IWorkflowDataUpdate | undefined> {
let workflowData: IWorkflowDataUpdate;
this.startLoading();
this.canvasStore.startLoading();
try {
workflowData = await this.workflowsStore.getWorkflowFromUrl(url);
} catch (error) {
this.stopLoading();
this.canvasStore.stopLoading();
this.showError(
error,
this.$locale.baseText('nodeView.showError.getWorkflowDataFromUrl.title'),
);
return;
}
this.stopLoading();
this.canvasStore.stopLoading();
return workflowData;
},
@ -3423,7 +3428,7 @@ export default defineComponent({
e.returnValue = true; //Gecko + IE
return true; //Gecko + Webkit, Safari, Chrome etc.
} else {
this.startLoading(this.$locale.baseText('nodeView.redirecting'));
this.canvasStore.startLoading(this.$locale.baseText('nodeView.redirecting'));
this.collaborationStore.notifyWorkflowClosed(this.workflowsStore.workflowId);
return;
}
@ -3434,7 +3439,7 @@ export default defineComponent({
clearTimeout(this.unloadTimeout);
},
async newWorkflow(): Promise<void> {
this.startLoading();
this.canvasStore.startLoading();
this.resetWorkspace();
this.workflowData = await this.workflowsStore.getNewWorkflowData();
this.workflowsStore.currentWorkflowExecutions = [];
@ -3446,7 +3451,7 @@ export default defineComponent({
this.uiStore.nodeViewInitialized = true;
this.historyStore.reset();
this.workflowsStore.activeWorkflowExecution = null;
this.stopLoading();
this.canvasStore.stopLoading();
},
async tryToAddWelcomeSticky(): Promise<void> {
this.canvasStore.zoomToFit();
@ -4574,9 +4579,9 @@ export default defineComponent({
if (nodesToBeFetched.length > 0) {
// Only call API if node information is actually missing
this.startLoading();
this.canvasStore.startLoading();
await this.nodeTypesStore.getNodesInformation(nodesToBeFetched);
this.stopLoading();
this.canvasStore.stopLoading();
}
},
async onPostMessageReceived(message: MessageEvent) {
@ -4829,7 +4834,7 @@ export default defineComponent({
onPageShow(e: PageTransitionEvent) {
// Page was restored from the bfcache (back-forward cache)
if (e.persisted) {
this.stopLoading();
this.canvasStore.stopLoading();
}
},
readOnlyEnvRouteCheck() {

View file

@ -30,7 +30,6 @@ import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useUIStore } from '@/stores/ui.store';
import { genericHelpers } from '@/mixins/genericHelpers';
export default defineComponent({
name: 'SigninView',
@ -38,7 +37,6 @@ export default defineComponent({
AuthView,
MfaView,
},
mixins: [genericHelpers],
setup() {
return {
...useToast(),
@ -116,6 +114,17 @@ export default defineComponent({
async onEmailPasswordSubmitted(form: { email: string; password: string }) {
await this.login(form);
},
isRedirectSafe() {
const redirect = this.getRedirectQueryParameter();
return redirect.startsWith('/');
},
getRedirectQueryParameter() {
let redirect = '';
if (typeof this.$route.query?.redirect === 'string') {
redirect = decodeURIComponent(this.$route.query?.redirect);
}
return redirect;
},
async login(form: { email: string; password: string; token?: string; recoveryCode?: string }) {
try {
this.loading = true;

View file

@ -84,7 +84,6 @@ import TemplateFilters from '@/components/TemplateFilters.vue';
import TemplateList from '@/components/TemplateList.vue';
import TemplatesView from '@/views/TemplatesView.vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import type {
ITemplatesCollection,
ITemplatesWorkflow,
@ -118,7 +117,7 @@ export default defineComponent({
TemplateList,
TemplatesView,
},
mixins: [genericHelpers, debounceHelper],
mixins: [debounceHelper],
setup() {
return {
...useToast(),

View file

@ -151,7 +151,6 @@ import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { genericHelpers } from '@/mixins/genericHelpers';
import { useTagsStore } from '@/stores/tags.store';
type IResourcesListLayoutInstance = InstanceType<typeof ResourcesListLayout>;
@ -171,7 +170,6 @@ const WorkflowsView = defineComponent({
SuggestedTemplatesPage,
SuggestedTemplatesSection,
},
mixins: [genericHelpers],
data() {
return {
filters: {
@ -194,6 +192,9 @@ const WorkflowsView = defineComponent({
useSourceControlStore,
useTagsStore,
),
readOnlyEnv(): boolean {
return this.sourceControlStore.preferences.branchReadOnly;
},
currentUser(): IUser {
return this.usersStore.currentUser || ({} as IUser);
},