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,
},
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 },
}) })
}} }}

View file

@ -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>

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,
},
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