refactor(editor): Convert credential related components to composition API (no-changelog) (#10530)

This commit is contained in:
Elias Meire 2024-08-29 16:30:19 +02:00 committed by GitHub
parent 405c55a1f7
commit 402a8b40c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 763 additions and 858 deletions

View file

@ -1130,10 +1130,7 @@ function resetCredentialData(): void {
/> />
</div> </div>
<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent"> <div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
<CredentialInfo <CredentialInfo :current-credential="currentCredential" />
:current-credential="currentCredential"
:credential-permissions="credentialPermissions"
/>
</div> </div>
<div v-else-if="activeTab.startsWith('coming-soon')" :class="$style.mainContent"> <div v-else-if="activeTab.startsWith('coming-soon')" :class="$style.mainContent">
<FeatureComingSoon :feature-id="activeTab.split('/')[1]"></FeatureComingSoon> <FeatureComingSoon :feature-id="activeTab.split('/')[1]"></FeatureComingSoon>

View file

@ -1,57 +1,52 @@
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
import TimeAgo from '../TimeAgo.vue'; import TimeAgo from '../TimeAgo.vue';
import type { INodeTypeDescription } from 'n8n-workflow'; import { useI18n } from '@/composables/useI18n';
import type { ICredentialsDecryptedResponse, ICredentialsResponse } from '@/Interface';
import { N8nText } from 'n8n-design-system';
export default defineComponent({ type Props = {
name: 'CredentialInfo', currentCredential: ICredentialsResponse | ICredentialsDecryptedResponse | null;
components: { };
TimeAgo,
}, defineProps<Props>();
props: ['currentCredential', 'credentialPermissions'],
methods: { const i18n = useI18n();
shortNodeType(nodeType: INodeTypeDescription) {
return this.$locale.shortNodeType(nodeType.name);
},
},
});
</script> </script>
<template> <template>
<div :class="$style.container"> <div :class="$style.container">
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label"> <el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true"> <N8nText :compact="true" :bold="true">
{{ $locale.baseText('credentialEdit.credentialInfo.created') }} {{ i18n.baseText('credentialEdit.credentialInfo.created') }}
</n8n-text> </N8nText>
</el-col> </el-col>
<el-col :span="16" :class="$style.valueLabel"> <el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true" <N8nText :compact="true"
><TimeAgo :date="currentCredential.createdAt" :capitalize="true" ><TimeAgo :date="currentCredential.createdAt" :capitalize="true"
/></n8n-text> /></N8nText>
</el-col> </el-col>
</el-row> </el-row>
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label"> <el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true"> <N8nText :compact="true" :bold="true">
{{ $locale.baseText('credentialEdit.credentialInfo.lastModified') }} {{ i18n.baseText('credentialEdit.credentialInfo.lastModified') }}
</n8n-text> </N8nText>
</el-col> </el-col>
<el-col :span="16" :class="$style.valueLabel"> <el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true" <N8nText :compact="true"
><TimeAgo :date="currentCredential.updatedAt" :capitalize="true" ><TimeAgo :date="currentCredential.updatedAt" :capitalize="true"
/></n8n-text> /></N8nText>
</el-col> </el-col>
</el-row> </el-row>
<el-row v-if="currentCredential"> <el-row v-if="currentCredential">
<el-col :span="8" :class="$style.label"> <el-col :span="8" :class="$style.label">
<n8n-text :compact="true" :bold="true"> <N8nText :compact="true" :bold="true">
{{ $locale.baseText('credentialEdit.credentialInfo.id') }} {{ i18n.baseText('credentialEdit.credentialInfo.id') }}
</n8n-text> </N8nText>
</el-col> </el-col>
<el-col :span="16" :class="$style.valueLabel"> <el-col :span="16" :class="$style.valueLabel">
<n8n-text :compact="true">{{ currentCredential.id }}</n8n-text> <N8nText :compact="true">{{ currentCredential.id }}</N8nText>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>

View file

@ -1,162 +1,114 @@
<script lang="ts"> <script setup lang="ts">
import type { import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
ICredentialsResponse, import { useI18n } from '@/composables/useI18n';
ICredentialsDecryptedResponse, import { EnterpriseEditionFeature } from '@/constants';
IUserListAction, import type { ICredentialsDecryptedResponse, ICredentialsResponse } from '@/Interface';
} from '@/Interface'; import type { PermissionsRecord } from '@/permissions';
import { defineComponent } from 'vue'; import { useProjectsStore } from '@/stores/projects.store';
import type { PropType } from 'vue'; import { useRolesStore } from '@/stores/roles.store';
import { useMessage } from '@/composables/useMessage';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useUsersStore } from '@/stores/users.store';
import { useUsageStore } from '@/stores/usage.store';
import { EnterpriseEditionFeature } from '@/constants';
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
import { useProjectsStore } from '@/stores/projects.store';
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types'; import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import type { PermissionsRecord } from '@/permissions';
import type { EventBus } from 'n8n-design-system/utils';
import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types'; import type { RoleMap } from '@/types/roles.types';
import { splitName } from '@/utils/projects.utils'; import { splitName } from '@/utils/projects.utils';
import type { EventBus } from 'n8n-design-system/utils';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { computed, onMounted, ref, watch } from 'vue';
export default defineComponent({ type Props = {
name: 'CredentialSharing', credentialId: string;
components: { credentialData: ICredentialDataDecryptedObject;
ProjectSharing, credentialPermissions: PermissionsRecord['credential'];
}, credential?: ICredentialsResponse | ICredentialsDecryptedResponse | null;
props: { modalBus: EventBus;
credential: { };
type: Object as PropType<ICredentialsResponse | ICredentialsDecryptedResponse | null>,
default: null, const props = withDefaults(defineProps<Props>(), { credential: null });
},
credentialId: { const emit = defineEmits<{
type: String, 'update:modelValue': [value: ProjectSharingData[]];
required: true, }>();
},
credentialData: { const i18n = useI18n();
type: Object as PropType<ICredentialDataDecryptedObject>,
required: true, const usersStore = useUsersStore();
}, const uiStore = useUIStore();
credentialPermissions: { const settingsStore = useSettingsStore();
type: Object as PropType<PermissionsRecord['credential']>, const projectsStore = useProjectsStore();
required: true, const rolesStore = useRolesStore();
},
modalBus: { const sharedWithProjects = ref([...(props.credential?.sharedWithProjects ?? [])]);
type: Object as PropType<EventBus>,
required: true, const isSharingEnabled = computed(
}, () => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing],
}, );
emits: ['update:modelValue'], const credentialOwnerName = computed(() => {
setup() { const { firstName, lastName, email } = splitName(props.credential?.homeProject?.name ?? '');
return {
...useMessage(),
};
},
data() {
return {
sharedWithProjects: [...(this.credential?.sharedWithProjects ?? [])] as ProjectSharingData[],
};
},
computed: {
...mapStores(
useCredentialsStore,
useUsersStore,
useUsageStore,
useUIStore,
useSettingsStore,
useProjectsStore,
useRolesStore,
),
usersListActions(): IUserListAction[] {
return [
{
label: this.$locale.baseText('credentialEdit.credentialSharing.list.delete'),
value: 'delete',
},
];
},
isSharingEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
},
credentialOwnerName(): string {
const { firstName, lastName, email } = splitName(this.credential?.homeProject?.name ?? '');
return firstName || lastName ? `${firstName}${lastName ? ' ' + lastName : ''}` : email ?? ''; return firstName || lastName ? `${firstName}${lastName ? ' ' + lastName : ''}` : email ?? '';
}, });
credentialDataHomeProject(): ProjectSharingData | undefined {
const credentialDataHomeProject = computed<ProjectSharingData | undefined>(() => {
const credentialContainsProjectSharingData = ( const credentialContainsProjectSharingData = (
data: ICredentialDataDecryptedObject, data: ICredentialDataDecryptedObject,
): data is { homeProject: ProjectSharingData } => { ): data is { homeProject: ProjectSharingData } => {
return 'homeProject' in data; return 'homeProject' in data;
}; };
return this.credentialData && credentialContainsProjectSharingData(this.credentialData) return props.credentialData && credentialContainsProjectSharingData(props.credentialData)
? this.credentialData.homeProject ? props.credentialData.homeProject
: undefined; : undefined;
}, });
isCredentialSharedWithCurrentUser(): boolean {
if (!Array.isArray(this.credentialData.sharedWithProjects)) return false;
return this.credentialData.sharedWithProjects.some((sharee) => { const projects = computed<ProjectListItem[]>(() => {
return typeof sharee === 'object' && 'id' in sharee return projectsStore.projects.filter(
? sharee.id === this.usersStore.currentUser?.id
: false;
});
},
projects(): ProjectListItem[] {
return this.projectsStore.projects.filter(
(project) => (project) =>
project.id !== this.credential?.homeProject?.id && project.id !== props.credential?.homeProject?.id &&
project.id !== this.credentialDataHomeProject?.id, project.id !== credentialDataHomeProject.value?.id,
); );
}, });
homeProject(): ProjectSharingData | undefined {
return this.credential?.homeProject ?? this.credentialDataHomeProject; const homeProject = computed<ProjectSharingData | undefined>(
}, () => props.credential?.homeProject ?? credentialDataHomeProject.value,
isHomeTeamProject(): boolean { );
return this.homeProject?.type === ProjectTypes.Team; const isHomeTeamProject = computed(() => homeProject.value?.type === ProjectTypes.Team);
}, const credentialRoleTranslations = computed<Record<string, string>>(() => {
credentialRoleTranslations(): Record<string, string> {
return { return {
'credential:user': this.$locale.baseText('credentialEdit.credentialSharing.role.user'), 'credential:user': i18n.baseText('credentialEdit.credentialSharing.role.user'),
}; };
}, });
credentialRoles(): RoleMap['credential'] {
return this.rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({ const credentialRoles = computed<RoleMap['credential']>(() => {
return rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
role, role,
name: this.credentialRoleTranslations[role], name: credentialRoleTranslations.value[role],
scopes, scopes,
licensed, licensed,
})); }));
},
sharingSelectPlaceholder() {
return this.projectsStore.teamProjects.length
? this.$locale.baseText('projects.sharing.select.placeholder.project')
: this.$locale.baseText('projects.sharing.select.placeholder.user');
},
},
watch: {
sharedWithProjects: {
handler(changedSharedWithProjects: ProjectSharingData[]) {
this.$emit('update:modelValue', changedSharedWithProjects);
},
deep: true,
},
},
async mounted() {
await Promise.all([this.usersStore.fetchUsers(), this.projectsStore.getAllProjects()]);
},
methods: {
goToUpgrade() {
void this.uiStore.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing');
},
},
}); });
const sharingSelectPlaceholder = computed(() =>
projectsStore.teamProjects.length
? i18n.baseText('projects.sharing.select.placeholder.project')
: i18n.baseText('projects.sharing.select.placeholder.user'),
);
watch(
sharedWithProjects,
(changedSharedWithProjects) => {
emit('update:modelValue', changedSharedWithProjects);
},
{ deep: true },
);
onMounted(async () => {
await Promise.all([usersStore.fetchUsers(), projectsStore.getAllProjects()]);
});
function goToUpgrade() {
void uiStore.goToUpgrade('credential_sharing', 'upgrade-credentials-sharing');
}
</script> </script>
<template> <template>
@ -164,33 +116,29 @@ export default defineComponent({
<div v-if="!isSharingEnabled"> <div v-if="!isSharingEnabled">
<N8nActionBox <N8nActionBox
:heading=" :heading="
$locale.baseText( i18n.baseText(uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title)
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title,
)
" "
:description=" :description="
$locale.baseText( i18n.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.description, uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.description,
) )
" "
:button-text=" :button-text="
$locale.baseText( i18n.baseText(uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.button)
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.button,
)
" "
@click:button="goToUpgrade" @click:button="goToUpgrade"
/> />
</div> </div>
<div v-else> <div v-else>
<N8nInfoTip v-if="credentialPermissions.share" :bold="false" class="mb-s"> <N8nInfoTip v-if="credentialPermissions.share" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }} {{ i18n.baseText('credentialEdit.credentialSharing.info.owner') }}
</N8nInfoTip> </N8nInfoTip>
<N8nInfoTip v-else-if="isHomeTeamProject" :bold="false" class="mb-s"> <N8nInfoTip v-else-if="isHomeTeamProject" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.sharee.team') }} {{ i18n.baseText('credentialEdit.credentialSharing.info.sharee.team') }}
</N8nInfoTip> </N8nInfoTip>
<N8nInfoTip v-else :bold="false" class="mb-s"> <N8nInfoTip v-else :bold="false" class="mb-s">
{{ {{
$locale.baseText('credentialEdit.credentialSharing.info.sharee.personal', { i18n.baseText('credentialEdit.credentialSharing.info.sharee.personal', {
interpolate: { credentialOwnerName }, interpolate: { credentialOwnerName },
}) })
}} }}

View file

@ -1,56 +1,60 @@
<script lang="ts"> <script setup lang="ts">
import type { ICredentialType } from 'n8n-workflow'; import type { ICredentialType, INodeProperties, NodeParameterValue } from 'n8n-workflow';
import { defineComponent } from 'vue'; import { computed, ref } from 'vue';
import ScopesNotice from '@/components/ScopesNotice.vue'; import ScopesNotice from '@/components/ScopesNotice.vue';
import NodeCredentials from '@/components/NodeCredentials.vue'; import NodeCredentials from '@/components/NodeCredentials.vue';
import { mapStores } from 'pinia';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { N8nOption, N8nSelect } from 'n8n-design-system';
import type { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
export default defineComponent({ type Props = {
name: 'CredentialsSelect', activeCredentialType: string;
components: { parameter: INodeProperties;
ScopesNotice, node?: INodeUi;
NodeCredentials, inputSize?: 'small' | 'large' | 'mini' | 'medium' | 'xlarge';
}, displayValue: NodeParameterValue;
props: [ isReadOnly: boolean;
'activeCredentialType', displayTitle: string;
'node', };
'parameter',
'inputSize',
'displayValue',
'isReadOnly',
'displayTitle',
],
emits: ['update:modelValue', 'setFocus', 'onBlur', 'credentialSelected'],
computed: {
...mapStores(useCredentialsStore),
allCredentialTypes(): ICredentialType[] {
return this.credentialsStore.allCredentialTypes;
},
scopes(): string[] {
if (!this.activeCredentialType) return [];
return this.credentialsStore.getScopesByCredentialType(this.activeCredentialType); const props = defineProps<Props>();
},
supportedCredentialTypes(): ICredentialType[] { const emit = defineEmits<{
return this.allCredentialTypes.filter((c: ICredentialType) => this.isSupported(c.name)); 'update:modelValue': [value: string];
}, setFocus: [];
}, onBlur: [];
methods: { credentialSelected: [update: INodeUpdatePropertiesInformation];
focus() { }>();
const selectRef = this.$refs.innerSelect as HTMLElement | undefined;
if (selectRef) { const credentialsStore = useCredentialsStore();
selectRef.focus();
const innerSelectRef = ref<HTMLSelectElement>();
const allCredentialTypes = computed(() => credentialsStore.allCredentialTypes);
const scopes = computed(() => {
if (!props.activeCredentialType) return [];
return credentialsStore.getScopesByCredentialType(props.activeCredentialType);
});
const supportedCredentialTypes = computed(() => {
return allCredentialTypes.value.filter((c: ICredentialType) => isSupported(c.name));
});
function focus() {
if (innerSelectRef.value) {
innerSelectRef.value.focus();
} }
}, }
/**
/**
* Check if a credential type belongs to one of the supported sets defined * Check if a credential type belongs to one of the supported sets defined
* in the `credentialTypes` key in a `credentialsSelect` parameter * in the `credentialTypes` key in a `credentialsSelect` parameter
*/ */
isSupported(name: string): boolean { function isSupported(name: string): boolean {
const supported = this.getSupportedSets(this.parameter.credentialTypes); const supported = getSupportedSets(props.parameter.credentialTypes ?? []);
const checkedCredType = this.credentialsStore.getCredentialTypeByName(name); const checkedCredType = credentialsStore.getCredentialTypeByName(name);
if (!checkedCredType) return false; if (!checkedCredType) return false;
for (const property of supported.has) { for (const property of supported.has) {
@ -73,14 +77,15 @@ export default defineComponent({
// recurse upward until base credential type // recurse upward until base credential type
// e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api // e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api
return checkedCredType.extends.reduce( return checkedCredType.extends.reduce(
(acc: boolean, parentType: string) => acc || this.isSupported(parentType), (acc: boolean, parentType: string) => acc || isSupported(parentType),
false, false,
); );
} }
return false; return false;
}, }
getSupportedSets(credentialTypes: string[]) {
function getSupportedSets(credentialTypes: string[]) {
return credentialTypes.reduce<{ extends: string[]; has: string[] }>( return credentialTypes.reduce<{ extends: string[]; has: string[] }>(
(acc, cur) => { (acc, cur) => {
const _extends = cur.split('extends:'); const _extends = cur.split('extends:');
@ -101,16 +106,16 @@ export default defineComponent({
}, },
{ extends: [], has: [] }, { extends: [], has: [] },
); );
}, }
},
}); defineExpose({ focus });
</script> </script>
<template> <template>
<div> <div>
<div :class="$style['parameter-value-container']"> <div :class="$style['parameter-value-container']">
<n8n-select <N8nSelect
ref="innerSelect" ref="innerSelectRef"
:size="inputSize" :size="inputSize"
filterable filterable
:model-value="displayValue" :model-value="displayValue"
@ -118,12 +123,12 @@ export default defineComponent({
:title="displayTitle" :title="displayTitle"
:disabled="isReadOnly" :disabled="isReadOnly"
data-test-id="credential-select" data-test-id="credential-select"
@update:model-value="(value: string) => $emit('update:modelValue', value)" @update:model-value="(value: string) => emit('update:modelValue', value)"
@keydown.stop @keydown.stop
@focus="$emit('setFocus')" @focus="emit('setFocus')"
@blur="$emit('onBlur')" @blur="emit('onBlur')"
> >
<n8n-option <N8nOption
v-for="credType in supportedCredentialTypes" v-for="credType in supportedCredentialTypes"
:key="credType.name" :key="credType.name"
:value="credType.name" :value="credType.name"
@ -135,8 +140,8 @@ export default defineComponent({
{{ credType.displayName }} {{ credType.displayName }}
</div> </div>
</div> </div>
</n8n-option> </N8nOption>
</n8n-select> </N8nSelect>
<slot name="issues-and-options" /> <slot name="issues-and-options" />
</div> </div>
@ -147,10 +152,11 @@ export default defineComponent({
/> />
<div> <div>
<NodeCredentials <NodeCredentials
v-if="node"
:node="node" :node="node"
:readonly="isReadOnly" :readonly="isReadOnly"
:override-cred-type="node.parameters[parameter.name]" :override-cred-type="node?.parameters[parameter.name]"
@credential-selected="(updateInformation) => $emit('credentialSelected', updateInformation)" @credential-selected="(updateInformation) => emit('credentialSelected', updateInformation)"
/> />
</div> </div>
</div> </div>

View file

@ -1,69 +1,59 @@
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { useExternalHooks } from '@/composables/useExternalHooks';
import Modal from './Modal.vue'; import { useTelemetry } from '@/composables/useTelemetry';
import { CREDENTIAL_SELECT_MODAL_KEY } from '../constants'; import { useCredentialsStore } from '@/stores/credentials.store';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useCredentialsStore } from '@/stores/credentials.store'; import { N8nButton, N8nSelect } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { onMounted, ref } from 'vue';
import { CREDENTIAL_SELECT_MODAL_KEY } from '../constants';
import Modal from './Modal.vue';
export default defineComponent({ const externalHooks = useExternalHooks();
name: 'CredentialsSelectModal', const telemetry = useTelemetry();
components: {
Modal, const modalBus = ref(createEventBus());
}, const selected = ref('');
setup() { const loading = ref(true);
const externalHooks = useExternalHooks(); const selectRef = ref<HTMLSelectElement>();
return {
externalHooks, const credentialsStore = useCredentialsStore();
}; const uiStore = useUIStore();
}, const workflowsStore = useWorkflowsStore();
data() {
return { onMounted(async () => {
modalBus: createEventBus(),
selected: '',
loading: true,
CREDENTIAL_SELECT_MODAL_KEY,
};
},
async mounted() {
try { try {
await this.credentialsStore.fetchCredentialTypes(false); await credentialsStore.fetchCredentialTypes(false);
} catch (e) {} } catch (e) {}
this.loading = false;
loading.value = false;
setTimeout(() => { setTimeout(() => {
const elementRef = this.$refs.select as HTMLSelectElement | undefined; if (selectRef.value) {
if (elementRef) { selectRef.value.focus();
elementRef.focus();
} }
}, 0); }, 0);
}, });
computed: {
...mapStores(useCredentialsStore, useUIStore, useWorkflowsStore), function onSelect(type: string) {
}, selected.value = type;
methods: { }
onSelect(type: string) {
this.selected = type; function openCredentialType() {
}, modalBus.value.emit('close');
openCredentialType() { uiStore.openNewCredential(selected.value);
this.modalBus.emit('close');
this.uiStore.openNewCredential(this.selected);
const telemetryPayload = { const telemetryPayload = {
credential_type: this.selected, credential_type: selected.value,
source: 'primary_menu', source: 'primary_menu',
new_credential: true, new_credential: true,
workflow_id: this.workflowsStore.workflowId, workflow_id: workflowsStore.workflowId,
}; };
this.$telemetry.track('User opened Credential modal', telemetryPayload); telemetry.track('User opened Credential modal', telemetryPayload);
void this.externalHooks.run('credentialsSelectModal.openCredentialType', telemetryPayload); void externalHooks.run('credentialsSelectModal.openCredentialType', telemetryPayload);
}, }
},
});
</script> </script>
<template> <template>
@ -86,8 +76,8 @@ export default defineComponent({
<div :class="$style.subtitle"> <div :class="$style.subtitle">
{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }} {{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}
</div> </div>
<n8n-select <N8nSelect
ref="select" ref="selectRef"
filterable filterable
default-first-option default-first-option
:placeholder="$locale.baseText('credentialSelectModal.searchForApp')" :placeholder="$locale.baseText('credentialSelectModal.searchForApp')"
@ -99,7 +89,7 @@ export default defineComponent({
<template #prefix> <template #prefix>
<font-awesome-icon icon="search" /> <font-awesome-icon icon="search" />
</template> </template>
<n8n-option <N8nOption
v-for="credential in credentialsStore.allCredentialTypes" v-for="credential in credentialsStore.allCredentialTypes"
:key="credential.name" :key="credential.name"
:value="credential.name" :value="credential.name"
@ -107,12 +97,12 @@ export default defineComponent({
filterable filterable
data-test-id="new-credential-type-select-option" data-test-id="new-credential-type-select-option"
/> />
</n8n-select> </N8nSelect>
</div> </div>
</template> </template>
<template #footer> <template #footer>
<div :class="$style.footer"> <div :class="$style.footer">
<n8n-button <N8nButton
:label="$locale.baseText('credentialSelectModal.continue')" :label="$locale.baseText('credentialSelectModal.continue')"
float="right" float="right"
size="large" size="large"

View file

@ -1,176 +1,158 @@
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import type {
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
IUser,
} from '@/Interface';
import type { import type {
INodeCredentialDescription, INodeCredentialDescription,
INodeCredentialsDetails, INodeCredentialsDetails,
INodeParameters, NodeParameterValueType,
INodeProperties,
INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import TitledList from '@/components/TitledList.vue'; import TitledList from '@/components/TitledList.vue';
import { useUIStore } from '@/stores/ui.store'; import { useI18n } from '@/composables/useI18n';
import { useUsersStore } from '@/stores/users.store'; import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { CREDENTIAL_ONLY_NODE_PREFIX, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { ndvEventBus } from '@/event-bus';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { CREDENTIAL_ONLY_NODE_PREFIX, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { assert } from '@/utils/assert';
import { import {
getAllNodeCredentialForAuthType,
getAuthTypeForNodeCredential, getAuthTypeForNodeCredential,
getMainAuthField, getMainAuthField,
getNodeCredentialForSelectedAuthType, getNodeCredentialForSelectedAuthType,
getAllNodeCredentialForAuthType,
updateNodeAuthType,
isRequiredCredential, isRequiredCredential,
updateNodeAuthType,
} from '@/utils/nodeTypesUtils'; } from '@/utils/nodeTypesUtils';
import { assert } from '@/utils/assert'; import {
import { ndvEventBus } from '@/event-bus'; N8nInput,
N8nInputLabel,
N8nOption,
N8nSelect,
N8nText,
N8nTooltip,
} from 'n8n-design-system';
interface CredentialDropdownOption extends ICredentialsResponse { interface CredentialDropdownOption extends ICredentialsResponse {
typeDisplayName: string; typeDisplayName: string;
} }
export default defineComponent({ type Props = {
name: 'NodeCredentials', node: INodeUi;
components: { overrideCredType?: NodeParameterValueType;
TitledList, readonly?: boolean;
}, showAll?: boolean;
props: { hideIssues?: boolean;
readonly: { };
type: Boolean,
default: false,
},
node: {
type: Object as PropType<INodeUi>,
required: true,
},
overrideCredType: {
type: String,
default: '',
},
showAll: {
type: Boolean,
default: false,
},
hideIssues: {
type: Boolean,
default: false,
},
},
emits: { credentialSelected: null, valueChanged: null, blur: null },
setup() {
const nodeHelpers = useNodeHelpers();
return { const props = withDefaults(defineProps<Props>(), {
...useToast(), readonly: false,
nodeHelpers, overrideCredType: '',
}; showAll: false,
}, hideIssues: false,
data() { });
return {
NEW_CREDENTIALS_TEXT: `- ${this.$locale.baseText('nodeCredentials.createNew')} -`, const emit = defineEmits<{
subscribedToCredentialType: '', credentialSelected: [credential: INodeUpdatePropertiesInformation];
listeningForAuthChange: false, valueChanged: [value: { name: string; value: string }];
}; blur: [source: string];
}, }>();
computed: {
...mapStores( const telemetry = useTelemetry();
useCredentialsStore, const i18n = useI18n();
useNodeTypesStore, const NEW_CREDENTIALS_TEXT = `- ${i18n.baseText('nodeCredentials.createNew')} -`;
useNDVStore,
useUIStore, const credentialsStore = useCredentialsStore();
useUsersStore, const nodeTypesStore = useNodeTypesStore();
useWorkflowsStore, const ndvStore = useNDVStore();
), const uiStore = useUIStore();
currentUser(): IUser { const workflowsStore = useWorkflowsStore();
return this.usersStore.currentUser ?? ({} as IUser);
}, const nodeHelpers = useNodeHelpers();
credentialTypesNode(): string[] {
return this.credentialTypesNodeDescription.map( const toast = useToast();
const subscribedToCredentialType = ref('');
const listeningForAuthChange = ref(false);
const credentialTypesNode = computed(() =>
credentialTypesNodeDescription.value.map(
(credentialTypeDescription) => credentialTypeDescription.name, (credentialTypeDescription) => credentialTypeDescription.name,
); ),
}, );
credentialTypesNodeDescriptionDisplayed(): INodeCredentialDescription[] {
return this.credentialTypesNodeDescription.filter((credentialTypeDescription) => { const credentialTypesNodeDescriptionDisplayed = computed(() =>
return this.displayCredentials(credentialTypeDescription); credentialTypesNodeDescription.value.filter((credentialTypeDescription) =>
}); displayCredentials(credentialTypeDescription),
}, ),
credentialTypesNodeDescription(): INodeCredentialDescription[] { );
const credType = this.credentialsStore.getCredentialTypeByName(this.overrideCredType); const credentialTypesNodeDescription = computed(() => {
if (typeof props.overrideCredType !== 'string') return [];
const credType = credentialsStore.getCredentialTypeByName(props.overrideCredType);
if (credType) return [credType]; if (credType) return [credType];
const activeNodeType = this.nodeType; const activeNodeType = nodeType.value;
if (activeNodeType?.credentials) { if (activeNodeType?.credentials) {
return activeNodeType.credentials; return activeNodeType.credentials;
} }
return []; return [];
}, });
credentialTypeNames() {
const credentialTypeNames = computed(() => {
const returnData: Record<string, string> = {}; const returnData: Record<string, string> = {};
for (const credentialTypeName of this.credentialTypesNode) { for (const credentialTypeName of credentialTypesNode.value) {
const credentialType = this.credentialsStore.getCredentialTypeByName(credentialTypeName); const credentialType = credentialsStore.getCredentialTypeByName(credentialTypeName);
returnData[credentialTypeName] = credentialType returnData[credentialTypeName] = credentialType
? credentialType.displayName ? credentialType.displayName
: credentialTypeName; : credentialTypeName;
} }
return returnData; return returnData;
}, });
selected(): Record<string, INodeCredentialsDetails> {
return this.node.credentials ?? {}; const selected = computed<Record<string, INodeCredentialsDetails>>(
}, () => props.node.credentials ?? {},
nodeType(): INodeTypeDescription | null { );
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion); const nodeType = computed(() =>
}, nodeTypesStore.getNodeType(props.node.type, props.node.typeVersion),
mainNodeAuthField(): INodeProperties | null { );
return getMainAuthField(this.nodeType); const mainNodeAuthField = computed(() => getMainAuthField(nodeType.value));
}, watch(
}, () => props.node.parameters,
watch: { (newValue, oldValue) => {
'node.parameters': {
immediate: true,
deep: true,
handler(newValue: INodeParameters, oldValue: INodeParameters) {
// When active node parameters change, check if authentication type has been changed // When active node parameters change, check if authentication type has been changed
// and set `subscribedToCredentialType` to corresponding credential type // and set `subscribedToCredentialType` to corresponding credential type
const isActive = this.node.name === this.ndvStore.activeNode?.name; const isActive = props.node.name === ndvStore.activeNode?.name;
const nodeType = this.nodeType;
// Only do this for active node and if it's listening for auth change // Only do this for active node and if it's listening for auth change
if (isActive && nodeType && this.listeningForAuthChange) { if (isActive && nodeType.value && listeningForAuthChange.value) {
if (this.mainNodeAuthField && oldValue && newValue) { if (mainNodeAuthField.value && oldValue && newValue) {
const newAuth = newValue[this.mainNodeAuthField.name]; const newAuth = newValue[mainNodeAuthField.value.name];
if (newAuth) { if (newAuth) {
const authType = const authType =
typeof newAuth === 'object' ? JSON.stringify(newAuth) : newAuth.toString(); typeof newAuth === 'object' ? JSON.stringify(newAuth) : newAuth.toString();
const credentialType = getNodeCredentialForSelectedAuthType(nodeType, authType); const credentialType = getNodeCredentialForSelectedAuthType(nodeType.value, authType);
if (credentialType) { if (credentialType) {
this.subscribedToCredentialType = credentialType.name; subscribedToCredentialType.value = credentialType.name;
} }
} }
} }
} }
}, },
}, { immediate: true, deep: true },
}, );
mounted() {
// Listen for credentials store changes so credential selection can be updated if creds are changed from the modal onMounted(() => {
this.credentialsStore.$onAction(({ name, after, args }) => { credentialsStore.$onAction(({ name, after, args }) => {
const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential']; const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential'];
const credentialType = this.subscribedToCredentialType; const credentialType = subscribedToCredentialType.value;
if (!credentialType) { if (!credentialType) {
return; return;
} }
@ -179,42 +161,40 @@ export default defineComponent({
if (!listeningForActions.includes(name)) { if (!listeningForActions.includes(name)) {
return; return;
} }
const current = this.selected[credentialType]; const current = selected.value[credentialType];
let credentialsOfType: ICredentialsResponse[] = []; let credentialsOfType: ICredentialsResponse[] = [];
if (this.showAll) { if (props.showAll) {
if (this.node) { if (props.node) {
credentialsOfType = [ credentialsOfType = [...(credentialsStore.allUsableCredentialsForNode(props.node) || [])];
...(this.credentialsStore.allUsableCredentialsForNode(this.node) || []),
];
} }
} else { } else {
credentialsOfType = [ credentialsOfType = [
...(this.credentialsStore.allUsableCredentialsByType[credentialType] || []), ...(credentialsStore.allUsableCredentialsByType[credentialType] || []),
]; ];
} }
switch (name) { switch (name) {
// new credential was added // new credential was added
case 'createNewCredential': case 'createNewCredential':
if (result) { if (result) {
this.onCredentialSelected(credentialType, (result as ICredentialsResponse).id); onCredentialSelected(credentialType, (result as ICredentialsResponse).id);
} }
break; break;
case 'updateCredential': case 'updateCredential':
const updatedCredential = result as ICredentialsResponse; const updatedCredential = result as ICredentialsResponse;
// credential name was changed, update it // credential name was changed, update it
if (updatedCredential.name !== current.name) { if (updatedCredential.name !== current.name) {
this.onCredentialSelected(credentialType, current.id); onCredentialSelected(credentialType, current.id);
} }
break; break;
case 'deleteCredential': case 'deleteCredential':
// all credentials were deleted // all credentials were deleted
if (credentialsOfType.length === 0) { if (credentialsOfType.length === 0) {
this.clearSelectedCredential(credentialType); clearSelectedCredential(credentialType);
} else { } else {
const id = args[0].id; const id = args[0].id;
// credential was deleted, select last one added to replace with // credential was deleted, select last one added to replace with
if (current.id === id) { if (current.id === id) {
this.onCredentialSelected( onCredentialSelected(
credentialType, credentialType,
credentialsOfType[credentialsOfType.length - 1].id, credentialsOfType[credentialsOfType.length - 1].id,
); );
@ -225,170 +205,160 @@ export default defineComponent({
}); });
}); });
ndvEventBus.on('credential.createNew', this.onCreateAndAssignNewCredential); ndvEventBus.on('credential.createNew', onCreateAndAssignNewCredential);
}, });
beforeUnmount() {
ndvEventBus.off('credential.createNew', this.onCreateAndAssignNewCredential); onBeforeUnmount(() => {
}, ndvEventBus.off('credential.createNew', onCreateAndAssignNewCredential);
methods: { });
getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] {
const credentialIsRequired = this.showMixedCredentials(credentialType); function getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] {
const credentialIsRequired = showMixedCredentials(credentialType);
if (credentialIsRequired) { if (credentialIsRequired) {
if (this.mainNodeAuthField) { if (mainNodeAuthField.value) {
const credentials = getAllNodeCredentialForAuthType( const credentials = getAllNodeCredentialForAuthType(
this.nodeType, nodeType.value,
this.mainNodeAuthField.name, mainNodeAuthField.value.name,
); );
return credentials.map((cred) => cred.name); return credentials.map((cred) => cred.name);
} }
} }
return [credentialType.name]; return [credentialType.name];
}, }
getCredentialOptions(types: string[]): CredentialDropdownOption[] {
function getCredentialOptions(types: string[]): CredentialDropdownOption[] {
let options: CredentialDropdownOption[] = []; let options: CredentialDropdownOption[] = [];
types.forEach((type) => { types.forEach((type) => {
options = options.concat( options = options.concat(
this.credentialsStore.allUsableCredentialsByType[type].map( credentialsStore.allUsableCredentialsByType[type].map(
(option: ICredentialsResponse) => (option: ICredentialsResponse) =>
({ ({
...option, ...option,
typeDisplayName: this.credentialsStore.getCredentialTypeByName(type)?.displayName, typeDisplayName: credentialsStore.getCredentialTypeByName(type)?.displayName,
}) as CredentialDropdownOption, }) as CredentialDropdownOption,
), ),
); );
}); });
return options; return options;
}, }
getSelectedId(type: string) {
if (this.isCredentialExisting(type)) { function getSelectedId(type: string) {
return this.selected[type].id; if (isCredentialExisting(type)) {
return selected.value[type].id;
} }
return undefined; return undefined;
}, }
getSelectedName(type: string) {
return this.selected?.[type]?.name; function getSelectedName(type: string) {
}, return selected.value?.[type]?.name;
getSelectPlaceholder(type: string, issues: string[]) { }
return issues.length && this.getSelectedName(type)
? this.$locale.baseText('nodeCredentials.selectedCredentialUnavailable', { function getSelectPlaceholder(type: string, issues: string[]) {
interpolate: { name: this.getSelectedName(type) }, return issues.length && getSelectedName(type)
? i18n.baseText('nodeCredentials.selectedCredentialUnavailable', {
interpolate: { name: getSelectedName(type) },
}) })
: this.$locale.baseText('nodeCredentials.selectCredential'); : i18n.baseText('nodeCredentials.selectCredential');
}, }
credentialInputWrapperStyle(credentialType: string) {
let deductWidth = 0;
const styles = {
width: '100%',
};
if (this.getIssues(credentialType).length) {
deductWidth += 20;
}
if (deductWidth !== 0) { function clearSelectedCredential(credentialType: string) {
styles.width = `calc(100% - ${deductWidth}px)`; const node = props.node;
}
return styles;
},
clearSelectedCredential(credentialType: string) {
const node: INodeUi = this.node;
const credentials = { const credentials = {
...(node.credentials || {}), ...(node.credentials ?? {}),
}; };
delete credentials[credentialType]; delete credentials[credentialType];
const updateInformation: INodeUpdatePropertiesInformation = { const updateInformation: INodeUpdatePropertiesInformation = {
name: this.node.name, name: props.node.name,
properties: { properties: {
credentials, credentials,
position: this.node.position, position: props.node.position,
}, },
}; };
this.$emit('credentialSelected', updateInformation); emit('credentialSelected', updateInformation);
}, }
createNewCredential( function createNewCredential(
credentialType: string, credentialType: string,
listenForAuthChange: boolean = false, listenForAuthChange: boolean = false,
showAuthOptions = false, showAuthOptions = false,
) { ) {
if (listenForAuthChange) { if (listenForAuthChange) {
// If new credential dialog is open, start listening for auth type change which should happen in the modal // If new credential dialog is open, start listening for auth type change which should happen in the modal
// this will be handled in this component's watcher which will set subscribed credential accordingly // this will be handled in this component's watcher which will set subscribed credential accordingly
this.listeningForAuthChange = true; listeningForAuthChange.value = true;
this.subscribedToCredentialType = credentialType; subscribedToCredentialType.value = credentialType;
} }
this.uiStore.openNewCredential(credentialType, showAuthOptions); uiStore.openNewCredential(credentialType, showAuthOptions);
this.$telemetry.track('User opened Credential modal', { telemetry.track('User opened Credential modal', {
credential_type: credentialType, credential_type: credentialType,
source: 'node', source: 'node',
new_credential: true, new_credential: true,
workflow_id: this.workflowsStore.workflowId, workflow_id: workflowsStore.workflowId,
}); });
}, }
onCreateAndAssignNewCredential({ function onCreateAndAssignNewCredential({
type, type,
showAuthOptions, showAuthOptions,
}: { }: {
type: string; type: string;
showAuthOptions: boolean; showAuthOptions: boolean;
}) { }) {
this.createNewCredential(type, true, showAuthOptions); createNewCredential(type, true, showAuthOptions);
}, }
onCredentialSelected( function onCredentialSelected(
credentialType: string, credentialType: string,
credentialId: string | null | undefined, credentialId: string | null | undefined,
showAuthOptions = false, showAuthOptions = false,
) { ) {
const newCredentialOptionSelected = credentialId === this.NEW_CREDENTIALS_TEXT; const newCredentialOptionSelected = credentialId === NEW_CREDENTIALS_TEXT;
if (!credentialId || newCredentialOptionSelected) { if (!credentialId || newCredentialOptionSelected) {
this.createNewCredential(credentialType, newCredentialOptionSelected, showAuthOptions); createNewCredential(credentialType, newCredentialOptionSelected, showAuthOptions);
return; return;
} }
this.$telemetry.track('User selected credential from node modal', { telemetry.track('User selected credential from node modal', {
credential_type: credentialType, credential_type: credentialType,
node_type: this.node.type, node_type: props.node.type,
...(this.nodeHelpers.hasProxyAuth(this.node) ? { is_service_specific: true } : {}), ...(nodeHelpers.hasProxyAuth(props.node) ? { is_service_specific: true } : {}),
workflow_id: this.workflowsStore.workflowId, workflow_id: workflowsStore.workflowId,
credential_id: credentialId, credential_id: credentialId,
}); });
const selectedCredentials = this.credentialsStore.getCredentialById(credentialId); const selectedCredentials = credentialsStore.getCredentialById(credentialId);
const selectedCredentialsType = this.showAll ? selectedCredentials.type : credentialType; const selectedCredentialsType = props.showAll ? selectedCredentials.type : credentialType;
const oldCredentials = this.node.credentials?.[selectedCredentialsType] ?? null; const oldCredentials = props.node.credentials?.[selectedCredentialsType] ?? null;
const selected = { id: selectedCredentials.id, name: selectedCredentials.name }; const newSelectedCredentials: INodeCredentialsDetails = {
id: selectedCredentials.id,
name: selectedCredentials.name,
};
// if credentials has been string or neither id matched nor name matched uniquely // if credentials has been string or neither id matched nor name matched uniquely
if ( if (
oldCredentials?.id === null || oldCredentials?.id === null ||
(oldCredentials?.id && (oldCredentials?.id &&
!this.credentialsStore.getCredentialByIdAndType( !credentialsStore.getCredentialByIdAndType(oldCredentials.id, selectedCredentialsType))
oldCredentials.id,
selectedCredentialsType,
))
) { ) {
// update all nodes in the workflow with the same old/invalid credentials // update all nodes in the workflow with the same old/invalid credentials
this.workflowsStore.replaceInvalidWorkflowCredentials({ workflowsStore.replaceInvalidWorkflowCredentials({
credentials: selected, credentials: newSelectedCredentials,
invalid: oldCredentials, invalid: oldCredentials,
type: selectedCredentialsType, type: selectedCredentialsType,
}); });
this.nodeHelpers.updateNodesCredentialsIssues(); nodeHelpers.updateNodesCredentialsIssues();
this.showMessage({ toast.showMessage({
title: this.$locale.baseText('nodeCredentials.showMessage.title'), title: i18n.baseText('nodeCredentials.showMessage.title'),
message: this.$locale.baseText('nodeCredentials.showMessage.message', { message: i18n.baseText('nodeCredentials.showMessage.message', {
interpolate: { interpolate: {
oldCredentialName: oldCredentials.name, oldCredentialName: oldCredentials.name,
newCredentialName: selected.name, newCredentialName: newSelectedCredentials.name,
}, },
}), }),
type: 'success', type: 'success',
@ -396,54 +366,54 @@ export default defineComponent({
} }
// If credential is selected from mixed credential dropdown, update node's auth filed based on selected credential // If credential is selected from mixed credential dropdown, update node's auth filed based on selected credential
if (this.showAll && this.mainNodeAuthField) { if (props.showAll && mainNodeAuthField.value) {
const nodeCredentialDescription = this.nodeType?.credentials?.find( const nodeCredentialDescription = nodeType.value?.credentials?.find(
(cred) => cred.name === selectedCredentialsType, (cred) => cred.name === selectedCredentialsType,
); );
const authOption = getAuthTypeForNodeCredential(this.nodeType, nodeCredentialDescription); const authOption = getAuthTypeForNodeCredential(nodeType.value, nodeCredentialDescription);
if (authOption) { if (authOption) {
updateNodeAuthType(this.node, authOption.value); updateNodeAuthType(props.node, authOption.value);
const parameterData = { const parameterData = {
name: `parameters.${this.mainNodeAuthField.name}`, name: `parameters.${mainNodeAuthField.value.name}`,
value: authOption.value, value: authOption.value,
}; };
this.$emit('valueChanged', parameterData); emit('valueChanged', parameterData);
} }
} }
const node: INodeUi = this.node; const node = props.node;
const credentials = { const credentials = {
...(node.credentials ?? {}), ...(node.credentials ?? {}),
[selectedCredentialsType]: selected, [selectedCredentialsType]: newSelectedCredentials,
}; };
const updateInformation: INodeUpdatePropertiesInformation = { const updateInformation: INodeUpdatePropertiesInformation = {
name: this.node.name, name: props.node.name,
properties: { properties: {
credentials, credentials,
position: this.node.position, position: props.node.position,
}, },
}; };
this.$emit('credentialSelected', updateInformation); emit('credentialSelected', updateInformation);
}, }
displayCredentials(credentialTypeDescription: INodeCredentialDescription): boolean { function displayCredentials(credentialTypeDescription: INodeCredentialDescription): boolean {
if (credentialTypeDescription.displayOptions === undefined) { if (credentialTypeDescription.displayOptions === undefined) {
// If it is not defined no need to do a proper check // If it is not defined no need to do a proper check
return true; return true;
} }
return this.nodeHelpers.displayParameter( return nodeHelpers.displayParameter(
this.node.parameters, props.node.parameters,
credentialTypeDescription, credentialTypeDescription,
'', '',
this.node, props.node,
); );
}, }
getIssues(credentialTypeName: string): string[] { function getIssues(credentialTypeName: string): string[] {
const node = this.node; const node = props.node;
if (node.issues?.credentials === undefined) { if (node.issues?.credentials === undefined) {
return []; return [];
@ -453,58 +423,57 @@ export default defineComponent({
return []; return [];
} }
return node.issues.credentials[credentialTypeName]; return node.issues.credentials[credentialTypeName];
}, }
isCredentialExisting(credentialType: string): boolean { function isCredentialExisting(credentialType: string): boolean {
if (!this.node.credentials?.[credentialType]?.id) { if (!props.node.credentials?.[credentialType]?.id) {
return false; return false;
} }
const { id } = this.node.credentials[credentialType]; const { id } = props.node.credentials[credentialType];
const options = this.getCredentialOptions([credentialType]); const options = getCredentialOptions([credentialType]);
return !!options.find((option: ICredentialsResponse) => option.id === id); return !!options.find((option: ICredentialsResponse) => option.id === id);
}, }
editCredential(credentialType: string): void { function editCredential(credentialType: string): void {
const credential = this.node.credentials?.[credentialType]; const credential = props.node.credentials?.[credentialType];
assert(credential?.id); assert(credential?.id);
this.uiStore.openExistingCredential(credential.id); uiStore.openExistingCredential(credential.id);
this.$telemetry.track('User opened Credential modal', { telemetry.track('User opened Credential modal', {
credential_type: credentialType, credential_type: credentialType,
source: 'node', source: 'node',
new_credential: false, new_credential: false,
workflow_id: this.workflowsStore.workflowId, workflow_id: workflowsStore.workflowId,
}); });
this.subscribedToCredentialType = credentialType; subscribedToCredentialType.value = credentialType;
}, }
showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
const nodeType = this.nodeType;
const isRequired = isRequiredCredential(nodeType, credentialType);
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node.type || '') && isRequired; function showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
}, const isRequired = isRequiredCredential(nodeType.value, credentialType);
getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(props.node.type ?? '') && isRequired;
}
function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
if (credentialType.displayName) return credentialType.displayName; if (credentialType.displayName) return credentialType.displayName;
const credentialTypeName = this.credentialTypeNames[credentialType.name]; const credentialTypeName = credentialTypeNames.value[credentialType.name];
const isCredentialOnlyNode = this.node.type.startsWith(CREDENTIAL_ONLY_NODE_PREFIX); const isCredentialOnlyNode = props.node.type.startsWith(CREDENTIAL_ONLY_NODE_PREFIX);
if (isCredentialOnlyNode) { if (isCredentialOnlyNode) {
return this.$locale.baseText('nodeCredentials.credentialFor', { return i18n.baseText('nodeCredentials.credentialFor', {
interpolate: { credentialType: this.nodeType?.displayName ?? credentialTypeName }, interpolate: { credentialType: nodeType.value?.displayName ?? credentialTypeName },
}); });
} }
if (!this.showMixedCredentials(credentialType)) { if (!showMixedCredentials(credentialType)) {
return this.$locale.baseText('nodeCredentials.credentialFor', { return i18n.baseText('nodeCredentials.credentialFor', {
interpolate: { credentialType: credentialTypeName }, interpolate: { credentialType: credentialTypeName },
}); });
} }
return this.$locale.baseText('nodeCredentials.credentialsLabel'); return i18n.baseText('nodeCredentials.credentialsLabel');
}, }
},
});
</script> </script>
<template> <template>
@ -516,7 +485,7 @@ export default defineComponent({
v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed" v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed"
:key="credentialTypeDescription.name" :key="credentialTypeDescription.name"
> >
<n8n-input-label <N8nInputLabel
:label="getCredentialsFieldLabel(credentialTypeDescription)" :label="getCredentialsFieldLabel(credentialTypeDescription)"
:bold="false" :bold="false"
size="small" size="small"
@ -524,7 +493,7 @@ export default defineComponent({
data-test-id="credentials-label" data-test-id="credentials-label"
> >
<div v-if="readonly"> <div v-if="readonly">
<n8n-input <N8nInput
:model-value="getSelectedName(credentialTypeDescription.name)" :model-value="getSelectedName(credentialTypeDescription.name)"
disabled disabled
size="small" size="small"
@ -540,7 +509,7 @@ export default defineComponent({
" "
data-test-id="node-credentials-select" data-test-id="node-credentials-select"
> >
<n8n-select <N8nSelect
:model-value="getSelectedId(credentialTypeDescription.name)" :model-value="getSelectedId(credentialTypeDescription.name)"
:placeholder=" :placeholder="
getSelectPlaceholder( getSelectPlaceholder(
@ -557,9 +526,9 @@ export default defineComponent({
showMixedCredentials(credentialTypeDescription), showMixedCredentials(credentialTypeDescription),
) )
" "
@blur="$emit('blur', 'credentials')" @blur="emit('blur', 'credentials')"
> >
<n8n-option <N8nOption
v-for="item in getCredentialOptions( v-for="item in getCredentialOptions(
getAllRelatedCredentialTypes(credentialTypeDescription), getAllRelatedCredentialTypes(credentialTypeDescription),
)" )"
@ -569,24 +538,24 @@ export default defineComponent({
:value="item.id" :value="item.id"
> >
<div :class="[$style.credentialOption, 'mt-2xs', 'mb-2xs']"> <div :class="[$style.credentialOption, 'mt-2xs', 'mb-2xs']">
<n8n-text bold>{{ item.name }}</n8n-text> <N8nText bold>{{ item.name }}</N8nText>
<n8n-text size="small">{{ item.typeDisplayName }}</n8n-text> <N8nText size="small">{{ item.typeDisplayName }}</N8nText>
</div> </div>
</n8n-option> </N8nOption>
<n8n-option <N8nOption
:key="NEW_CREDENTIALS_TEXT" :key="NEW_CREDENTIALS_TEXT"
data-test-id="node-credentials-select-item-new" data-test-id="node-credentials-select-item-new"
:value="NEW_CREDENTIALS_TEXT" :value="NEW_CREDENTIALS_TEXT"
:label="NEW_CREDENTIALS_TEXT" :label="NEW_CREDENTIALS_TEXT"
> >
</n8n-option> </N8nOption>
</n8n-select> </N8nSelect>
<div <div
v-if="getIssues(credentialTypeDescription.name).length && !hideIssues" v-if="getIssues(credentialTypeDescription.name).length && !hideIssues"
:class="$style.warning" :class="$style.warning"
> >
<n8n-tooltip placement="top"> <N8nTooltip placement="top">
<template #content> <template #content>
<TitledList <TitledList
:title="`${$locale.baseText('nodeCredentials.issues')}:`" :title="`${$locale.baseText('nodeCredentials.issues')}:`"
@ -594,7 +563,7 @@ export default defineComponent({
/> />
</template> </template>
<font-awesome-icon icon="exclamation-triangle" /> <font-awesome-icon icon="exclamation-triangle" />
</n8n-tooltip> </N8nTooltip>
</div> </div>
<div <div
@ -613,7 +582,7 @@ export default defineComponent({
/> />
</div> </div>
</div> </div>
</n8n-input-label> </N8nInputLabel>
</div> </div>
</div> </div>
</template> </template>