mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
refactor(editor): Convert credential related components to composition API (no-changelog) (#10530)
This commit is contained in:
parent
405c55a1f7
commit
402a8b40c0
|
@ -1130,10 +1130,7 @@ function resetCredentialData(): void {
|
|||
/>
|
||||
</div>
|
||||
<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
|
||||
<CredentialInfo
|
||||
:current-credential="currentCredential"
|
||||
:credential-permissions="credentialPermissions"
|
||||
/>
|
||||
<CredentialInfo :current-credential="currentCredential" />
|
||||
</div>
|
||||
<div v-else-if="activeTab.startsWith('coming-soon')" :class="$style.mainContent">
|
||||
<FeatureComingSoon :feature-id="activeTab.split('/')[1]"></FeatureComingSoon>
|
||||
|
|
|
@ -1,57 +1,52 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
<script setup lang="ts">
|
||||
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({
|
||||
name: 'CredentialInfo',
|
||||
components: {
|
||||
TimeAgo,
|
||||
},
|
||||
props: ['currentCredential', 'credentialPermissions'],
|
||||
methods: {
|
||||
shortNodeType(nodeType: INodeTypeDescription) {
|
||||
return this.$locale.shortNodeType(nodeType.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
type Props = {
|
||||
currentCredential: ICredentialsResponse | ICredentialsDecryptedResponse | null;
|
||||
};
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const i18n = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.created') }}
|
||||
</n8n-text>
|
||||
<N8nText :compact="true" :bold="true">
|
||||
{{ i18n.baseText('credentialEdit.credentialInfo.created') }}
|
||||
</N8nText>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<n8n-text :compact="true"
|
||||
<N8nText :compact="true"
|
||||
><TimeAgo :date="currentCredential.createdAt" :capitalize="true"
|
||||
/></n8n-text>
|
||||
/></N8nText>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.lastModified') }}
|
||||
</n8n-text>
|
||||
<N8nText :compact="true" :bold="true">
|
||||
{{ i18n.baseText('credentialEdit.credentialInfo.lastModified') }}
|
||||
</N8nText>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<n8n-text :compact="true"
|
||||
<N8nText :compact="true"
|
||||
><TimeAgo :date="currentCredential.updatedAt" :capitalize="true"
|
||||
/></n8n-text>
|
||||
/></N8nText>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.id') }}
|
||||
</n8n-text>
|
||||
<N8nText :compact="true" :bold="true">
|
||||
{{ i18n.baseText('credentialEdit.credentialInfo.id') }}
|
||||
</N8nText>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<n8n-text :compact="true">{{ currentCredential.id }}</n8n-text>
|
||||
<N8nText :compact="true">{{ currentCredential.id }}</N8nText>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
|
|
@ -1,162 +1,114 @@
|
|||
<script lang="ts">
|
||||
import type {
|
||||
ICredentialsResponse,
|
||||
ICredentialsDecryptedResponse,
|
||||
IUserListAction,
|
||||
} from '@/Interface';
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { useMessage } from '@/composables/useMessage';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
<script setup lang="ts">
|
||||
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { EnterpriseEditionFeature } from '@/constants';
|
||||
import type { ICredentialsDecryptedResponse, ICredentialsResponse } from '@/Interface';
|
||||
import type { PermissionsRecord } from '@/permissions';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { useRolesStore } from '@/stores/roles.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.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 { useUsersStore } from '@/stores/users.store';
|
||||
import type { ProjectListItem, ProjectSharingData } 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 { 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({
|
||||
name: 'CredentialSharing',
|
||||
components: {
|
||||
ProjectSharing,
|
||||
},
|
||||
props: {
|
||||
credential: {
|
||||
type: Object as PropType<ICredentialsResponse | ICredentialsDecryptedResponse | null>,
|
||||
default: null,
|
||||
},
|
||||
credentialId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
credentialData: {
|
||||
type: Object as PropType<ICredentialDataDecryptedObject>,
|
||||
required: true,
|
||||
},
|
||||
credentialPermissions: {
|
||||
type: Object as PropType<PermissionsRecord['credential']>,
|
||||
required: true,
|
||||
},
|
||||
modalBus: {
|
||||
type: Object as PropType<EventBus>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup() {
|
||||
return {
|
||||
...useMessage(),
|
||||
type Props = {
|
||||
credentialId: string;
|
||||
credentialData: ICredentialDataDecryptedObject;
|
||||
credentialPermissions: PermissionsRecord['credential'];
|
||||
credential?: ICredentialsResponse | ICredentialsDecryptedResponse | null;
|
||||
modalBus: EventBus;
|
||||
};
|
||||
},
|
||||
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 ?? '');
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), { credential: null });
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ProjectSharingData[]];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
const uiStore = useUIStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const rolesStore = useRolesStore();
|
||||
|
||||
const sharedWithProjects = ref([...(props.credential?.sharedWithProjects ?? [])]);
|
||||
|
||||
const isSharingEnabled = computed(
|
||||
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing],
|
||||
);
|
||||
const credentialOwnerName = computed(() => {
|
||||
const { firstName, lastName, email } = splitName(props.credential?.homeProject?.name ?? '');
|
||||
return firstName || lastName ? `${firstName}${lastName ? ' ' + lastName : ''}` : email ?? '';
|
||||
},
|
||||
credentialDataHomeProject(): ProjectSharingData | undefined {
|
||||
});
|
||||
|
||||
const credentialDataHomeProject = computed<ProjectSharingData | undefined>(() => {
|
||||
const credentialContainsProjectSharingData = (
|
||||
data: ICredentialDataDecryptedObject,
|
||||
): data is { homeProject: ProjectSharingData } => {
|
||||
return 'homeProject' in data;
|
||||
};
|
||||
|
||||
return this.credentialData && credentialContainsProjectSharingData(this.credentialData)
|
||||
? this.credentialData.homeProject
|
||||
return props.credentialData && credentialContainsProjectSharingData(props.credentialData)
|
||||
? props.credentialData.homeProject
|
||||
: undefined;
|
||||
},
|
||||
isCredentialSharedWithCurrentUser(): boolean {
|
||||
if (!Array.isArray(this.credentialData.sharedWithProjects)) return false;
|
||||
|
||||
return this.credentialData.sharedWithProjects.some((sharee) => {
|
||||
return typeof sharee === 'object' && 'id' in sharee
|
||||
? sharee.id === this.usersStore.currentUser?.id
|
||||
: false;
|
||||
});
|
||||
},
|
||||
projects(): ProjectListItem[] {
|
||||
return this.projectsStore.projects.filter(
|
||||
|
||||
const projects = computed<ProjectListItem[]>(() => {
|
||||
return projectsStore.projects.filter(
|
||||
(project) =>
|
||||
project.id !== this.credential?.homeProject?.id &&
|
||||
project.id !== this.credentialDataHomeProject?.id,
|
||||
project.id !== props.credential?.homeProject?.id &&
|
||||
project.id !== credentialDataHomeProject.value?.id,
|
||||
);
|
||||
},
|
||||
homeProject(): ProjectSharingData | undefined {
|
||||
return this.credential?.homeProject ?? this.credentialDataHomeProject;
|
||||
},
|
||||
isHomeTeamProject(): boolean {
|
||||
return this.homeProject?.type === ProjectTypes.Team;
|
||||
},
|
||||
credentialRoleTranslations(): Record<string, string> {
|
||||
});
|
||||
|
||||
const homeProject = computed<ProjectSharingData | undefined>(
|
||||
() => props.credential?.homeProject ?? credentialDataHomeProject.value,
|
||||
);
|
||||
const isHomeTeamProject = computed(() => homeProject.value?.type === ProjectTypes.Team);
|
||||
const credentialRoleTranslations = computed<Record<string, string>>(() => {
|
||||
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,
|
||||
name: this.credentialRoleTranslations[role],
|
||||
name: credentialRoleTranslations.value[role],
|
||||
scopes,
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -164,33 +116,29 @@ export default defineComponent({
|
|||
<div v-if="!isSharingEnabled">
|
||||
<N8nActionBox
|
||||
:heading="
|
||||
$locale.baseText(
|
||||
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title,
|
||||
)
|
||||
i18n.baseText(uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title)
|
||||
"
|
||||
:description="
|
||||
$locale.baseText(
|
||||
i18n.baseText(
|
||||
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.description,
|
||||
)
|
||||
"
|
||||
:button-text="
|
||||
$locale.baseText(
|
||||
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.button,
|
||||
)
|
||||
i18n.baseText(uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.button)
|
||||
"
|
||||
@click:button="goToUpgrade"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<N8nInfoTip v-if="credentialPermissions.share" :bold="false" class="mb-s">
|
||||
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
|
||||
{{ i18n.baseText('credentialEdit.credentialSharing.info.owner') }}
|
||||
</N8nInfoTip>
|
||||
<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 v-else :bold="false" class="mb-s">
|
||||
{{
|
||||
$locale.baseText('credentialEdit.credentialSharing.info.sharee.personal', {
|
||||
i18n.baseText('credentialEdit.credentialSharing.info.sharee.personal', {
|
||||
interpolate: { credentialOwnerName },
|
||||
})
|
||||
}}
|
||||
|
|
|
@ -1,56 +1,60 @@
|
|||
<script lang="ts">
|
||||
import type { ICredentialType } from 'n8n-workflow';
|
||||
import { defineComponent } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import type { ICredentialType, INodeProperties, NodeParameterValue } from 'n8n-workflow';
|
||||
import { computed, ref } from 'vue';
|
||||
import ScopesNotice from '@/components/ScopesNotice.vue';
|
||||
import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { N8nOption, N8nSelect } from 'n8n-design-system';
|
||||
import type { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CredentialsSelect',
|
||||
components: {
|
||||
ScopesNotice,
|
||||
NodeCredentials,
|
||||
},
|
||||
props: [
|
||||
'activeCredentialType',
|
||||
'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 [];
|
||||
type Props = {
|
||||
activeCredentialType: string;
|
||||
parameter: INodeProperties;
|
||||
node?: INodeUi;
|
||||
inputSize?: 'small' | 'large' | 'mini' | 'medium' | 'xlarge';
|
||||
displayValue: NodeParameterValue;
|
||||
isReadOnly: boolean;
|
||||
displayTitle: string;
|
||||
};
|
||||
|
||||
return this.credentialsStore.getScopesByCredentialType(this.activeCredentialType);
|
||||
},
|
||||
supportedCredentialTypes(): ICredentialType[] {
|
||||
return this.allCredentialTypes.filter((c: ICredentialType) => this.isSupported(c.name));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
const selectRef = this.$refs.innerSelect as HTMLElement | undefined;
|
||||
if (selectRef) {
|
||||
selectRef.focus();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
setFocus: [];
|
||||
onBlur: [];
|
||||
credentialSelected: [update: INodeUpdatePropertiesInformation];
|
||||
}>();
|
||||
|
||||
const credentialsStore = useCredentialsStore();
|
||||
|
||||
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
|
||||
* in the `credentialTypes` key in a `credentialsSelect` parameter
|
||||
*/
|
||||
isSupported(name: string): boolean {
|
||||
const supported = this.getSupportedSets(this.parameter.credentialTypes);
|
||||
function isSupported(name: string): boolean {
|
||||
const supported = getSupportedSets(props.parameter.credentialTypes ?? []);
|
||||
|
||||
const checkedCredType = this.credentialsStore.getCredentialTypeByName(name);
|
||||
const checkedCredType = credentialsStore.getCredentialTypeByName(name);
|
||||
if (!checkedCredType) return false;
|
||||
|
||||
for (const property of supported.has) {
|
||||
|
@ -73,14 +77,15 @@ export default defineComponent({
|
|||
// recurse upward until base credential type
|
||||
// e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api
|
||||
return checkedCredType.extends.reduce(
|
||||
(acc: boolean, parentType: string) => acc || this.isSupported(parentType),
|
||||
(acc: boolean, parentType: string) => acc || isSupported(parentType),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
getSupportedSets(credentialTypes: string[]) {
|
||||
}
|
||||
|
||||
function getSupportedSets(credentialTypes: string[]) {
|
||||
return credentialTypes.reduce<{ extends: string[]; has: string[] }>(
|
||||
(acc, cur) => {
|
||||
const _extends = cur.split('extends:');
|
||||
|
@ -101,16 +106,16 @@ export default defineComponent({
|
|||
},
|
||||
{ extends: [], has: [] },
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({ focus });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div :class="$style['parameter-value-container']">
|
||||
<n8n-select
|
||||
ref="innerSelect"
|
||||
<N8nSelect
|
||||
ref="innerSelectRef"
|
||||
:size="inputSize"
|
||||
filterable
|
||||
:model-value="displayValue"
|
||||
|
@ -118,12 +123,12 @@ export default defineComponent({
|
|||
:title="displayTitle"
|
||||
:disabled="isReadOnly"
|
||||
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
|
||||
@focus="$emit('setFocus')"
|
||||
@blur="$emit('onBlur')"
|
||||
@focus="emit('setFocus')"
|
||||
@blur="emit('onBlur')"
|
||||
>
|
||||
<n8n-option
|
||||
<N8nOption
|
||||
v-for="credType in supportedCredentialTypes"
|
||||
:key="credType.name"
|
||||
:value="credType.name"
|
||||
|
@ -135,8 +140,8 @@ export default defineComponent({
|
|||
{{ credType.displayName }}
|
||||
</div>
|
||||
</div>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
<slot name="issues-and-options" />
|
||||
</div>
|
||||
|
||||
|
@ -147,10 +152,11 @@ export default defineComponent({
|
|||
/>
|
||||
<div>
|
||||
<NodeCredentials
|
||||
v-if="node"
|
||||
:node="node"
|
||||
:readonly="isReadOnly"
|
||||
:override-cred-type="node.parameters[parameter.name]"
|
||||
@credential-selected="(updateInformation) => $emit('credentialSelected', updateInformation)"
|
||||
:override-cred-type="node?.parameters[parameter.name]"
|
||||
@credential-selected="(updateInformation) => emit('credentialSelected', updateInformation)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,69 +1,59 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Modal from './Modal.vue';
|
||||
import { CREDENTIAL_SELECT_MODAL_KEY } from '../constants';
|
||||
import { mapStores } from 'pinia';
|
||||
<script setup lang="ts">
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useUIStore } from '@/stores/ui.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 { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { CREDENTIAL_SELECT_MODAL_KEY } from '../constants';
|
||||
import Modal from './Modal.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CredentialsSelectModal',
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
setup() {
|
||||
const externalHooks = useExternalHooks();
|
||||
return {
|
||||
externalHooks,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalBus: createEventBus(),
|
||||
selected: '',
|
||||
loading: true,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const modalBus = ref(createEventBus());
|
||||
const selected = ref('');
|
||||
const loading = ref(true);
|
||||
const selectRef = ref<HTMLSelectElement>();
|
||||
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await this.credentialsStore.fetchCredentialTypes(false);
|
||||
await credentialsStore.fetchCredentialTypes(false);
|
||||
} catch (e) {}
|
||||
this.loading = false;
|
||||
|
||||
loading.value = false;
|
||||
|
||||
setTimeout(() => {
|
||||
const elementRef = this.$refs.select as HTMLSelectElement | undefined;
|
||||
if (elementRef) {
|
||||
elementRef.focus();
|
||||
if (selectRef.value) {
|
||||
selectRef.value.focus();
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useCredentialsStore, useUIStore, useWorkflowsStore),
|
||||
},
|
||||
methods: {
|
||||
onSelect(type: string) {
|
||||
this.selected = type;
|
||||
},
|
||||
openCredentialType() {
|
||||
this.modalBus.emit('close');
|
||||
this.uiStore.openNewCredential(this.selected);
|
||||
});
|
||||
|
||||
function onSelect(type: string) {
|
||||
selected.value = type;
|
||||
}
|
||||
|
||||
function openCredentialType() {
|
||||
modalBus.value.emit('close');
|
||||
uiStore.openNewCredential(selected.value);
|
||||
|
||||
const telemetryPayload = {
|
||||
credential_type: this.selected,
|
||||
credential_type: selected.value,
|
||||
source: 'primary_menu',
|
||||
new_credential: true,
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
};
|
||||
|
||||
this.$telemetry.track('User opened Credential modal', telemetryPayload);
|
||||
void this.externalHooks.run('credentialsSelectModal.openCredentialType', telemetryPayload);
|
||||
},
|
||||
},
|
||||
});
|
||||
telemetry.track('User opened Credential modal', telemetryPayload);
|
||||
void externalHooks.run('credentialsSelectModal.openCredentialType', telemetryPayload);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -86,8 +76,8 @@ export default defineComponent({
|
|||
<div :class="$style.subtitle">
|
||||
{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}
|
||||
</div>
|
||||
<n8n-select
|
||||
ref="select"
|
||||
<N8nSelect
|
||||
ref="selectRef"
|
||||
filterable
|
||||
default-first-option
|
||||
:placeholder="$locale.baseText('credentialSelectModal.searchForApp')"
|
||||
|
@ -99,7 +89,7 @@ export default defineComponent({
|
|||
<template #prefix>
|
||||
<font-awesome-icon icon="search" />
|
||||
</template>
|
||||
<n8n-option
|
||||
<N8nOption
|
||||
v-for="credential in credentialsStore.allCredentialTypes"
|
||||
:key="credential.name"
|
||||
:value="credential.name"
|
||||
|
@ -107,12 +97,12 @@ export default defineComponent({
|
|||
filterable
|
||||
data-test-id="new-credential-type-select-option"
|
||||
/>
|
||||
</n8n-select>
|
||||
</N8nSelect>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<n8n-button
|
||||
<N8nButton
|
||||
:label="$locale.baseText('credentialSelectModal.continue')"
|
||||
float="right"
|
||||
size="large"
|
||||
|
|
|
@ -1,176 +1,158 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type {
|
||||
ICredentialsResponse,
|
||||
INodeUi,
|
||||
INodeUpdatePropertiesInformation,
|
||||
IUser,
|
||||
} from '@/Interface';
|
||||
<script setup lang="ts">
|
||||
import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
|
||||
import type {
|
||||
INodeCredentialDescription,
|
||||
INodeCredentialsDetails,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
NodeParameterValueType,
|
||||
} from 'n8n-workflow';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
import TitledList from '@/components/TitledList.vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { CREDENTIAL_ONLY_NODE_PREFIX, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import { ndvEventBus } from '@/event-bus';
|
||||
import { useCredentialsStore } from '@/stores/credentials.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 {
|
||||
getAllNodeCredentialForAuthType,
|
||||
getAuthTypeForNodeCredential,
|
||||
getMainAuthField,
|
||||
getNodeCredentialForSelectedAuthType,
|
||||
getAllNodeCredentialForAuthType,
|
||||
updateNodeAuthType,
|
||||
isRequiredCredential,
|
||||
updateNodeAuthType,
|
||||
} from '@/utils/nodeTypesUtils';
|
||||
import { assert } from '@/utils/assert';
|
||||
import { ndvEventBus } from '@/event-bus';
|
||||
import {
|
||||
N8nInput,
|
||||
N8nInputLabel,
|
||||
N8nOption,
|
||||
N8nSelect,
|
||||
N8nText,
|
||||
N8nTooltip,
|
||||
} from 'n8n-design-system';
|
||||
|
||||
interface CredentialDropdownOption extends ICredentialsResponse {
|
||||
typeDisplayName: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeCredentials',
|
||||
components: {
|
||||
TitledList,
|
||||
},
|
||||
props: {
|
||||
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() {
|
||||
type Props = {
|
||||
node: INodeUi;
|
||||
overrideCredType?: NodeParameterValueType;
|
||||
readonly?: boolean;
|
||||
showAll?: boolean;
|
||||
hideIssues?: boolean;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
readonly: false,
|
||||
overrideCredType: '',
|
||||
showAll: false,
|
||||
hideIssues: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
credentialSelected: [credential: INodeUpdatePropertiesInformation];
|
||||
valueChanged: [value: { name: string; value: string }];
|
||||
blur: [source: string];
|
||||
}>();
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
const i18n = useI18n();
|
||||
const NEW_CREDENTIALS_TEXT = `- ${i18n.baseText('nodeCredentials.createNew')} -`;
|
||||
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = useNDVStore();
|
||||
const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
|
||||
return {
|
||||
...useToast(),
|
||||
nodeHelpers,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
NEW_CREDENTIALS_TEXT: `- ${this.$locale.baseText('nodeCredentials.createNew')} -`,
|
||||
subscribedToCredentialType: '',
|
||||
listeningForAuthChange: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useCredentialsStore,
|
||||
useNodeTypesStore,
|
||||
useNDVStore,
|
||||
useUIStore,
|
||||
useUsersStore,
|
||||
useWorkflowsStore,
|
||||
),
|
||||
currentUser(): IUser {
|
||||
return this.usersStore.currentUser ?? ({} as IUser);
|
||||
},
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
credentialTypesNodeDescriptionDisplayed(): INodeCredentialDescription[] {
|
||||
return this.credentialTypesNodeDescription.filter((credentialTypeDescription) => {
|
||||
return this.displayCredentials(credentialTypeDescription);
|
||||
});
|
||||
},
|
||||
credentialTypesNodeDescription(): INodeCredentialDescription[] {
|
||||
const credType = this.credentialsStore.getCredentialTypeByName(this.overrideCredType);
|
||||
|
||||
const credentialTypesNodeDescriptionDisplayed = computed(() =>
|
||||
credentialTypesNodeDescription.value.filter((credentialTypeDescription) =>
|
||||
displayCredentials(credentialTypeDescription),
|
||||
),
|
||||
);
|
||||
const credentialTypesNodeDescription = computed(() => {
|
||||
if (typeof props.overrideCredType !== 'string') return [];
|
||||
|
||||
const credType = credentialsStore.getCredentialTypeByName(props.overrideCredType);
|
||||
|
||||
if (credType) return [credType];
|
||||
|
||||
const activeNodeType = this.nodeType;
|
||||
const activeNodeType = nodeType.value;
|
||||
if (activeNodeType?.credentials) {
|
||||
return activeNodeType.credentials;
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
credentialTypeNames() {
|
||||
});
|
||||
|
||||
const credentialTypeNames = computed(() => {
|
||||
const returnData: Record<string, string> = {};
|
||||
for (const credentialTypeName of this.credentialTypesNode) {
|
||||
const credentialType = this.credentialsStore.getCredentialTypeByName(credentialTypeName);
|
||||
for (const credentialTypeName of credentialTypesNode.value) {
|
||||
const credentialType = credentialsStore.getCredentialTypeByName(credentialTypeName);
|
||||
returnData[credentialTypeName] = credentialType
|
||||
? credentialType.displayName
|
||||
: credentialTypeName;
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
selected(): Record<string, INodeCredentialsDetails> {
|
||||
return this.node.credentials ?? {};
|
||||
},
|
||||
nodeType(): INodeTypeDescription | null {
|
||||
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
|
||||
},
|
||||
mainNodeAuthField(): INodeProperties | null {
|
||||
return getMainAuthField(this.nodeType);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'node.parameters': {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(newValue: INodeParameters, oldValue: INodeParameters) {
|
||||
});
|
||||
|
||||
const selected = computed<Record<string, INodeCredentialsDetails>>(
|
||||
() => props.node.credentials ?? {},
|
||||
);
|
||||
const nodeType = computed(() =>
|
||||
nodeTypesStore.getNodeType(props.node.type, props.node.typeVersion),
|
||||
);
|
||||
const mainNodeAuthField = computed(() => getMainAuthField(nodeType.value));
|
||||
watch(
|
||||
() => props.node.parameters,
|
||||
(newValue, oldValue) => {
|
||||
// When active node parameters change, check if authentication type has been changed
|
||||
// and set `subscribedToCredentialType` to corresponding credential type
|
||||
const isActive = this.node.name === this.ndvStore.activeNode?.name;
|
||||
const nodeType = this.nodeType;
|
||||
const isActive = props.node.name === ndvStore.activeNode?.name;
|
||||
// Only do this for active node and if it's listening for auth change
|
||||
if (isActive && nodeType && this.listeningForAuthChange) {
|
||||
if (this.mainNodeAuthField && oldValue && newValue) {
|
||||
const newAuth = newValue[this.mainNodeAuthField.name];
|
||||
if (isActive && nodeType.value && listeningForAuthChange.value) {
|
||||
if (mainNodeAuthField.value && oldValue && newValue) {
|
||||
const newAuth = newValue[mainNodeAuthField.value.name];
|
||||
|
||||
if (newAuth) {
|
||||
const authType =
|
||||
typeof newAuth === 'object' ? JSON.stringify(newAuth) : newAuth.toString();
|
||||
const credentialType = getNodeCredentialForSelectedAuthType(nodeType, authType);
|
||||
const credentialType = getNodeCredentialForSelectedAuthType(nodeType.value, authType);
|
||||
if (credentialType) {
|
||||
this.subscribedToCredentialType = credentialType.name;
|
||||
subscribedToCredentialType.value = credentialType.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Listen for credentials store changes so credential selection can be updated if creds are changed from the modal
|
||||
this.credentialsStore.$onAction(({ name, after, args }) => {
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
credentialsStore.$onAction(({ name, after, args }) => {
|
||||
const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential'];
|
||||
const credentialType = this.subscribedToCredentialType;
|
||||
const credentialType = subscribedToCredentialType.value;
|
||||
if (!credentialType) {
|
||||
return;
|
||||
}
|
||||
|
@ -179,42 +161,40 @@ export default defineComponent({
|
|||
if (!listeningForActions.includes(name)) {
|
||||
return;
|
||||
}
|
||||
const current = this.selected[credentialType];
|
||||
const current = selected.value[credentialType];
|
||||
let credentialsOfType: ICredentialsResponse[] = [];
|
||||
if (this.showAll) {
|
||||
if (this.node) {
|
||||
credentialsOfType = [
|
||||
...(this.credentialsStore.allUsableCredentialsForNode(this.node) || []),
|
||||
];
|
||||
if (props.showAll) {
|
||||
if (props.node) {
|
||||
credentialsOfType = [...(credentialsStore.allUsableCredentialsForNode(props.node) || [])];
|
||||
}
|
||||
} else {
|
||||
credentialsOfType = [
|
||||
...(this.credentialsStore.allUsableCredentialsByType[credentialType] || []),
|
||||
...(credentialsStore.allUsableCredentialsByType[credentialType] || []),
|
||||
];
|
||||
}
|
||||
switch (name) {
|
||||
// new credential was added
|
||||
case 'createNewCredential':
|
||||
if (result) {
|
||||
this.onCredentialSelected(credentialType, (result as ICredentialsResponse).id);
|
||||
onCredentialSelected(credentialType, (result as ICredentialsResponse).id);
|
||||
}
|
||||
break;
|
||||
case 'updateCredential':
|
||||
const updatedCredential = result as ICredentialsResponse;
|
||||
// credential name was changed, update it
|
||||
if (updatedCredential.name !== current.name) {
|
||||
this.onCredentialSelected(credentialType, current.id);
|
||||
onCredentialSelected(credentialType, current.id);
|
||||
}
|
||||
break;
|
||||
case 'deleteCredential':
|
||||
// all credentials were deleted
|
||||
if (credentialsOfType.length === 0) {
|
||||
this.clearSelectedCredential(credentialType);
|
||||
clearSelectedCredential(credentialType);
|
||||
} else {
|
||||
const id = args[0].id;
|
||||
// credential was deleted, select last one added to replace with
|
||||
if (current.id === id) {
|
||||
this.onCredentialSelected(
|
||||
onCredentialSelected(
|
||||
credentialType,
|
||||
credentialsOfType[credentialsOfType.length - 1].id,
|
||||
);
|
||||
|
@ -225,93 +205,83 @@ export default defineComponent({
|
|||
});
|
||||
});
|
||||
|
||||
ndvEventBus.on('credential.createNew', this.onCreateAndAssignNewCredential);
|
||||
},
|
||||
beforeUnmount() {
|
||||
ndvEventBus.off('credential.createNew', this.onCreateAndAssignNewCredential);
|
||||
},
|
||||
methods: {
|
||||
getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] {
|
||||
const credentialIsRequired = this.showMixedCredentials(credentialType);
|
||||
ndvEventBus.on('credential.createNew', onCreateAndAssignNewCredential);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ndvEventBus.off('credential.createNew', onCreateAndAssignNewCredential);
|
||||
});
|
||||
|
||||
function getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] {
|
||||
const credentialIsRequired = showMixedCredentials(credentialType);
|
||||
if (credentialIsRequired) {
|
||||
if (this.mainNodeAuthField) {
|
||||
if (mainNodeAuthField.value) {
|
||||
const credentials = getAllNodeCredentialForAuthType(
|
||||
this.nodeType,
|
||||
this.mainNodeAuthField.name,
|
||||
nodeType.value,
|
||||
mainNodeAuthField.value.name,
|
||||
);
|
||||
return credentials.map((cred) => cred.name);
|
||||
}
|
||||
}
|
||||
return [credentialType.name];
|
||||
},
|
||||
getCredentialOptions(types: string[]): CredentialDropdownOption[] {
|
||||
}
|
||||
|
||||
function getCredentialOptions(types: string[]): CredentialDropdownOption[] {
|
||||
let options: CredentialDropdownOption[] = [];
|
||||
types.forEach((type) => {
|
||||
options = options.concat(
|
||||
this.credentialsStore.allUsableCredentialsByType[type].map(
|
||||
credentialsStore.allUsableCredentialsByType[type].map(
|
||||
(option: ICredentialsResponse) =>
|
||||
({
|
||||
...option,
|
||||
typeDisplayName: this.credentialsStore.getCredentialTypeByName(type)?.displayName,
|
||||
typeDisplayName: credentialsStore.getCredentialTypeByName(type)?.displayName,
|
||||
}) as CredentialDropdownOption,
|
||||
),
|
||||
);
|
||||
});
|
||||
return options;
|
||||
},
|
||||
getSelectedId(type: string) {
|
||||
if (this.isCredentialExisting(type)) {
|
||||
return this.selected[type].id;
|
||||
}
|
||||
|
||||
function getSelectedId(type: string) {
|
||||
if (isCredentialExisting(type)) {
|
||||
return selected.value[type].id;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
getSelectedName(type: string) {
|
||||
return this.selected?.[type]?.name;
|
||||
},
|
||||
getSelectPlaceholder(type: string, issues: string[]) {
|
||||
return issues.length && this.getSelectedName(type)
|
||||
? this.$locale.baseText('nodeCredentials.selectedCredentialUnavailable', {
|
||||
interpolate: { name: this.getSelectedName(type) },
|
||||
}
|
||||
|
||||
function getSelectedName(type: string) {
|
||||
return selected.value?.[type]?.name;
|
||||
}
|
||||
|
||||
function getSelectPlaceholder(type: string, issues: string[]) {
|
||||
return issues.length && getSelectedName(type)
|
||||
? i18n.baseText('nodeCredentials.selectedCredentialUnavailable', {
|
||||
interpolate: { name: getSelectedName(type) },
|
||||
})
|
||||
: this.$locale.baseText('nodeCredentials.selectCredential');
|
||||
},
|
||||
credentialInputWrapperStyle(credentialType: string) {
|
||||
let deductWidth = 0;
|
||||
const styles = {
|
||||
width: '100%',
|
||||
};
|
||||
if (this.getIssues(credentialType).length) {
|
||||
deductWidth += 20;
|
||||
: i18n.baseText('nodeCredentials.selectCredential');
|
||||
}
|
||||
|
||||
if (deductWidth !== 0) {
|
||||
styles.width = `calc(100% - ${deductWidth}px)`;
|
||||
}
|
||||
|
||||
return styles;
|
||||
},
|
||||
|
||||
clearSelectedCredential(credentialType: string) {
|
||||
const node: INodeUi = this.node;
|
||||
function clearSelectedCredential(credentialType: string) {
|
||||
const node = props.node;
|
||||
|
||||
const credentials = {
|
||||
...(node.credentials || {}),
|
||||
...(node.credentials ?? {}),
|
||||
};
|
||||
|
||||
delete credentials[credentialType];
|
||||
|
||||
const updateInformation: INodeUpdatePropertiesInformation = {
|
||||
name: this.node.name,
|
||||
name: props.node.name,
|
||||
properties: {
|
||||
credentials,
|
||||
position: this.node.position,
|
||||
position: props.node.position,
|
||||
},
|
||||
};
|
||||
|
||||
this.$emit('credentialSelected', updateInformation);
|
||||
},
|
||||
emit('credentialSelected', updateInformation);
|
||||
}
|
||||
|
||||
createNewCredential(
|
||||
function createNewCredential(
|
||||
credentialType: string,
|
||||
listenForAuthChange: boolean = false,
|
||||
showAuthOptions = false,
|
||||
|
@ -319,76 +289,76 @@ export default defineComponent({
|
|||
if (listenForAuthChange) {
|
||||
// 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.listeningForAuthChange = true;
|
||||
this.subscribedToCredentialType = credentialType;
|
||||
listeningForAuthChange.value = true;
|
||||
subscribedToCredentialType.value = credentialType;
|
||||
}
|
||||
|
||||
this.uiStore.openNewCredential(credentialType, showAuthOptions);
|
||||
this.$telemetry.track('User opened Credential modal', {
|
||||
uiStore.openNewCredential(credentialType, showAuthOptions);
|
||||
telemetry.track('User opened Credential modal', {
|
||||
credential_type: credentialType,
|
||||
source: 'node',
|
||||
new_credential: true,
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onCreateAndAssignNewCredential({
|
||||
function onCreateAndAssignNewCredential({
|
||||
type,
|
||||
showAuthOptions,
|
||||
}: {
|
||||
type: string;
|
||||
showAuthOptions: boolean;
|
||||
}) {
|
||||
this.createNewCredential(type, true, showAuthOptions);
|
||||
},
|
||||
createNewCredential(type, true, showAuthOptions);
|
||||
}
|
||||
|
||||
onCredentialSelected(
|
||||
function onCredentialSelected(
|
||||
credentialType: string,
|
||||
credentialId: string | null | undefined,
|
||||
showAuthOptions = false,
|
||||
) {
|
||||
const newCredentialOptionSelected = credentialId === this.NEW_CREDENTIALS_TEXT;
|
||||
const newCredentialOptionSelected = credentialId === NEW_CREDENTIALS_TEXT;
|
||||
if (!credentialId || newCredentialOptionSelected) {
|
||||
this.createNewCredential(credentialType, newCredentialOptionSelected, showAuthOptions);
|
||||
createNewCredential(credentialType, newCredentialOptionSelected, showAuthOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$telemetry.track('User selected credential from node modal', {
|
||||
telemetry.track('User selected credential from node modal', {
|
||||
credential_type: credentialType,
|
||||
node_type: this.node.type,
|
||||
...(this.nodeHelpers.hasProxyAuth(this.node) ? { is_service_specific: true } : {}),
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
node_type: props.node.type,
|
||||
...(nodeHelpers.hasProxyAuth(props.node) ? { is_service_specific: true } : {}),
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
credential_id: credentialId,
|
||||
});
|
||||
|
||||
const selectedCredentials = this.credentialsStore.getCredentialById(credentialId);
|
||||
const selectedCredentialsType = this.showAll ? selectedCredentials.type : credentialType;
|
||||
const oldCredentials = this.node.credentials?.[selectedCredentialsType] ?? null;
|
||||
const selectedCredentials = credentialsStore.getCredentialById(credentialId);
|
||||
const selectedCredentialsType = props.showAll ? selectedCredentials.type : credentialType;
|
||||
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 (
|
||||
oldCredentials?.id === null ||
|
||||
(oldCredentials?.id &&
|
||||
!this.credentialsStore.getCredentialByIdAndType(
|
||||
oldCredentials.id,
|
||||
selectedCredentialsType,
|
||||
))
|
||||
!credentialsStore.getCredentialByIdAndType(oldCredentials.id, selectedCredentialsType))
|
||||
) {
|
||||
// update all nodes in the workflow with the same old/invalid credentials
|
||||
this.workflowsStore.replaceInvalidWorkflowCredentials({
|
||||
credentials: selected,
|
||||
workflowsStore.replaceInvalidWorkflowCredentials({
|
||||
credentials: newSelectedCredentials,
|
||||
invalid: oldCredentials,
|
||||
type: selectedCredentialsType,
|
||||
});
|
||||
this.nodeHelpers.updateNodesCredentialsIssues();
|
||||
this.showMessage({
|
||||
title: this.$locale.baseText('nodeCredentials.showMessage.title'),
|
||||
message: this.$locale.baseText('nodeCredentials.showMessage.message', {
|
||||
nodeHelpers.updateNodesCredentialsIssues();
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('nodeCredentials.showMessage.title'),
|
||||
message: i18n.baseText('nodeCredentials.showMessage.message', {
|
||||
interpolate: {
|
||||
oldCredentialName: oldCredentials.name,
|
||||
newCredentialName: selected.name,
|
||||
newCredentialName: newSelectedCredentials.name,
|
||||
},
|
||||
}),
|
||||
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 (this.showAll && this.mainNodeAuthField) {
|
||||
const nodeCredentialDescription = this.nodeType?.credentials?.find(
|
||||
if (props.showAll && mainNodeAuthField.value) {
|
||||
const nodeCredentialDescription = nodeType.value?.credentials?.find(
|
||||
(cred) => cred.name === selectedCredentialsType,
|
||||
);
|
||||
const authOption = getAuthTypeForNodeCredential(this.nodeType, nodeCredentialDescription);
|
||||
const authOption = getAuthTypeForNodeCredential(nodeType.value, nodeCredentialDescription);
|
||||
if (authOption) {
|
||||
updateNodeAuthType(this.node, authOption.value);
|
||||
updateNodeAuthType(props.node, authOption.value);
|
||||
const parameterData = {
|
||||
name: `parameters.${this.mainNodeAuthField.name}`,
|
||||
name: `parameters.${mainNodeAuthField.value.name}`,
|
||||
value: authOption.value,
|
||||
};
|
||||
this.$emit('valueChanged', parameterData);
|
||||
emit('valueChanged', parameterData);
|
||||
}
|
||||
}
|
||||
|
||||
const node: INodeUi = this.node;
|
||||
const node = props.node;
|
||||
|
||||
const credentials = {
|
||||
...(node.credentials ?? {}),
|
||||
[selectedCredentialsType]: selected,
|
||||
[selectedCredentialsType]: newSelectedCredentials,
|
||||
};
|
||||
|
||||
const updateInformation: INodeUpdatePropertiesInformation = {
|
||||
name: this.node.name,
|
||||
name: props.node.name,
|
||||
properties: {
|
||||
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 it is not defined no need to do a proper check
|
||||
return true;
|
||||
}
|
||||
return this.nodeHelpers.displayParameter(
|
||||
this.node.parameters,
|
||||
return nodeHelpers.displayParameter(
|
||||
props.node.parameters,
|
||||
credentialTypeDescription,
|
||||
'',
|
||||
this.node,
|
||||
props.node,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
getIssues(credentialTypeName: string): string[] {
|
||||
const node = this.node;
|
||||
function getIssues(credentialTypeName: string): string[] {
|
||||
const node = props.node;
|
||||
|
||||
if (node.issues?.credentials === undefined) {
|
||||
return [];
|
||||
|
@ -453,58 +423,57 @@ export default defineComponent({
|
|||
return [];
|
||||
}
|
||||
return node.issues.credentials[credentialTypeName];
|
||||
},
|
||||
}
|
||||
|
||||
isCredentialExisting(credentialType: string): boolean {
|
||||
if (!this.node.credentials?.[credentialType]?.id) {
|
||||
function isCredentialExisting(credentialType: string): boolean {
|
||||
if (!props.node.credentials?.[credentialType]?.id) {
|
||||
return false;
|
||||
}
|
||||
const { id } = this.node.credentials[credentialType];
|
||||
const options = this.getCredentialOptions([credentialType]);
|
||||
const { id } = props.node.credentials[credentialType];
|
||||
const options = getCredentialOptions([credentialType]);
|
||||
|
||||
return !!options.find((option: ICredentialsResponse) => option.id === id);
|
||||
},
|
||||
}
|
||||
|
||||
editCredential(credentialType: string): void {
|
||||
const credential = this.node.credentials?.[credentialType];
|
||||
function editCredential(credentialType: string): void {
|
||||
const credential = props.node.credentials?.[credentialType];
|
||||
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,
|
||||
source: 'node',
|
||||
new_credential: false,
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
});
|
||||
this.subscribedToCredentialType = credentialType;
|
||||
},
|
||||
showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
|
||||
const nodeType = this.nodeType;
|
||||
const isRequired = isRequiredCredential(nodeType, credentialType);
|
||||
subscribedToCredentialType.value = credentialType;
|
||||
}
|
||||
|
||||
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node.type || '') && isRequired;
|
||||
},
|
||||
getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
|
||||
function showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
|
||||
const isRequired = isRequiredCredential(nodeType.value, credentialType);
|
||||
|
||||
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(props.node.type ?? '') && isRequired;
|
||||
}
|
||||
|
||||
function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
|
||||
if (credentialType.displayName) return credentialType.displayName;
|
||||
const credentialTypeName = this.credentialTypeNames[credentialType.name];
|
||||
const isCredentialOnlyNode = this.node.type.startsWith(CREDENTIAL_ONLY_NODE_PREFIX);
|
||||
const credentialTypeName = credentialTypeNames.value[credentialType.name];
|
||||
const isCredentialOnlyNode = props.node.type.startsWith(CREDENTIAL_ONLY_NODE_PREFIX);
|
||||
|
||||
if (isCredentialOnlyNode) {
|
||||
return this.$locale.baseText('nodeCredentials.credentialFor', {
|
||||
interpolate: { credentialType: this.nodeType?.displayName ?? credentialTypeName },
|
||||
return i18n.baseText('nodeCredentials.credentialFor', {
|
||||
interpolate: { credentialType: nodeType.value?.displayName ?? credentialTypeName },
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.showMixedCredentials(credentialType)) {
|
||||
return this.$locale.baseText('nodeCredentials.credentialFor', {
|
||||
if (!showMixedCredentials(credentialType)) {
|
||||
return i18n.baseText('nodeCredentials.credentialFor', {
|
||||
interpolate: { credentialType: credentialTypeName },
|
||||
});
|
||||
}
|
||||
return this.$locale.baseText('nodeCredentials.credentialsLabel');
|
||||
},
|
||||
},
|
||||
});
|
||||
return i18n.baseText('nodeCredentials.credentialsLabel');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -516,7 +485,7 @@ export default defineComponent({
|
|||
v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed"
|
||||
:key="credentialTypeDescription.name"
|
||||
>
|
||||
<n8n-input-label
|
||||
<N8nInputLabel
|
||||
:label="getCredentialsFieldLabel(credentialTypeDescription)"
|
||||
:bold="false"
|
||||
size="small"
|
||||
|
@ -524,7 +493,7 @@ export default defineComponent({
|
|||
data-test-id="credentials-label"
|
||||
>
|
||||
<div v-if="readonly">
|
||||
<n8n-input
|
||||
<N8nInput
|
||||
:model-value="getSelectedName(credentialTypeDescription.name)"
|
||||
disabled
|
||||
size="small"
|
||||
|
@ -540,7 +509,7 @@ export default defineComponent({
|
|||
"
|
||||
data-test-id="node-credentials-select"
|
||||
>
|
||||
<n8n-select
|
||||
<N8nSelect
|
||||
:model-value="getSelectedId(credentialTypeDescription.name)"
|
||||
:placeholder="
|
||||
getSelectPlaceholder(
|
||||
|
@ -557,9 +526,9 @@ export default defineComponent({
|
|||
showMixedCredentials(credentialTypeDescription),
|
||||
)
|
||||
"
|
||||
@blur="$emit('blur', 'credentials')"
|
||||
@blur="emit('blur', 'credentials')"
|
||||
>
|
||||
<n8n-option
|
||||
<N8nOption
|
||||
v-for="item in getCredentialOptions(
|
||||
getAllRelatedCredentialTypes(credentialTypeDescription),
|
||||
)"
|
||||
|
@ -569,24 +538,24 @@ export default defineComponent({
|
|||
:value="item.id"
|
||||
>
|
||||
<div :class="[$style.credentialOption, 'mt-2xs', 'mb-2xs']">
|
||||
<n8n-text bold>{{ item.name }}</n8n-text>
|
||||
<n8n-text size="small">{{ item.typeDisplayName }}</n8n-text>
|
||||
<N8nText bold>{{ item.name }}</N8nText>
|
||||
<N8nText size="small">{{ item.typeDisplayName }}</N8nText>
|
||||
</div>
|
||||
</n8n-option>
|
||||
<n8n-option
|
||||
</N8nOption>
|
||||
<N8nOption
|
||||
:key="NEW_CREDENTIALS_TEXT"
|
||||
data-test-id="node-credentials-select-item-new"
|
||||
:value="NEW_CREDENTIALS_TEXT"
|
||||
:label="NEW_CREDENTIALS_TEXT"
|
||||
>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
|
||||
<div
|
||||
v-if="getIssues(credentialTypeDescription.name).length && !hideIssues"
|
||||
:class="$style.warning"
|
||||
>
|
||||
<n8n-tooltip placement="top">
|
||||
<N8nTooltip placement="top">
|
||||
<template #content>
|
||||
<TitledList
|
||||
:title="`${$locale.baseText('nodeCredentials.issues')}:`"
|
||||
|
@ -594,7 +563,7 @@ export default defineComponent({
|
|||
/>
|
||||
</template>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -613,7 +582,7 @@ export default defineComponent({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
</N8nInputLabel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
Loading…
Reference in a new issue