mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
fix styles
This commit is contained in:
parent
d400001ec6
commit
19a830363c
|
|
@ -53,6 +53,7 @@ describe('CatalogRunService', () => {
|
|||
executions.runWorkflow.mockResolvedValue('execution-1');
|
||||
schemas.describe.mockResolvedValue({
|
||||
eligible: true,
|
||||
trigger: 'execute-workflow-trigger',
|
||||
fields: [{ name: 'customer', type: 'string' }],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@ describe('CatalogService', () => {
|
|||
schemas = mock<WorkflowInputSchemaService>();
|
||||
executions = mock<ExecutionService>();
|
||||
|
||||
schemas.describe.mockResolvedValue({ eligible: true, fields: [] });
|
||||
schemas.describe.mockResolvedValue({
|
||||
eligible: true,
|
||||
trigger: 'manual-trigger',
|
||||
fields: [],
|
||||
});
|
||||
executions.findRangeWithCount.mockResolvedValue({ results: [], count: 0, estimated: false });
|
||||
|
||||
service = new CatalogService(logger, finder, schemas, executions);
|
||||
|
|
@ -51,6 +55,7 @@ describe('CatalogService', () => {
|
|||
finder.findAllWorkflowsForUser.mockResolvedValue([entity('a', 'Weekly report')]);
|
||||
schemas.describe.mockResolvedValue({
|
||||
eligible: true,
|
||||
trigger: 'execute-workflow-trigger',
|
||||
fields: [{ name: 'customer', type: 'string' }],
|
||||
});
|
||||
|
||||
|
|
@ -61,6 +66,7 @@ describe('CatalogService', () => {
|
|||
id: 'a',
|
||||
name: 'Weekly report',
|
||||
description: null,
|
||||
trigger: 'execute-workflow-trigger',
|
||||
fields: [{ name: 'customer', type: 'string' }],
|
||||
},
|
||||
]);
|
||||
|
|
@ -69,7 +75,7 @@ describe('CatalogService', () => {
|
|||
it('should leave out workflows with no readable contract', async () => {
|
||||
finder.findAllWorkflowsForUser.mockResolvedValue([entity('a'), entity('b')]);
|
||||
schemas.describe
|
||||
.mockResolvedValueOnce({ eligible: true, fields: [] })
|
||||
.mockResolvedValueOnce({ eligible: true, trigger: 'manual-trigger', fields: [] })
|
||||
.mockResolvedValueOnce({ eligible: false, reason: 'own-schedule' });
|
||||
|
||||
const result = await service.list(user);
|
||||
|
|
|
|||
|
|
@ -80,13 +80,13 @@ describe('WorkflowInputSchemaService', () => {
|
|||
]),
|
||||
);
|
||||
|
||||
expect(result).toEqual({ eligible: true, fields: [] });
|
||||
expect(result).toEqual({ eligible: true, trigger: 'manual-trigger', fields: [] });
|
||||
});
|
||||
|
||||
it('should accept a manual trigger as taking no input', async () => {
|
||||
const result = await service.describe(workflow([node(MANUAL_TRIGGER_NODE_TYPE)]));
|
||||
|
||||
expect(result).toEqual({ eligible: true, fields: [] });
|
||||
expect(result).toEqual({ eligible: true, trigger: 'manual-trigger', fields: [] });
|
||||
});
|
||||
|
||||
it('should reject a trigger that accepts arbitrary data', async () => {
|
||||
|
|
@ -131,6 +131,7 @@ describe('WorkflowInputSchemaService', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
eligible: true,
|
||||
trigger: 'execute-workflow-trigger',
|
||||
fields: [
|
||||
{ name: 'customer', type: 'string' },
|
||||
{ name: 'amount', type: 'number' },
|
||||
|
|
@ -150,6 +151,7 @@ describe('WorkflowInputSchemaService', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
eligible: true,
|
||||
trigger: 'execute-workflow-trigger',
|
||||
fields: [
|
||||
{ name: 'customer', type: 'string' },
|
||||
{ name: 'amount', type: 'number' },
|
||||
|
|
@ -162,7 +164,7 @@ describe('WorkflowInputSchemaService', () => {
|
|||
workflow([node(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, { inputSource: 'workflowInputs' })]),
|
||||
);
|
||||
|
||||
expect(result).toEqual({ eligible: true, fields: [] });
|
||||
expect(result).toEqual({ eligible: true, trigger: 'execute-workflow-trigger', fields: [] });
|
||||
});
|
||||
|
||||
it('should prefer the declared contract over a manual trigger', async () => {
|
||||
|
|
@ -178,6 +180,7 @@ describe('WorkflowInputSchemaService', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
eligible: true,
|
||||
trigger: 'execute-workflow-trigger',
|
||||
fields: [{ name: 'customer', type: 'string' }],
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { ExecutionStatus, FieldValueOption } from 'n8n-workflow';
|
|||
import { ExecutionService } from '@/executions/execution.service';
|
||||
import { CATALOG_RUN_USER_KEY } from '@/workflows/catalog-run.service';
|
||||
import { WorkflowFinderService } from '@/workflows/workflow-finder.service';
|
||||
import type { StartTrigger } from '@/workflows/workflow-input-schema.service';
|
||||
import { WorkflowInputSchemaService } from '@/workflows/workflow-input-schema.service';
|
||||
|
||||
/**
|
||||
|
|
@ -19,6 +20,12 @@ export type CatalogEntry = {
|
|||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
/**
|
||||
* How the workflow is entered. Surfaced rather than kept internal: a
|
||||
* manual-trigger workflow takes no input at all, which is otherwise
|
||||
* indistinguishable from one whose builder simply declared no fields yet.
|
||||
*/
|
||||
trigger: StartTrigger;
|
||||
fields: FieldValueOption[];
|
||||
};
|
||||
|
||||
|
|
@ -93,6 +100,7 @@ export class CatalogService {
|
|||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
trigger: schema.trigger,
|
||||
fields: schema.fields,
|
||||
};
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -25,8 +25,15 @@ export type IneligibleReason =
|
|||
/** Accepts arbitrary data, so there is no contract to build a form from. */
|
||||
| 'passthrough-input';
|
||||
|
||||
/** The trigger a caller enters the workflow through. */
|
||||
export type StartTrigger =
|
||||
/** Declares named, typed inputs — the run form is built from them. */
|
||||
| 'execute-workflow-trigger'
|
||||
/** Compat path for workflows written before the contract existed: takes no input. */
|
||||
| 'manual-trigger';
|
||||
|
||||
export type WorkflowInputSchema =
|
||||
| { eligible: true; fields: FieldValueOption[] }
|
||||
| { eligible: true; trigger: StartTrigger; fields: FieldValueOption[] }
|
||||
| { eligible: false; reason: IneligibleReason };
|
||||
|
||||
/**
|
||||
|
|
@ -56,7 +63,7 @@ export class WorkflowInputSchemaService {
|
|||
// Execute Workflow Trigger became the way to declare one.
|
||||
const hasManualTrigger = enabled.some((node) => node.type === MANUAL_TRIGGER_NODE_TYPE);
|
||||
return hasManualTrigger
|
||||
? { eligible: true, fields: [] }
|
||||
? { eligible: true, trigger: 'manual-trigger', fields: [] }
|
||||
: { eligible: false, reason: 'no-start-node' };
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +73,7 @@ export class WorkflowInputSchemaService {
|
|||
return { eligible: false, reason: 'passthrough-input' };
|
||||
}
|
||||
|
||||
return { eligible: true, fields };
|
||||
return { eligible: true, trigger: 'execute-workflow-trigger', fields };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5007,14 +5007,23 @@
|
|||
"contextual.feature.unavailable.title.cloud": "Available on the Pro Plan",
|
||||
"catalog.heading": "Run a workflow",
|
||||
"catalog.subheading": "Workflows you can run without opening the editor",
|
||||
"catalog.empty": "No workflows have been shared with you to run yet",
|
||||
"catalog.empty": "Nothing to run yet",
|
||||
"catalog.empty.description": "Workflows shared with you to run will show up here",
|
||||
"catalog.truncated": "Showing the first workflows only. Ask your admin if one you expect is missing.",
|
||||
"catalog.load.error": "Couldn't load the catalog",
|
||||
"catalog.card.noDescription": "No description",
|
||||
"catalog.trigger.declared": "Takes input",
|
||||
"catalog.trigger.manual": "Runs as is",
|
||||
"catalog.inputs.none": "No inputs",
|
||||
"catalog.inputs.count": "{count} input | {count} inputs",
|
||||
"catalog.form.description": "Fill in what this workflow needs, then run it.",
|
||||
"catalog.run": "Run",
|
||||
"catalog.run.started": "Workflow started",
|
||||
"catalog.run.started.message": "{name} is running. It'll show up under your runs.",
|
||||
"catalog.run.error": "Couldn't start the workflow",
|
||||
"catalog.runs.heading": "Your runs",
|
||||
"catalog.runs.empty": "You haven't run anything yet",
|
||||
"catalog.runs.unknownWorkflow": "Unknown workflow",
|
||||
"dataTable.dataTables": "Data tables",
|
||||
"dataTables.noResults": "No data tables found",
|
||||
"dataTable.empty.heading": "Create your first data table",
|
||||
|
|
|
|||
|
|
@ -4,19 +4,26 @@ import {
|
|||
N8nBadge,
|
||||
N8nButton,
|
||||
N8nCard,
|
||||
N8nDialog,
|
||||
N8nDialogFooter,
|
||||
N8nEmptyState,
|
||||
N8nHeading,
|
||||
N8nIcon,
|
||||
N8nInput,
|
||||
N8nInputLabel,
|
||||
N8nLoading,
|
||||
N8nNotice,
|
||||
N8nSpinner,
|
||||
N8nText,
|
||||
type BadgeTheme,
|
||||
} from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { ExecutionStatus } from 'n8n-workflow';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
|
||||
import { convertToDisplayDate } from '@/app/utils/formatters/dateFormatter';
|
||||
import { useCatalogStore } from '@/features/catalog/catalog.store';
|
||||
import type { CatalogEntry } from '@/features/catalog/catalog.types';
|
||||
import type { CatalogEntry, CatalogRun } from '@/features/catalog/catalog.types';
|
||||
|
||||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
|
|
@ -24,9 +31,11 @@ const documentTitle = useDocumentTitle();
|
|||
const catalogStore = useCatalogStore();
|
||||
|
||||
const loading = ref(true);
|
||||
/** The workflow whose input form is open. Null while nothing is being filled in. */
|
||||
const selected = ref<CatalogEntry | null>(null);
|
||||
const inputs = ref<Record<string, string>>({});
|
||||
const running = ref(false);
|
||||
/** Which card is mid-run, so only that card's button spins. */
|
||||
const runningId = ref<string | null>(null);
|
||||
|
||||
const runs = computed(() => catalogStore.runs);
|
||||
|
||||
|
|
@ -41,95 +50,210 @@ onMounted(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
const select = (entry: CatalogEntry) => {
|
||||
const triggerLabel = (entry: CatalogEntry) =>
|
||||
entry.trigger === 'manual-trigger'
|
||||
? i18n.baseText('catalog.trigger.manual')
|
||||
: i18n.baseText('catalog.trigger.declared');
|
||||
|
||||
const inputsLabel = (entry: CatalogEntry) =>
|
||||
entry.fields.length === 0
|
||||
? i18n.baseText('catalog.inputs.none')
|
||||
: i18n.baseText('catalog.inputs.count', {
|
||||
adjustToNumber: entry.fields.length,
|
||||
interpolate: { count: entry.fields.length },
|
||||
});
|
||||
|
||||
const RUN_STATUS_THEMES: Partial<Record<ExecutionStatus, BadgeTheme>> = {
|
||||
success: 'success',
|
||||
error: 'danger',
|
||||
crashed: 'danger',
|
||||
canceled: 'default',
|
||||
running: 'warning',
|
||||
waiting: 'warning',
|
||||
new: 'warning',
|
||||
};
|
||||
|
||||
const runTheme = (status: ExecutionStatus): BadgeTheme => RUN_STATUS_THEMES[status] ?? 'default';
|
||||
|
||||
const runTime = (run: CatalogRun) => {
|
||||
if (!run.startedAt) return '';
|
||||
const { date, time } = convertToDisplayDate(run.startedAt);
|
||||
return `${date} ${time}`;
|
||||
};
|
||||
|
||||
const start = async (entry: CatalogEntry, values: Record<string, unknown>) => {
|
||||
runningId.value = entry.id;
|
||||
try {
|
||||
await catalogStore.run(entry.id, values);
|
||||
selected.value = null;
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('catalog.run.started'),
|
||||
message: i18n.baseText('catalog.run.started.message', { interpolate: { name: entry.name } }),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('catalog.run.error'));
|
||||
} finally {
|
||||
runningId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A workflow that declares no fields has nothing to ask for, so the button runs
|
||||
* it outright rather than opening an empty form.
|
||||
*/
|
||||
const onRunClick = async (entry: CatalogEntry) => {
|
||||
if (entry.fields.length === 0) {
|
||||
await start(entry, {});
|
||||
return;
|
||||
}
|
||||
|
||||
selected.value = entry;
|
||||
// Start each form clean rather than carrying the previous workflow's values.
|
||||
inputs.value = Object.fromEntries(entry.fields.map((field) => [field.name, '']));
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
if (!selected.value) return;
|
||||
const closeDialog = () => {
|
||||
if (runningId.value) return;
|
||||
selected.value = null;
|
||||
};
|
||||
|
||||
running.value = true;
|
||||
try {
|
||||
await catalogStore.run(selected.value.id, { ...inputs.value });
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('catalog.run.started'),
|
||||
type: 'success',
|
||||
});
|
||||
selected.value = null;
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('catalog.run.error'));
|
||||
} finally {
|
||||
running.value = false;
|
||||
}
|
||||
const submit = async () => {
|
||||
if (!selected.value) return;
|
||||
await start(selected.value, { ...inputs.value });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.page" data-test-id="catalog-view">
|
||||
<N8nHeading tag="h1" size="xlarge">{{ i18n.baseText('catalog.heading') }}</N8nHeading>
|
||||
<N8nText color="text-light">{{ i18n.baseText('catalog.subheading') }}</N8nText>
|
||||
<header :class="$style.header">
|
||||
<N8nHeading tag="h1" size="xlarge" bold>{{ i18n.baseText('catalog.heading') }}</N8nHeading>
|
||||
<N8nText color="text-light">{{ i18n.baseText('catalog.subheading') }}</N8nText>
|
||||
</header>
|
||||
|
||||
<N8nSpinner v-if="loading" />
|
||||
<N8nLoading v-if="loading" :rows="3" :class="$style.grid" />
|
||||
|
||||
<template v-else>
|
||||
<N8nNotice v-if="catalogStore.truncated" theme="warning">
|
||||
{{ i18n.baseText('catalog.truncated') }}
|
||||
</N8nNotice>
|
||||
|
||||
<N8nText v-if="catalogStore.isEmpty" color="text-light" data-test-id="catalog-empty">
|
||||
{{ i18n.baseText('catalog.empty') }}
|
||||
</N8nText>
|
||||
<N8nEmptyState
|
||||
v-if="catalogStore.isEmpty"
|
||||
data-test-id="catalog-empty"
|
||||
:icon="{ type: 'icon', value: 'play' }"
|
||||
:heading="i18n.baseText('catalog.empty')"
|
||||
:description="i18n.baseText('catalog.empty.description')"
|
||||
/>
|
||||
|
||||
<div v-else :class="$style.list">
|
||||
<div v-else :class="$style.grid">
|
||||
<N8nCard
|
||||
v-for="entry in catalogStore.workflows"
|
||||
:key="entry.id"
|
||||
:class="$style.card"
|
||||
data-test-id="catalog-workflow"
|
||||
@click="select(entry)"
|
||||
>
|
||||
<N8nText bold>{{ entry.name }}</N8nText>
|
||||
<N8nText v-if="entry.description" size="small" color="text-light">
|
||||
{{ entry.description }}
|
||||
<template #header>
|
||||
<N8nText bold :class="$style.name" :title="entry.name">{{ entry.name }}</N8nText>
|
||||
</template>
|
||||
|
||||
<N8nText
|
||||
size="small"
|
||||
:color="entry.description ? 'text-base' : 'text-light'"
|
||||
:class="$style.description"
|
||||
>
|
||||
{{ entry.description || i18n.baseText('catalog.card.noDescription') }}
|
||||
</N8nText>
|
||||
|
||||
<template #footer>
|
||||
<div :class="$style.meta">
|
||||
<N8nBadge theme="tertiary">
|
||||
<N8nIcon
|
||||
:icon="entry.trigger === 'manual-trigger' ? 'mouse-pointer' : 'workflow'"
|
||||
size="xsmall"
|
||||
:class="$style.badgeIcon"
|
||||
/>
|
||||
{{ triggerLabel(entry) }}
|
||||
</N8nBadge>
|
||||
<N8nBadge theme="default">{{ inputsLabel(entry) }}</N8nBadge>
|
||||
</div>
|
||||
|
||||
<N8nButton
|
||||
size="small"
|
||||
icon="play"
|
||||
:label="i18n.baseText('catalog.run')"
|
||||
:loading="runningId === entry.id"
|
||||
:disabled="runningId !== null && runningId !== entry.id"
|
||||
data-test-id="catalog-run-button"
|
||||
@click="onRunClick(entry)"
|
||||
/>
|
||||
</template>
|
||||
</N8nCard>
|
||||
</div>
|
||||
|
||||
<div v-if="selected" :class="$style.runner" data-test-id="catalog-runner">
|
||||
<N8nHeading tag="h2" size="medium">{{ selected.name }}</N8nHeading>
|
||||
<section :class="$style.runs">
|
||||
<N8nHeading tag="h2" size="medium" bold>
|
||||
{{ i18n.baseText('catalog.runs.heading') }}
|
||||
</N8nHeading>
|
||||
|
||||
<N8nText v-if="runs.length === 0" color="text-light">
|
||||
{{ i18n.baseText('catalog.runs.empty') }}
|
||||
</N8nText>
|
||||
|
||||
<ul v-else :class="$style.runList">
|
||||
<li v-for="item in runs" :key="item.id" :class="$style.run" data-test-id="catalog-run">
|
||||
<N8nText size="small" :class="$style.name">
|
||||
{{ item.workflowName ?? i18n.baseText('catalog.runs.unknownWorkflow') }}
|
||||
</N8nText>
|
||||
<N8nText size="small" color="text-light">{{ runTime(item) }}</N8nText>
|
||||
<N8nBadge :theme="runTheme(item.status)">{{ item.status }}</N8nBadge>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<N8nDialog
|
||||
v-if="selected"
|
||||
:open="true"
|
||||
size="medium"
|
||||
:header="selected.name"
|
||||
:description="i18n.baseText('catalog.form.description')"
|
||||
@update:open="closeDialog"
|
||||
>
|
||||
<form :class="$style.form" data-test-id="catalog-runner" @submit.prevent="submit">
|
||||
<N8nInputLabel
|
||||
v-for="field in selected.fields"
|
||||
:key="field.name"
|
||||
:input-name="`catalog-input-${field.name}`"
|
||||
:label="field.name"
|
||||
:class="$style.field"
|
||||
>
|
||||
<N8nInput v-model="inputs[field.name]" :name="field.name" />
|
||||
<N8nInput
|
||||
:id="`catalog-input-${field.name}`"
|
||||
v-model="inputs[field.name]"
|
||||
:name="field.name"
|
||||
:placeholder="field.type"
|
||||
/>
|
||||
</N8nInputLabel>
|
||||
|
||||
<N8nButton
|
||||
:label="i18n.baseText('catalog.run')"
|
||||
:loading="running"
|
||||
data-test-id="catalog-run-button"
|
||||
@click="run"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<N8nHeading tag="h2" size="medium">{{ i18n.baseText('catalog.runs.heading') }}</N8nHeading>
|
||||
|
||||
<N8nText v-if="runs.length === 0" color="text-light">
|
||||
{{ i18n.baseText('catalog.runs.empty') }}
|
||||
</N8nText>
|
||||
|
||||
<div v-else :class="$style.list">
|
||||
<N8nCard v-for="item in runs" :key="item.id" data-test-id="catalog-run">
|
||||
<N8nText>{{ item.workflowName }}</N8nText>
|
||||
<N8nBadge>{{ item.status }}</N8nBadge>
|
||||
</N8nCard>
|
||||
</div>
|
||||
</template>
|
||||
<N8nDialogFooter>
|
||||
<N8nButton
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="runningId !== null"
|
||||
@click="closeDialog"
|
||||
>
|
||||
{{ i18n.baseText('generic.cancel') }}
|
||||
</N8nButton>
|
||||
<N8nButton
|
||||
type="submit"
|
||||
:loading="runningId !== null"
|
||||
data-test-id="catalog-submit-button"
|
||||
>
|
||||
{{ i18n.baseText('catalog.run') }}
|
||||
</N8nButton>
|
||||
</N8nDialogFooter>
|
||||
</form>
|
||||
</N8nDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -137,30 +261,91 @@ const run = async () => {
|
|||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-s);
|
||||
padding: var(--spacing-l);
|
||||
gap: var(--spacing--md);
|
||||
padding: var(--spacing--lg);
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list {
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2xs);
|
||||
gap: var(--spacing--4xs);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: var(--spacing--xs);
|
||||
}
|
||||
|
||||
.card {
|
||||
cursor: pointer;
|
||||
--card--padding: var(--spacing--sm);
|
||||
|
||||
align-items: stretch;
|
||||
gap: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.runner {
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing--4xs);
|
||||
}
|
||||
|
||||
.badgeIcon {
|
||||
margin-right: var(--spacing--5xs);
|
||||
}
|
||||
|
||||
.runs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2xs);
|
||||
padding: var(--spacing-s);
|
||||
border: var(--border-base);
|
||||
border-radius: var(--border-radius-large);
|
||||
gap: var(--spacing--2xs);
|
||||
margin-top: var(--spacing--sm);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: block;
|
||||
.runList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: var(--border);
|
||||
border-radius: var(--radius--lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.run {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
align-items: center;
|
||||
gap: var(--spacing--2xs);
|
||||
padding: var(--spacing--2xs) var(--spacing--sm);
|
||||
background-color: var(--background--surface);
|
||||
|
||||
& + & {
|
||||
border-top: var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--xs);
|
||||
margin-top: var(--spacing--xs);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ import { createPinia, setActivePinia } from 'pinia';
|
|||
|
||||
import * as api from '@/features/catalog/catalog.api';
|
||||
import { useCatalogStore } from '@/features/catalog/catalog.store';
|
||||
import type { CatalogEntry } from '@/features/catalog/catalog.types';
|
||||
|
||||
vi.mock('@/features/catalog/catalog.api');
|
||||
|
||||
const entry = (id: string) => ({ id, name: `Workflow ${id}`, description: null, fields: [] });
|
||||
const entry = (id: string): CatalogEntry => ({
|
||||
id,
|
||||
name: `Workflow ${id}`,
|
||||
description: null,
|
||||
trigger: 'manual-trigger',
|
||||
fields: [],
|
||||
});
|
||||
|
||||
describe('catalog.store', () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ export type CatalogField = {
|
|||
type: string;
|
||||
};
|
||||
|
||||
/** How the workflow is entered, which decides whether it takes input at all. */
|
||||
export type CatalogTrigger = 'execute-workflow-trigger' | 'manual-trigger';
|
||||
|
||||
export type CatalogEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
trigger: CatalogTrigger;
|
||||
fields: CatalogField[];
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user