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:
Azri Kahar
2022-10-15 09:47:54 +08:00
committed by GitHub
parent 67c3834783
commit dc7c62f0ef
4 changed files with 182 additions and 7 deletions

View File

@@ -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...

View File

@@ -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,
},
},
],
},
];

View 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)
)
);
});

View File

@@ -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');