feat: Add and refactor MIME type management
Some checks failed
CI Pipeline / japa-tests (push) Failing after 47s
Some checks failed
CI Pipeline / japa-tests (push) Failing after 47s
- Added BaseModel with fillable attributes and mergeFillableAttributes method - Refactored MimeType model to extend BaseModel - Implemented destroy method in MimetypeController for deleting MIME types - Updated Create.vue component with refactoring and improved type safety - Fixed issues with ref usage in Create.vue - Updated routes to include new and refactored endpoints
This commit is contained in:
parent
d1480b1240
commit
537c6fd81a
10 changed files with 321 additions and 449 deletions
|
@ -27,23 +27,42 @@ const props = defineProps({
|
|||
ctrlKFocus: Boolean,
|
||||
});
|
||||
|
||||
const isReadOnly = true;
|
||||
// Get keys from standardTypes and otherTypes
|
||||
const isReadOnly: boolean = true;
|
||||
const standardMimeTypes = Object.keys(standardTypes);
|
||||
const otherMimeTypes = Object.keys(otherTypes);
|
||||
// MIME types list (you can expand this as needed)
|
||||
// const mimeTypes = Object.keys(standardTypes);
|
||||
|
||||
// Concatenate the keys from both types
|
||||
const mimeTypes = [...standardMimeTypes, ...otherMimeTypes];
|
||||
|
||||
const file_extensions = reactive({});
|
||||
const form = useForm({
|
||||
const file_extensions = reactive<Record<string, string>>({});
|
||||
interface FormData {
|
||||
name: string;
|
||||
file_extension: string[];
|
||||
enabled: boolean;
|
||||
}
|
||||
const form = useForm<FormData>({
|
||||
name: '',
|
||||
file_extension: [] as Array<string>,
|
||||
file_extension: [],
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const filteredMimetypes = ref<string[]>([]); // Stores the filtered MIME types for the dropdown
|
||||
const showDropdown = ref(false); // Controls the visibility of the autocomplete dropdown
|
||||
const selectedIndex: Ref<number> = ref(0); // Track selected MIME type in the dropdown
|
||||
const ul: Ref<HTMLLIElement[] | null> = ref<HTMLLIElement[]>([]);
|
||||
const newExtension: Ref = ref(''); //reactive([] as Array<string>);
|
||||
const mimetypeError = ref<string | null>(null);
|
||||
|
||||
watch(selectedIndex, (selectedIndex: number) => {
|
||||
if (selectedIndex != null && ul.value != null) {
|
||||
const currentElement: HTMLLIElement = ul.value[selectedIndex];
|
||||
currentElement &&
|
||||
currentElement?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'start',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to reset the object
|
||||
function resetFileExtensions() {
|
||||
// Reset to an empty object
|
||||
|
@ -66,89 +85,15 @@ const inputElClass = computed(() => {
|
|||
return base;
|
||||
});
|
||||
|
||||
|
||||
// Handle form submission
|
||||
const submit = async () => {
|
||||
if (isValidForm()) {
|
||||
await form.post(stardust.route('settings.mimetype.store'), form);
|
||||
}
|
||||
};
|
||||
|
||||
// Form validation before submission
|
||||
const isValidForm = (): boolean => {
|
||||
if (!form.name) {
|
||||
form.errors.name = 'Name is required.';
|
||||
return false;
|
||||
} else {
|
||||
form.errors.name = '';
|
||||
}
|
||||
if (!form.file_extension.length) {
|
||||
form.errors.file_extension = ['At least one file extension is required.'];
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const newExtension: Ref = ref(''); //reactive([] as Array<string>);
|
||||
const filteredMimetypes = ref<string[]>([]); // Stores the filtered MIME types for the dropdown
|
||||
const showDropdown = ref(false); // Controls the visibility of the autocomplete dropdown
|
||||
const selectedIndex: Ref<number> = ref(0); // Track selected MIME type in the dropdown
|
||||
const ul: Ref<Array<HTMLLIElement>> = ref([]);
|
||||
watch(selectedIndex, (selectedIndex: number) => {
|
||||
if (selectedIndex != null && ul.value != null) {
|
||||
const currentElement: HTMLLIElement = ul.value[selectedIndex];
|
||||
currentElement &&
|
||||
currentElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'start',
|
||||
});
|
||||
}
|
||||
});
|
||||
// const extensionError = ref<string | null>(null);
|
||||
const mimetypeError = ref<string | null>(null);
|
||||
|
||||
// const addFileExtension = () => {
|
||||
// if (newExtension.value && !form.file_extensions.includes(newExtension.value)) {
|
||||
// if (isValidFileExtension(newExtension.value)) {
|
||||
// form.file_extensions.push(newExtension.value);
|
||||
// newExtension.value = ''; // Clear the input field
|
||||
// extensionError.value = null; // Clear any existing error
|
||||
// } else {
|
||||
// extensionError.value = 'Invalid file extension or MIME type.';
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// // Helper to validate file extension
|
||||
// const isValidFileExtension = (extension: string): boolean => {
|
||||
// const mimeType = mime.getType(extension); // e.g. 'application/pdf'
|
||||
// return mimeType !== null;
|
||||
// };
|
||||
|
||||
|
||||
// const removeFileExtension = (index: number) => {
|
||||
// file_extensions.splice(index, 1); // Remove the extension at the given index
|
||||
// };
|
||||
|
||||
// Check if the MIME type is valid
|
||||
const isValidMimeType = (mimeType: string): boolean => {
|
||||
let extensions = mime.getExtension(mimeType)
|
||||
return extensions !== null;
|
||||
};
|
||||
|
||||
// watch(newExtension, (value) => {
|
||||
// if (value) {
|
||||
// filteredMimetypes.value = mimeTypes.filter(mimeType =>
|
||||
// mimeType.toLowerCase().includes(value.toLowerCase())
|
||||
// );
|
||||
// showDropdown.value = true;
|
||||
// } else {
|
||||
// showDropdown.value = false;
|
||||
// }
|
||||
// });
|
||||
async function handleInputChange(e: Event) {
|
||||
// const target = <HTMLInputElement>e.target;
|
||||
|
||||
const target = <HTMLInputElement>e.target;
|
||||
newExtension.value = target.value;
|
||||
|
||||
if (newExtension.value.length >= 2) {
|
||||
showDropdown.value = true;
|
||||
|
@ -172,15 +117,10 @@ const selectResult = (mimeType: string) => {
|
|||
|
||||
if (form.name && isValidMimeType(form.name)) {
|
||||
const extensions = mime.getAllExtensions(form.name) as Set<string>;
|
||||
// Convert the Set to an array of objects
|
||||
// Convert the Set to an object
|
||||
// Iterate over each extension and set both key and value to the extension
|
||||
Array.from(extensions).forEach(extension => {
|
||||
file_extensions[extension] = extension;
|
||||
});
|
||||
|
||||
// file_extensions.push(...Array.from(formattedExtensions));
|
||||
|
||||
} else {
|
||||
mimetypeError.value = 'Invalid MIME type.';
|
||||
}
|
||||
|
@ -201,7 +141,6 @@ function onArrowUp() {
|
|||
|
||||
function onEnter() {
|
||||
if (Array.isArray(filteredMimetypes.value) && filteredMimetypes.value.length && selectedIndex.value !== -1 && selectedIndex.value < filteredMimetypes.value.length) {
|
||||
//this.display = this.results[this.selectedIndex];
|
||||
const mimeType = filteredMimetypes.value[selectedIndex.value];
|
||||
// this.$emit('person', person);
|
||||
form.name = mimeType;
|
||||
|
@ -221,7 +160,7 @@ function onEnter() {
|
|||
// Convert the Set to an object
|
||||
Array.from(extensions).forEach(extension => {
|
||||
file_extensions[extension] = extension;
|
||||
});
|
||||
});
|
||||
|
||||
// file_extensions.push(...formattedExtensions);
|
||||
} else {
|
||||
|
@ -230,6 +169,31 @@ function onEnter() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
const submit = async () => {
|
||||
if (isValidForm()) {
|
||||
await form.post(stardust.route('settings.mimetype.store'), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Form validation before submission
|
||||
const isValidForm = (): boolean => {
|
||||
if (!form.name) {
|
||||
form.errors.name = 'Name is required.';
|
||||
return false;
|
||||
} else {
|
||||
form.errors.name = '';
|
||||
}
|
||||
if (!form.file_extension.length) {
|
||||
form.errors.file_extension = 'At least one file extension is required.';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -268,11 +232,6 @@ function onEnter() {
|
|||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
<!-- <button type="button" @click="addDefaultFileExtensions"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300"
|
||||
:disabled="!form.name">
|
||||
+
|
||||
</button> -->
|
||||
<ul v-if="showDropdown && filteredMimetypes.length"
|
||||
class="bg-white dark:bg-slate-800 w-full mt-2 max-h-28 overflow-y-auto scroll-smooth">
|
||||
<li v-for="(mimeType, index) in filteredMimetypes" :key="index" @click="selectResult(mimeType)"
|
||||
|
@ -280,10 +239,11 @@ function onEnter() {
|
|||
:class="{
|
||||
'bg-blue-500 text-white': selectedIndex === index,
|
||||
'bg-white text-gray-900': selectedIndex !== index
|
||||
}" :ref="(el: HTMLLIElement) => {
|
||||
ul[index] = el;
|
||||
}
|
||||
">
|
||||
}" :ref="(el) => {
|
||||
if (ul) {
|
||||
ul[index] = el as HTMLLIElement;
|
||||
}
|
||||
}">
|
||||
<svg class="absolute w-4 h-4 left-2 top-2" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
|
@ -298,21 +258,9 @@ function onEnter() {
|
|||
{{ mimetypeError }}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Enabled (Boolean) Input Field -->
|
||||
<!-- <div class="mb-4 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.enabled"
|
||||
id="enabled"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="enabled" class="ml-2 block text-sm font-medium text-gray-700">Enabled</label>
|
||||
</div> -->
|
||||
|
||||
<FormField v-if="form.name" label="Mimetype Name" :class="{ 'text-red-400': form.errors.name }">
|
||||
<FormControl v-model="form.name" name="display_name" :error="form.errors.name"
|
||||
:is-read-only="isReadOnly">
|
||||
:is-read-only=isReadOnly>
|
||||
<div class="text-red-400 text-sm" v-if="form.errors.name">
|
||||
{{ form.errors.name }}
|
||||
</div>
|
||||
|
@ -330,51 +278,15 @@ function onEnter() {
|
|||
</label>
|
||||
</FormField>
|
||||
|
||||
<!-- File Extensions Input Field -->
|
||||
<!-- <div class="space-y-2">
|
||||
<label for="newExtension" class="block text-sm font-medium text-gray-700">
|
||||
File Extensions
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="newExtension" type="text" id="newExtension"
|
||||
placeholder="Enter file extension (e.g., pdf)"
|
||||
class="block w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-blue-500 focus:border-blue-500" />
|
||||
<button type="button" @click="addFileExtension"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300"
|
||||
:disabled="!newExtension">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="extensionError" class="text-red-400 text-sm mt-1">
|
||||
{{ extensionError }}
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- File Extensions List -->
|
||||
<!-- <div class="mt-4">
|
||||
<h3 v-if="form.file_extensions.length > 0" class="text-lg font-medium text-gray-900">Added File
|
||||
Extensions</h3>
|
||||
<ul class="space-y-2 mt-2">
|
||||
<li v-for="(extension, index) in form.file_extensions" :key="index"
|
||||
class="flex justify-between items-center p-2 bg-gray-100 rounded-lg">
|
||||
<span class="text-gray-800">{{ extension }}</span>
|
||||
<button @click.prevent="removeFileExtension(index)"
|
||||
class="text-red-500 hover:text-red-700 text-sm font-medium">
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div> -->
|
||||
<FormField label="Extensions" wrap-body>
|
||||
<FormCheckRadioGroup v-model="form.file_extension" :options="file_extensions" name="file_extensions"
|
||||
is-column />
|
||||
</FormField>
|
||||
<div class="text-red-400 text-sm"
|
||||
v-if="form.errors.file_extension && Array.isArray(form.errors.file_extension)">
|
||||
<!-- {{ errors.password_confirmation }} -->
|
||||
{{ form.errors.file_extension.join(', ') }}
|
||||
</div>
|
||||
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
|
||||
|
|
80
resources/js/Pages/Admin/Mimetype/Delete.vue
Normal file
80
resources/js/Pages/Admin/Mimetype/Delete.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<script setup lang="ts">
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||
import { useForm, Head, usePage } from '@inertiajs/vue3';
|
||||
import { computed, Ref } from 'vue';
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||
import { mdiArrowLeftBoldOutline, mdiDeleteForever, mdiTrashCan } from '@mdi/js';
|
||||
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
|
||||
|
||||
const props = defineProps({
|
||||
mimetype: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const flash: Ref<any> = computed(() => {
|
||||
return usePage().props.flash;
|
||||
});
|
||||
const errors: Ref<any> = computed(() => {
|
||||
return usePage().props.errors;
|
||||
});
|
||||
const form = useForm({
|
||||
preferred_reviewer: '',
|
||||
preferred_reviewer_email: '',
|
||||
preferation: 'yes_preferation',
|
||||
|
||||
// preferation: '',
|
||||
// isPreferationRequired: false,
|
||||
});
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
await form.delete(stardust.route('settings.mimetype.deleteStore', [props.mimetype.id]));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutAuthenticated>
|
||||
<Head title="Delete mimetype" />
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton :icon="mdiDeleteForever" title="Delete Mimetype" main>
|
||||
<BaseButton :route-name="stardust.route('settings.mimetype.index')" :icon="mdiArrowLeftBoldOutline" label="Back"
|
||||
color="white" rounded-full small />
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox form @submit.prevent="handleSubmit">
|
||||
<FormValidationErrors v-bind:errors="errors" />
|
||||
<div class="modal-content py-4 text-left px-6">
|
||||
<div class="text-xl font-bold mb-4">Confirm Deletion</div>
|
||||
<p>Are you sure you want to delete the mimetype?</p>
|
||||
<ul class="mt-4 space-y-4 text-left text-gray-500 dark:text-gray-400">
|
||||
<li class="flex items-center space-x-3">
|
||||
<svg class="flex-shrink-0 w-3.5 h-3.5 text-green-500 dark:text-green-400" aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M1 5.917 5.724 10.5 15 1.5" />
|
||||
</svg>
|
||||
<span>{{ props.mimetype.name }} ({{ props.mimetype.file_extension }})</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="flash && flash.warning" class="flex flex-col mt-6 animate-fade-in">
|
||||
<div class="bg-yellow-500 border-l-4 border-orange-400 text-white p-4" role="alert">
|
||||
<p class="font-bold">Be Warned</p>
|
||||
<p>{{ flash.warning }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="danger" :icon="mdiTrashCan" :small="true"
|
||||
:class="{ 'opacity-25': form.processing }" :disabled="form.processing" label="Confirm Delete" />
|
||||
</BaseButtons>
|
||||
</template>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { Head, usePage } from '@inertiajs/vue3';
|
||||
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus } from '@mdi/js';
|
||||
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus, mdiTrashCan } from '@mdi/js';
|
||||
import { computed, ComputedRef } from 'vue';
|
||||
import type { PropType } from "vue";
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
|
@ -33,19 +33,12 @@ defineProps({
|
|||
const flash: ComputedRef<any> = computed(() => {
|
||||
return usePage().props.flash;
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<LayoutAuthenticated>
|
||||
|
||||
<Head title="Mime Types" />
|
||||
<SectionMain>
|
||||
<!--
|
||||
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Mime Types" main>
|
||||
</SectionTitleLineWithButton> -->
|
||||
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Mime Types" main>
|
||||
<BaseButton
|
||||
v-if="can.create"
|
||||
|
@ -61,24 +54,14 @@ const flash: ComputedRef<any> = computed(() => {
|
|||
{{ flash.message }}
|
||||
</NotificationBar>
|
||||
<CardBox class="mb-6" has-table>
|
||||
</CardBox>
|
||||
<CardBox class="mb-6" has-form-data>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<!-- <Sort label="Name" attribute="name" /> -->
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
<!-- <Sort label="Sort Order" attribute="sort_order" /> -->
|
||||
Status
|
||||
</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th v-if="can.edit">Actions</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="mimetype in mimetypes" :key="mimetype.id">
|
||||
<td data-label="Name">
|
||||
|
@ -88,7 +71,6 @@ const flash: ComputedRef<any> = computed(() => {
|
|||
<template v-if="mimetype.enabled">Active</template>
|
||||
<template v-else>Inactive</template>
|
||||
</td>
|
||||
|
||||
<td v-if="can.edit" class="before:hidden lg:w-1 whitespace-nowrap">
|
||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||
<BaseButton v-if="mimetype.enabled"
|
||||
|
@ -97,6 +79,9 @@ const flash: ComputedRef<any> = computed(() => {
|
|||
<BaseButton v-else
|
||||
:route-name="stardust.route('settings.mimetype.up', [mimetype.id])"
|
||||
color="success" :icon="mdiSquareEditOutline" label="enable" small />
|
||||
<BaseButton v-if="can.edit" color="danger"
|
||||
:route-name="stardust.route('settings.mimetype.delete', [mimetype.id])" :icon="mdiTrashCan"
|
||||
small />
|
||||
</BaseButtons>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -15,4 +15,5 @@
|
|||
},
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.vue"],
|
||||
"exclude": ["./utils/*.js"],
|
||||
}
|
||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue