Implement Duplicate Component Feature and Enhance Delete Confirmation in FormBuilder
- Added a duplicate component button in FormBuilderCanvas.vue, allowing users to easily duplicate existing components. - Introduced a delete confirmation modal in both FormBuilderCanvas.vue and FormBuilderFieldSettingsModal.vue to prevent accidental deletions, enhancing user experience. - Updated delete functionality to utilize the confirmation modal, ensuring users are aware of the consequences of their actions. - Refactored related methods to support the new duplication and confirmation features, maintaining clean and organized code. - Enhanced user feedback with toast notifications upon successful deletion and duplication of components.
This commit is contained in:
parent
8d6184fd8b
commit
72a70972fb
@ -64,16 +64,23 @@
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="p-1 text-gray-400 hover:text-blue-500 rounded"
|
||||
title="Duplicate component"
|
||||
@click.stop="duplicateComponent(element)"
|
||||
>
|
||||
<Icon name="heroicons:document-duplicate" class="w-4 h-4" />
|
||||
</button>
|
||||
<!-- <button
|
||||
class="p-1 text-gray-400 hover:text-gray-600 rounded"
|
||||
title="Resize component"
|
||||
@click.stop="toggleResizeMode(element)"
|
||||
>
|
||||
<Icon name="material-symbols:resize" class="w-4 h-4" />
|
||||
</button>
|
||||
</button> -->
|
||||
<button
|
||||
class="p-1 text-gray-400 hover:text-red-500 rounded"
|
||||
title="Delete component"
|
||||
@click.stop="deleteComponent(element.id)"
|
||||
@click.stop="showDeleteModal(element.id)"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
@ -101,6 +108,32 @@
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<RsModal v-model="showDeleteConfirmModal" title="Confirm Delete" size="md" position="center">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-4">
|
||||
<Icon name="material-symbols:warning-outline" class="text-yellow-500 w-8 h-8 mr-3 flex-shrink-0" />
|
||||
<div>
|
||||
<p class="text-gray-600 font-medium mb-1">Delete Component</p>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Are you sure you want to delete this component? This action cannot be undone and will permanently remove the component from your form.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<RsButton @click="cancelDelete" variant="tertiary">
|
||||
Cancel
|
||||
</RsButton>
|
||||
<RsButton @click="confirmDelete" variant="danger">
|
||||
<Icon name="material-symbols:delete" class="w-4 h-4 mr-1" />
|
||||
Delete
|
||||
</RsButton>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -115,7 +148,7 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select-component', 'move-component', 'delete-component', 'update-component', 'optimize-layout', 'select-nested-component', 'insert-component-at-index']);
|
||||
const emit = defineEmits(['select-component', 'move-component', 'delete-component', 'update-component', 'optimize-layout', 'select-nested-component', 'insert-component-at-index', 'duplicate-component']);
|
||||
|
||||
const selectedComponentId = ref(null);
|
||||
const resizeMode = ref(false);
|
||||
@ -123,6 +156,10 @@ const resizing = ref(false);
|
||||
const initialWidth = ref(0);
|
||||
const initialX = ref(0);
|
||||
|
||||
// Delete modal state
|
||||
const showDeleteConfirmModal = ref(false);
|
||||
const componentToDelete = ref(null);
|
||||
|
||||
// Watch for changes in formComponents
|
||||
watch(() => props.formComponents, (newComponents) => {
|
||||
// If the currently selected component is no longer in the list, deselect it
|
||||
@ -155,13 +192,35 @@ const selectComponent = (component) => {
|
||||
emit('select-component', componentCopy);
|
||||
};
|
||||
|
||||
// Handle component deletion
|
||||
const deleteComponent = (id) => {
|
||||
if (selectedComponentId.value === id) {
|
||||
selectedComponentId.value = null;
|
||||
resizeMode.value = false;
|
||||
// Handle component duplication
|
||||
const duplicateComponent = (component) => {
|
||||
emit('duplicate-component', component);
|
||||
};
|
||||
|
||||
// Show delete confirmation modal
|
||||
const showDeleteModal = (id) => {
|
||||
componentToDelete.value = id;
|
||||
showDeleteConfirmModal.value = true;
|
||||
};
|
||||
|
||||
// Cancel delete operation
|
||||
const cancelDelete = () => {
|
||||
showDeleteConfirmModal.value = false;
|
||||
componentToDelete.value = null;
|
||||
};
|
||||
|
||||
// Confirm delete operation
|
||||
const confirmDelete = () => {
|
||||
if (componentToDelete.value) {
|
||||
const id = componentToDelete.value;
|
||||
if (selectedComponentId.value === id) {
|
||||
selectedComponentId.value = null;
|
||||
resizeMode.value = false;
|
||||
}
|
||||
emit('delete-component', id);
|
||||
}
|
||||
emit('delete-component', id);
|
||||
showDeleteConfirmModal.value = false;
|
||||
componentToDelete.value = null;
|
||||
};
|
||||
|
||||
// Handle nested component selection from sections
|
||||
|
@ -2918,7 +2918,7 @@ if (this.element.querySelector('.file-upload')) {
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<RsButton
|
||||
@click="handleReset"
|
||||
@click="showResetConfirmation"
|
||||
variant="warning-outline"
|
||||
>
|
||||
<Icon name="heroicons:arrow-path" class="w-4 h-4 mr-1" />
|
||||
@ -2937,6 +2937,32 @@ if (this.element.querySelector('.file-upload')) {
|
||||
</template>
|
||||
</RsModal>
|
||||
|
||||
<!-- Reset Confirmation Modal -->
|
||||
<RsModal v-model="showResetModal" title="Confirm Reset" size="md" position="center">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-4">
|
||||
<Icon name="material-symbols:warning-outline" class="text-yellow-500 w-8 h-8 mr-3 flex-shrink-0" />
|
||||
<div>
|
||||
<p class="text-gray-600 font-medium mb-1">Reset to Default Settings</p>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Are you sure you want to reset this component to its default settings? This action will overwrite all current configuration and cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<RsButton @click="cancelReset" variant="tertiary">
|
||||
Cancel
|
||||
</RsButton>
|
||||
<RsButton @click="confirmReset" variant="warning">
|
||||
<Icon name="heroicons:arrow-path" class="w-4 h-4 mr-1" />
|
||||
Reset to Default
|
||||
</RsButton>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -2970,6 +2996,7 @@ const configModel = ref({
|
||||
// Type changing state
|
||||
const pendingTypeChange = ref(null)
|
||||
const showTypeChangePreview = ref(false)
|
||||
const showResetModal = ref(false)
|
||||
|
||||
// Component info helpers
|
||||
const modalTitle = computed(() => {
|
||||
@ -3756,6 +3783,19 @@ const handleSave = () => {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const showResetConfirmation = () => {
|
||||
showResetModal.value = true;
|
||||
};
|
||||
|
||||
const cancelReset = () => {
|
||||
showResetModal.value = false;
|
||||
};
|
||||
|
||||
const confirmReset = () => {
|
||||
handleReset();
|
||||
showResetModal.value = false;
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (props.component) {
|
||||
// Reset to default props from the component definition
|
||||
|
@ -371,6 +371,21 @@ const formatCurrentCode = async () => {
|
||||
|
||||
const debouncedFormatCode = useDebounceFn(formatCurrentCode, 300);
|
||||
|
||||
// Helper functions for format button
|
||||
const getFormatButtonTitle = () => {
|
||||
const langName = props.language === 'json' ? 'JSON' : props.language.toUpperCase();
|
||||
return `Format ${langName} code (Shift+Alt+F)`;
|
||||
};
|
||||
|
||||
const getFormatButtonIcon = () => {
|
||||
return 'material-symbols:auto-fix-high';
|
||||
};
|
||||
|
||||
const getFormatButtonText = () => {
|
||||
const langName = props.language === 'json' ? 'JSON' : props.language.toUpperCase();
|
||||
return `Format ${langName}`;
|
||||
};
|
||||
|
||||
// Fullscreen functionality
|
||||
const isFullscreen = ref(false);
|
||||
const fullscreenContainer = ref(null);
|
||||
|
@ -330,6 +330,7 @@
|
||||
@select-nested-component="handleSelectNestedComponent"
|
||||
@move-component="handleMoveComponent"
|
||||
@delete-component="handleDeleteComponent"
|
||||
@duplicate-component="handleDuplicateComponent"
|
||||
@update-component="handleUpdateComponent"
|
||||
@optimize-layout="handleOptimizeLayout"
|
||||
@insert-component-at-index="handleInsertComponentAtIndex"
|
||||
@ -426,16 +427,9 @@
|
||||
<Icon name="heroicons:adjustments-horizontal" class="w-4 h-4 mr-1.5" />
|
||||
Full Settings
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="duplicateComponent"
|
||||
class="action-btn secondary"
|
||||
title="Duplicate this component"
|
||||
>
|
||||
<Icon name="heroicons:document-duplicate" class="w-4 h-4 mr-1.5" />
|
||||
Duplicate
|
||||
</button>
|
||||
<button
|
||||
@click="deleteComponent"
|
||||
@click="showDeleteConfirmation"
|
||||
class="action-btn danger"
|
||||
title="Delete this component"
|
||||
>
|
||||
@ -1288,6 +1282,84 @@
|
||||
@restored="handleFormRestored"
|
||||
/>
|
||||
|
||||
<!-- Delete Confirmation Modal for Settings Panel -->
|
||||
<RsModal v-model="showDeleteModal" title="Confirm Delete" size="md" position="center">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-4">
|
||||
<Icon name="material-symbols:warning-outline" class="text-yellow-500 w-8 h-8 mr-3 flex-shrink-0" />
|
||||
<div>
|
||||
<p class="text-gray-600 font-medium mb-1">Delete Component</p>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Are you sure you want to delete this component? This action cannot be undone and will permanently remove the component from your form.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<RsButton @click="cancelDelete" variant="tertiary">
|
||||
Cancel
|
||||
</RsButton>
|
||||
<RsButton @click="confirmDelete" variant="danger">
|
||||
<Icon name="material-symbols:delete" class="w-4 h-4 mr-1" />
|
||||
Delete
|
||||
</RsButton>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
|
||||
<!-- Replace Content Confirmation Modal -->
|
||||
<RsModal v-model="showReplaceContentModal" title="Confirm Replace Content" size="md" position="center">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-4">
|
||||
<Icon name="material-symbols:warning-outline" class="text-yellow-500 w-8 h-8 mr-3 flex-shrink-0" />
|
||||
<div>
|
||||
<p class="text-gray-600 font-medium mb-1">Replace Current Form Content</p>
|
||||
<p class="text-gray-600 text-sm">
|
||||
This will replace your current form content. Any unsaved changes will be lost. Are you sure you want to continue?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<RsButton @click="cancelReplaceContent" variant="tertiary">
|
||||
Cancel
|
||||
</RsButton>
|
||||
<RsButton @click="confirmReplaceContent" variant="warning">
|
||||
<Icon name="material-symbols:swap-horiz" class="w-4 h-4 mr-1" />
|
||||
Replace Content
|
||||
</RsButton>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
|
||||
<!-- Update Form Name Confirmation Modal -->
|
||||
<RsModal v-model="showUpdateFormNameModal" title="Update Form Name" size="md" position="center">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-4">
|
||||
<Icon name="material-symbols:warning-outline" class="text-yellow-500 w-8 h-8 mr-3 flex-shrink-0" />
|
||||
<div>
|
||||
<p class="text-gray-600 font-medium mb-1">Update Form Name</p>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Do you want to update the form name to match the template name "{{ pendingFormNameUpdate }}"?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<RsButton @click="cancelUpdateFormName" variant="tertiary">
|
||||
Keep Current Name
|
||||
</RsButton>
|
||||
<RsButton @click="confirmUpdateFormName" variant="primary">
|
||||
<Icon name="material-symbols:edit" class="w-4 h-4 mr-1" />
|
||||
Update Name
|
||||
</RsButton>
|
||||
</div>
|
||||
</template>
|
||||
</RsModal>
|
||||
|
||||
<!-- Help Modal -->
|
||||
<RsModal v-model="showHelpModal" title="How to Use Form Builder" size="xl" position="center">
|
||||
<div class="p-6 space-y-6">
|
||||
@ -1980,11 +2052,11 @@ const handleJsonImport = (event) => {
|
||||
} else {
|
||||
// Confirm before replacing current form
|
||||
if (formStore.formComponents.length > 0) {
|
||||
if (!confirm("This will replace your current form content. Continue?")) {
|
||||
// Reset file input
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
// Store the pending import for later confirmation
|
||||
pendingJsonImport.value = importedJson;
|
||||
fileInputToReset.value = event.target;
|
||||
showReplaceContentModal.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate imported JSON
|
||||
@ -2400,13 +2472,157 @@ const handleDeleteComponent = (id) => {
|
||||
}
|
||||
|
||||
console.log('Deleted nested component from container:', deletedComponent);
|
||||
toast.success('Component deleted successfully');
|
||||
}
|
||||
} else {
|
||||
// If not found in sections, delete as a main component
|
||||
formStore.deleteComponent(id);
|
||||
toast.success('Component deleted successfully');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicateComponent = (component) => {
|
||||
if (!component) return;
|
||||
|
||||
// Create a deep copy of the component props to avoid reference issues
|
||||
const duplicatedProps = JSON.parse(JSON.stringify(component.props));
|
||||
|
||||
// Update name to avoid conflicts
|
||||
if (duplicatedProps.name) {
|
||||
duplicatedProps.name = duplicatedProps.name + '_copy';
|
||||
}
|
||||
|
||||
// Update label to indicate it's a copy
|
||||
if (duplicatedProps.label) {
|
||||
duplicatedProps.label = duplicatedProps.label + ' (Copy)';
|
||||
}
|
||||
|
||||
// Create the component definition for duplication
|
||||
// Note: We don't set an ID here as the store's addComponent method will generate a new unique ID
|
||||
const componentToDuplicate = {
|
||||
type: component.type,
|
||||
name: getComponentTypeName(component.type) + ' (Copy)',
|
||||
category: getDefaultCategory(component.type),
|
||||
icon: getComponentIcon(component.type),
|
||||
defaultProps: duplicatedProps
|
||||
};
|
||||
|
||||
// The addComponent method in the store will automatically assign a new unique ID
|
||||
formStore.addComponent(componentToDuplicate);
|
||||
toast.success('Component duplicated successfully');
|
||||
};
|
||||
|
||||
// Delete confirmation modal state
|
||||
const showDeleteModal = ref(false);
|
||||
|
||||
// Replace content confirmation modal state
|
||||
const showReplaceContentModal = ref(false);
|
||||
const pendingJsonImport = ref(null);
|
||||
const pendingTemplate = ref(null);
|
||||
const fileInputToReset = ref(null);
|
||||
|
||||
// Update form name confirmation modal state
|
||||
const showUpdateFormNameModal = ref(false);
|
||||
const pendingFormNameUpdate = ref('');
|
||||
|
||||
const showDeleteConfirmation = () => {
|
||||
if (!formStore.selectedComponent) return;
|
||||
showDeleteModal.value = true;
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
showDeleteModal.value = false;
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (formStore.selectedComponent) {
|
||||
formStore.deleteComponent(formStore.selectedComponent.id);
|
||||
toast.success('Component deleted successfully');
|
||||
}
|
||||
showDeleteModal.value = false;
|
||||
};
|
||||
|
||||
// Replace content confirmation functions
|
||||
const confirmReplaceContent = () => {
|
||||
try {
|
||||
if (pendingJsonImport.value) {
|
||||
// Handle JSON import
|
||||
if (!pendingJsonImport.value.formName || !Array.isArray(pendingJsonImport.value.components)) {
|
||||
throw new Error('Invalid form JSON structure');
|
||||
}
|
||||
|
||||
formStore.formName = pendingJsonImport.value.formName || 'Imported Form';
|
||||
formStore.formDescription = pendingJsonImport.value.formDescription || '';
|
||||
formStore.formComponents = [];
|
||||
formStore.currentFormId = null;
|
||||
|
||||
pendingJsonImport.value.components.forEach(component => {
|
||||
formStore.addComponent(component);
|
||||
});
|
||||
|
||||
if (pendingJsonImport.value.customScript) {
|
||||
formStore.formCustomScript = pendingJsonImport.value.customScript;
|
||||
}
|
||||
|
||||
if (pendingJsonImport.value.customCSS) {
|
||||
formStore.formCustomCSS = pendingJsonImport.value.customCSS;
|
||||
}
|
||||
|
||||
toast.success('JSON imported successfully');
|
||||
} else if (pendingTemplate.value) {
|
||||
// Handle template application
|
||||
const template = pendingTemplate.value;
|
||||
|
||||
formStore.formComponents = [];
|
||||
formStore.currentFormId = null;
|
||||
|
||||
if (template.components && Array.isArray(template.components)) {
|
||||
template.components.forEach(component => {
|
||||
formStore.addComponent(component);
|
||||
});
|
||||
}
|
||||
|
||||
toast.success(`Template "${template.name}" applied successfully`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error replacing content:', error);
|
||||
toast.error('Failed to replace content: ' + error.message);
|
||||
} finally {
|
||||
// Reset state
|
||||
showReplaceContentModal.value = false;
|
||||
pendingJsonImport.value = null;
|
||||
pendingTemplate.value = null;
|
||||
if (fileInputToReset.value) {
|
||||
fileInputToReset.value.value = '';
|
||||
fileInputToReset.value = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cancelReplaceContent = () => {
|
||||
showReplaceContentModal.value = false;
|
||||
pendingJsonImport.value = null;
|
||||
pendingTemplate.value = null;
|
||||
if (fileInputToReset.value) {
|
||||
fileInputToReset.value.value = '';
|
||||
fileInputToReset.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Update form name confirmation functions
|
||||
const confirmUpdateFormName = () => {
|
||||
if (pendingFormNameUpdate.value) {
|
||||
formStore.setFormName(pendingFormNameUpdate.value);
|
||||
}
|
||||
showUpdateFormNameModal.value = false;
|
||||
pendingFormNameUpdate.value = '';
|
||||
};
|
||||
|
||||
const cancelUpdateFormName = () => {
|
||||
showUpdateFormNameModal.value = false;
|
||||
pendingFormNameUpdate.value = '';
|
||||
};
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
// Always prevent default to enable drop
|
||||
event.preventDefault();
|
||||
@ -2812,9 +3028,9 @@ function handleClickOutside(event) {
|
||||
const applyFormTemplate = (template) => {
|
||||
// Confirm if there's already content in the form
|
||||
if (formStore.formComponents.length > 0) {
|
||||
if (!confirm("This will replace your current form content. Continue?")) {
|
||||
return;
|
||||
}
|
||||
pendingTemplate.value = template;
|
||||
showReplaceContentModal.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -2822,8 +3038,11 @@ const applyFormTemplate = (template) => {
|
||||
console.log('Template components:', template.components ? template.components.length : 0);
|
||||
|
||||
// Set form name if it's a new form or user allows overwrite
|
||||
if (formStore.formName === 'New Form' || confirm("Update the form name to match the template?")) {
|
||||
if (formStore.formName === 'New Form') {
|
||||
formStore.setFormName(template.name);
|
||||
} else {
|
||||
pendingFormNameUpdate.value = template.name;
|
||||
showUpdateFormNameModal.value = true;
|
||||
}
|
||||
|
||||
// Reset the form components and form ID (to ensure this becomes a new form)
|
||||
@ -3336,44 +3555,9 @@ const openFullSettingsModal = () => {
|
||||
showFieldSettings.value = true;
|
||||
};
|
||||
|
||||
// Duplicate component
|
||||
const duplicateComponent = () => {
|
||||
if (!formStore.selectedComponent) return;
|
||||
|
||||
const originalComponent = formStore.selectedComponent;
|
||||
const duplicatedProps = { ...originalComponent.props };
|
||||
|
||||
// Update name to avoid conflicts
|
||||
if (duplicatedProps.name) {
|
||||
duplicatedProps.name = duplicatedProps.name + '_copy';
|
||||
}
|
||||
|
||||
// Update label to indicate it's a copy
|
||||
if (duplicatedProps.label) {
|
||||
duplicatedProps.label = duplicatedProps.label + ' (Copy)';
|
||||
}
|
||||
|
||||
const componentToDuplicate = {
|
||||
type: originalComponent.type,
|
||||
name: getComponentTypeName(originalComponent.type) + ' (Copy)',
|
||||
category: getDefaultCategory(originalComponent.type),
|
||||
icon: getComponentIcon(originalComponent.type),
|
||||
defaultProps: duplicatedProps
|
||||
};
|
||||
|
||||
formStore.addComponent(componentToDuplicate);
|
||||
toast.success('Component duplicated successfully');
|
||||
};
|
||||
|
||||
// Delete component
|
||||
const deleteComponent = () => {
|
||||
if (!formStore.selectedComponent) return;
|
||||
|
||||
if (confirm('Are you sure you want to delete this component?')) {
|
||||
formStore.deleteComponent(formStore.selectedComponent.id);
|
||||
toast.success('Component deleted successfully');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConditionalLogicGenerated = (script) => {
|
||||
// Add the generated script to the form's custom script
|
||||
|
Loading…
x
Reference in New Issue
Block a user