mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -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>
|
||||||
<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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
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(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
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 ?? '';
|
|
||||||
},
|
|
||||||
credentialDataHomeProject(): ProjectSharingData | undefined {
|
|
||||||
const credentialContainsProjectSharingData = (
|
|
||||||
data: ICredentialDataDecryptedObject,
|
|
||||||
): data is { homeProject: ProjectSharingData } => {
|
|
||||||
return 'homeProject' in data;
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.credentialData && credentialContainsProjectSharingData(this.credentialData)
|
const props = withDefaults(defineProps<Props>(), { credential: null });
|
||||||
? this.credentialData.homeProject
|
|
||||||
: undefined;
|
|
||||||
},
|
|
||||||
isCredentialSharedWithCurrentUser(): boolean {
|
|
||||||
if (!Array.isArray(this.credentialData.sharedWithProjects)) return false;
|
|
||||||
|
|
||||||
return this.credentialData.sharedWithProjects.some((sharee) => {
|
const emit = defineEmits<{
|
||||||
return typeof sharee === 'object' && 'id' in sharee
|
'update:modelValue': [value: ProjectSharingData[]];
|
||||||
? sharee.id === this.usersStore.currentUser?.id
|
}>();
|
||||||
: false;
|
|
||||||
});
|
const i18n = useI18n();
|
||||||
},
|
|
||||||
projects(): ProjectListItem[] {
|
const usersStore = useUsersStore();
|
||||||
return this.projectsStore.projects.filter(
|
const uiStore = useUIStore();
|
||||||
(project) =>
|
const settingsStore = useSettingsStore();
|
||||||
project.id !== this.credential?.homeProject?.id &&
|
const projectsStore = useProjectsStore();
|
||||||
project.id !== this.credentialDataHomeProject?.id,
|
const rolesStore = useRolesStore();
|
||||||
);
|
|
||||||
},
|
const sharedWithProjects = ref([...(props.credential?.sharedWithProjects ?? [])]);
|
||||||
homeProject(): ProjectSharingData | undefined {
|
|
||||||
return this.credential?.homeProject ?? this.credentialDataHomeProject;
|
const isSharingEnabled = computed(
|
||||||
},
|
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing],
|
||||||
isHomeTeamProject(): boolean {
|
);
|
||||||
return this.homeProject?.type === ProjectTypes.Team;
|
const credentialOwnerName = computed(() => {
|
||||||
},
|
const { firstName, lastName, email } = splitName(props.credential?.homeProject?.name ?? '');
|
||||||
credentialRoleTranslations(): Record<string, string> {
|
return firstName || lastName ? `${firstName}${lastName ? ' ' + lastName : ''}` : email ?? '';
|
||||||
return {
|
|
||||||
'credential:user': this.$locale.baseText('credentialEdit.credentialSharing.role.user'),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
credentialRoles(): RoleMap['credential'] {
|
|
||||||
return this.rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
|
|
||||||
role,
|
|
||||||
name: this.credentialRoleTranslations[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 credentialDataHomeProject = computed<ProjectSharingData | undefined>(() => {
|
||||||
|
const credentialContainsProjectSharingData = (
|
||||||
|
data: ICredentialDataDecryptedObject,
|
||||||
|
): data is { homeProject: ProjectSharingData } => {
|
||||||
|
return 'homeProject' in data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return props.credentialData && credentialContainsProjectSharingData(props.credentialData)
|
||||||
|
? props.credentialData.homeProject
|
||||||
|
: undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const projects = computed<ProjectListItem[]>(() => {
|
||||||
|
return projectsStore.projects.filter(
|
||||||
|
(project) =>
|
||||||
|
project.id !== props.credential?.homeProject?.id &&
|
||||||
|
project.id !== credentialDataHomeProject.value?.id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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': i18n.baseText('credentialEdit.credentialSharing.role.user'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const credentialRoles = computed<RoleMap['credential']>(() => {
|
||||||
|
return rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
|
||||||
|
role,
|
||||||
|
name: credentialRoleTranslations.value[role],
|
||||||
|
scopes,
|
||||||
|
licensed,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
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 },
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,116 +1,121 @@
|
||||||
<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[] {
|
|
||||||
return this.allCredentialTypes.filter((c: ICredentialType) => this.isSupported(c.name));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
focus() {
|
|
||||||
const selectRef = this.$refs.innerSelect as HTMLElement | undefined;
|
|
||||||
if (selectRef) {
|
|
||||||
selectRef.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);
|
|
||||||
|
|
||||||
const checkedCredType = this.credentialsStore.getCredentialTypeByName(name);
|
const emit = defineEmits<{
|
||||||
if (!checkedCredType) return false;
|
'update:modelValue': [value: string];
|
||||||
|
setFocus: [];
|
||||||
|
onBlur: [];
|
||||||
|
credentialSelected: [update: INodeUpdatePropertiesInformation];
|
||||||
|
}>();
|
||||||
|
|
||||||
for (const property of supported.has) {
|
const credentialsStore = useCredentialsStore();
|
||||||
if (checkedCredType[property as keyof ICredentialType] !== undefined) {
|
|
||||||
// edge case: `httpHeaderAuth` has `authenticate` auth but belongs to generic auth
|
|
||||||
if (name === 'httpHeaderAuth' && property === 'authenticate') continue;
|
|
||||||
|
|
||||||
return true;
|
const innerSelectRef = ref<HTMLSelectElement>();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
const allCredentialTypes = computed(() => credentialsStore.allCredentialTypes);
|
||||||
checkedCredType.extends &&
|
const scopes = computed(() => {
|
||||||
checkedCredType.extends.some((parentType: string) => supported.extends.includes(parentType))
|
if (!props.activeCredentialType) return [];
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkedCredType.extends && supported.extends.length) {
|
return credentialsStore.getScopesByCredentialType(props.activeCredentialType);
|
||||||
// recurse upward until base credential type
|
|
||||||
// e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api
|
|
||||||
return checkedCredType.extends.reduce(
|
|
||||||
(acc: boolean, parentType: string) => acc || this.isSupported(parentType),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
getSupportedSets(credentialTypes: string[]) {
|
|
||||||
return credentialTypes.reduce<{ extends: string[]; has: string[] }>(
|
|
||||||
(acc, cur) => {
|
|
||||||
const _extends = cur.split('extends:');
|
|
||||||
|
|
||||||
if (_extends.length === 2) {
|
|
||||||
acc.extends.push(_extends[1]);
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _has = cur.split('has:');
|
|
||||||
|
|
||||||
if (_has.length === 2) {
|
|
||||||
acc.has.push(_has[1]);
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ extends: [], has: [] },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
function isSupported(name: string): boolean {
|
||||||
|
const supported = getSupportedSets(props.parameter.credentialTypes ?? []);
|
||||||
|
|
||||||
|
const checkedCredType = credentialsStore.getCredentialTypeByName(name);
|
||||||
|
if (!checkedCredType) return false;
|
||||||
|
|
||||||
|
for (const property of supported.has) {
|
||||||
|
if (checkedCredType[property as keyof ICredentialType] !== undefined) {
|
||||||
|
// edge case: `httpHeaderAuth` has `authenticate` auth but belongs to generic auth
|
||||||
|
if (name === 'httpHeaderAuth' && property === 'authenticate') continue;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
checkedCredType.extends &&
|
||||||
|
checkedCredType.extends.some((parentType: string) => supported.extends.includes(parentType))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkedCredType.extends && supported.extends.length) {
|
||||||
|
// recurse upward until base credential type
|
||||||
|
// e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api
|
||||||
|
return checkedCredType.extends.reduce(
|
||||||
|
(acc: boolean, parentType: string) => acc || isSupported(parentType),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSupportedSets(credentialTypes: string[]) {
|
||||||
|
return credentialTypes.reduce<{ extends: string[]; has: string[] }>(
|
||||||
|
(acc, cur) => {
|
||||||
|
const _extends = cur.split('extends:');
|
||||||
|
|
||||||
|
if (_extends.length === 2) {
|
||||||
|
acc.extends.push(_extends[1]);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _has = cur.split('has:');
|
||||||
|
|
||||||
|
if (_has.length === 2) {
|
||||||
|
acc.has.push(_has[1]);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ 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>
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const externalHooks = useExternalHooks();
|
|
||||||
return {
|
|
||||||
externalHooks,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
modalBus: createEventBus(),
|
|
||||||
selected: '',
|
|
||||||
loading: true,
|
|
||||||
CREDENTIAL_SELECT_MODAL_KEY,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
try {
|
|
||||||
await this.credentialsStore.fetchCredentialTypes(false);
|
|
||||||
} catch (e) {}
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
const modalBus = ref(createEventBus());
|
||||||
const elementRef = this.$refs.select as HTMLSelectElement | undefined;
|
const selected = ref('');
|
||||||
if (elementRef) {
|
const loading = ref(true);
|
||||||
elementRef.focus();
|
const selectRef = ref<HTMLSelectElement>();
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useCredentialsStore, useUIStore, useWorkflowsStore),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onSelect(type: string) {
|
|
||||||
this.selected = type;
|
|
||||||
},
|
|
||||||
openCredentialType() {
|
|
||||||
this.modalBus.emit('close');
|
|
||||||
this.uiStore.openNewCredential(this.selected);
|
|
||||||
|
|
||||||
const telemetryPayload = {
|
const credentialsStore = useCredentialsStore();
|
||||||
credential_type: this.selected,
|
const uiStore = useUIStore();
|
||||||
source: 'primary_menu',
|
const workflowsStore = useWorkflowsStore();
|
||||||
new_credential: true,
|
|
||||||
workflow_id: this.workflowsStore.workflowId,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$telemetry.track('User opened Credential modal', telemetryPayload);
|
onMounted(async () => {
|
||||||
void this.externalHooks.run('credentialsSelectModal.openCredentialType', telemetryPayload);
|
try {
|
||||||
},
|
await credentialsStore.fetchCredentialTypes(false);
|
||||||
},
|
} catch (e) {}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (selectRef.value) {
|
||||||
|
selectRef.value.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onSelect(type: string) {
|
||||||
|
selected.value = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCredentialType() {
|
||||||
|
modalBus.value.emit('close');
|
||||||
|
uiStore.openNewCredential(selected.value);
|
||||||
|
|
||||||
|
const telemetryPayload = {
|
||||||
|
credential_type: selected.value,
|
||||||
|
source: 'primary_menu',
|
||||||
|
new_credential: true,
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
};
|
||||||
|
|
||||||
|
telemetry.track('User opened Credential modal', 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"
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue