Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
- timio23
- Dominic-Marcelino
- ukmadlz
- MarkBurvs
3 changes: 2 additions & 1 deletion packages/flow-trigger-bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"dependencies": {
"@directus/format-title": "11.0.0",
"@directus/types": "11.1.1",
"vue-i18n": "9.13.1"
"vue-i18n": "9.13.1",
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@directus/extensions-sdk": "12.0.1",
Expand Down
96 changes: 96 additions & 0 deletions packages/flow-trigger-bundle/src/composables/use-flow-triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { useApi, useStores } from '@directus/extensions-sdk';
import formatTitle from '@directus/format-title';
import { computed, ref, unref } from 'vue';
import { useI18n } from 'vue-i18n';
import { isEqual } from 'lodash-es';

interface FlowTriggerContext {
collection: (trigger: Trigger) => string | undefined;
keys: (trigger: Trigger) => PrimaryKey[] | undefined;
autoRefresh?: () => boolean;
formValues?: () => Record<string, any>;
}

export function useFlowTriggers(context: FlowTriggerContext) {
Expand Down Expand Up @@ -54,6 +57,10 @@ export function useFlowTriggers(context: FlowTriggerContext) {
fields: Record<string, any>[];
} | null>(null);

const showUnsavedWarning = ref(false);

const checkingUnsavedChanges = ref<string[]>([]);

const isConfirmButtonDisabled = computed(() => {
if (!selectedTrigger.value) {
return true;
Expand Down Expand Up @@ -161,6 +168,15 @@ export function useFlowTriggers(context: FlowTriggerContext) {
title: t('run_flow_success', { flow: flow.name }),
});

// Refresh the current page to show updated data (if enabled)
const shouldAutoRefresh = context.autoRefresh?.() ?? true;
if (shouldAutoRefresh) {
// Use a small delay to ensure the notification is visible before refresh
setTimeout(() => {
window.location.reload();
}, 500);
}

resetConfirm();
}
catch (error) {
Expand Down Expand Up @@ -195,15 +211,92 @@ export function useFlowTriggers(context: FlowTriggerContext) {
});
}

async function hasUnsavedChanges(trigger: Trigger): Promise<boolean> {
try {
const collection = context.collection(trigger);
const keys = context.keys(trigger);

// Can't detect unsaved changes if no collection, keys, or form values
if (!collection || !keys || keys.length === 0 || keys[0] === '+') {
return false;
}

// Can't detect if no form values are available
if (!context.formValues) {
return false;
}

// Fetch the saved item from API
const response = await api.get(`/items/${collection}/${keys[0]}`);
const savedValues = response.data.data;
const currentValues = context.formValues();

// Deep compare each field to detect differences
for (const key in currentValues) {
if (key in savedValues && !isEqual(currentValues[key], savedValues[key])) {
return true; // Found a difference
}
}

return false; // No differences found
}
catch (error) {
// If we can't fetch the item or compare, assume no unsaved changes
console.warn('Could not detect unsaved changes:', error);
return false;
}
}

async function onTriggerClick(trigger: Trigger) {
selectedTrigger.value = trigger;

// Check if we should warn about unsaved changes
const shouldAutoRefresh = context.autoRefresh?.() ?? true;
const keys = context.keys(trigger);
const isItemDetailPage = keys && keys.length > 0 && keys[0] !== '+';

if (shouldAutoRefresh && isItemDetailPage) {
// Add to checking state
const flowId = trigger.flowId;
checkingUnsavedChanges.value = [...checkingUnsavedChanges.value, flowId];

try {
// Check for actual unsaved changes
const hasChanges = await hasUnsavedChanges(trigger);

if (hasChanges) {
// Show unsaved changes warning
showUnsavedWarning.value = true;
return;
}
}
finally {
// Remove from checking state
checkingUnsavedChanges.value = checkingUnsavedChanges.value.filter(
(id) => id !== flowId,
);
}
}

// No unsaved changes or no auto-refresh, proceed directly
confirmRunFlow();
}

function proceedAfterWarning() {
showUnsavedWarning.value = false;
confirmRunFlow();
}

function cancelWarning() {
showUnsavedWarning.value = false;
selectedTrigger.value = null;
}

return {
getFlow,
runFlow,
runningFlows,
checkingUnsavedChanges,
onTriggerClick,
getButtonText,
getButtonIcon,
Expand All @@ -213,5 +306,8 @@ export function useFlowTriggers(context: FlowTriggerContext) {
isConfirmButtonDisabled,
getConfirmButtonText,
resetConfirm,
showUnsavedWarning,
proceedAfterWarning,
cancelWarning,
};
}
15 changes: 15 additions & 0 deletions packages/flow-trigger-bundle/src/flow-triggers-interface/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ export default defineInterface({
},
},
},
{
field: 'autoRefresh',
name: 'Auto Refresh',
type: 'boolean',
meta: {
interface: 'boolean',
options: {
label: 'Automatically refresh item data after flow completes',
},
width: 'full',
},
schema: {
default_value: true,
},
},
];
},
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { PrimaryKey } from '@directus/types';
import type { Trigger } from '../types/trigger';
import { computed } from 'vue';
import { computed, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useFlowTriggers } from '../composables/use-flow-triggers';

Expand All @@ -19,16 +19,23 @@ const props = withDefaults(
primaryKey?: PrimaryKey;
field: string;
width: string;
autoRefresh?: boolean;
}>(),
{},
{
autoRefresh: true,
},
);

const { t } = useI18n();

// Inject form values from Directus form context
const formValues = inject('values', ref<Record<string, any>>({}));

const {
getFlow,
runFlow,
runningFlows,
checkingUnsavedChanges,
onTriggerClick,
getButtonText,
getButtonIcon,
Expand All @@ -38,9 +45,14 @@ const {
isConfirmButtonDisabled,
getConfirmButtonText,
resetConfirm,
showUnsavedWarning,
proceedAfterWarning,
cancelWarning,
} = useFlowTriggers({
collection: (_) => props.collection,
keys: (_) => props.primaryKey ? [props.primaryKey] : undefined,
autoRefresh: () => props.autoRefresh ?? true,
formValues: () => formValues.value,
});

const displayInterface = computed(
Expand Down Expand Up @@ -74,14 +86,34 @@ function getButtonDisabled(trigger: Trigger) {
<div v-for="{ trigger } in props.triggers" class="trigger">
<v-button
full-width
:loading="runningFlows.includes(trigger.flowId)"
:loading="runningFlows.includes(trigger.flowId) || checkingUnsavedChanges.includes(trigger.flowId)"
:disabled="getButtonDisabled(trigger)"
@click="onTriggerClick(trigger)"
>
<v-icon :name="getButtonIcon(trigger)" small left />
{{ getButtonText(trigger) }}
</v-button>
</div>
<!-- Unsaved Changes Warning Dialog -->
<v-dialog :model-value="showUnsavedWarning" @esc="cancelWarning">
<v-card>
<v-card-title>Unsaved Changes</v-card-title>
<v-card-text>
<p>You have unsaved changes in this form. If you continue, the page will refresh and these changes will be lost.</p>
<p><strong>Do you want to continue?</strong></p>
</v-card-text>
<v-card-actions>
<v-button secondary @click="cancelWarning">
{{ t('cancel') }}
</v-button>
<v-button kind="warning" @click="proceedAfterWarning">
Discard Changes & Continue
</v-button>
</v-card-actions>
</v-card>
</v-dialog>

<!-- Flow Confirmation Dialog -->
<v-dialog :model-value="displayCustomConfirmDialog" @esc="resetConfirm">
<v-card class="allow-drawer">
<v-card-title>{{ confirmDetails!.description ?? t('run_flow_confirm') }}</v-card-title>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const permissionsStore = usePermissionsStore();
const {
runFlow,
runningFlows,
checkingUnsavedChanges,
onTriggerClick,
getButtonText,
getButtonIcon,
Expand All @@ -41,6 +42,7 @@ const {
} = useFlowTriggers({
collection: (trigger) => trigger.collection,
keys: (trigger) => trigger.keys,
autoRefresh: () => false, // Don't refresh the page when used in a panel
});

const triggers = computed(() => {
Expand All @@ -56,7 +58,7 @@ const triggers = computed(() => {
<div v-for="{ trigger } in triggers" class="trigger">
<v-button
full-width
:loading="runningFlows.includes(trigger.flowId)"
:loading="runningFlows.includes(trigger.flowId) || checkingUnsavedChanges.includes(trigger.flowId)"
@click="onTriggerClick(trigger)"
>
<v-icon :name="getButtonIcon(trigger)" small left />
Expand Down