corrad-bp/components/CustomActionButton.vue
Afiq 2562ebbc74 Enhance Icon Selection and External URL Handling in Form Builder Components
- Updated FormBuilderFieldSettingsModal.vue and sideMenuNested.vue to replace text input for icon selection with an IconBrowser component, improving user experience for selecting Material Design icons.
- Enhanced CustomActionButton.vue to differentiate between internal and external URLs when navigating, ensuring proper handling of external links.
- Modified settings.local.json to include additional WebFetch commands for improved documentation access during development.
2025-08-08 11:33:58 +08:00

186 lines
4.9 KiB
Vue

<template>
<RsButton
:variant="getButtonVariant()"
:size="action.size || 'sm'"
:disabled="disabled || loading"
@click="handleClick"
:class="buttonClasses"
:title="action.label"
>
<Icon v-if="loading" name="material-symbols:progress-activity" class="animate-spin w-4 h-4" />
<Icon v-else :name="getButtonIcon()" class="w-4 h-4" />
<span v-if="showLabel" class="ml-1">{{ action.label }}</span>
</RsButton>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
action: {
type: Object,
required: true,
validator: (action) => {
return action && typeof action.label === 'string' && typeof action.url === 'string';
}
},
rowData: {
type: Object,
default: () => ({})
},
showLabel: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['click', 'navigate']);
const loading = ref(false);
// Computed properties
const buttonClasses = computed(() => {
const classes = ['custom-action-btn'];
return classes.join(' ');
});
// Get button variant - match edit/delete button styling
const getButtonVariant = () => {
const variantMapping = {
'primary': 'info-text',
'secondary': 'info-text',
'success': 'success-text',
'warning': 'warning-text',
'danger': 'danger-text',
'info': 'info-text'
};
return variantMapping[props.action.variant] || 'info-text';
};
// Get button icon with default fallback
const getButtonIcon = () => {
if (props.action.icon) {
return props.action.icon;
}
// Default icons based on variant/action type
const variantIcons = {
'primary': 'heroicons:cursor-arrow-rays',
'secondary': 'heroicons:cursor-arrow-rays',
'success': 'heroicons:check-circle',
'warning': 'heroicons:exclamation-triangle',
'danger': 'heroicons:exclamation-triangle',
'info': 'heroicons:information-circle'
};
return variantIcons[props.action.variant] || 'heroicons:cursor-arrow-rays';
};
// Methods
const substituteUrlPlaceholders = (url, rowData) => {
if (!url || typeof url !== 'string') return url;
// Replace placeholders like {id}, {name}, etc. with actual row data values
return url.replace(/\{([^}]+)\}/g, (match, key) => {
const value = rowData[key];
if (value !== undefined && value !== null) {
return encodeURIComponent(String(value));
}
// If key not found, leave placeholder as is or return empty string
console.warn(`CustomActionButton: Placeholder '${key}' not found in row data:`, rowData);
return '';
});
};
const handleClick = async (event) => {
event.preventDefault();
event.stopPropagation();
try {
loading.value = true;
// Emit click event for custom handling
emit('click', { action: props.action, rowData: props.rowData, event });
// Handle confirmation dialog if specified
if (props.action.confirmMessage) {
const confirmed = confirm(props.action.confirmMessage);
if (!confirmed) {
return;
}
}
// Process URL with substitutions
let finalUrl = props.action.url;
if (props.action.urlType === 'dynamic') {
finalUrl = substituteUrlPlaceholders(props.action.url, props.rowData);
}
if (!finalUrl) {
console.error('CustomActionButton: No URL provided or URL substitution failed');
return;
}
console.log(`[CustomActionButton] Navigating to: ${finalUrl}`);
// Emit navigate event
emit('navigate', {
action: props.action,
url: finalUrl,
rowData: props.rowData
});
// Handle navigation based on target type
const openInNewTab = props.action.target === '_blank' || props.action.openInNewTab;
// Check if URL is external (has protocol or starts with //)
const isExternalUrl = (url) => {
return /^https?:\/\//.test(url) || /^\/\//.test(url) || !url.startsWith('/');
};
if (openInNewTab) {
window.open(finalUrl, '_blank', 'noopener,noreferrer');
} else {
// Use navigateTo for SPA navigation if available, otherwise use window.location
if (typeof navigateTo === 'function') {
if (isExternalUrl(finalUrl)) {
// For external URLs, use navigateTo with external: true option
await navigateTo(finalUrl, { external: true });
} else {
// For internal URLs, use regular navigateTo
await navigateTo(finalUrl);
}
} else {
window.location.href = finalUrl;
}
}
} catch (error) {
console.error('CustomActionButton: Error handling click:', error);
} finally {
loading.value = false;
}
};
</script>
<style scoped>
.custom-action-btn {
transition: all 0.2s ease;
}
.custom-action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.custom-action-btn:active {
transform: translateY(0);
}
</style>