mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
fix(editor): Follow up fixes and improvements to viewer role (#10684)
This commit is contained in:
parent
efa5573278
commit
63548e6ead
|
@ -85,4 +85,10 @@ describe('CredentialCard', () => {
|
|||
}
|
||||
expect(actions).toHaveTextContent('Move');
|
||||
});
|
||||
|
||||
it('should set readOnly variant based on prop', () => {
|
||||
const { getByRole } = renderComponent({ props: { readOnly: true } });
|
||||
const heading = getByRole('heading');
|
||||
expect(heading).toHaveTextContent('Read only');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -138,16 +138,19 @@ function moveResource() {
|
|||
<template #header>
|
||||
<n8n-heading tag="h2" bold :class="$style.cardHeading">
|
||||
{{ data.name }}
|
||||
<N8nBadge v-if="readOnly" class="ml-3xs" theme="tertiary" bold>
|
||||
{{ locale.baseText('credentials.item.readonly') }}
|
||||
</N8nBadge>
|
||||
</n8n-heading>
|
||||
</template>
|
||||
<div :class="$style.cardDescription">
|
||||
<n8n-text color="text-light" size="small">
|
||||
<span v-if="credentialType">{{ credentialType.displayName }} | </span>
|
||||
<span v-show="data"
|
||||
>{{ $locale.baseText('credentials.item.updated') }} <TimeAgo :date="data.updatedAt" /> |
|
||||
>{{ locale.baseText('credentials.item.updated') }} <TimeAgo :date="data.updatedAt" /> |
|
||||
</span>
|
||||
<span v-show="data"
|
||||
>{{ $locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }}
|
||||
>{{ locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }}
|
||||
</span>
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
|
|
@ -407,7 +407,7 @@ async function beforeClose() {
|
|||
},
|
||||
);
|
||||
keepEditing = confirmAction === MODAL_CONFIRM;
|
||||
} else if (isOAuthType.value && !isOAuthConnected.value) {
|
||||
} else if (credentialPermissions.value.update && isOAuthType.value && !isOAuthConnected.value) {
|
||||
const confirmAction = await message.confirm(
|
||||
i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.message'),
|
||||
i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline'),
|
||||
|
|
|
@ -57,7 +57,7 @@ describe('WorkflowCard', () => {
|
|||
it('should render a card with the workflow name and open workflow clicking on it', async () => {
|
||||
const data = createWorkflow();
|
||||
const { getByRole } = renderComponent({ props: { data } });
|
||||
const cardTitle = getByRole('heading', { level: 2, name: data.name });
|
||||
const cardTitle = getByRole('heading', { level: 2, name: new RegExp(data.name) });
|
||||
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
|
||||
|
@ -166,4 +166,12 @@ describe('WorkflowCard', () => {
|
|||
}
|
||||
expect(actions).toHaveTextContent('Move');
|
||||
});
|
||||
|
||||
it('should show Read only mode', async () => {
|
||||
const data = createWorkflow();
|
||||
const { getByRole } = renderComponent({ props: { data } });
|
||||
|
||||
const heading = getByRole('heading');
|
||||
expect(heading).toHaveTextContent('Read only');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -236,16 +236,19 @@ function moveResource() {
|
|||
<template #header>
|
||||
<n8n-heading tag="h2" bold :class="$style.cardHeading" data-test-id="workflow-card-name">
|
||||
{{ data.name }}
|
||||
<N8nBadge v-if="!workflowPermissions.update" class="ml-3xs" theme="tertiary" bold>
|
||||
{{ locale.baseText('workflows.item.readonly') }}
|
||||
</N8nBadge>
|
||||
</n8n-heading>
|
||||
</template>
|
||||
<div :class="$style.cardDescription">
|
||||
<n8n-text color="text-light" size="small">
|
||||
<span v-show="data"
|
||||
>{{ $locale.baseText('workflows.item.updated') }}
|
||||
>{{ locale.baseText('workflows.item.updated') }}
|
||||
<TimeAgo :date="String(data.updatedAt)" /> |
|
||||
</span>
|
||||
<span v-show="data" class="mr-2xs"
|
||||
>{{ $locale.baseText('workflows.item.created') }} {{ formattedCreatedAtDate }}
|
||||
>{{ locale.baseText('workflows.item.created') }} {{ formattedCreatedAtDate }}
|
||||
</span>
|
||||
<span
|
||||
v-if="settingsStore.areTagsEnabled && data.tags && data.tags.length > 0"
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import { describe, expect } from 'vitest';
|
||||
import { render } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { createPinia, PiniaVuePlugin, setActivePinia } from 'pinia';
|
||||
import { createRouter, createWebHistory, RouterLink } from 'vue-router';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { randomInt, type ExecutionSummary } from 'n8n-workflow';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import WorkflowExecutionsPreview from '@/components/executions/workflow/WorkflowExecutionsPreview.vue';
|
||||
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||
import { i18nInstance, I18nPlugin } from '@/plugins/i18n';
|
||||
import { FontAwesomePlugin } from '@/plugins/icons';
|
||||
import { GlobalComponentsPlugin } from '@/plugins/components';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { ExecutionSummaryWithScopes, IWorkflowDb } from '@/Interface';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
|
||||
let pinia: ReturnType<typeof createPinia>;
|
||||
|
||||
|
@ -64,6 +61,19 @@ const executionDataFactory = (): ExecutionSummaryWithScopes => ({
|
|||
scopes: ['workflow:update'],
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowExecutionsPreview, {
|
||||
global: {
|
||||
stubs: {
|
||||
// UN STUB router-link
|
||||
'router-link': RouterLink,
|
||||
},
|
||||
plugins: [router],
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('WorkflowExecutionsPreview.vue', () => {
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
|
@ -93,30 +103,26 @@ describe('WorkflowExecutionsPreview.vue', () => {
|
|||
|
||||
vi.spyOn(workflowsStore, 'getWorkflowById').mockReturnValue({ scopes } as IWorkflowDb);
|
||||
|
||||
// Not using createComponentRenderer helper here because this component should not stub `router-link`
|
||||
const { getByTestId } = render(WorkflowExecutionsPreview, {
|
||||
props: {
|
||||
execution: executionData,
|
||||
},
|
||||
global: {
|
||||
plugins: [
|
||||
I18nPlugin,
|
||||
i18nInstance,
|
||||
PiniaVuePlugin,
|
||||
FontAwesomePlugin,
|
||||
GlobalComponentsPlugin,
|
||||
pinia,
|
||||
router,
|
||||
],
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
},
|
||||
});
|
||||
const { getByTestId } = renderComponent({ props: { execution: executionData } });
|
||||
|
||||
await userEvent.click(getByTestId('execution-debug-button'));
|
||||
|
||||
expect(router.currentRoute.value.path).toBe(path);
|
||||
},
|
||||
);
|
||||
|
||||
it('disables the stop execution button when the user cannot update', () => {
|
||||
settingsStore.settings.enterprise = {
|
||||
...(settingsStore.settings.enterprise ?? {}),
|
||||
};
|
||||
vi.spyOn(workflowsStore, 'getWorkflowById').mockReturnValue({
|
||||
scopes: undefined,
|
||||
} as IWorkflowDb);
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
props: { execution: { ...executionData, status: 'running' } },
|
||||
});
|
||||
|
||||
expect(getByTestId('stop-execution')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -120,7 +120,13 @@ function onRetryButtonBlur(event: FocusEvent) {
|
|||
<N8nText :class="$style.runningMessage" color="text-light">
|
||||
{{ locale.baseText('executionDetails.runningMessage') }}
|
||||
</N8nText>
|
||||
<N8nButton class="mt-l" type="tertiary" @click="handleStopClick">
|
||||
<N8nButton
|
||||
data-test-id="stop-execution"
|
||||
class="mt-l"
|
||||
type="tertiary"
|
||||
:disabled="!workflowPermissions.execute"
|
||||
@click="handleStopClick"
|
||||
>
|
||||
{{ locale.baseText('executionsList.stopExecution') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
|
|
|
@ -600,6 +600,7 @@
|
|||
"credentials.item.updated": "Last updated",
|
||||
"credentials.item.created": "Created",
|
||||
"credentials.item.owner": "Owner",
|
||||
"credentials.item.readonly": "Read only",
|
||||
"credentials.search.placeholder": "Search credentials...",
|
||||
"credentials.filters.type": "Type",
|
||||
"credentials.filters.active": "Some credentials may be hidden since filters are applied.",
|
||||
|
@ -2206,6 +2207,7 @@
|
|||
"workflows.item.move": "Move",
|
||||
"workflows.item.updated": "Last updated",
|
||||
"workflows.item.created": "Created",
|
||||
"workflows.item.readonly": "Read only",
|
||||
"workflows.search.placeholder": "Search workflows...",
|
||||
"workflows.filters": "Filters",
|
||||
"workflows.filters.tags": "Tags",
|
||||
|
|
|
@ -76,5 +76,40 @@ describe('CredentialsView', () => {
|
|||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable cards based on permissions', () => {
|
||||
vi.spyOn(credentialsStore, 'allCredentials', 'get').mockReturnValue([
|
||||
{
|
||||
id: '1',
|
||||
name: 'test',
|
||||
type: 'test',
|
||||
createdAt: '2021-05-05T00:00:00Z',
|
||||
updatedAt: '2021-05-05T00:00:00Z',
|
||||
scopes: ['credential:update'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'test2',
|
||||
type: 'test2',
|
||||
createdAt: '2021-05-05T00:00:00Z',
|
||||
updatedAt: '2021-05-05T00:00:00Z',
|
||||
},
|
||||
]);
|
||||
|
||||
renderComponent();
|
||||
expect(ResourcesListLayout.setup).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resources: [
|
||||
expect.objectContaining({
|
||||
readOnly: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
readOnly: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -57,6 +57,7 @@ export default defineComponent({
|
|||
scopes: credential.scopes,
|
||||
type: credential.type,
|
||||
sharedWithProjects: credential.sharedWithProjects,
|
||||
readOnly: !getResourcePermissions(credential.scopes).credential.update,
|
||||
}));
|
||||
},
|
||||
allCredentialTypes(): ICredentialType[] {
|
||||
|
@ -179,7 +180,12 @@ export default defineComponent({
|
|||
</div>
|
||||
</template>
|
||||
<template #default="{ data }">
|
||||
<CredentialCard data-test-id="resources-list-item" class="mb-2xs" :data="data" />
|
||||
<CredentialCard
|
||||
data-test-id="resources-list-item"
|
||||
class="mb-2xs"
|
||||
:data="data"
|
||||
:read-only="data.readOnly"
|
||||
/>
|
||||
</template>
|
||||
<template #filters="{ setKeyValue }">
|
||||
<div class="mb-s">
|
||||
|
|
Loading…
Reference in a new issue