diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index f67feb21fb2..b5ef6859364 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -1199,8 +1199,10 @@ "credentialEdit.credentialSharing.unshare.confirm.cancelButtonText": "Cancel", "credentialEdit.credentialSharing.usedIn": "Used in \"{workflowName}\"", "credentialEdit.credentialSharing.usedIn.count": "Used in {count} workflows", - "credentialEdit.credentialSharing.onlyYou": "Only you", - "credentialEdit.credentialSharing.onlyOwner": "Only {name}", + "credentialEdit.credentialSharing.availableToYou": "Available to you", + "credentialEdit.credentialSharing.availableToYou.tooltip": "Only you can use this credential in {project}. Share it to let others in that project use it.", + "credentialEdit.credentialSharing.availableToOwner": "Available to {name}", + "credentialEdit.credentialSharing.availableToOwner.tooltip": "Only {name} can use this credential in {project}.", "credentialEdit.credentialSharing.share": "Share", "credentialEdit.credentialSharing.shareWith": "Share with {project}", "credentialSelectModal.addNewCredential": "Add new credential", diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.test.ts b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.test.ts index 59a7a9284b7..24b94727930 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.test.ts +++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.test.ts @@ -1,5 +1,6 @@ import { createComponentRenderer } from '@/__tests__/render'; import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/vue'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import CredentialSharing from './CredentialSharing.ee.vue'; @@ -9,7 +10,7 @@ import { useSettingsStore } from '@n8n/stores/settings.store'; import { useRolesStore } from '@n8n/stores/roles.store'; import type { ICredentialsResponse } from '../../credentials.types'; import { createEventBus } from '@n8n/utils/event-bus'; -import { getDropdownItems } from '@/__tests__/utils'; +import { getDropdownItems, getTooltip, hoverTooltipTrigger } from '@/__tests__/utils'; import { useI18n } from '@n8n/i18n'; import type * as I18nModule from '@n8n/i18n'; import { ProjectTypes } from '@/features/collaboration/projects/projects.types'; @@ -53,8 +54,12 @@ const mockBaseText = vi.fn((key: string, options?: { interpolate?: Record { projectsStore.personalProject = ownerPersonalProject; }); - it('shows a project the credential is used in but not shared with, with "Only you" and a Share action', () => { + it('shows a project the credential is used in but not shared with, with "Available to you" and a Share action', () => { getDependenciesMock.mockReturnValue({ dependencies: [ { @@ -696,7 +701,7 @@ describe('CredentialSharing.ee', () => { const row = getByTestId('project-sharing-unshared-item'); expect(row).toHaveTextContent('Marketing'); expect(row).toHaveTextContent('Used in "Email summary"'); - expect(row).toHaveTextContent('Only you'); + expect(row).toHaveTextContent('Available to you'); expect(getByTestId('credential-used-in-project-share')).toHaveTextContent( 'Share with Marketing', ); @@ -774,6 +779,75 @@ describe('CredentialSharing.ee', () => { expect(getByTestId('credential-used-in-project-share')).toHaveTextContent(/^\s*Share\s*$/); }); + it('explains in a tooltip that only the owner can use the credential in the project', async () => { + getDependenciesMock.mockReturnValue({ + dependencies: [ + { + id: 'wf-1', + name: 'Email summary', + type: 'workflowParent', + projectId: 'marketing-project', + }, + ], + inaccessibleCount: 0, + }); + + const credential = createCredential({ + homeProject: ownerPersonalProject, + sharedWithProjects: [], + }); + const { getByText } = renderComponent({ + props: { + credentialId: credential.id, + credentialData: {}, + credentialPermissions: { share: true }, + credential, + modalBus: createEventBus(), + }, + }); + + await hoverTooltipTrigger(getByText('Available to you')); + + await waitFor(() => + expect(getTooltip()).toHaveTextContent('Only you can use this credential in Marketing.'), + ); + }); + + it('still explains the access in a tooltip when the project has no name', async () => { + projectsStore.myProjects = [{ ...marketingProject, name: null }]; + getDependenciesMock.mockReturnValue({ + dependencies: [ + { + id: 'wf-1', + name: 'Email summary', + type: 'workflowParent', + projectId: 'marketing-project', + }, + ], + inaccessibleCount: 0, + }); + + const credential = createCredential({ + homeProject: ownerPersonalProject, + sharedWithProjects: [], + }); + const { getByText } = renderComponent({ + props: { + credentialId: credential.id, + credentialData: {}, + credentialPermissions: { share: true }, + credential, + modalBus: createEventBus(), + }, + }); + + await hoverTooltipTrigger(getByText('Available to you')); + + await waitFor(() => + expect(getTooltip()).toHaveTextContent('Only you can use this credential in'), + ); + }); + it('summarizes as "Used in N workflows" when the credential is used in more than one workflow in the same project', () => { getDependenciesMock.mockReturnValue({ dependencies: [ @@ -943,7 +1017,7 @@ describe('CredentialSharing.ee', () => { expect(queryByTestId('project-sharing-unshared-item')).not.toBeInTheDocument(); }); - it('shows "Only {owner}" instead of "Only you" when the viewer has share permission but is not the credential\'s owner (e.g. an instance admin)', () => { + it('shows "Available to {owner}" instead of "Available to you" when the viewer has share permission but is not the credential\'s owner (e.g. an instance admin)', () => { // Viewer's own personal project differs from the credential's home // project, even though they can still share it (broad admin scope). projectsStore.personalProject = { @@ -984,9 +1058,9 @@ describe('CredentialSharing.ee', () => { }); const row = getByTestId('project-sharing-unshared-item'); - expect(row).toHaveTextContent('Only Mona'); - expect(row).not.toHaveTextContent('Only Mona Pfeffer'); - expect(queryByText('Only you')).not.toBeInTheDocument(); + expect(row).toHaveTextContent('Available to Mona'); + expect(row).not.toHaveTextContent('Available to Mona Pfeffer'); + expect(queryByText('Available to you')).not.toBeInTheDocument(); expect(getByTestId('credential-used-in-project-share')).toBeInTheDocument(); }); }); diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.vue b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.vue index f0548dd8a75..004148a4909 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.vue +++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialEdit/CredentialSharing.ee.vue @@ -26,7 +26,7 @@ import { getResourcePermissions } from '@n8n/permissions'; import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag'; import { useDependencies } from '@/app/composables/useDependencies'; -import { N8nButton, N8nEmptyState, N8nInfoTip, N8nText } from '@n8n/design-system'; +import { N8nButton, N8nEmptyState, N8nInfoTip, N8nText, N8nTooltip } from '@n8n/design-system'; type Props = { credentialId: string; credentialData: ICredentialDataDecryptedObject; @@ -203,8 +203,8 @@ const usedInProjects = computed(() => { const usedInAccessText = computed(() => isOwnedByViewer.value - ? i18n.baseText('credentialEdit.credentialSharing.onlyYou') - : i18n.baseText('credentialEdit.credentialSharing.onlyOwner', { + ? i18n.baseText('credentialEdit.credentialSharing.availableToYou') + : i18n.baseText('credentialEdit.credentialSharing.availableToOwner', { interpolate: { name: credentialOwnerFirstName.value }, }), ); @@ -220,6 +220,18 @@ function usedInShareLabel(project: Pick) { : i18n.baseText('credentialEdit.credentialSharing.share'); } +function usedInAccessTooltip(project: Pick) { + const projectName = project.name ?? ''; + + return isOwnedByViewer.value + ? i18n.baseText('credentialEdit.credentialSharing.availableToYou.tooltip', { + interpolate: { project: projectName }, + }) + : i18n.baseText('credentialEdit.credentialSharing.availableToOwner.tooltip', { + interpolate: { name: credentialOwnerFirstName.value, project: projectName }, + }); +} + function shareUsedInProject(projectId: string) { const project = projectsStore.myProjects.find((p) => p.id === projectId); if (!project) return; @@ -314,9 +326,11 @@ function goToUpgrade() { >