mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Allow triggering manual flow without selection(s) (#15977)
* allow running manual flow without selection * test for flows store * update no items selected tooltip
This commit is contained in:
@@ -793,9 +793,10 @@ label: Label
|
||||
flows: Flows
|
||||
flow: Flow
|
||||
select_a_flow: Select a Flow
|
||||
run_flow_on_current_collection: Run flow on Current Collection
|
||||
run_flow_on_current: Run flow on Current Item
|
||||
run_flow_on_current_edited_confirm: This item may be updated by this flow, are you sure you want to execute it?
|
||||
run_flow_on_selected: No Items Selected | Run flow on 1 Selected Item | Run flow on {n} Selected Items
|
||||
run_flow_on_selected: Select One or More Items First | Run flow on 1 Selected Item | Run flow on {n} Selected Items
|
||||
run_flow_success: Flow "{flow}" ran successfully
|
||||
trigger: Trigger
|
||||
trigger_options: Trigger Options
|
||||
@@ -2083,6 +2084,8 @@ triggers:
|
||||
collection_and_item: Collection & Item Pages
|
||||
collection_only: Collection Page Only
|
||||
item_only: Item Page Only
|
||||
collection_page: Collection Page
|
||||
require_selection: Requires Selection
|
||||
a_flow_uuid: Select a Flow to trigger...
|
||||
any_string_or_json: Any string or JSON...
|
||||
item_payload_placeholder: This is the JSON used to update the item's field values...
|
||||
|
||||
@@ -380,6 +380,32 @@ export function getTriggers() {
|
||||
default_value: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'requireSelection',
|
||||
name: t('triggers.manual.collection_page'),
|
||||
type: 'boolean',
|
||||
meta: {
|
||||
interface: 'boolean',
|
||||
width: 'half' as Width,
|
||||
options: {
|
||||
label: t('triggers.manual.require_selection'),
|
||||
},
|
||||
hidden: false,
|
||||
conditions: [
|
||||
{
|
||||
rule: {
|
||||
location: {
|
||||
_eq: 'item',
|
||||
},
|
||||
},
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: {
|
||||
default_value: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
126
app/src/stores/flows.test.ts
Normal file
126
app/src/stores/flows.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
import { useFlowsStore } from './flows';
|
||||
import { useUserStore } from './user';
|
||||
|
||||
const mockFlows = [
|
||||
{
|
||||
name: 'Flow 1',
|
||||
status: 'active',
|
||||
trigger: 'manual',
|
||||
accountability: 'all',
|
||||
options: {
|
||||
collections: ['a'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Flow 2',
|
||||
status: 'inactive',
|
||||
trigger: 'event',
|
||||
accountability: 'all',
|
||||
options: {
|
||||
type: 'action',
|
||||
scope: ['items.create'],
|
||||
collections: ['a'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Flow 3',
|
||||
status: 'inactive',
|
||||
trigger: 'manual',
|
||||
accountability: 'all',
|
||||
options: {
|
||||
collections: ['a'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Flow 4',
|
||||
status: 'active',
|
||||
trigger: 'manual',
|
||||
accountability: 'all',
|
||||
options: {
|
||||
collections: ['b'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockAdminUser = { role: { admin_access: true } } as any;
|
||||
|
||||
const mockNonAdminUser = { role: { admin_access: false } } as any;
|
||||
|
||||
vi.mock('@/api', () => {
|
||||
return {
|
||||
default: {
|
||||
get: (path: string) => {
|
||||
if (path === '/flows') {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
data: mockFlows,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Path "${path}" is not mocked in this test`));
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test('hydrate action for admin', async () => {
|
||||
const userStore = useUserStore();
|
||||
userStore.currentUser = mockAdminUser;
|
||||
|
||||
const flowsStore = useFlowsStore();
|
||||
await flowsStore.hydrate();
|
||||
|
||||
expect(flowsStore.flows).toEqual(mockFlows);
|
||||
});
|
||||
|
||||
test('hydrate action for non-admin', async () => {
|
||||
const userStore = useUserStore();
|
||||
userStore.currentUser = mockNonAdminUser;
|
||||
|
||||
const flowsStore = useFlowsStore();
|
||||
await flowsStore.hydrate();
|
||||
|
||||
expect(flowsStore.flows).toEqual([]);
|
||||
});
|
||||
|
||||
test('dehydrate action resets store', async () => {
|
||||
const userStore = useUserStore();
|
||||
userStore.currentUser = mockAdminUser;
|
||||
|
||||
const flowsStore = useFlowsStore();
|
||||
await flowsStore.hydrate();
|
||||
await flowsStore.dehydrate();
|
||||
|
||||
expect(flowsStore.flows).toEqual([]);
|
||||
});
|
||||
|
||||
test('getManualFlowsForCollection action returns active manual flows of specified collection only', async () => {
|
||||
const userStore = useUserStore();
|
||||
userStore.currentUser = mockAdminUser;
|
||||
|
||||
const flowsStore = useFlowsStore();
|
||||
await flowsStore.hydrate();
|
||||
|
||||
const testCollection = 'a';
|
||||
|
||||
expect(flowsStore.getManualFlowsForCollection(testCollection)).toEqual(
|
||||
mockFlows.filter(
|
||||
(flow) =>
|
||||
flow.trigger === 'manual' && flow.status === 'active' && flow.options?.collections?.includes(testCollection)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -3,11 +3,11 @@
|
||||
<div class="fields">
|
||||
<div v-for="manualFlow in manualFlows" :key="manualFlow.id" class="field full">
|
||||
<v-button
|
||||
v-tooltip="primaryKey ? t('run_flow_on_current') : t('run_flow_on_selected', selection.length)"
|
||||
v-tooltip="getFlowTooltip(manualFlow)"
|
||||
small
|
||||
full-width
|
||||
:loading="runningFlows.includes(manualFlow.id)"
|
||||
:disabled="!primaryKey && selection.length === 0"
|
||||
:disabled="isFlowDisabled(manualFlow)"
|
||||
@click="onFlowClick(manualFlow.id)"
|
||||
>
|
||||
<v-icon :name="manualFlow.icon ?? 'bolt'" small left />
|
||||
@@ -40,6 +40,7 @@ import { useFlowsStore } from '@/stores/flows';
|
||||
import { notify } from '@/utils/notify';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import { useCollection } from '@directus/shared/composables';
|
||||
import { FlowRaw } from '@directus/shared/types';
|
||||
import { computed, ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -61,7 +62,7 @@ const emit = defineEmits(['refresh']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { collection, primaryKey, selection, hasEdits } = toRefs(props);
|
||||
const { collection, primaryKey, selection, location, hasEdits } = toRefs(props);
|
||||
|
||||
const { primaryKeyField } = useCollection(collection);
|
||||
|
||||
@@ -80,6 +81,18 @@ const runningFlows = ref<string[]>([]);
|
||||
|
||||
const confirmRunFlow = ref<string | null>(null);
|
||||
|
||||
function getFlowTooltip(manualFlow: FlowRaw) {
|
||||
if (location.value === 'item') return t('run_flow_on_current');
|
||||
if (manualFlow.options?.requireSelection === false && selection.value.length === 0)
|
||||
return t('run_flow_on_current_collection');
|
||||
return t('run_flow_on_selected', selection.value.length);
|
||||
}
|
||||
|
||||
function isFlowDisabled(manualFlow: FlowRaw) {
|
||||
if (location.value === 'item' || manualFlow.options?.requireSelection === false) return false;
|
||||
return !primaryKey.value && selection.value.length === 0;
|
||||
}
|
||||
|
||||
async function onFlowClick(flowId: string) {
|
||||
if (hasEdits.value) {
|
||||
confirmRunFlow.value = flowId;
|
||||
@@ -98,9 +111,16 @@ async function runManualFlow(flowId: string) {
|
||||
runningFlows.value = [...runningFlows.value, flowId];
|
||||
|
||||
try {
|
||||
const keys = primaryKey.value ? [primaryKey.value] : selection.value;
|
||||
|
||||
await api.post(`/flows/trigger/${flowId}`, { collection: collection.value, keys });
|
||||
if (
|
||||
location.value === 'collection' &&
|
||||
selectedFlow.options?.requireSelection === false &&
|
||||
selection.value.length === 0
|
||||
) {
|
||||
await api.post(`/flows/trigger/${flowId}`, { collection: collection.value });
|
||||
} else {
|
||||
const keys = primaryKey.value ? [primaryKey.value] : selection.value;
|
||||
await api.post(`/flows/trigger/${flowId}`, { collection: collection.value, keys });
|
||||
}
|
||||
|
||||
emit('refresh');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user