fix(editor): Fix broken types for globally defined components (no-changelog) (#16505)

Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
Alex Grozav
2025-06-24 14:01:23 +03:00
committed by GitHub
parent 21ff173070
commit 20c63436d2
150 changed files with 1332 additions and 960 deletions

View File

@@ -76,7 +76,7 @@ const getExpirationTime = (apiKey: ApiKey): string => {
<template #append>
<div ref="cardActions" :class="$style.cardActions">
<n8n-action-toggle :actions="ACTION_LIST" theme="dark" @action="onAction" />
<N8nActionToggle :actions="ACTION_LIST" theme="dark" @action="onAction" />
</div>
</template>
</n8n-card>

View File

@@ -6,7 +6,7 @@ import Modal from '@/components/Modal.vue';
import { useUsersStore } from '@/stores/users.store';
import { createFormEventBus } from '@n8n/design-system/utils';
import { createEventBus } from '@n8n/utils/event-bus';
import type { IFormInputs, IFormInput } from '@/Interface';
import type { IFormInputs, IFormInput, FormFieldValueUpdate, FormValues } from '@/Interface';
import { useI18n } from '@n8n/i18n';
const config = ref<IFormInputs | null>(null);
@@ -33,17 +33,14 @@ const passwordsMatch = (value: string | number | boolean | null | undefined) =>
return false;
};
const onInput = (e: { name: string; value: string }) => {
if (e.name === 'password') {
const onInput = (e: FormFieldValueUpdate) => {
if (e.name === 'password' && typeof e.value === 'string') {
password.value = e.value;
}
};
const onSubmit = async (values: {
currentPassword: string;
password: string;
mfaCode?: string;
}) => {
const onSubmit = async (data: FormValues) => {
const values = data as { currentPassword: string; password: string; mfaCode?: string };
try {
loading.value = true;
await usersStore.updateCurrentUserPassword({
@@ -143,6 +140,7 @@ onMounted(() => {
>
<template #content>
<n8n-form-inputs
v-if="config"
:inputs="config"
:event-bus="formBus"
:column-view="true"

View File

@@ -1,10 +1,11 @@
<script lang="ts" setup>
import { useUIStore } from '@/stores/ui.store';
import type { PublicInstalledPackage } from 'n8n-workflow';
import type { IUser, PublicInstalledPackage } from 'n8n-workflow';
import { NPM_PACKAGE_DOCS_BASE_URL, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '@/constants';
import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useSettingsStore } from '@/stores/settings.store';
import type { UserAction } from '@n8n/design-system';
interface Props {
communityPackage?: PublicInstalledPackage | null;
@@ -22,7 +23,7 @@ const i18n = useI18n();
const telemetry = useTelemetry();
const settingsStore = useSettingsStore();
const packageActions = [
const packageActions: Array<UserAction<IUser>> = [
{
label: i18n.baseText('settings.communityNodes.viewDocsAction.label'),
value: COMMUNITY_PACKAGE_MANAGE_ACTIONS.VIEW_DOCS,

View File

@@ -12,7 +12,7 @@ import { useProjectsStore } from '@/stores/projects.store';
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
import { useI18n } from '@n8n/i18n';
import { ResourceType } from '@/utils/projects.utils';
import type { CredentialsResource } from './layouts/ResourcesListLayout.vue';
import type { CredentialsResource } from '@/Interface';
const CREDENTIAL_LIST_ITEM_ACTIONS = {
OPEN: 'open',

View File

@@ -4,6 +4,7 @@ import type { BaseTextKey } from '@n8n/i18n';
import type { TestTableColumn } from '@/components/Evaluations.ee/shared/TestTableBase.vue';
import { useI18n } from '@n8n/i18n';
import { useRouter } from 'vue-router';
import type { BadgeTheme } from '@n8n/design-system';
defineProps<{
column: TestTableColumn<T>;
@@ -39,7 +40,7 @@ const errorTooltipMap: Record<string, BaseTextKey> = {
};
// FIXME: move status logic to a parent component
const statusThemeMap: Record<string, string> = {
const statusThemeMap: Record<string, BadgeTheme> = {
new: 'default',
running: 'warning',
evaluation_running: 'warning',

View File

@@ -206,7 +206,7 @@ defineExpose({ focus, select });
outline
type="tertiary"
icon="external-link-alt"
size="xsmall"
size="mini"
:class="$style['expression-editor-modal-opener']"
data-test-id="expander"
@click="emit('modal-opener-click')"

View File

@@ -9,6 +9,7 @@ import { mockedStore } from '@/__tests__/utils';
import { useProjectsStore } from '@/stores/projects.store';
import { ProjectTypes, type Project } from '@/types/projects.types';
import { useFoldersStore } from '@/stores/folders.store';
import type { IUser } from 'n8n-workflow';
vi.mock('vue-router', async (importOriginal) => ({
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
@@ -41,7 +42,7 @@ const TEST_FOLDER_CHILD: FolderShortInfo = {
parentFolder: TEST_FOLDER.id,
};
const TEST_ACTIONS: UserAction[] = [
const TEST_ACTIONS: Array<UserAction<IUser>> = [
{ label: 'Action 1', value: 'action1', disabled: false },
{ label: 'Action 2', value: 'action2', disabled: true },
];

View File

@@ -7,11 +7,12 @@ import { type PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Brea
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { useFoldersStore } from '@/stores/folders.store';
import type { FolderPathItem, FolderShortInfo } from '@/Interface';
import type { IUser } from 'n8n-workflow';
type Props = {
// Current folder can be null when showing breadcrumbs for workflows in project root
currentFolder?: FolderShortInfo | null;
actions?: UserAction[];
actions?: Array<UserAction<IUser>>;
hiddenItemsTrigger?: 'hover' | 'click';
currentFolderAsLink?: boolean;
visibleLevels?: 1 | 2;

View File

@@ -2,8 +2,8 @@ import { createComponentRenderer } from '@/__tests__/render';
import userEvent from '@testing-library/user-event';
import FolderCard from './FolderCard.vue';
import { createPinia, setActivePinia } from 'pinia';
import type { FolderResource } from '../layouts/ResourcesListLayout.vue';
import type { FolderPathItem, UserAction } from '@/Interface';
import type { FolderResource, FolderPathItem, UserAction } from '@/Interface';
import type { IUser } from 'n8n-workflow';
vi.mock('vue-router', () => {
const push = vi.fn();
@@ -54,7 +54,7 @@ const renderComponent = createComponentRenderer(FolderCard, {
actions: [
{ label: 'Open', value: 'open', disabled: false },
{ label: 'Delete', value: 'delete', disabled: false },
] as const satisfies UserAction[],
] as const satisfies Array<UserAction<IUser>>,
breadcrumbs: DEFAULT_BREADCRUMBS,
},
global: {

View File

@@ -1,20 +1,20 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { FOLDER_LIST_ITEM_ACTIONS } from './constants';
import type { FolderResource } from '../layouts/ResourcesListLayout.vue';
import { ProjectTypes, type Project } from '@/types/projects.types';
import { useI18n } from '@n8n/i18n';
import { useRoute, useRouter } from 'vue-router';
import { VIEWS } from '@/constants';
import type { UserAction } from '@/Interface';
import type { FolderResource, UserAction } from '@/Interface';
import { ResourceType } from '@/utils/projects.utils';
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
import { useFoldersStore } from '@/stores/folders.store';
import { type IUser } from 'n8n-workflow';
type Props = {
data: FolderResource;
personalProject: Project | null;
actions: UserAction[];
actions: Array<UserAction<IUser>>;
readOnly?: boolean;
showOwnershipBadge?: boolean;
};
@@ -36,6 +36,7 @@ const emit = defineEmits<{
}>();
const hiddenBreadcrumbsItemsAsync = ref<Promise<PathItem[]>>(new Promise(() => {}));
const cachedHiddenBreadcrumbsItems = ref<PathItem[]>([]);
const resourceTypeLabel = computed(() => i18n.baseText('generic.folder').toLowerCase());

View File

@@ -118,7 +118,7 @@ const onClaimCreditsClicked = async () => {
})
}}</n8n-text
>&nbsp;
<n8n-text size="small" bold="true">
<n8n-text size="small" :bold="true">
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}</n8n-text
>
</n8n-callout>

View File

@@ -11,7 +11,7 @@ import {
NodeConnectionTypes,
traverseNodeParameters,
} from 'n8n-workflow';
import type { IFormInput } from '@n8n/design-system';
import type { FormFieldValueUpdate, IFormInput } from '@n8n/design-system';
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry';
@@ -249,9 +249,11 @@ const onExecute = async () => {
};
// Add handler for tool selection change
const onUpdate = (change: { name: string; value: string }) => {
const onUpdate = (change: FormFieldValueUpdate) => {
if (change.name !== 'toolName') return;
selectedTool.value = change.value;
if (typeof change.value === 'string') {
selectedTool.value = change.value;
}
};
</script>

View File

@@ -2,17 +2,18 @@
import { nextTick, ref } from 'vue';
import { useToast } from '@/composables/useToast';
import { onClickOutside } from '@vueuse/core';
import type { InputType } from '@n8n/design-system';
interface Props {
modelValue: string;
subtitle?: string;
type: string;
type: InputType;
readonly?: boolean;
placeholder?: string;
maxlength?: number;
required?: boolean;
autosize?: boolean | { minRows: number; maxRows: number };
inputType?: string;
inputType?: InputType;
maxHeight?: string;
}
const props = withDefaults(defineProps<Props>(), {

View File

@@ -2,7 +2,13 @@
import { computed, onMounted, ref } from 'vue';
import { useToast } from '@/composables/useToast';
import Modal from './Modal.vue';
import type { IFormInputs, IInviteResponse, IUser, InvitableRoleName } from '@/Interface';
import type {
FormFieldValueUpdate,
IFormInputs,
IInviteResponse,
IUser,
InvitableRoleName,
} from '@/Interface';
import { EnterpriseEditionFeature, VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
import { ROLE } from '@n8n/api-types';
import { useUsersStore } from '@/stores/users.store';
@@ -127,11 +133,15 @@ const validateEmails = (value: string | number | boolean | null | undefined) =>
return false;
};
function onInput(e: { name: string; value: InvitableRoleName }) {
if (e.name === 'emails') {
function isInvitableRoleName(val: unknown): val is InvitableRoleName {
return typeof val === 'string' && [ROLE.Member, ROLE.Admin].includes(val as InvitableRoleName);
}
function onInput(e: FormFieldValueUpdate) {
if (e.name === 'emails' && typeof e.value === 'string') {
emails.value = e.value;
}
if (e.name === 'role') {
if (e.name === 'role' && isInvitableRoleName(e.value)) {
role.value = e.value;
}
}
@@ -312,7 +322,7 @@ function getEmail(email: string): string {
</n8n-users-list>
</div>
<n8n-form-inputs
v-else
v-else-if="config"
:inputs="config"
:event-bus="formBus"
:column-view="true"

View File

@@ -16,8 +16,8 @@ const emit = defineEmits<{
'update:modelValue': [tab: MAIN_HEADER_TABS, event: MouseEvent];
}>();
function onUpdateModelValue(tab: MAIN_HEADER_TABS, event: MouseEvent): void {
emit('update:modelValue', tab, event);
function onUpdateModelValue(tab: string, event: MouseEvent): void {
emit('update:modelValue', tab as MAIN_HEADER_TABS, event);
}
</script>

View File

@@ -428,7 +428,8 @@ async function handleFileImport(): Promise<void> {
}
}
async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void> {
async function onWorkflowMenuSelect(value: string): Promise<void> {
const action = value as WORKFLOW_MENU_ACTIONS;
switch (action) {
case WORKFLOW_MENU_ACTIONS.DUPLICATE: {
uiStore.openModalWithData({

View File

@@ -25,6 +25,7 @@ import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper
import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation';
import { N8nNavigationDropdown, N8nTooltip, N8nLink, N8nIconButton } from '@n8n/design-system';
import type { IMenuItem } from '@n8n/design-system';
import { onClickOutside, type VueInstance } from '@vueuse/core';
import Logo from './Logo/Logo.vue';
@@ -67,7 +68,7 @@ const userMenuItems = ref([
},
]);
const mainMenuItems = computed(() => [
const mainMenuItems = computed<IMenuItem[]>(() => [
{
id: 'cloud-admin',
position: 'bottom',

View File

@@ -8,7 +8,7 @@ import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '
import { useNDVStore } from '@/stores/ndv.store';
import { ndvEventBus } from '@/event-bus';
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
import type { MainPanelType, XYPosition } from '@/Interface';
import type { Direction, MainPanelType, XYPosition } from '@/Interface';
import { ref, onMounted, onBeforeUnmount, computed, watch, nextTick } from 'vue';
import { useUIStore } from '@/stores/ui.store';
import { useThrottleFn } from '@vueuse/core';
@@ -151,8 +151,8 @@ const outputPanelRelativeTranslate = computed((): number => {
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
});
const supportedResizeDirections = computed((): string[] => {
const supportedDirections = ['right'];
const supportedResizeDirections = computed((): Direction[] => {
const supportedDirections = ['right' as Direction];
if (props.isDraggable) supportedDirections.push('left');
return supportedDirections;

View File

@@ -99,10 +99,11 @@ describe('NDVSubConnections', () => {
<div class="connectionType"><span class="connectionLabel">Tools</span>
<div>
<div class="connectedNodesWrapper" style="--nodes-length: 0;">
<div class="plusButton">
<n8n-tooltip placement="top" teleported="true" offset="10" show-after="300" disabled="false">
<n8n-icon-button size="medium" icon="plus" type="tertiary" data-test-id="add-subnode-ai_tool-0"></n8n-icon-button>
</n8n-tooltip>
<div class="plusButton"><button class="button button tertiary medium withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="add-subnode-ai_tool-0"><span class="icon"><span class="n8n-text compact size-medium regular n8n-icon n8n-icon"><!----></span></span>
<!--v-if-->
</button>
<!--teleport start-->
<!--teleport end-->
</div>
<!--v-if-->
</div>

View File

@@ -26,10 +26,11 @@ import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useRouter } from 'vue-router';
import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { type IUpdateInformation } from '@/Interface';
import type { ButtonSize, IUpdateInformation } from '@/Interface';
import { generateCodeForAiTransform } from '@/components/ButtonParameter/utils';
import { needsAgentInput } from '@/utils/nodes/nodeTransforms';
import { useUIStore } from '@/stores/ui.store';
import type { ButtonType } from '@n8n/design-system';
const NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
const MAX_POPUP_COUNT = 10;
@@ -41,8 +42,8 @@ const props = withDefaults(
telemetrySource: string;
disabled?: boolean;
label?: string;
type?: string;
size?: string;
type?: ButtonType;
size?: ButtonSize;
transparent?: boolean;
hideIcon?: boolean;
tooltip?: string;

View File

@@ -119,7 +119,7 @@ const options = computed<ITab[]>(() => {
return options;
});
function onTabSelect(tab: string) {
function onTabSelect(tab: string | number) {
if (tab === 'docs' && props.nodeType) {
void externalHooks.run('dataDisplay.onDocumentationUrlClick', {
nodeType: props.nodeType,
@@ -147,7 +147,7 @@ function onTabSelect(tab: string) {
}
}
function onTooltipClick(tab: string, event: MouseEvent) {
function onTooltipClick(tab: string | number, event: MouseEvent) {
if (tab === 'communityNode' && (event.target as Element).localName === 'a') {
telemetry.track('user clicked cnr docs link', { source: 'node details view' });
}

View File

@@ -26,9 +26,9 @@ const emit = defineEmits<{
v-if="!isReadOnly"
type="tertiary"
:class="['n8n-input', $style.overrideCloseButton]"
outline="false"
:outline="false"
icon="xmark"
size="xsmall"
size="mini"
@click="emit('close')"
/>
</div>

View File

@@ -114,438 +114,441 @@ const isSaving = ref(false);
const userPermissions = computed(() =>
getResourcePermissions(usersStore.currentUser?.globalScopes),
);
const survey = computed<IFormInputs>(() => [
{
name: COMPANY_TYPE_KEY,
properties: {
label: i18n.baseText('personalizationModal.whatBestDescribesYourCompany'),
type: 'select',
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: i18n.baseText('personalizationModal.saas'),
value: SAAS_COMPANY_TYPE,
},
{
label: i18n.baseText('personalizationModal.eCommerce'),
value: ECOMMERCE_COMPANY_TYPE,
},
const survey = computed<IFormInputs>(
() =>
[
{
name: COMPANY_TYPE_KEY,
properties: {
label: i18n.baseText('personalizationModal.whatBestDescribesYourCompany'),
type: 'select',
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: i18n.baseText('personalizationModal.saas'),
value: SAAS_COMPANY_TYPE,
},
{
label: i18n.baseText('personalizationModal.eCommerce'),
value: ECOMMERCE_COMPANY_TYPE,
},
{
label: i18n.baseText('personalizationModal.digitalAgencyOrConsultant'),
value: DIGITAL_AGENCY_COMPANY_TYPE,
{
label: i18n.baseText('personalizationModal.digitalAgencyOrConsultant'),
value: DIGITAL_AGENCY_COMPANY_TYPE,
},
{
label: i18n.baseText('personalizationModal.systemsIntegrator'),
value: SYSTEMS_INTEGRATOR_COMPANY_TYPE,
},
{
value: EDUCATION_TYPE,
label: i18n.baseText('personalizationModal.education'),
},
{
label: i18n.baseText('personalizationModal.other'),
value: OTHER_COMPANY_TYPE,
},
{
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
value: PERSONAL_COMPANY_TYPE,
},
],
},
{
label: i18n.baseText('personalizationModal.systemsIntegrator'),
value: SYSTEMS_INTEGRATOR_COMPANY_TYPE,
},
{
name: COMPANY_INDUSTRY_EXTENDED_KEY,
properties: {
type: 'multi-select',
label: i18n.baseText('personalizationModal.whichIndustriesIsYourCompanyIn'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
value: FINANCE_INSURANCE_INDUSTRY,
label: i18n.baseText('personalizationModal.financeOrInsurance'),
},
{
value: GOVERNMENT_INDUSTRY,
label: i18n.baseText('personalizationModal.government'),
},
{
value: HEALTHCARE_INDUSTRY,
label: i18n.baseText('personalizationModal.healthcare'),
},
{
value: IT_INDUSTRY,
label: i18n.baseText('personalizationModal.it'),
},
{
value: LEGAL_INDUSTRY,
label: i18n.baseText('personalizationModal.legal'),
},
{
value: MSP_INDUSTRY,
label: i18n.baseText('personalizationModal.managedServiceProvider'),
},
{
value: MARKETING_INDUSTRY,
label: i18n.baseText('personalizationModal.marketing'),
},
{
value: MEDIA_INDUSTRY,
label: i18n.baseText('personalizationModal.media'),
},
{
value: MANUFACTURING_INDUSTRY,
label: i18n.baseText('personalizationModal.manufacturing'),
},
{
value: PHYSICAL_RETAIL_OR_SERVICES,
label: i18n.baseText('personalizationModal.physicalRetailOrServices'),
},
{
value: REAL_ESTATE_OR_CONSTRUCTION,
label: i18n.baseText('personalizationModal.realEstateOrConstruction'),
},
{
value: SECURITY_INDUSTRY,
label: i18n.baseText('personalizationModal.security'),
},
{
value: TELECOMS_INDUSTRY,
label: i18n.baseText('personalizationModal.telecoms'),
},
{
value: OTHER_INDUSTRY_OPTION,
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
},
],
},
{
value: EDUCATION_TYPE,
label: i18n.baseText('personalizationModal.education'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
return companyType === OTHER_COMPANY_TYPE;
},
{
label: i18n.baseText('personalizationModal.other'),
value: OTHER_COMPANY_TYPE,
},
{
name: OTHER_COMPANY_INDUSTRY_EXTENDED_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyYourCompanysIndustry'),
},
{
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
value: PERSONAL_COMPANY_TYPE,
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const companyIndustry = (values as IPersonalizationLatestVersion)[
COMPANY_INDUSTRY_EXTENDED_KEY
];
return (
companyType === OTHER_COMPANY_TYPE &&
!!companyIndustry &&
companyIndustry.includes(OTHER_INDUSTRY_OPTION)
);
},
],
},
},
{
name: COMPANY_INDUSTRY_EXTENDED_KEY,
properties: {
type: 'multi-select',
label: i18n.baseText('personalizationModal.whichIndustriesIsYourCompanyIn'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
value: FINANCE_INSURANCE_INDUSTRY,
label: i18n.baseText('personalizationModal.financeOrInsurance'),
},
{
name: ROLE_KEY,
properties: {
type: 'select',
label: i18n.baseText('personalizationModal.whichRoleBestDescribesYou'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
value: ROLE_BUSINESS_OWNER,
label: i18n.baseText('personalizationModal.businessOwner'),
},
{
value: ROLE_CUSTOMER_SUPPORT,
label: i18n.baseText('personalizationModal.customerSupport'),
},
{
value: ROLE_DATA_SCIENCE,
label: i18n.baseText('personalizationModal.dataScience'),
},
{
value: ROLE_DEVOPS,
label: i18n.baseText('personalizationModal.devops'),
},
{
value: ROLE_IT,
label: i18n.baseText('personalizationModal.it'),
},
{
value: ROLE_ENGINEERING,
label: i18n.baseText('personalizationModal.engineering'),
},
{
value: ROLE_SALES_AND_MARKETING,
label: i18n.baseText('personalizationModal.salesAndMarketing'),
},
{
value: ROLE_SECURITY,
label: i18n.baseText('personalizationModal.security'),
},
{
value: ROLE_OTHER,
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
},
],
},
{
value: GOVERNMENT_INDUSTRY,
label: i18n.baseText('personalizationModal.government'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE;
},
{
value: HEALTHCARE_INDUSTRY,
label: i18n.baseText('personalizationModal.healthcare'),
},
{
name: ROLE_OTHER_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyYourRole'),
},
{
value: IT_INDUSTRY,
label: i18n.baseText('personalizationModal.it'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_OTHER;
},
{
value: LEGAL_INDUSTRY,
label: i18n.baseText('personalizationModal.legal'),
},
{
name: DEVOPS_AUTOMATION_GOAL_KEY,
properties: {
type: 'multi-select',
label: i18n.baseText('personalizationModal.whatAreYouLookingToAutomate'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
value: DEVOPS_AUTOMATION_CI_CD_GOAL,
label: i18n.baseText('personalizationModal.cicd'),
},
{
value: DEVOPS_AUTOMATION_CLOUD_INFRASTRUCTURE_ORCHESTRATION_GOAL,
label: i18n.baseText('personalizationModal.cloudInfrastructureOrchestration'),
},
{
value: DEVOPS_AUTOMATION_DATA_SYNCING_GOAL,
label: i18n.baseText('personalizationModal.dataSynching'),
},
{
value: DEVOPS_INCIDENT_RESPONSE_GOAL,
label: i18n.baseText('personalizationModal.incidentResponse'),
},
{
value: DEVOPS_MONITORING_AND_ALERTING_GOAL,
label: i18n.baseText('personalizationModal.monitoringAndAlerting'),
},
{
value: DEVOPS_REPORTING_GOAL,
label: i18n.baseText('personalizationModal.reporting'),
},
{
value: DEVOPS_TICKETING_SYSTEMS_INTEGRATIONS_GOAL,
label: i18n.baseText('personalizationModal.ticketingSystemsIntegrations'),
},
{
value: OTHER_AUTOMATION_GOAL,
label: i18n.baseText('personalizationModal.other'),
},
],
},
{
value: MSP_INDUSTRY,
label: i18n.baseText('personalizationModal.managedServiceProvider'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
return (
companyType !== PERSONAL_COMPANY_TYPE &&
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role)
);
},
{
value: MARKETING_INDUSTRY,
label: i18n.baseText('personalizationModal.marketing'),
},
{
name: DEVOPS_AUTOMATION_GOAL_OTHER_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyYourAutomationGoal'),
},
{
value: MEDIA_INDUSTRY,
label: i18n.baseText('personalizationModal.media'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const goals = (values as IPersonalizationLatestVersion)[DEVOPS_AUTOMATION_GOAL_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
return (
companyType !== PERSONAL_COMPANY_TYPE &&
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role) &&
!!goals &&
goals.includes(DEVOPS_AUTOMATION_OTHER)
);
},
{
value: MANUFACTURING_INDUSTRY,
label: i18n.baseText('personalizationModal.manufacturing'),
},
{
name: MARKETING_AUTOMATION_GOAL_KEY,
properties: {
type: 'multi-select',
label: i18n.baseText('personalizationModal.specifySalesMarketingGoal'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: i18n.baseText('personalizationModal.leadGeneration'),
value: MARKETING_AUTOMATION_LEAD_GENERATION_GOAL,
},
{
label: i18n.baseText('personalizationModal.customerCommunication'),
value: MARKETING_AUTOMATION_CUSTOMER_COMMUNICATION,
},
{
label: i18n.baseText('personalizationModal.customerActions'),
value: MARKETING_AUTOMATION_ACTIONS,
},
{
label: i18n.baseText('personalizationModal.adCampaign'),
value: MARKETING_AUTOMATION_AD_CAMPAIGN,
},
{
label: i18n.baseText('personalizationModal.reporting'),
value: MARKETING_AUTOMATION_REPORTING,
},
{
label: i18n.baseText('personalizationModal.dataSynching'),
value: MARKETING_AUTOMATION_DATA_SYNCHING,
},
{
label: i18n.baseText('personalizationModal.other'),
value: MARKETING_AUTOMATION_OTHER,
},
],
},
{
value: PHYSICAL_RETAIL_OR_SERVICES,
label: i18n.baseText('personalizationModal.physicalRetailOrServices'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_SALES_AND_MARKETING;
},
{
value: REAL_ESTATE_OR_CONSTRUCTION,
label: i18n.baseText('personalizationModal.realEstateOrConstruction'),
},
{
name: OTHER_MARKETING_AUTOMATION_GOAL_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyOtherSalesAndMarketingGoal'),
},
{
value: SECURITY_INDUSTRY,
label: i18n.baseText('personalizationModal.security'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const goals = (values as IPersonalizationLatestVersion)[MARKETING_AUTOMATION_GOAL_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
return (
companyType !== PERSONAL_COMPANY_TYPE &&
role === ROLE_SALES_AND_MARKETING &&
!!goals &&
goals.includes(MARKETING_AUTOMATION_OTHER)
);
},
{
value: TELECOMS_INDUSTRY,
label: i18n.baseText('personalizationModal.telecoms'),
},
{
name: AUTOMATION_BENEFICIARY_KEY,
properties: {
type: 'select',
label: i18n.baseText('personalizationModal.specifyAutomationBeneficiary'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: i18n.baseText('personalizationModal.myself'),
value: AUTOMATION_BENEFICIARY_SELF,
},
{
label: i18n.baseText('personalizationModal.myTeam'),
value: AUTOMATION_BENEFICIARY_MY_TEAM,
},
{
label: i18n.baseText('personalizationModal.otherTeams'),
value: AUTOMATION_BENEFICIARY_OTHER_TEAMS,
},
],
},
{
value: OTHER_INDUSTRY_OPTION,
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE;
},
],
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
return companyType === OTHER_COMPANY_TYPE;
},
},
{
name: OTHER_COMPANY_INDUSTRY_EXTENDED_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyYourCompanysIndustry'),
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const companyIndustry = (values as IPersonalizationLatestVersion)[
COMPANY_INDUSTRY_EXTENDED_KEY
];
return (
companyType === OTHER_COMPANY_TYPE &&
!!companyIndustry &&
companyIndustry.includes(OTHER_INDUSTRY_OPTION)
);
},
},
{
name: ROLE_KEY,
properties: {
type: 'select',
label: i18n.baseText('personalizationModal.whichRoleBestDescribesYou'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
value: ROLE_BUSINESS_OWNER,
label: i18n.baseText('personalizationModal.businessOwner'),
},
{
name: COMPANY_SIZE_KEY,
properties: {
type: 'select',
label: i18n.baseText('personalizationModal.howBigIsYourCompany'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: i18n.baseText('personalizationModal.lessThan20People'),
value: COMPANY_SIZE_20_OR_LESS,
},
{
label: `20-99 ${i18n.baseText('personalizationModal.people')}`,
value: COMPANY_SIZE_20_99,
},
{
label: `100-499 ${i18n.baseText('personalizationModal.people')}`,
value: COMPANY_SIZE_100_499,
},
{
label: `500-999 ${i18n.baseText('personalizationModal.people')}`,
value: COMPANY_SIZE_500_999,
},
{
label: `1000+ ${i18n.baseText('personalizationModal.people')}`,
value: COMPANY_SIZE_1000_OR_MORE,
},
{
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
value: COMPANY_SIZE_PERSONAL_USE,
},
],
},
{
value: ROLE_CUSTOMER_SUPPORT,
label: i18n.baseText('personalizationModal.customerSupport'),
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE;
},
{
value: ROLE_DATA_SCIENCE,
label: i18n.baseText('personalizationModal.dataScience'),
},
{
name: REPORTED_SOURCE_KEY,
properties: {
type: 'select',
label: i18n.baseText('personalizationModal.howDidYouHearAboutN8n'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: 'Google',
value: REPORTED_SOURCE_GOOGLE,
},
{
label: 'Twitter',
value: REPORTED_SOURCE_TWITTER,
},
{
label: 'LinkedIn',
value: REPORTED_SOURCE_LINKEDIN,
},
{
label: 'YouTube',
value: REPORTED_SOURCE_YOUTUBE,
},
{
label: i18n.baseText('personalizationModal.friendWordOfMouth'),
value: REPORTED_SOURCE_FRIEND,
},
{
label: i18n.baseText('personalizationModal.podcast'),
value: REPORTED_SOURCE_PODCAST,
},
{
label: i18n.baseText('personalizationModal.event'),
value: REPORTED_SOURCE_EVENT,
},
{
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
value: REPORTED_SOURCE_OTHER,
},
],
},
{
value: ROLE_DEVOPS,
label: i18n.baseText('personalizationModal.devops'),
},
{
name: REPORTED_SOURCE_OTHER_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyReportedSource'),
},
{
value: ROLE_IT,
label: i18n.baseText('personalizationModal.it'),
shouldDisplay(values): boolean {
const reportedSource = (values as IPersonalizationLatestVersion)[REPORTED_SOURCE_KEY];
return reportedSource === REPORTED_SOURCE_OTHER;
},
{
value: ROLE_ENGINEERING,
label: i18n.baseText('personalizationModal.engineering'),
},
{
value: ROLE_SALES_AND_MARKETING,
label: i18n.baseText('personalizationModal.salesAndMarketing'),
},
{
value: ROLE_SECURITY,
label: i18n.baseText('personalizationModal.security'),
},
{
value: ROLE_OTHER,
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
},
],
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE;
},
},
{
name: ROLE_OTHER_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyYourRole'),
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_OTHER;
},
},
{
name: DEVOPS_AUTOMATION_GOAL_KEY,
properties: {
type: 'multi-select',
label: i18n.baseText('personalizationModal.whatAreYouLookingToAutomate'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
value: DEVOPS_AUTOMATION_CI_CD_GOAL,
label: i18n.baseText('personalizationModal.cicd'),
},
{
value: DEVOPS_AUTOMATION_CLOUD_INFRASTRUCTURE_ORCHESTRATION_GOAL,
label: i18n.baseText('personalizationModal.cloudInfrastructureOrchestration'),
},
{
value: DEVOPS_AUTOMATION_DATA_SYNCING_GOAL,
label: i18n.baseText('personalizationModal.dataSynching'),
},
{
value: DEVOPS_INCIDENT_RESPONSE_GOAL,
label: i18n.baseText('personalizationModal.incidentResponse'),
},
{
value: DEVOPS_MONITORING_AND_ALERTING_GOAL,
label: i18n.baseText('personalizationModal.monitoringAndAlerting'),
},
{
value: DEVOPS_REPORTING_GOAL,
label: i18n.baseText('personalizationModal.reporting'),
},
{
value: DEVOPS_TICKETING_SYSTEMS_INTEGRATIONS_GOAL,
label: i18n.baseText('personalizationModal.ticketingSystemsIntegrations'),
},
{
value: OTHER_AUTOMATION_GOAL,
label: i18n.baseText('personalizationModal.other'),
},
],
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
return (
companyType !== PERSONAL_COMPANY_TYPE &&
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role)
);
},
},
{
name: DEVOPS_AUTOMATION_GOAL_OTHER_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyYourAutomationGoal'),
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const goals = (values as IPersonalizationLatestVersion)[DEVOPS_AUTOMATION_GOAL_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY] as string;
return (
companyType !== PERSONAL_COMPANY_TYPE &&
[ROLE_DEVOPS, ROLE_ENGINEERING, ROLE_IT].includes(role) &&
!!goals &&
goals.includes(DEVOPS_AUTOMATION_OTHER)
);
},
},
{
name: MARKETING_AUTOMATION_GOAL_KEY,
properties: {
type: 'multi-select',
label: i18n.baseText('personalizationModal.specifySalesMarketingGoal'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: i18n.baseText('personalizationModal.leadGeneration'),
value: MARKETING_AUTOMATION_LEAD_GENERATION_GOAL,
},
{
label: i18n.baseText('personalizationModal.customerCommunication'),
value: MARKETING_AUTOMATION_CUSTOMER_COMMUNICATION,
},
{
label: i18n.baseText('personalizationModal.customerActions'),
value: MARKETING_AUTOMATION_ACTIONS,
},
{
label: i18n.baseText('personalizationModal.adCampaign'),
value: MARKETING_AUTOMATION_AD_CAMPAIGN,
},
{
label: i18n.baseText('personalizationModal.reporting'),
value: MARKETING_AUTOMATION_REPORTING,
},
{
label: i18n.baseText('personalizationModal.dataSynching'),
value: MARKETING_AUTOMATION_DATA_SYNCHING,
},
{
label: i18n.baseText('personalizationModal.other'),
value: MARKETING_AUTOMATION_OTHER,
},
],
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE && role === ROLE_SALES_AND_MARKETING;
},
},
{
name: OTHER_MARKETING_AUTOMATION_GOAL_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyOtherSalesAndMarketingGoal'),
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
const goals = (values as IPersonalizationLatestVersion)[MARKETING_AUTOMATION_GOAL_KEY];
const role = (values as IPersonalizationLatestVersion)[ROLE_KEY];
return (
companyType !== PERSONAL_COMPANY_TYPE &&
role === ROLE_SALES_AND_MARKETING &&
!!goals &&
goals.includes(MARKETING_AUTOMATION_OTHER)
);
},
},
{
name: AUTOMATION_BENEFICIARY_KEY,
properties: {
type: 'select',
label: i18n.baseText('personalizationModal.specifyAutomationBeneficiary'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: i18n.baseText('personalizationModal.myself'),
value: AUTOMATION_BENEFICIARY_SELF,
},
{
label: i18n.baseText('personalizationModal.myTeam'),
value: AUTOMATION_BENEFICIARY_MY_TEAM,
},
{
label: i18n.baseText('personalizationModal.otherTeams'),
value: AUTOMATION_BENEFICIARY_OTHER_TEAMS,
},
],
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE;
},
},
{
name: COMPANY_SIZE_KEY,
properties: {
type: 'select',
label: i18n.baseText('personalizationModal.howBigIsYourCompany'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: i18n.baseText('personalizationModal.lessThan20People'),
value: COMPANY_SIZE_20_OR_LESS,
},
{
label: `20-99 ${i18n.baseText('personalizationModal.people')}`,
value: COMPANY_SIZE_20_99,
},
{
label: `100-499 ${i18n.baseText('personalizationModal.people')}`,
value: COMPANY_SIZE_100_499,
},
{
label: `500-999 ${i18n.baseText('personalizationModal.people')}`,
value: COMPANY_SIZE_500_999,
},
{
label: `1000+ ${i18n.baseText('personalizationModal.people')}`,
value: COMPANY_SIZE_1000_OR_MORE,
},
{
label: i18n.baseText('personalizationModal.imNotUsingN8nForWork'),
value: COMPANY_SIZE_PERSONAL_USE,
},
],
},
shouldDisplay(values): boolean {
const companyType = (values as IPersonalizationLatestVersion)[COMPANY_TYPE_KEY];
return companyType !== PERSONAL_COMPANY_TYPE;
},
},
{
name: REPORTED_SOURCE_KEY,
properties: {
type: 'select',
label: i18n.baseText('personalizationModal.howDidYouHearAboutN8n'),
placeholder: i18n.baseText('personalizationModal.select'),
options: [
{
label: 'Google',
value: REPORTED_SOURCE_GOOGLE,
},
{
label: 'Twitter',
value: REPORTED_SOURCE_TWITTER,
},
{
label: 'LinkedIn',
value: REPORTED_SOURCE_LINKEDIN,
},
{
label: 'YouTube',
value: REPORTED_SOURCE_YOUTUBE,
},
{
label: i18n.baseText('personalizationModal.friendWordOfMouth'),
value: REPORTED_SOURCE_FRIEND,
},
{
label: i18n.baseText('personalizationModal.podcast'),
value: REPORTED_SOURCE_PODCAST,
},
{
label: i18n.baseText('personalizationModal.event'),
value: REPORTED_SOURCE_EVENT,
},
{
label: i18n.baseText('personalizationModal.otherPleaseSpecify'),
value: REPORTED_SOURCE_OTHER,
},
],
},
},
{
name: REPORTED_SOURCE_OTHER_KEY,
properties: {
placeholder: i18n.baseText('personalizationModal.specifyReportedSource'),
},
shouldDisplay(values): boolean {
const reportedSource = (values as IPersonalizationLatestVersion)[REPORTED_SOURCE_KEY];
return reportedSource === REPORTED_SOURCE_OTHER;
},
},
]);
},
] as const,
);
const onSave = () => {
formBus.emit('submit');
@@ -575,7 +578,7 @@ const closeDialog = () => {
}
};
const onSubmit = async (values: IPersonalizationLatestVersion) => {
const onSubmit = async (values: object) => {
isSaving.value = true;
try {

View File

@@ -4,11 +4,7 @@ import { useI18n } from '@n8n/i18n';
import { ResourceType, splitName } from '@/utils/projects.utils';
import type { Project, ProjectIcon as BadgeIcon } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types';
import type {
CredentialsResource,
FolderResource,
WorkflowResource,
} from '../layouts/ResourcesListLayout.vue';
import type { CredentialsResource, FolderResource, WorkflowResource } from '@/Interface';
import { VIEWS } from '@/constants';
type Props = {

View File

@@ -1,15 +1,11 @@
<script lang="ts" setup>
import type { ButtonType } from '@n8n/design-system';
import type { ButtonType, UserAction } from '@n8n/design-system';
import { N8nIconButton, N8nActionToggle } from '@n8n/design-system';
import { ref } from 'vue';
import type { IUser } from 'n8n-workflow';
import { useTemplateRef } from 'vue';
type Action = {
label: string;
value: string;
disabled: boolean;
};
defineProps<{
actions: Action[];
actions: Array<UserAction<IUser>>;
disabled?: boolean;
type?: ButtonType;
}>();
@@ -18,7 +14,7 @@ const emit = defineEmits<{
action: [id: string];
}>();
const actionToggleRef = ref<InstanceType<typeof N8nActionToggle> | null>(null);
const actionToggleRef = useTemplateRef('actionToggleRef');
defineExpose({
openActionToggle: (isOpen: boolean) => actionToggleRef.value?.openActionToggle(isOpen),

View File

@@ -16,6 +16,7 @@ import ProjectCreateResource from '@/components/Projects/ProjectCreateResource.v
import { useSettingsStore } from '@/stores/settings.store';
import { useProjectPages } from '@/composables/useProjectPages';
import { truncateTextToFitWidth } from '@/utils/formatters/textFormatter';
import type { IUser } from 'n8n-workflow';
const route = useRoute();
const router = useRouter();
@@ -96,7 +97,7 @@ const createWorkflowButton = computed(() => ({
}));
const menu = computed(() => {
const items: UserAction[] = [
const items: Array<UserAction<IUser>> = [
{
value: ACTION_TYPES.CREDENTIAL,
label: i18n.baseText('projects.header.create.credential'),

View File

@@ -240,7 +240,7 @@ onMounted(async () => {
v-for="p in filteredProjects"
:key="p.id"
:value="p.id"
:label="p.name"
:label="p.name ?? ''"
></N8nOption>
</N8nSelect>
<N8nText>

View File

@@ -43,10 +43,10 @@ const shared = computed<IMenuItem>(() => ({
},
}));
const getProjectMenuItem = (project: ProjectListItem) => ({
const getProjectMenuItem = (project: ProjectListItem): IMenuItem => ({
id: project.id,
label: project.name,
icon: project.icon,
label: project.name ?? '',
icon: project.icon as IMenuItem['icon'],
route: {
to: {
name: VIEWS.PROJECTS_WORKFLOWS,
@@ -70,6 +70,14 @@ const personalProject = computed<IMenuItem>(() => ({
const showAddFirstProject = computed(
() => projectsStore.isTeamProjectFeatureEnabled && !displayProjects.value.length,
);
const activeTabId = computed(() => {
return (
(Array.isArray(projectsStore.projectNavActiveId)
? projectsStore.projectNavActiveId[0]
: projectsStore.projectNavActiveId) ?? undefined
);
});
</script>
<template>
@@ -78,7 +86,7 @@ const showAddFirstProject = computed(
<N8nMenuItem
:item="home"
:compact="props.collapsed"
:active-tab="projectsStore.projectNavActiveId"
:active-tab="activeTabId"
mode="tabs"
data-test-id="project-home-menu-item"
/>
@@ -86,7 +94,7 @@ const showAddFirstProject = computed(
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
:item="personalProject"
:compact="props.collapsed"
:active-tab="projectsStore.projectNavActiveId"
:active-tab="activeTabId"
mode="tabs"
data-test-id="project-personal-menu-item"
/>
@@ -94,7 +102,7 @@ const showAddFirstProject = computed(
v-if="projectsStore.isTeamProjectFeatureEnabled || isFoldersFeatureEnabled"
:item="shared"
:compact="props.collapsed"
:active-tab="projectsStore.projectNavActiveId"
:active-tab="activeTabId"
mode="tabs"
data-test-id="project-shared-menu-item"
/>
@@ -136,7 +144,7 @@ const showAddFirstProject = computed(
}"
:item="getProjectMenuItem(project)"
:compact="props.collapsed"
:active-tab="projectsStore.projectNavActiveId"
:active-tab="activeTabId"
mode="tabs"
data-test-id="project-menu-item"
/>

View File

@@ -136,7 +136,7 @@ watch(
v-for="project in filteredProjects"
:key="project.id"
:value="project.id"
:label="project.name"
:label="project.name ?? ''"
>
<ProjectSharingInfo :project="project" />
</N8nOption>

View File

@@ -5,6 +5,7 @@ import { useRoute } from 'vue-router';
import { VIEWS } from '@/constants';
import { useI18n } from '@n8n/i18n';
import type { BaseTextKey } from '@n8n/i18n';
import type { TabOptions } from '@n8n/design-system';
type Props = {
showSettings?: boolean;
@@ -23,6 +24,8 @@ const route = useRoute();
const selectedTab = ref<RouteRecordName | null | undefined>('');
const selectedTabLabel = computed(() => (selectedTab.value ? String(selectedTab.value) : ''));
const projectId = computed(() => {
return Array.isArray(route?.params?.projectId)
? route.params.projectId[0]
@@ -70,16 +73,16 @@ const createTab = (
label: BaseTextKey,
routeKey: string,
routes: Record<string, { name: RouteRecordName; params?: Record<string, string | number> }>,
) => {
): TabOptions<string> => {
return {
label: locale.baseText(label),
value: routes[routeKey].name,
value: routes[routeKey].name as string,
to: routes[routeKey],
};
};
// Generate the tabs configuration
const options = computed(() => {
const options = computed<Array<TabOptions<string>>>(() => {
const routes = getRouteConfigs();
const tabs = [
createTab('mainSidebar.workflows', 'workflows', routes),
@@ -93,7 +96,7 @@ const options = computed(() => {
if (props.showSettings) {
tabs.push({
label: locale.baseText('projects.settings'),
value: VIEWS.PROJECT_SETTINGS,
value: VIEWS.PROJECT_SETTINGS as string,
to: { name: VIEWS.PROJECT_SETTINGS, params: { projectId: projectId.value } },
});
}
@@ -110,8 +113,17 @@ watch(
},
{ immediate: true },
);
function onSelectTab(value: string | number) {
selectedTab.value = value as RouteRecordName;
}
</script>
<template>
<N8nTabs v-model="selectedTab" :options="options" data-test-id="project-tabs" />
<N8nTabs
:model-value="selectedTabLabel"
:options="options"
data-test-id="project-tabs"
@update:model-value="onSelectTab"
/>
</template>

View File

@@ -4,7 +4,7 @@ import Modal from '../Modal.vue';
import { PROMPT_MFA_CODE_MODAL_KEY } from '@/constants';
import { useI18n } from '@n8n/i18n';
import { promptMfaCodeBus } from '@/event-bus';
import type { IFormInputs } from '@/Interface';
import { type IFormInput } from '@/Interface';
import { createFormEventBus } from '@n8n/design-system/utils';
import { validate as validateUuid } from 'uuid';
@@ -13,7 +13,7 @@ const i18n = useI18n();
const formBus = createFormEventBus();
const readyToSubmit = ref(false);
const formFields: IFormInputs = [
const formFields: IFormInput[] = [
{
name: 'mfaCodeOrMfaRecoveryCode',
initialValue: '',
@@ -25,9 +25,14 @@ const formFields: IFormInputs = [
required: true,
},
},
];
] as const;
function onSubmit(values: { mfaCodeOrMfaRecoveryCode: string }) {
function onSubmit(values: object) {
if (
!('mfaCodeOrMfaRecoveryCode' in values && typeof values.mfaCodeOrMfaRecoveryCode === 'string')
) {
return;
}
if (validateUuid(values.mfaCodeOrMfaRecoveryCode)) {
promptMfaCodeBus.emit('close', {
mfaRecoveryCode: values.mfaCodeOrMfaRecoveryCode,

View File

@@ -220,9 +220,9 @@ const hasMultipleModes = computed(() => {
});
const hasOnlyListMode = computed(() => hasOnlyListModeUtil(props.parameter));
const valueToDisplay = computed<NodeParameterValue>(() => {
const valueToDisplay = computed<INodeParameterResourceLocator['value']>(() => {
if (typeof props.modelValue !== 'object') {
return props.modelValue;
return `${props.modelValue}`;
}
if (isListMode.value) {
@@ -398,6 +398,9 @@ const handleAddResourceClick = async () => {
const newResource = (await nodeTypesStore.getNodeParameterActionResult(
requestParams,
)) as NodeParameterValue;
if (typeof newResource === 'boolean') {
return;
}
refreshList();
await loadResources();
@@ -563,7 +566,7 @@ function findModeByName(name: string): INodePropertyMode | null {
return null;
}
function getModeLabel(mode: INodePropertyMode): string | null {
function getModeLabel(mode: INodePropertyMode): string | undefined {
if (mode.name === 'id' || mode.name === 'url' || mode.name === 'list') {
return i18n.baseText(`resourceLocator.mode.${mode.name}`);
}
@@ -571,7 +574,7 @@ function getModeLabel(mode: INodePropertyMode): string | null {
return mode.displayName;
}
function onInputChange(value: NodeParameterValue): void {
function onInputChange(value: INodeParameterResourceLocator['value']): void {
const params: INodeParameterResourceLocator = { __rl: true, value, mode: selectedMode.value };
if (isListMode.value) {
const resource = currentQueryResults.value.find((result) => result.value === value);
@@ -823,7 +826,7 @@ function showResourceDropdown() {
resourceDropdownVisible.value = true;
}
function onListItemSelected(value: NodeParameterValue) {
function onListItemSelected(value: INodeParameterResourceLocator['value']) {
onInputChange(value);
hideResourceDropdown();
}

View File

@@ -4,7 +4,7 @@ import type { IResourceLocatorResultExpanded } from '@/Interface';
import { N8nLoading } from '@n8n/design-system';
import type { EventBus } from '@n8n/utils/event-bus';
import { createEventBus } from '@n8n/utils/event-bus';
import type { NodeParameterValue } from 'n8n-workflow';
import type { INodeParameterResourceLocator, NodeParameterValue } from 'n8n-workflow';
import { computed, onBeforeUnmount, onMounted, ref, useCssModule, watch } from 'vue';
const SEARCH_BAR_HEIGHT_PX = 40;
@@ -43,7 +43,7 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{
'update:modelValue': [value: NodeParameterValue];
'update:modelValue': [value: INodeParameterResourceLocator['value']];
loadMore: [];
filter: [filter: string];
addResourceClick: [];
@@ -164,7 +164,7 @@ function onKeyDown(e: KeyboardEvent) {
const selected = sortedResources.value[hoverIndex.value - 1]?.value;
// Selected resource can be empty when loading or empty results
if (selected) {
if (selected && typeof selected !== 'boolean') {
emit('update:modelValue', selected);
}
}
@@ -175,6 +175,10 @@ function onFilterInput(value: string) {
}
function onItemClick(selected: string | number | boolean) {
if (typeof selected === 'boolean') {
return;
}
emit('update:modelValue', selected);
}

View File

@@ -28,7 +28,7 @@ const onSSOLogin = async () => {
<div :class="$style.divider">
<span>{{ i18n.baseText('sso.login.divider') }}</span>
</div>
<n8n-button
<N8nButton
size="large"
type="primary"
outline

View File

@@ -2,13 +2,14 @@
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
import { useI18n } from '@n8n/i18n';
import { computed } from 'vue';
import type { ButtonType } from '@n8n/design-system';
const props = withDefaults(
defineProps<{
saved: boolean;
isSaving?: boolean;
disabled?: boolean;
type?: string;
type?: ButtonType;
withShortcut?: boolean;
shortcutTooltip?: string;
savingLabel?: string;

View File

@@ -1,5 +1,4 @@
<script lang="ts" setup>
import type { WorkflowResource } from '@/components/layouts/ResourcesListLayout.vue';
import ProjectCardBadge from '@/components/Projects/ProjectCardBadge.vue';
import { useLoadingService } from '@/composables/useLoadingService';
import { useTelemetry } from '@/composables/useTelemetry';
@@ -41,6 +40,7 @@ import { useRoute } from 'vue-router';
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
import Modal from './Modal.vue';
import { type WorkflowResource } from '@/Interface';
const props = defineProps<{
data: { eventBus: EventBus; status: SourceControlledFile[] };

View File

@@ -9,13 +9,15 @@ import type {
ITemplatesNode,
ITemplatesWorkflow,
} from '@n8n/rest-api-client/api/templates';
import type { ITag } from '@n8n/rest-api-client/api/tags';
import { useTemplatesStore } from '@/stores/templates.store';
import TimeAgo from '@/components/TimeAgo.vue';
import { isFullTemplatesCollection, isTemplatesWorkflow } from '@/utils/templates/typeGuards';
import { useRouter } from 'vue-router';
import { useI18n } from '@n8n/i18n';
import { computed } from 'vue';
defineProps<{
const props = defineProps<{
template: ITemplatesWorkflow | ITemplatesCollection | ITemplatesCollectionFull | null;
blockTitle: string;
loading: boolean;
@@ -26,6 +28,15 @@ const i18n = useI18n();
const templatesStore = useTemplatesStore();
const categoriesAsTags = computed<ITag[]>(() =>
props.template && 'categories' in props.template
? props.template.categories.map((category) => ({
id: `${category.id}`,
name: category.name,
}))
: [],
);
const redirectToCategory = (id: string) => {
templatesStore.resetSessionId();
void router.push(`/templates?categories=${id}`);
@@ -62,10 +73,10 @@ const redirectToSearchPage = (node: ITemplatesNode) => {
</TemplateDetailsBlock>
<TemplateDetailsBlock
v-if="!loading && isFullTemplatesCollection(template) && template.categories.length > 0"
v-if="!loading && isFullTemplatesCollection(template) && categoriesAsTags.length > 0"
:title="i18n.baseText('template.details.categories')"
>
<n8n-tags :tags="template.categories" @click:tag="redirectToCategory" />
<n8n-tags :tags="categoriesAsTags" @click:tag="redirectToCategory" />
</TemplateDetailsBlock>
<TemplateDetailsBlock

View File

@@ -473,7 +473,7 @@ const onDragEnd = (el: HTMLElement) => {
</VirtualSchemaItem>
<N8nTooltip v-else-if="item.type === 'icon'" :content="item.tooltip" placement="top">
<N8nIcon :size="14" :icon="item.icon" class="icon" />
<N8nIcon size="small" :icon="item.icon" class="icon" />
</N8nTooltip>
<div

View File

@@ -1,15 +1,16 @@
<script setup lang="ts">
import { ref } from 'vue';
import type { IconColor } from '@n8n/design-system';
const props = withDefaults(
defineProps<{
icon?: string;
iconColor?: string;
iconColor?: IconColor;
initialExpanded?: boolean;
}>(),
{
icon: 'tasks',
iconColor: 'black',
iconColor: 'text-dark',
initialExpanded: true,
},
);

View File

@@ -95,7 +95,7 @@ orchestrationStore.$onAction(({ name, store }) => {
</script>
<template>
<WorkerAccordion icon="tasks" icon-color="black" :initial-expanded="false">
<WorkerAccordion icon="tasks" icon-color="text-dark" :initial-expanded="false">
<template #title>
{{ i18n.baseText('workerList.item.chartsTitle') }}
</template>

View File

@@ -20,7 +20,7 @@ function runningSince(started: Date): string {
</script>
<template>
<WorkerAccordion icon="tasks" icon-color="black" :initial-expanded="true">
<WorkerAccordion icon="tasks" icon-color="text-dark" :initial-expanded="true">
<template #title>
{{ i18n.baseText('workerList.item.jobListTitle') }} ({{ items.length }})
</template>

View File

@@ -25,7 +25,7 @@ function onCopyToClipboard(content: string) {
</script>
<template>
<WorkerAccordion icon="tasks" icon-color="black" :initial-expanded="false">
<WorkerAccordion icon="tasks" icon-color="text-dark" :initial-expanded="false">
<template #title>
{{ i18n.baseText('workerList.item.netListTitle') }} ({{ items.length }})
</template>

View File

@@ -51,7 +51,7 @@ const onClick = async () => {
<div :class="$style.container">
<div>
<n8n-text color="text-base"> You can deactivate </n8n-text>
<n8n-link :to="workflowUrl" underline="true"> '{{ data.workflowName }}' </n8n-link>
<n8n-link :to="workflowUrl" :underline="true"> '{{ data.workflowName }}' </n8n-link>
<n8n-text color="text-base">
and activate this one, or adjust the following URL path in either workflow:
</n8n-text>

View File

@@ -24,7 +24,7 @@ import { useRoute, useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry';
import { ResourceType } from '@/utils/projects.utils';
import type { EventBus } from '@n8n/utils/event-bus';
import type { WorkflowResource } from './layouts/ResourcesListLayout.vue';
import type { WorkflowResource } from '@/Interface';
import type { IUser } from 'n8n-workflow';
import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
@@ -406,6 +406,11 @@ const onBreadcrumbItemClick = async (item: PathItem) => {
await router.push(item.href);
}
};
const tags = computed(
() =>
props.data.tags?.map((tag) => (typeof tag === 'string' ? { id: tag, name: tag } : tag)) ?? [],
);
</script>
<template>
@@ -448,7 +453,7 @@ const onBreadcrumbItemClick = async (item: PathItem) => {
:class="$style.cardTags"
>
<n8n-tags
:tags="data.tags"
:tags="tags"
:truncate-at="3"
truncate
data-test-id="workflow-card-tags"

View File

@@ -9,9 +9,10 @@ import WorkflowHistoryContent from '@/components/WorkflowHistory/WorkflowHistory
import type { WorkflowHistoryActionTypes } from '@n8n/rest-api-client/api/workflowHistory';
import { workflowVersionDataFactory } from '@/stores/__tests__/utils/workflowHistoryTestUtils';
import type { IWorkflowDb } from '@/Interface';
import type { IUser } from 'n8n-workflow';
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];
const actions: UserAction[] = actionTypes.map((value) => ({
const actions: Array<UserAction<IUser>> = actionTypes.map((value) => ({
label: value,
disabled: false,
value,

View File

@@ -9,13 +9,14 @@ import type {
import WorkflowPreview from '@/components/WorkflowPreview.vue';
import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistoryListItem.vue';
import { useI18n } from '@n8n/i18n';
import type { IUser } from 'n8n-workflow';
const i18n = useI18n();
const props = defineProps<{
workflow: IWorkflowDb | null;
workflowVersion: WorkflowVersion | null;
actions: UserAction[];
actions: Array<UserAction<IUser>>;
isListLoading?: boolean;
isFirstItemShown?: boolean;
}>();

View File

@@ -7,6 +7,7 @@ import { createComponentRenderer } from '@/__tests__/render';
import WorkflowHistoryList from '@/components/WorkflowHistory/WorkflowHistoryList.vue';
import type { WorkflowHistoryActionTypes } from '@n8n/rest-api-client/api/workflowHistory';
import { workflowHistoryDataFactory } from '@/stores/__tests__/utils/workflowHistoryTestUtils';
import type { IUser } from 'n8n-workflow';
vi.stubGlobal(
'IntersectionObserver',
@@ -19,7 +20,7 @@ vi.stubGlobal(
);
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];
const actions: UserAction[] = actionTypes.map((value) => ({
const actions: Array<UserAction<IUser>> = actionTypes.map((value) => ({
label: value,
disabled: false,
value,

View File

@@ -9,11 +9,12 @@ import type {
WorkflowHistoryRequestParams,
} from '@n8n/rest-api-client/api/workflowHistory';
import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistoryListItem.vue';
import type { IUser } from 'n8n-workflow';
const props = defineProps<{
items: WorkflowHistory[];
activeItem: WorkflowHistory | null;
actions: UserAction[];
actions: Array<UserAction<IUser>>;
requestNumberOfItems: number;
lastReceivedItemsLength: number;
evaluatedPruneTime: number;

View File

@@ -5,9 +5,10 @@ import { createComponentRenderer } from '@/__tests__/render';
import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistoryListItem.vue';
import type { WorkflowHistoryActionTypes } from '@n8n/rest-api-client/api/workflowHistory';
import { workflowHistoryDataFactory } from '@/stores/__tests__/utils/workflowHistoryTestUtils';
import { type IUser } from 'n8n-workflow';
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];
const actions: UserAction[] = actionTypes.map((value) => ({
const actions: Array<UserAction<IUser>> = actionTypes.map((value) => ({
label: value,
disabled: false,
value,

View File

@@ -8,11 +8,12 @@ import type {
WorkflowHistoryActionTypes,
} from '@n8n/rest-api-client/api/workflowHistory';
import { useI18n } from '@n8n/i18n';
import type { IUser } from 'n8n-workflow';
const props = defineProps<{
item: WorkflowHistory;
index: number;
actions: UserAction[];
actions: Array<UserAction<IUser>>;
isActive: boolean;
}>();
const emit = defineEmits<{
@@ -62,7 +63,8 @@ const idLabel = computed<string>(() =>
i18n.baseText('workflowHistory.item.id', { interpolate: { id: props.item.versionId } }),
);
const onAction = (action: WorkflowHistoryActionTypes[number]) => {
const onAction = (value: string) => {
const action = value as WorkflowHistoryActionTypes[number];
emit('action', {
action,
id: props.item.versionId,

View File

@@ -2,6 +2,7 @@
import { useI18n } from '@n8n/i18n';
import Modal from '@/components/Modal.vue';
import { useUIStore } from '@/stores/ui.store';
import type { ButtonType } from '@n8n/design-system';
const props = defineProps<{
modalName: string;
@@ -11,7 +12,7 @@ const props = defineProps<{
beforeClose: () => void;
buttons: Array<{
text: string;
type: string;
type: ButtonType;
action: () => void;
}>;
};

View File

@@ -122,9 +122,9 @@ const getCreateResourceLabel = computed(() => {
});
});
const valueToDisplay = computed<NodeParameterValue>(() => {
const valueToDisplay = computed<INodeParameterResourceLocator['value']>(() => {
if (typeof props.modelValue !== 'object') {
return props.modelValue;
return props.modelValue ?? '';
}
if (isListMode.value) {
@@ -208,9 +208,6 @@ async function refreshCachedWorkflow() {
}
const workflowId = props.modelValue.value;
if (workflowId === true) {
return;
}
try {
await workflowsStore.fetchWorkflow(`${workflowId}`);
onInputChange(workflowId);

View File

@@ -49,7 +49,7 @@ export function useWorkflowResourceLocatorModes(
};
}
function getModeLabel(mode: INodePropertyMode): string | null {
function getModeLabel(mode: INodePropertyMode): string | undefined {
if (mode.name === 'id' || mode.name === 'list') {
return i18n.baseText(`resourceLocator.mode.${mode.name}`);
}

View File

@@ -701,7 +701,7 @@ onMounted(async () => {
>
<N8nOption
v-for="option of saveManualOptions"
:key="option.key"
:key="`${option.key}`"
:label="option.value"
:value="option.key"
>
@@ -730,7 +730,7 @@ onMounted(async () => {
>
<N8nOption
v-for="option of saveExecutionProgressOptions"
:key="option.key"
:key="`${option.key}`"
:label="option.value"
:value="option.key"
>

View File

@@ -316,7 +316,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
<span
class="n8n-text compact size-14 regular n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger"
class="n8n-text compact size-small regular n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger"
data-v-d00cba9a=""
>
@@ -325,7 +325,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
beatfade="false"
border="false"
bounce="false"
class="14"
class="small"
fade="false"
fixedwidth="false"
flash="false"

View File

@@ -3,10 +3,11 @@ import { useUIStore } from '@/stores/ui.store';
import { computed, useSlots } from 'vue';
import type { BannerName } from '@n8n/api-types';
import { useI18n } from '@n8n/i18n';
import type { CalloutTheme } from '@n8n/design-system';
interface Props {
name: BannerName;
theme?: string;
theme?: CalloutTheme;
customIcon?: string;
dismissible?: boolean;
}

View File

@@ -157,6 +157,12 @@ const goToUpgrade = () => {
void pageRedirectionHelper.goToUpgrade('custom-data-filter', 'upgrade-custom-data-filter');
};
const onExactMatchChange = (e: string | number | boolean) => {
if (typeof e === 'boolean') {
onFilterMetaChange(0, 'exactMatch', e);
}
};
onBeforeMount(() => {
isCustomDataFilterTracked.value = false;
});
@@ -348,7 +354,7 @@ onBeforeMount(() => {
:model-value="filter.metadata[0]?.exactMatch"
:disabled="!isAdvancedExecutionFilterEnabled"
data-test-id="execution-filter-saved-data-exact-match-checkbox"
@update:model-value="onFilterMetaChange(0, 'exactMatch', $event)"
@update:model-value="onExactMatchChange"
/>
</n8n-tooltip>
</div>

View File

@@ -10,6 +10,8 @@ import { deepCopy } from 'n8n-workflow';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { useI18n } from '@n8n/i18n';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
import type { IconColor } from '@n8n/design-system';
import { type IAccordionItem } from '@n8n/design-system/components/N8nInfoAccordion/InfoAccordion.vue';
interface IWorkflowSaveSettings {
saveFailedExecutions: boolean;
@@ -47,7 +49,7 @@ const workflowSaveSettings = ref({
saveTestExecutions: false,
} as IWorkflowSaveSettings);
const accordionItems = computed(() => [
const accordionItems = computed((): IAccordionItem[] => [
{
id: 'productionExecutions',
label: locale.baseText('executionsLandingPage.emptyState.accordion.productionExecutions'),
@@ -77,7 +79,7 @@ const shouldExpandAccordion = computed(() => {
!workflowSaveSettings.value.saveTestExecutions
);
});
const productionExecutionsIcon = computed(() => {
const productionExecutionsIcon = computed((): { color: IconColor; icon: string } => {
if (productionExecutionsStatus.value === 'saving') {
return { icon: 'check', color: 'success' };
} else if (productionExecutionsStatus.value === 'not-saving') {
@@ -104,9 +106,9 @@ const accordionIcon = computed(() => {
!workflowSaveSettings.value.saveTestExecutions ||
productionExecutionsStatus.value !== 'saving'
) {
return { icon: 'exclamation-triangle', color: 'warning' };
return { icon: 'exclamation-triangle', color: 'warning' as IconColor };
}
return null;
return undefined;
});
const currentWorkflowId = computed(() => workflowsStore.workflowId);
const isNewWorkflow = computed(() => {

View File

@@ -4,7 +4,7 @@ import { EnterpriseEditionFeature } from '@/constants';
import { useProjectsStore } from '@/stores/projects.store';
import type { ProjectSharingData } from '@/types/projects.types';
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
import type { BaseFilters } from '../layouts/ResourcesListLayout.vue';
import type { BaseFilters } from '@/Interface';
import { useI18n } from '@n8n/i18n';
type IResourceFiltersType = Record<string, boolean | string | string[]>;

View File

@@ -1,7 +1,8 @@
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { createComponentRenderer } from '@/__tests__/render';
import ResourcesListLayout, { type Resource } from '@/components/layouts/ResourcesListLayout.vue';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
import type { Resource } from '@/Interface';
import type router from 'vue-router';
import type { ProjectSharingData } from 'n8n-workflow';
import { waitAllPromises } from '@/__tests__/utils';

View File

@@ -1,7 +1,6 @@
<script lang="ts" setup>
<script lang="ts" setup generic="ResourceType extends Resource = Resource">
import { computed, nextTick, ref, onMounted, watch, onBeforeUnmount } from 'vue';
import { type ProjectSharingData } from '@/types/projects.types';
import PageViewLayout from '@/components/layouts/PageViewLayout.vue';
import PageViewLayoutList from '@/components/layouts/PageViewLayoutList.vue';
import ResourceFiltersDropdown from '@/components/forms/ResourceFiltersDropdown.vue';
@@ -13,64 +12,12 @@ import { useTelemetry } from '@/composables/useTelemetry';
import { useRoute, useRouter } from 'vue-router';
import type { BaseTextKey } from '@n8n/i18n';
import type { Scope } from '@n8n/permissions';
import type { ITag } from '@n8n/rest-api-client/api/tags';
import type { BaseFolderItem, BaseResource, ResourceParentFolder } from '@/Interface';
import type { BaseFilters, Resource, SortingAndPaginationUpdates } from '@/Interface';
import { isSharedResource, isResourceSortableByDate } from '@/utils/typeGuards';
import { useN8nLocalStorage } from '@/composables/useN8nLocalStorage';
type ResourceKeyType = 'credentials' | 'workflows' | 'variables' | 'folders';
export type FolderResource = BaseFolderItem & {
resourceType: 'folder';
};
export type WorkflowResource = BaseResource & {
resourceType: 'workflow';
updatedAt: string;
createdAt: string;
active: boolean;
isArchived: boolean;
homeProject?: ProjectSharingData;
scopes?: Scope[];
tags?: ITag[] | string[];
sharedWithProjects?: ProjectSharingData[];
readOnly: boolean;
parentFolder?: ResourceParentFolder;
};
export type VariableResource = BaseResource & {
resourceType: 'variable';
key?: string;
value?: string;
};
export type CredentialsResource = BaseResource & {
resourceType: 'credential';
updatedAt: string;
createdAt: string;
type: string;
homeProject?: ProjectSharingData;
scopes?: Scope[];
sharedWithProjects?: ProjectSharingData[];
readOnly: boolean;
needsSetup: boolean;
};
export type Resource = WorkflowResource | FolderResource | CredentialsResource | VariableResource;
export type BaseFilters = {
search: string;
homeProject: string;
[key: string]: boolean | string | string[];
};
export type SortingAndPaginationUpdates = {
page?: number;
pageSize?: number;
sort?: string;
};
const route = useRoute();
const router = useRouter();
const i18n = useI18n();
@@ -82,19 +29,19 @@ const n8nLocalStorage = useN8nLocalStorage();
const props = withDefaults(
defineProps<{
resourceKey: ResourceKeyType;
displayName?: (resource: Resource) => string;
resources: Resource[];
displayName?: (resource: ResourceType) => string;
resources: ResourceType[];
disabled: boolean;
initialize?: () => Promise<void>;
filters?: BaseFilters;
additionalFiltersHandler?: (
resource: Resource,
resource: ResourceType,
filters: BaseFilters,
matches: boolean,
) => boolean;
shareable?: boolean;
showFiltersDropdown?: boolean;
sortFns?: Record<string, (a: Resource, b: Resource) => number>;
sortFns?: Record<string, (a: ResourceType, b: ResourceType) => number>;
sortOptions?: string[];
type?: 'datatable' | 'list-full' | 'list-paginated';
typeProps: { itemSize: number } | { columns: DatatableColumn[] };
@@ -108,7 +55,7 @@ const props = withDefaults(
hasEmptyState?: boolean;
}>(),
{
displayName: (resource: Resource) => resource.name || '',
displayName: (resource: ResourceType) => resource.name || '',
initialize: async () => {},
filters: () => ({ search: '', homeProject: '' }),
sortFns: () => ({}),
@@ -190,7 +137,7 @@ const filterKeys = computed(() => {
return Object.keys(filtersModel.value);
});
const filteredAndSortedResources = computed(() => {
const filteredAndSortedResources = computed((): ResourceType[] => {
if (props.dontPerformSortingAndFiltering) {
return props.resources;
}
@@ -200,7 +147,11 @@ const filteredAndSortedResources = computed(() => {
if (filtersModel.value.homeProject && isSharedResource(resource)) {
matches =
matches &&
!!(resource.homeProject && resource.homeProject.id === filtersModel.value.homeProject);
!!(
'homeProject' in resource &&
resource.homeProject &&
resource.homeProject.id === filtersModel.value.homeProject
);
}
if (filtersModel.value.search) {
@@ -222,16 +173,27 @@ const filteredAndSortedResources = computed(() => {
if (!sortableByDate) {
return 0;
}
return props.sortFns.lastUpdated
? props.sortFns.lastUpdated(a, b)
: new Date(b.updatedAt ?? '').valueOf() - new Date(a.updatedAt ?? '').valueOf();
if ('updatedAt' in a && 'updatedAt' in b) {
return props.sortFns.lastUpdated
? props.sortFns.lastUpdated(a, b)
: new Date(b.updatedAt ?? '').valueOf() - new Date(a.updatedAt ?? '').valueOf();
}
return 0;
case 'lastCreated':
if (!sortableByDate) {
return 0;
}
return props.sortFns.lastCreated
? props.sortFns.lastCreated(a, b)
: new Date(b.createdAt ?? '').valueOf() - new Date(a.createdAt ?? '').valueOf();
if ('createdAt' in a && 'createdAt' in b) {
return props.sortFns.lastCreated
? props.sortFns.lastCreated(a, b)
: new Date(b.createdAt ?? '').valueOf() - new Date(a.createdAt ?? '').valueOf();
}
return 0;
case 'nameAsc':
return props.sortFns.nameAsc
? props.sortFns.nameAsc(a, b)
@@ -718,7 +680,7 @@ defineExpose({
data-test-id="resources-list"
:items="filteredAndSortedResources"
:item-size="itemSize()"
item-key="id"
:item-key="'id'"
>
<template #default="{ item, updateItemSize }">
<slot :data="item" :update-item-size="updateItemSize" />