<!-- MejikModalForm.vue -->
<template>
<!-- We don't render anything directly. The b-modal is the component's entire visible structure. -->
<b-modal
:id="modalId"
:title="title"
v-bind="$attrs"
no-close-on-backdrop
@show="handleShow"
@hidden="handleHidden"
v-on="$listeners"
>
<!-- Loading Overlay -->
<b-overlay :show="isLoading" rounded="sm">
<!-- Scoped Slot for the form layout -->
<slot
name="form"
:form-object="formObject"
:form-mode="currentMode"
:save-item="saveItem"
:form-params="internalParams"
:reset-form="resetForm"
></slot>
</b-overlay>
<!-- Custom Footer for dynamic actions -->
<template #modal-footer>
<button class="btn btn-secondary" @click="hide">Cancel</button>
<!-- Show "Edit" button in 'view' mode if canEdit is true -->
<button
v-if="currentMode === 'view' && canEdit"
class="btn btn-info"
@click="switchToEditMode"
>
<i class="las la-pencil-alt"></i> Edit
</button>
<!-- Show "Save" button in 'add' or 'edit' mode -->
<button
v-if="(currentMode === 'add' && canAdd) || (currentMode === 'edit' && canEdit)"
class="btn btn-primary"
:disabled="isLoading"
@click="saveItem"
>
<b-spinner v-if="isLoading" small></b-spinner>
<i v-else class="las la-save"></i>
Save
</button>
</template>
</b-modal>
</template>
<script>
import _ from 'lodash';
export default {
name: "MejikModalForm",
// Inherit attributes like 'size', 'centered', etc., and pass them to b-modal
inheritAttrs: false,
props: {
// --- Behavior & Mode ---
mode: {
type: String,
default: 'add',
validator: (value) => ['add', 'edit', 'view'].includes(value),
},
// --- Data & Defaults ---
objectDefault: {
type: Object,
default: () => ({}),
},
// --- ACL ---
canAdd: { type: Boolean, default: true },
canEdit: { type: Boolean, default: true },
canView: { type: Boolean, default: true },
// canUpload prop is not directly used here but kept for consistency if needed later
canUpload: { type: Boolean, default: false },
// --- Validation ---
validator: {
type: Function,
default: null, // Should be an async function: async () => boolean
},
// --- Parameters ---
params: {
type: [Object, Array],
default: () => ({}),
},
// --- API URLs ---
urls: {
type: Object,
default: () => ({
add: '',
edit: '', // Should be the base URL, e.g., '/api/items'
view: '', // Should be the base URL, e.g., '/api/items'
param: '',
}),
},
// --- Customization ---
titlePrefix: {
type: String,
default: 'Item'
},
// Key to get the unique ID from the data object
uniqueKey: {
type: String,
default: 'id'
}
},
data() {
return {
modalId: `mejik-modal-form-${this._uid}`,
isLoading: false,
currentMode: 'add',
currentId: null,
formObject: {},
initialFormObject: {}, // For reset functionality
internalParams: {},
};
},
computed: {
title() {
// Creates a dynamic title like "Add New Item", "Edit Item", "View Item Details"
const modeText = {
add: 'Add New',
edit: 'Edit',
view: 'View Details of'
}[this.currentMode];
return `${modeText} ${this.titlePrefix}`;
},
},
methods: {
// --- Public Methods (to be called via $refs) ---
/**
* Public method to open the modal.
* @param {object} options - Configuration for the modal opening.
* @param {string} options.mode - 'add', 'edit', or 'view'.
* @param {any} [options.id=null] - The ID of the item for 'edit' or 'view' mode.
*/
show({ mode, id = null }) {
if (!this.canView && (mode === 'edit' || mode === 'view')) {
console.warn("MejikModalForm: Insufficient permissions to view or edit.");
return;
}
if (!this.canAdd && mode === 'add') {
console.warn("MejikModalForm: Insufficient permissions to add.");
return;
}
this.currentMode = mode;
this.currentId = id;
this.$bvModal.show(this.modalId);
},
hide() {
this.$bvModal.hide(this.modalId);
},
// --- Internal Event Handlers ---
// Fired when the modal begins to show. Good place to load data.
async handleShow() {
this.isLoading = true;
try {
// 1. Load additional parameters if URL is provided
if (this.urls.param) {
const res = await window.axios.get(this.urls.param);
this.internalParams = res.data;
} else {
this.internalParams = _.cloneDeep(this.params);
}
// 2. Load main form object based on mode
if (this.currentMode === 'add') {
this.formObject = _.cloneDeep(this.objectDefault);
} else if (this.currentId && (this.currentMode === 'edit' || this.currentMode === 'view')) {
if (!this.urls.view) {
console.error("MejikModalForm: `urls.view` prop is required for 'edit' or 'view' mode.");
this.hide();
return;
}
const res = await window.axios.get(`${this.urls.view}/${this.currentId}`);
this.formObject = res.data;
}
// Store the initial state for the reset functionality
this.initialFormObject = _.cloneDeep(this.formObject);
} catch (error) {
console.error("MejikModalForm: Error loading data.", error);
this.$emit('error', { type: 'load', error });
this.hide(); // Close modal on data load failure
} finally {
this.isLoading = false;
}
},
// Fired when the modal is completely hidden. Good place for cleanup.
handleHidden() {
this.formObject = {};
this.initialFormObject = {};
this.currentId = null;
this.isLoading = false;
this.internalParams = {};
},
// --- Form Actions (passed to slot) ---
async saveItem() {
// 1. External Validation
if (this.validator) {
const isValid = await this.validator(this.formObject);
if (!isValid) {
this.$emit('validation-failed');
return;
}
}
this.isLoading = true;
try {
let response;
if (this.currentMode === 'add') {
if (!this.urls.add) throw new Error("`urls.add` prop is not defined.");
response = await window.axios.post(this.urls.add, this.formObject);
} else { // 'edit' mode
const editUrl = this.urls.edit || this.urls.add; // Fallback to add url
if (!editUrl) throw new Error("`urls.edit` or `urls.add` prop is not defined for editing.");
const id = this.currentId || this.formObject[this.uniqueKey];
response = await window.axios.put(`${editUrl}/${id}`, this.formObject);
}
this.$emit('saved', { mode: this.currentMode, data: response.data });
this.hide();
} catch (error) {
console.error("MejikModalForm: Error saving data.", error);
this.$emit('error', { type: 'save', error });
} finally {
this.isLoading = false;
}
},
resetForm() {
this.formObject = _.cloneDeep(this.initialFormObject);
},
// --- Mode Switching ---
switchToEditMode() {
if (this.canEdit) {
this.currentMode = 'edit';
}
}
},
};
</script>