mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 09:17:08 +02:00
feat(editor): Apply instance redaction floor per-select in workflow settings (#31229)
This commit is contained in:
parent
153f6c47b4
commit
d431710a4c
|
|
@ -3921,8 +3921,7 @@
|
|||
"workflowSettings.helpTexts.redactManualData": "Controls whether execution data from manually triggered executions is redacted.",
|
||||
"workflowSettings.redactionPermissionNotice": "You don't have permission to change data redaction settings.",
|
||||
"workflowSettings.redactionPermissionNotice.viewUsers": "View users with access",
|
||||
"workflowSettings.redactionEnforcementNotice": "Enforced at instance level",
|
||||
"workflowSettings.redactionEnforcementNotice.label": "Enforced at instance level",
|
||||
"workflowSettings.redactionFloorNotice": "This option is enforced by your instance's redaction policy.",
|
||||
"workflowSettings.redactionMembersModal.title": "Users who can edit data redaction settings",
|
||||
"workflowSettings.redactionMembersModal.description": "These users can change data redaction settings. Contact one of them to request a change.",
|
||||
"workflowSettings.saveManualExecutions": "Save manual executions",
|
||||
|
|
|
|||
|
|
@ -1401,55 +1401,21 @@ describe('WorkflowSettingsVue', () => {
|
|||
await flushPromises();
|
||||
|
||||
// No other lock is in play.
|
||||
expect(queryByTestId('workflow-settings-redaction-enforced-lock')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('workflow-settings-redaction-floor-lock')).not.toBeInTheDocument();
|
||||
|
||||
const manualCombobox = within(
|
||||
getByTestId('workflow-settings-redact-manual-select'),
|
||||
).getByRole('combobox');
|
||||
expect(manualCombobox).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows the enforcement lock copy (not the new hint) when instance enforcement is also on', async () => {
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
settingsStore.settings.enterprise[EnterpriseEditionFeature.DataRedaction] = true;
|
||||
settingsStore.settings.envFeatureFlags = {
|
||||
...settingsStore.settings.envFeatureFlags,
|
||||
N8N_ENV_FEAT_REDACTION_ENFORCEMENT: 'true',
|
||||
};
|
||||
getSecuritySettings.mockResolvedValue({
|
||||
...DEFAULT_SECURITY_SETTINGS,
|
||||
redactionEnforcement: { floor: 'production' },
|
||||
});
|
||||
projectsStore.personalProject = mock<Project>({
|
||||
scopes: ['workflow:enableRedaction', 'workflow:disableRedaction'],
|
||||
});
|
||||
const workflow = createTestWorkflow({
|
||||
id: '1',
|
||||
name: 'Test Workflow',
|
||||
active: true,
|
||||
scopes: ['workflow:update', 'workflow:enableRedaction', 'workflow:disableRedaction'],
|
||||
});
|
||||
workflowsListStore.workflowsById = { '1': workflow };
|
||||
workflowsListStore.getWorkflowById.mockImplementation(() => workflow);
|
||||
workflowDocumentStore.setSettings({ redactionPolicy: 'none' });
|
||||
|
||||
const { getAllByTestId, queryByText } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
expect(getAllByTestId('workflow-settings-redaction-enforced-lock')).toHaveLength(2);
|
||||
expect(
|
||||
queryByText(
|
||||
'Manual execution data can only be redacted when production execution data is also redacted.',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('instance enforcement', () => {
|
||||
const setUpEnforcement = (params: {
|
||||
enforced: boolean;
|
||||
describe('instance floor', () => {
|
||||
const setUpFloor = (params: {
|
||||
floor: 'off' | 'production' | 'all';
|
||||
flagEnabled: boolean;
|
||||
hasUpdatePermission?: boolean;
|
||||
redactionPolicy?: 'none' | 'non-manual' | 'manual-only' | 'all';
|
||||
}) => {
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
settingsStore.settings.enterprise[EnterpriseEditionFeature.DataRedaction] = true;
|
||||
|
|
@ -1459,7 +1425,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
};
|
||||
getSecuritySettings.mockResolvedValue({
|
||||
...DEFAULT_SECURITY_SETTINGS,
|
||||
redactionEnforcement: { floor: params.enforced ? 'production' : 'off' },
|
||||
redactionEnforcement: { floor: params.floor },
|
||||
});
|
||||
|
||||
const hasPermission = params.hasUpdatePermission ?? true;
|
||||
|
|
@ -1479,12 +1445,40 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
workflowsListStore.workflowsById = { '1': workflow };
|
||||
workflowsListStore.getWorkflowById.mockImplementation(() => workflow);
|
||||
|
||||
if (params.redactionPolicy) {
|
||||
workflowDocumentStore.setSettings({ redactionPolicy: params.redactionPolicy });
|
||||
}
|
||||
};
|
||||
|
||||
it('locks both redaction selects with enforcement copy when enforcement is on', async () => {
|
||||
setUpEnforcement({ enforced: true, flagEnabled: true });
|
||||
it('locks production select with floor copy under floor "production"; manual stays editable', async () => {
|
||||
setUpFloor({ floor: 'production', flagEnabled: true, redactionPolicy: 'none' });
|
||||
|
||||
const { getByTestId, getAllByTestId, queryByText } = createComponent({ pinia });
|
||||
const { getByTestId, getAllByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
const productionInput = within(
|
||||
getByTestId('workflow-settings-redact-production-select'),
|
||||
).getByRole('combobox');
|
||||
expect(productionInput).toBeDisabled();
|
||||
expect(
|
||||
getByTestId('workflow-settings-redact-production-select').querySelector('input')?.value,
|
||||
).toBe('Redact');
|
||||
|
||||
const floorIcons = getAllByTestId('workflow-settings-redaction-floor-lock');
|
||||
expect(floorIcons).toHaveLength(1);
|
||||
|
||||
// Production was coerced to 'redact', so the IAM-697 rule no longer blocks the manual select.
|
||||
const manualInput = within(getByTestId('workflow-settings-redact-manual-select')).getByRole(
|
||||
'combobox',
|
||||
);
|
||||
expect(manualInput).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('locks both selects with floor copy under floor "all"', async () => {
|
||||
setUpFloor({ floor: 'all', flagEnabled: true, redactionPolicy: 'none' });
|
||||
|
||||
const { getByTestId, getAllByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
const productionInput = within(
|
||||
|
|
@ -1496,18 +1490,18 @@ describe('WorkflowSettingsVue', () => {
|
|||
expect(productionInput).toBeDisabled();
|
||||
expect(manualInput).toBeDisabled();
|
||||
|
||||
const lockIcons = getAllByTestId('workflow-settings-redaction-enforced-lock');
|
||||
expect(lockIcons).toHaveLength(2);
|
||||
expect(
|
||||
getByTestId('workflow-settings-redact-production-select').querySelector('input')?.value,
|
||||
).toBe('Redact');
|
||||
expect(
|
||||
getByTestId('workflow-settings-redact-manual-select').querySelector('input')?.value,
|
||||
).toBe('Redact');
|
||||
|
||||
// Permission-only tooltip copy must not be shown when enforcement is the lock reason.
|
||||
expect(queryByText('View users with access')).not.toBeInTheDocument();
|
||||
expect(getAllByTestId('workflow-settings-redaction-floor-lock')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('leaves both redaction selects editable when enforcement is off', async () => {
|
||||
setUpEnforcement({ enforced: false, flagEnabled: true });
|
||||
// Seed with production=redact so the manual select is editable (the new
|
||||
// workflow-level invariant disables manual when production is default).
|
||||
workflowDocumentStore.setSettings({ redactionPolicy: 'non-manual' });
|
||||
it('leaves both selects editable under floor "off"', async () => {
|
||||
setUpFloor({ floor: 'off', flagEnabled: true, redactionPolicy: 'non-manual' });
|
||||
|
||||
const { getByTestId, queryByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
|
@ -1520,11 +1514,12 @@ describe('WorkflowSettingsVue', () => {
|
|||
);
|
||||
expect(productionInput).not.toBeDisabled();
|
||||
expect(manualInput).not.toBeDisabled();
|
||||
expect(queryByTestId('workflow-settings-redaction-enforced-lock')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('workflow-settings-redaction-floor-lock')).not.toBeInTheDocument();
|
||||
expect(getSecuritySettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores enforcement state when the feature flag is off', async () => {
|
||||
setUpEnforcement({ enforced: true, flagEnabled: false });
|
||||
it('does not apply the floor lock when the feature flag is off', async () => {
|
||||
setUpFloor({ floor: 'all', flagEnabled: false, redactionPolicy: 'non-manual' });
|
||||
|
||||
const { getByTestId, queryByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
|
@ -1532,19 +1527,125 @@ describe('WorkflowSettingsVue', () => {
|
|||
const productionInput = within(
|
||||
getByTestId('workflow-settings-redact-production-select'),
|
||||
).getByRole('combobox');
|
||||
const manualInput = within(getByTestId('workflow-settings-redact-manual-select')).getByRole(
|
||||
'combobox',
|
||||
);
|
||||
expect(productionInput).not.toBeDisabled();
|
||||
expect(queryByTestId('workflow-settings-redaction-enforced-lock')).not.toBeInTheDocument();
|
||||
expect(manualInput).not.toBeDisabled();
|
||||
expect(queryByTestId('workflow-settings-redaction-floor-lock')).not.toBeInTheDocument();
|
||||
expect(getSecuritySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prefers enforcement copy when both enforcement and missing permission would lock', async () => {
|
||||
setUpEnforcement({ enforced: true, flagEnabled: true, hasUpdatePermission: false });
|
||||
it('keeps manual select enabled under floor "production" (production coerced to redact)', async () => {
|
||||
setUpFloor({ floor: 'production', flagEnabled: true, redactionPolicy: 'none' });
|
||||
|
||||
const { getByTestId, queryByText } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
const manualInput = within(getByTestId('workflow-settings-redact-manual-select')).getByRole(
|
||||
'combobox',
|
||||
);
|
||||
expect(manualInput).not.toBeDisabled();
|
||||
// IAM-697 hint must not be shown — production is forced to redact.
|
||||
expect(
|
||||
queryByText(
|
||||
'Manual execution data can only be redacted when production execution data is also redacted.',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the floor lock on manual (not the IAM-697 hint) when floor "all" and policy "none"', async () => {
|
||||
setUpFloor({ floor: 'all', flagEnabled: true, redactionPolicy: 'none' });
|
||||
|
||||
const { getAllByTestId, queryByText } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
expect(getAllByTestId('workflow-settings-redaction-enforced-lock')).toHaveLength(2);
|
||||
expect(queryByText('View users with access')).not.toBeInTheDocument();
|
||||
expect(getAllByTestId('workflow-settings-redaction-floor-lock')).toHaveLength(2);
|
||||
expect(
|
||||
queryByText(
|
||||
'Manual execution data can only be redacted when production execution data is also redacted.',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('persists coerced redactionPolicy on save under floor "production"', async () => {
|
||||
setUpFloor({ floor: 'production', flagEnabled: true, redactionPolicy: 'none' });
|
||||
|
||||
const { getByRole } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
toast.showError.mockClear();
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
expect(toast.showError).not.toHaveBeenCalled();
|
||||
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
settings: expect.objectContaining({ redactionPolicy: 'non-manual' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('persists coerced redactionPolicy on save under floor "all"', async () => {
|
||||
setUpFloor({ floor: 'all', flagEnabled: true, redactionPolicy: 'none' });
|
||||
|
||||
const { getByRole } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
toast.showError.mockClear();
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
expect(toast.showError).not.toHaveBeenCalled();
|
||||
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
settings: expect.objectContaining({ redactionPolicy: 'all' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('fails open when getSecuritySettings rejects (no floor lock, selects editable)', async () => {
|
||||
setUpFloor({ floor: 'production', flagEnabled: true, redactionPolicy: 'non-manual' });
|
||||
// Override the resolved mock with a rejection — the component must swallow the error
|
||||
// and leave instanceRedactionFloor at its default 'off'.
|
||||
getSecuritySettings.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const { getByTestId, queryByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
const productionInput = within(
|
||||
getByTestId('workflow-settings-redact-production-select'),
|
||||
).getByRole('combobox');
|
||||
const manualInput = within(getByTestId('workflow-settings-redact-manual-select')).getByRole(
|
||||
'combobox',
|
||||
);
|
||||
expect(productionInput).not.toBeDisabled();
|
||||
expect(manualInput).not.toBeDisabled();
|
||||
expect(queryByTestId('workflow-settings-redaction-floor-lock')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the permission lock active when the flag is off (no floor lock applies)', async () => {
|
||||
setUpFloor({
|
||||
floor: 'all',
|
||||
flagEnabled: false,
|
||||
hasUpdatePermission: false,
|
||||
redactionPolicy: 'all',
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
const productionInput = within(
|
||||
getByTestId('workflow-settings-redact-production-select'),
|
||||
).getByRole('combobox');
|
||||
const manualInput = within(getByTestId('workflow-settings-redact-manual-select')).getByRole(
|
||||
'combobox',
|
||||
);
|
||||
expect(productionInput).toBeDisabled();
|
||||
expect(manualInput).toBeDisabled();
|
||||
|
||||
// No floor-lock indicator under flag-off (the lock comes from missing permission).
|
||||
expect(queryByTestId('workflow-settings-redaction-floor-lock')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import { useCredentialResolvers } from '@/features/resolvers/composables/useCred
|
|||
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
|
||||
import { useRedactionEnforcementFeatureFlag } from '@/features/redaction-enforcement/composables/useRedactionEnforcementFeatureFlag';
|
||||
import * as securitySettingsApi from '@n8n/rest-api-client/api/security-settings';
|
||||
import type { RedactionFloor } from '@n8n/api-types';
|
||||
import { hasPermission } from '@/app/utils/rbac/permissions';
|
||||
|
||||
import { ElCol, ElRow, ElSwitch } from 'element-plus';
|
||||
|
|
@ -70,7 +71,7 @@ const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
|
|||
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||
const { isEnabled: isCredentialResolverEnabled } = useDynamicCredentials();
|
||||
const { isEnabled: isRedactionEnforcementFlagEnabled } = useRedactionEnforcementFeatureFlag();
|
||||
const isInstanceRedactionEnforced = ref(false);
|
||||
const instanceRedactionFloor = ref<RedactionFloor>('off');
|
||||
const canListCredentialResolvers = hasPermission(['rbac'], {
|
||||
rbac: { scope: 'credentialResolver:list' },
|
||||
});
|
||||
|
|
@ -239,14 +240,25 @@ const isDataRedactionLicensed = computed(
|
|||
|
||||
const isRedactionSettingVisible = computed(() => settingsStore.isModuleActive('redaction'));
|
||||
|
||||
const isRedactionEnforcedByInstance = computed(
|
||||
() => isRedactionEnforcementFlagEnabled.value && isInstanceRedactionEnforced.value,
|
||||
const isProductionRedactionLockedByFloor = computed(
|
||||
() =>
|
||||
isRedactionEnforcementFlagEnabled.value &&
|
||||
(instanceRedactionFloor.value === 'production' || instanceRedactionFloor.value === 'all'),
|
||||
);
|
||||
|
||||
// Enforcement wins over permission — an admin cannot override an instance-level policy.
|
||||
function getRedactionLockReason(currentValue: string): 'enforcement' | 'permission' | null {
|
||||
const isManualRedactionLockedByFloor = computed(
|
||||
() => isRedactionEnforcementFlagEnabled.value && instanceRedactionFloor.value === 'all',
|
||||
);
|
||||
|
||||
type RedactionLockReason = 'floor' | 'permission' | null;
|
||||
|
||||
// Floor lock wins over permission — an admin cannot override an instance-level policy.
|
||||
function resolveRedactionLockReason(
|
||||
currentValue: string,
|
||||
lockedByFloor: boolean,
|
||||
): RedactionLockReason {
|
||||
if (!isDataRedactionLicensed.value) return null;
|
||||
if (isRedactionEnforcedByInstance.value) return 'enforcement';
|
||||
if (lockedByFloor) return 'floor';
|
||||
if (
|
||||
currentValue === 'default'
|
||||
? !projectPermissions.value.workflow.enableRedaction
|
||||
|
|
@ -257,10 +269,12 @@ function getRedactionLockReason(currentValue: string): 'enforcement' | 'permissi
|
|||
}
|
||||
|
||||
const productionRedactionLockReason = computed(() =>
|
||||
getRedactionLockReason(redactProductionData.value),
|
||||
resolveRedactionLockReason(redactProductionData.value, isProductionRedactionLockedByFloor.value),
|
||||
);
|
||||
|
||||
const manualRedactionLockReason = computed(() => getRedactionLockReason(redactManualData.value));
|
||||
const manualRedactionLockReason = computed(() =>
|
||||
resolveRedactionLockReason(redactManualData.value, isManualRedactionLockedByFloor.value),
|
||||
);
|
||||
|
||||
const isProductionRedactionLocked = computed(() => productionRedactionLockReason.value !== null);
|
||||
const isManualRedactionLocked = computed(() => manualRedactionLockReason.value !== null);
|
||||
|
|
@ -328,6 +342,22 @@ watch(redactProductionData, (newVal) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Coerce the locked select(s) to 'redact' when an instance floor applies.
|
||||
// No `immediate: true` — the natural change of `instanceRedactionFloor` from
|
||||
// its `'off'` default to the fetched value triggers this after both the
|
||||
// floor and workflow settings have settled in `onMounted`.
|
||||
watch(
|
||||
[isProductionRedactionLockedByFloor, isManualRedactionLockedByFloor],
|
||||
([prodLocked, manualLocked]) => {
|
||||
if (prodLocked && redactProductionData.value !== 'redact') {
|
||||
redactProductionData.value = 'redact';
|
||||
}
|
||||
if (manualLocked && redactManualData.value !== 'redact') {
|
||||
redactManualData.value = 'redact';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const mcpToggleDisabled = computed(() => {
|
||||
return readOnlyEnv.value || !workflowPermissions.value.update;
|
||||
});
|
||||
|
|
@ -750,15 +780,6 @@ const onExecutionLogicModeChange = (value: string) => {
|
|||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (isRedactionEnforcementFlagEnabled.value) {
|
||||
try {
|
||||
const response = await securitySettingsApi.getSecuritySettings(rootStore.restApiContext);
|
||||
isInstanceRedactionEnforced.value = (response.redactionEnforcement?.floor ?? 'off') !== 'off';
|
||||
} catch (error) {
|
||||
console.debug('Failed to fetch redaction enforcement state', error);
|
||||
}
|
||||
}
|
||||
|
||||
executionTimeout.value = rootStore.executionTimeout;
|
||||
maxExecutionTimeout.value = rootStore.maxExecutionTimeout;
|
||||
|
||||
|
|
@ -858,6 +879,17 @@ onMounted(async () => {
|
|||
originalBinaryMode.value = workflowSettingsData.binaryMode;
|
||||
workflowSettings.value = workflowSettingsData;
|
||||
|
||||
// Fetch the instance redaction floor AFTER workflowSettings has been assigned, so
|
||||
// the floor-coercion watch sees the loaded settings (not the initial empty object).
|
||||
if (isRedactionEnforcementFlagEnabled.value) {
|
||||
try {
|
||||
const response = await securitySettingsApi.getSecuritySettings(rootStore.restApiContext);
|
||||
instanceRedactionFloor.value = response.redactionEnforcement?.floor ?? 'off';
|
||||
} catch (error) {
|
||||
console.debug('Failed to fetch redaction enforcement state', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear stale credential resolver references (resolver was deleted externally)
|
||||
// Only clear if resolvers loaded successfully — on API failure the list is empty
|
||||
// and we must not falsely treat a valid ID as stale.
|
||||
|
|
@ -1265,8 +1297,8 @@ onBeforeUnmount(() => {
|
|||
size="xsmall"
|
||||
style="opacity: 1"
|
||||
:data-test-id="
|
||||
productionRedactionLockReason === 'enforcement'
|
||||
? 'workflow-settings-redaction-enforced-lock'
|
||||
productionRedactionLockReason === 'floor'
|
||||
? 'workflow-settings-redaction-floor-lock'
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
|
|
@ -1295,8 +1327,8 @@ onBeforeUnmount(() => {
|
|||
placement="top"
|
||||
>
|
||||
<template #content>
|
||||
<span v-if="productionRedactionLockReason === 'enforcement'">{{
|
||||
i18n.baseText('workflowSettings.redactionEnforcementNotice')
|
||||
<span v-if="productionRedactionLockReason === 'floor'">{{
|
||||
i18n.baseText('workflowSettings.redactionFloorNotice')
|
||||
}}</span>
|
||||
<span v-else
|
||||
>{{ i18n.baseText('workflowSettings.redactionPermissionNotice') }}
|
||||
|
|
@ -1361,8 +1393,8 @@ onBeforeUnmount(() => {
|
|||
size="xsmall"
|
||||
style="opacity: 1"
|
||||
:data-test-id="
|
||||
manualRedactionLockReason === 'enforcement'
|
||||
? 'workflow-settings-redaction-enforced-lock'
|
||||
manualRedactionLockReason === 'floor'
|
||||
? 'workflow-settings-redaction-floor-lock'
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
|
|
@ -1394,8 +1426,8 @@ onBeforeUnmount(() => {
|
|||
placement="top"
|
||||
>
|
||||
<template #content>
|
||||
<span v-if="manualRedactionLockReason === 'enforcement'">{{
|
||||
i18n.baseText('workflowSettings.redactionEnforcementNotice')
|
||||
<span v-if="manualRedactionLockReason === 'floor'">{{
|
||||
i18n.baseText('workflowSettings.redactionFloorNotice')
|
||||
}}</span>
|
||||
<span v-else-if="isManualRedactionLocked"
|
||||
>{{ i18n.baseText('workflowSettings.redactionPermissionNotice') }}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user