feat: Add alternate mimetype support, enhance validation for alternate mimetypes, and improve script loading performance
All checks were successful
CI / container-job (push) Successful in 36s

- mime_type.ts: Added a new column `public alternate_mimetype: string;`
- MimetypeController.ts: Extended validation and storage logic to accommodate the new `alternate_mimetype` attribute
- adonisrc.ts: Integrated new validation rule to validate user-provided mimetypes
- vite.ts: Set `defer: true` for script attributes to improve loading performance
- update_1_to_mime_types.ts: Added migration for the new `alternate_mimetype` column in the database
- UI improvements: Updated components such as AsideMenuLayer.vue, FormCheckRadioGroup.vue, MimeTypeInput.vue, NavBar.vue (lime-green background), NavBarMenu.vue, SectionBannerStarOnGitea.vue, Admin/mimetype/Create.vue, Admin/mimetype/Delete.vue, Admin/mimetype/Index.vue
- allowed_extensions_mimetype.ts: Enhanced rule to also check for alternate mimetypes
- referenceValidation.ts: Improved validation to allow only ISBNs with a '-' delimiter
- package-lock.json: Updated npm dependencie
This commit is contained in:
Kaimbacher 2025-02-13 15:49:09 +01:00
parent 4c5a8f5a42
commit a3031169ca
20 changed files with 719 additions and 704 deletions

View file

@ -36,13 +36,24 @@ const logoutItemClick = async () => {
await router.post(stardust.route('logout'));
};
const menuClick = (event, item) => {
interface MenuItem {
name: string;
label: string;
icon: string;
color: string;
link: string;
}
const menuClick = (event: Event, item: MenuItem) => {
emit('menu-click', event, item);
};
</script>
<template>
<aside id="aside" class="lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden">
<aside
id="aside"
class="lg:pb-2 lg:pl-2 w-60 fixed flex z-40 top-0 lg:top-16 h-screen lg:h-[calc(100vh-64px)] transition-position overflow-hidden"
>
<div :class="styleStore.asideStyle" class="lg:rounded-xl flex-1 flex flex-col overflow-hidden dark:bg-slate-900">
<div :class="styleStore.asideBrandStyle" class="flex flex-row h-14 items-center justify-between dark:bg-slate-900">
<div class="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">

View file

@ -1,10 +1,22 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import FormCheckRadio from '@/Components/FormCheckRadio.vue';
import BaseButton from '@/Components/BaseButton.vue';
import FormControl from '@/Components/FormControl.vue';
import { mdiPlusCircle } from '@mdi/js';
const props = defineProps({
options: {
type: Object,
default: () => {},
default: () => { },
},
allowManualAdding: {
type: Boolean,
default: false,
},
manualAddingPlaceholder: {
type: String,
default: 'Add manually',
required: false,
},
name: {
type: String,
@ -55,6 +67,14 @@ const computedValue = computed({
const hasIdAttribute = (obj: any): obj is { id: any } => {
return typeof obj === 'object' && 'id' in obj;
};
const newOption = ref<string>('');
const addOption = () => {
if (newOption.value && !props.options[newOption.value]) {
props.options[newOption.value] = newOption.value;
newOption.value = '';
}
};
</script>
<template>
@ -63,15 +83,11 @@ const hasIdAttribute = (obj: any): obj is { id: any } => {
<!-- :label="value" -->
<!-- :input-value="value.id"
:label="value.name" -->
<FormCheckRadio
v-for="(value, key) in options"
:key="key"
v-model="computedValue"
:type="type"
:name="name"
:input-value="key"
:label="value"
:class="componentClass"
/>
<div v-if="allowManualAdding && type === 'checkbox'" class="flex items-center mt-2 mb-2">
<FormControl v-model="newOption" :placeholder="manualAddingPlaceholder" class="mr-2" />
<BaseButton small rounded-full color="info" :icon="mdiPlusCircle" @click.prevent="addOption" />
</div>
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type" :name="name"
:input-value="key" :label="value" :class="componentClass" />
</div>
</template>

View file

@ -0,0 +1,138 @@
<template>
<div class="relative mb-4">
<!-- <label for="mimetype-input" class="block text-sm font-medium text-gray-700">Search for Mimetypes</label> -->
<input id="mimetype-input" v-model="newExtension" type="text" placeholder="Enter Mimetype Name"
class="block w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-blue-500 focus:border-blue-500"
:class="inputElClass" @input="handleInputChange" @keydown.down="onArrowDown" @keydown.up="onArrowUp"
@keydown.prevent.enter="onEnter">
</input>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="newExtension.length < 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="newExtension.length >= 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
@click="clearInput">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<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)"
class="pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-blue-500 hover:text-white"
:class="{
'bg-blue-500 text-white': selectedIndex === index,
'bg-white text-gray-900': selectedIndex !== index
}" :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"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
<span>{{ mimeType }}</span>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, Ref, watch, computed } from 'vue';
// import mime from 'mime';
const emit = defineEmits(['onSelectResult', 'onClearInput'])
const props = defineProps({
borderless: Boolean,
transparent: Boolean,
mimeTypes: {
type: Array as () => string[],
required: true
},
// form: Object,
// isValidMimeType: Function,
});
const newExtension = ref('');
const showDropdown = ref(false);
const filteredMimetypes = ref<string[]>([]);
const selectedIndex: Ref<number> = ref(0);
const ul: Ref<HTMLLIElement[] | null> = ref<HTMLLIElement[]>([]);
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 inputElClass = computed(() => {
const base = [
'block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg',
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
'h-12',
props.borderless ? 'border-0' : 'border',
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
];
base.push('pl-10');
return base;
});
const handleInputChange = (e: Event) => {
const target = <HTMLInputElement>e.target;
newExtension.value = target.value;
if (newExtension.value.length >= 2) {
showDropdown.value = true;
filteredMimetypes.value = props.mimeTypes.filter(mimeType =>
mimeType.toLowerCase().includes(newExtension.value.toLowerCase())
);
} else {
showDropdown.value = false;
}
};
const selectResult = (mimeType: string) => {
showDropdown.value = false;
newExtension.value = '';
selectedIndex.value = -1;
emit('onSelectResult', mimeType);
};
const clearInput = () => {
newExtension.value = '';
showDropdown.value = false;
// props.form.name = '';
// props.resetFileExtensions();
emit('onClearInput');
};
const onArrowDown = () => {
if (filteredMimetypes.value.length > 0) {
selectedIndex.value = selectedIndex.value === filteredMimetypes.value.length - 1 ? 0 : selectedIndex.value + 1;
}
};
const onArrowUp = () => {
if (filteredMimetypes.value.length > 0) {
selectedIndex.value = selectedIndex.value == 0 || selectedIndex.value == -1 ? filteredMimetypes.value.length - 1 : selectedIndex.value - 1;
}
};
const onEnter = () => {
if (Array.isArray(filteredMimetypes.value) && filteredMimetypes.value.length && selectedIndex.value !== -1 && selectedIndex.value < filteredMimetypes.value.length) {
const mimeType = filteredMimetypes.value[selectedIndex.value];
selectResult(mimeType);
}
};
</script>

View file

@ -95,7 +95,7 @@ const showAbout = async () => {
</script>
<template>
<nav class="text-base top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position lg:w-auto dark:bg-slate-800"
<nav class="text-base top-0 left-0 right-0 fixed bg-lime h-14 z-50 w-screen transition-position lg:w-auto dark:bg-slate-800"
:class="{ 'xl:pl-60': props.showBurger == true }">
<FirstrunWizard ref="about"></FirstrunWizard>
<div class="flex lg:items-stretch" :class="containerMaxW">
@ -122,10 +122,10 @@ const showAbout = async () => {
<BaseIcon :path="isMenuNavBarActive ? mdiClose : mdiDotsVertical" size="24" />
</NavBarItem>
</div>
<div class="absolute w-screen top-14 left-0 bg-gray-50 shadow lg:w-auto lg:items-stretch lg:flex lg:grow lg:static lg:border-b-0 lg:overflow-visible lg:shadow-none dark:bg-slate-800"
<div class="fixed w-screen top-14 left-0 shadow lg:w-auto lg:items-stretch lg:flex lg:grow lg:static lg:border-b-0 lg:overflow-visible lg:shadow-none dark:bg-slate-800"
:class="[isMenuNavBarActive ? 'block' : 'hidden']">
<div
class="max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto">
class="bg-white lg:bg-lime dark:bg-transparent max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto">
<!-- help menu -->
<NavBarMenu>
@ -186,7 +186,7 @@ const showAbout = async () => {
<NavBarItem is-desktop-icon-only @click.prevent="toggleLightDark">
<NavBarItemLabel v-bind:icon="mdiThemeLightDark" label="Light/Dark" is-desktop-icon-only />
</NavBarItem>
<NavBarItem href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" is-desktop-icon-only>
<NavBarItem href="https://gitea.geosphere.at/geolba/tethys.backend" target="_blank" is-desktop-icon-only>
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
</NavBarItem>
<NavBarItem is-desktop-icon-only @click="showAbout">

View file

@ -1,4 +1,4 @@
<script setup>
<script lang="ts" setup>
import { StyleService } from '@/Stores/style.service';
import { computed, ref, onMounted, onBeforeUnmount } from 'vue';
import { mdiChevronUp, mdiChevronDown } from '@mdi/js';
@ -15,10 +15,10 @@ const toggle = () => {
isDropdownActive.value = !isDropdownActive.value;
};
const root = ref(null);
const root = ref(NavBarItem);
const forceClose = (event) => {
if (!root.value.$el.contains(event.target)) {
const forceClose = (event: MouseEvent) => {
if (!root.value?.$el.contains(event.target)) {
isDropdownActive.value = false;
}
};

View file

@ -5,9 +5,9 @@ import SectionBanner from '@/Components/SectionBanner.vue';
</script>
<template>
<SectionBanner bg="greenBlue">
<h1 class="text-3xl text-white mb-6">Like the project? Please star on <b>Gitea</b>!</h1>
<h1 class="text-3xl text-white mb-6">Like the project? Please star on <b>GeoSphere Git Repository</b>!</h1>
<div>
<BaseButton href="https://gitea.geologie.ac.at/geolba/tethys" :icon="mdiGithub" label="Gitea" target="_blank" rounded-full />
<BaseButton href="https://gitea.geosphere.at/geolba/tethys.backend" :icon="mdiGithub" label="Forgejo" target="_blank" rounded-full />
</div>
</SectionBanner>
</template>