mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 21:49:36 +02:00
feat(editor): Show partial/failed publication status on workflow list cards (no-changelog) (#34069)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a3aadec2a2
commit
496b81b750
|
|
@ -122,6 +122,7 @@ export { default as N8nResizeWrapper } from './N8nResizeWrapper';
|
|||
export { default as N8nSelect } from './N8nSelect';
|
||||
export { default as N8nSpinner } from './N8nSpinner';
|
||||
export { default as N8nStatusDot } from './N8nStatusDot';
|
||||
export type { StatusDotProps, StatusDotVariant } from './N8nStatusDot';
|
||||
export { default as N8nStepper } from './N8nStepper/Stepper.vue';
|
||||
export { default as N8nSticky } from './N8nSticky';
|
||||
export { default as N8nResizeableSticky } from './N8nResizeableSticky';
|
||||
|
|
|
|||
|
|
@ -4875,6 +4875,10 @@
|
|||
"resourceActions.mcpAccess.disabled.message": "Turn on instance-level MCP access before managing workflow access<br><br><a href=\"{link}\">Manage MCP settings</a>",
|
||||
"workflows.publish": "Publish",
|
||||
"workflows.published": "Published",
|
||||
"workflows.publicationStatus.partial": "Partial publish",
|
||||
"workflows.publicationStatus.failed": "Failed publish",
|
||||
"workflows.publicationStatus.partial.tooltip": "Some triggers failed to activate — open the workflow to see which.",
|
||||
"workflows.publicationStatus.failed.tooltip": "This workflow isn't running — open the workflow for details.",
|
||||
"workflows.publish.permissionDenied": "You don't have permission to publish this workflow",
|
||||
"workflows.publish.personalSpaceRestricted": "Workflow publishing is disabled in personal spaces. Move the workflow to a team space to activate",
|
||||
"workflows.publish.publishing": "Publishing…",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type {
|
|||
IUserManagementSettings,
|
||||
IVersionNotificationSettings,
|
||||
Role,
|
||||
WorkflowListPublicationStatus,
|
||||
} from '@n8n/api-types';
|
||||
import type { ILogInStatus } from '@/features/settings/users/users.types';
|
||||
import type { NodeViewItemSection } from '@/features/shared/nodeCreator/views/viewsData';
|
||||
|
|
@ -329,6 +330,7 @@ export type WorkflowResource = BaseResource & {
|
|||
parentFolder?: ResourceParentFolder;
|
||||
settings?: Partial<IWorkflowSettings>;
|
||||
hasResolvableCredentials?: boolean;
|
||||
publicationStatus?: WorkflowListPublicationStatus;
|
||||
};
|
||||
|
||||
export type VariableResource = BaseResource & {
|
||||
|
|
@ -387,6 +389,7 @@ export type WorkflowListItem = Omit<
|
|||
resource?: 'workflow'; // only included if list may contain folders
|
||||
description?: string;
|
||||
hasResolvableCredentials?: boolean;
|
||||
publicationStatus?: WorkflowListPublicationStatus;
|
||||
};
|
||||
|
||||
export type WorkflowListResource = WorkflowListItem | FolderListItem;
|
||||
|
|
|
|||
|
|
@ -1436,4 +1436,47 @@ describe('WorkflowCard', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Publication status indicator', () => {
|
||||
const renderCard = (overrides: Partial<WorkflowResource> = {}) =>
|
||||
renderComponent({ props: { data: createWorkflow(overrides) } });
|
||||
|
||||
it.each([
|
||||
['partial', 'Partial publish'],
|
||||
['failed', 'Failed publish'],
|
||||
] as const)('shows the %s indicator with its label and state', (status, label) => {
|
||||
const { getByTestId, getByText } = renderCard({
|
||||
publicationStatus: status,
|
||||
activeVersionId: 'v1',
|
||||
});
|
||||
const indicator = getByTestId('workflow-card-publish-indicator');
|
||||
expect(indicator).toBeVisible();
|
||||
expect(indicator).toHaveAttribute('data-state', status);
|
||||
expect(getByText(label)).toBeVisible();
|
||||
// The explanation lives in a focus-openable tooltip; the trigger must be tabbable.
|
||||
expect(indicator).toHaveAttribute('tabindex', '0');
|
||||
});
|
||||
|
||||
it('lets the server status win over activeVersionId', () => {
|
||||
const { getByTestId } = renderCard({ publicationStatus: 'failed', activeVersionId: null });
|
||||
expect(getByTestId('workflow-card-publish-indicator')).toHaveAttribute(
|
||||
'data-state',
|
||||
'failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the legacy "Published" indicator when publicationStatus is absent', () => {
|
||||
const { getByTestId, getByText } = renderCard({ activeVersionId: 'v1' });
|
||||
const indicator = getByTestId('workflow-card-publish-indicator');
|
||||
expect(indicator).toHaveAttribute('data-state', 'published');
|
||||
expect(getByText('Published')).toBeVisible();
|
||||
// The published state has no tooltip, so it must not be a focus stop.
|
||||
expect(indicator).not.toHaveAttribute('tabindex');
|
||||
});
|
||||
|
||||
it('shows no indicator when not published and no publicationStatus', () => {
|
||||
const { queryByTestId } = renderCard({ activeVersionId: null });
|
||||
expect(queryByTestId('workflow-card-publish-indicator')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import TimeAgo from '@/app/components/TimeAgo.vue';
|
|||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
import ProjectCardBadge from '@/features/collaboration/projects/components/ProjectCardBadge.vue';
|
||||
import DependencyPill from '@/app/components/DependencyPill.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { type BaseTextKey, useI18n } from '@n8n/i18n';
|
||||
import type { WorkflowListPublicationStatus } from '@n8n/api-types';
|
||||
import type { StatusDotVariant } from '@n8n/design-system';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useTelemetry } from '@n8n/composables/useTelemetry';
|
||||
import { ResourceType } from '@/features/collaboration/projects/projects.utils';
|
||||
|
|
@ -40,6 +42,7 @@ import {
|
|||
N8nBreadcrumbs,
|
||||
N8nCard,
|
||||
N8nIcon,
|
||||
N8nStatusDot,
|
||||
N8nTags,
|
||||
N8nText,
|
||||
N8nTooltip,
|
||||
|
|
@ -324,6 +327,39 @@ const isWorkflowPublished = computed(() => {
|
|||
return props.data.activeVersionId !== null;
|
||||
});
|
||||
|
||||
// Keyed by the full status union so a new backend status is a compile error
|
||||
// here instead of silently rendering as the green 'Published' dot.
|
||||
const publicationIndicators: Record<
|
||||
WorkflowListPublicationStatus,
|
||||
{ variant: StatusDotVariant; labelKey: BaseTextKey; tooltipKey?: BaseTextKey }
|
||||
> = {
|
||||
published: { variant: 'success', labelKey: 'workflows.published' },
|
||||
partial: {
|
||||
variant: 'warning',
|
||||
labelKey: 'workflows.publicationStatus.partial',
|
||||
tooltipKey: 'workflows.publicationStatus.partial.tooltip',
|
||||
},
|
||||
failed: {
|
||||
variant: 'danger',
|
||||
labelKey: 'workflows.publicationStatus.failed',
|
||||
tooltipKey: 'workflows.publicationStatus.failed.tooltip',
|
||||
},
|
||||
};
|
||||
|
||||
// The server-derived status is authoritative when present; workflows with no
|
||||
// settled trigger rows omit it and fall back to the legacy activeVersionId indicator.
|
||||
const publicationIndicator = computed(() => {
|
||||
const state = props.data.publicationStatus ?? (isWorkflowPublished.value ? 'published' : null);
|
||||
if (state === null) return null;
|
||||
const { variant, labelKey, tooltipKey } = publicationIndicators[state];
|
||||
return {
|
||||
state,
|
||||
variant,
|
||||
label: locale.baseText(labelKey),
|
||||
tooltip: tooltipKey ? locale.baseText(tooltipKey) : null,
|
||||
};
|
||||
});
|
||||
|
||||
const hasDynamicCredentials = computed(() => {
|
||||
return isPrivateCredentialsEnabled.value && props.data.hasResolvableCredentials;
|
||||
});
|
||||
|
|
@ -722,16 +758,32 @@ const tags = computed(
|
|||
>
|
||||
{{ locale.baseText('workflows.item.archived') }}
|
||||
</N8nText>
|
||||
<div
|
||||
v-else-if="isWorkflowPublished"
|
||||
:class="$style.publishIndicator"
|
||||
data-test-id="workflow-card-publish-indicator"
|
||||
>
|
||||
<span :class="$style.publishIndicatorDot" />
|
||||
<N8nText size="small" color="text-base">{{
|
||||
locale.baseText('workflows.published')
|
||||
}}</N8nText>
|
||||
</div>
|
||||
<!-- The tooltip is only mounted for the rare partial/failed states; the
|
||||
common published case keeps the plain markup. -->
|
||||
<template v-else-if="publicationIndicator">
|
||||
<div
|
||||
v-if="!publicationIndicator.tooltip"
|
||||
:class="$style.publishIndicator"
|
||||
:data-state="publicationIndicator.state"
|
||||
data-test-id="workflow-card-publish-indicator"
|
||||
>
|
||||
<N8nStatusDot :variant="publicationIndicator.variant" />
|
||||
<N8nText size="small" color="text-base">{{ publicationIndicator.label }}</N8nText>
|
||||
</div>
|
||||
<N8nTooltip v-else placement="top" as-child>
|
||||
<template #content>{{ publicationIndicator.tooltip }}</template>
|
||||
<!-- tabindex makes the explanation keyboard-reachable: the tooltip opens on focus. -->
|
||||
<div
|
||||
:class="$style.publishIndicator"
|
||||
:data-state="publicationIndicator.state"
|
||||
data-test-id="workflow-card-publish-indicator"
|
||||
tabindex="0"
|
||||
>
|
||||
<N8nStatusDot :variant="publicationIndicator.variant" />
|
||||
<N8nText size="small" color="text-base">{{ publicationIndicator.label }}</N8nText>
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
</template>
|
||||
<WorkflowCardMcpToggle
|
||||
v-if="props.isWorkflowCardMcpToggleEnabled"
|
||||
:workflow-id="data.id"
|
||||
|
|
@ -857,13 +909,6 @@ const tags = computed(
|
|||
}
|
||||
}
|
||||
|
||||
.publishIndicatorDot {
|
||||
width: var(--spacing--2xs);
|
||||
height: var(--spacing--2xs);
|
||||
border-radius: 50%;
|
||||
background-color: var(--color--mint-600);
|
||||
}
|
||||
|
||||
@include mixins.breakpoint('sm-and-down') {
|
||||
.cardLink {
|
||||
--card--padding: 0 var(--spacing--sm) var(--spacing--sm);
|
||||
|
|
|
|||
|
|
@ -537,6 +537,7 @@ const workflowListResources = computed<Resource[]>(() => {
|
|||
parentFolder: resource.parentFolder,
|
||||
settings: resource.settings,
|
||||
hasResolvableCredentials: resource.hasResolvableCredentials,
|
||||
publicationStatus: resource.publicationStatus,
|
||||
} satisfies WorkflowResource;
|
||||
}
|
||||
});
|
||||
|
|
@ -1254,6 +1255,9 @@ const onWorkflowActiveToggle = async (data: { id: string; active: boolean }) =>
|
|||
if (!workflow) return;
|
||||
workflow.active = data.active;
|
||||
workflow.activeVersionId = data.active ? workflow.versionId : null;
|
||||
// The server-derived status outranks activeVersionId on the card and is now
|
||||
// stale either way; clearing it falls back to the legacy indicator until a refetch.
|
||||
workflow.publicationStatus = undefined;
|
||||
|
||||
// Fetch the updated workflow to get the latest settings
|
||||
try {
|
||||
|
|
@ -1274,6 +1278,8 @@ const onWorkflowUnpublished = async (data: { id: string }) => {
|
|||
|
||||
// Update the workflow to reflect unpublished state
|
||||
workflow.activeVersionId = null;
|
||||
// A stale server-derived status would keep the card indicator lit; clear it too.
|
||||
workflow.publicationStatus = undefined;
|
||||
};
|
||||
|
||||
const getFolderListItem = (folderId: string): FolderListItem | undefined => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user