Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(editor): Add back credential type icon (no-changelog) #9704

Merged
merged 10 commits into from
Jun 11, 2024
5 changes: 5 additions & 0 deletions cypress/e2e/39-projects.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ describe('Projects', { disableAutoLogin: true }, () => {

credentialsModal.actions.close();
credentialsPage.getters.credentialCards().should('have.length', 1);
credentialsPage.getters
.credentialCards()
.first()
.find('.n8n-node-icon img')
.should('be.visible');

projects.getProjectTabWorkflows().click();
workflowsPage.getters.workflowCards().should('have.length', 1);
Expand Down
2 changes: 1 addition & 1 deletion packages/editor-ui/src/components/CredentialCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function moveResource() {
<template>
<n8n-card :class="$style.cardLink" @click="onClick">
<template #prepend>
<CredentialIcon :credential-type-name="credentialType ? credentialType.name : ''" />
<CredentialIcon :credential-type-name="credentialType?.name ?? ''" />
</template>
<template #header>
<n8n-heading tag="h2" bold :class="$style.cardHeading">
Expand Down
162 changes: 75 additions & 87 deletions packages/editor-ui/src/components/CredentialIcon.vue
Original file line number Diff line number Diff line change
@@ -1,100 +1,88 @@
<template>
<div>
<img v-if="filePath" :class="$style.credIcon" :src="filePath" />
<NodeIcon v-else-if="relevantNode" :node-type="relevantNode" :size="28" />
<span v-else :class="$style.fallback"></span>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';

<script setup lang="ts">
import { computed } from 'vue';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
import type { ICredentialType } from 'n8n-workflow';
import NodeIcon from '@/components/NodeIcon.vue';
import { getThemedValue } from '@/utils/nodeTypesUtils';
import { useUIStore } from '@/stores/ui.store';

export default defineComponent({
components: {
NodeIcon,
},
props: {
credentialTypeName: {
type: String,
},
},
computed: {
...mapStores(useCredentialsStore, useNodeTypesStore, useRootStore, useUIStore),
credentialWithIcon(): ICredentialType | null {
return this.credentialTypeName ? this.getCredentialWithIcon(this.credentialTypeName) : null;
},

filePath(): string | null {
const themeIconUrl = getThemedValue(
this.credentialWithIcon?.iconUrl,
this.uiStore.appliedTheme,
);

if (!themeIconUrl) {
return null;
}

return this.rootStore.getBaseUrl + themeIconUrl;
},

relevantNode(): INodeTypeDescription | null {
const icon = this.credentialWithIcon?.icon;
if (typeof icon === 'string' && icon.startsWith('node:')) {
const nodeType = icon.replace('node:', '');
return this.nodeTypesStore.getNodeType(nodeType);
}
if (!this.credentialTypeName) {
return null;
}

const nodesWithAccess = this.credentialsStore.getNodesWithAccess(this.credentialTypeName);
if (nodesWithAccess.length) {
return nodesWithAccess[0];
}

return null;
},
},
methods: {
getCredentialWithIcon(name: string | null): ICredentialType | null {
if (!name) {
return null;
}

const type = this.credentialsStore.getCredentialTypeByName(name);

if (!type) {
return null;
}

if (type.icon || type.iconUrl) {
return type;
}

if (type.extends) {
let parentCred = null;
type.extends.forEach((name) => {
parentCred = this.getCredentialWithIcon(name);
if (parentCred !== null) return;
});
return parentCred;
}

return null;
},
},
const props = defineProps<{
credentialTypeName: string | null;
}>();

const credentialsStore = useCredentialsStore();
const nodeTypesStore = useNodeTypesStore();
const rootStore = useRootStore();
const uiStore = useUIStore();

const credentialWithIcon = computed(() => getCredentialWithIcon(props.credentialTypeName));

const filePath = computed(() => {
const themeIconUrl = getThemedValue(credentialWithIcon.value?.iconUrl, uiStore.appliedTheme);

if (!themeIconUrl) {
return null;
}

return rootStore.getBaseUrl + themeIconUrl;
});

const relevantNode = computed(() => {
const icon = credentialWithIcon.value?.icon;
if (typeof icon === 'string' && icon.startsWith('node:')) {
const nodeType = icon.replace('node:', '');
return nodeTypesStore.getNodeType(nodeType);
}
if (!props.credentialTypeName) {
return null;
}

const nodesWithAccess = credentialsStore.getNodesWithAccess(props.credentialTypeName);
if (nodesWithAccess.length) {
return nodesWithAccess[0];
}

return null;
});

function getCredentialWithIcon(name: string | null): ICredentialType | null {
if (!name) {
return null;
}

const type = credentialsStore.getCredentialTypeByName(name);

if (!type) {
return null;
}

if (type.icon ?? type.iconUrl) {
return type;
}

if (type.extends) {
let parentCred = null;
type.extends.forEach((iconName) => {
parentCred = getCredentialWithIcon(iconName);
if (parentCred !== null) return;
});
return parentCred;
}

return null;
}
</script>

<template>
<div>
<img v-if="filePath" :class="$style.credIcon" :src="filePath" />
<NodeIcon v-else-if="relevantNode" :node-type="relevantNode" :size="28" />
<span v-else :class="$style.fallback"></span>
</div>
</template>

<style lang="scss" module>
.credIcon {
height: 26px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export type IResource = {
createdAt?: string;
homeProject?: ProjectSharingData;
scopes?: Scope[];
type?: string;
sharedWithProjects?: ProjectSharingData[];
};

interface IFilters {
Expand Down
80 changes: 80 additions & 0 deletions packages/editor-ui/src/views/CredentialsView.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing';
import type { Scope } from '@n8n/permissions';
import { useCredentialsStore } from '@/stores/credentials.store';
import type { ProjectSharingData } from '@/types/projects.types';
import CredentialsView from '@/views/CredentialsView.vue';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';

vi.mock('@/components/layouts/ResourcesListLayout.vue', async (importOriginal) => {
const original = await importOriginal<typeof ResourcesListLayout>();
return {
default: {
...original.default,
render: vi.fn(),
setup: vi.fn(),
},
};
});

const renderComponent = createComponentRenderer(CredentialsView);

describe('CredentialsView', () => {
describe('with fake stores', () => {
let credentialsStore: ReturnType<typeof useCredentialsStore>;

beforeEach(() => {
createTestingPinia();
credentialsStore = useCredentialsStore();
});

afterAll(() => {
vi.resetAllMocks();
});
it('should have ResourcesListLayout render with necessary keys from credential object', () => {
const homeProject: ProjectSharingData = {
id: '1',
name: 'test',
type: 'personal',
createdAt: '2021-05-05T00:00:00Z',
updatedAt: '2021-05-05T00:00:00Z',
};
const scopes: Scope[] = ['credential:move', 'credential:delete'];
const sharedWithProjects: ProjectSharingData[] = [
{
id: '2',
name: 'test 2',
type: 'personal',
createdAt: '2021-05-05T00:00:00Z',
updatedAt: '2021-05-05T00:00:00Z',
},
];
vi.spyOn(credentialsStore, 'allCredentials', 'get').mockReturnValue([
{
id: '1',
name: 'test',
type: 'test',
createdAt: '2021-05-05T00:00:00Z',
updatedAt: '2021-05-05T00:00:00Z',
homeProject,
scopes,
sharedWithProjects,
},
]);
renderComponent();
expect(ResourcesListLayout.setup).toHaveBeenCalledWith(
expect.objectContaining({
resources: [
expect.objectContaining({
type: 'test',
homeProject,
scopes,
sharedWithProjects,
}),
],
}),
null,
);
});
});
});
2 changes: 2 additions & 0 deletions packages/editor-ui/src/views/CredentialsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export default defineComponent({
createdAt: credential.createdAt,
homeProject: credential.homeProject,
scopes: credential.scopes,
type: credential.type,
sharedWithProjects: credential.sharedWithProjects,
}));
},
allCredentialTypes(): ICredentialType[] {
Expand Down
Loading