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
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:
parent
4c5a8f5a42
commit
a3031169ca
20 changed files with 719 additions and 704 deletions
|
@ -34,6 +34,7 @@ export default defineConfig({
|
|||
() => import('#start/rules/allowed_extensions_mimetypes'),
|
||||
() => import('#start/rules/dependent_array_min_length'),
|
||||
() => import('#start/rules/referenceValidation'),
|
||||
() => import('#start/rules/valid_mimetype'),
|
||||
],
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class MimetypeController {
|
|||
const newDatasetSchema = vine.object({
|
||||
name: vine.string().trim().isUnique({ table: 'mime_types', column: 'name' }),
|
||||
file_extension: vine.array(vine.string()).minLength(1), // define at least one extension for the new mimetype
|
||||
alternate_mimetype: vine.array(vine.string().isValidMimetype()).distinct().optional(), // define alias mimetypes
|
||||
enabled: vine.boolean(),
|
||||
});
|
||||
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
||||
|
@ -33,17 +34,21 @@ export default class MimetypeController {
|
|||
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
||||
const validator = vine.compile(newDatasetSchema);
|
||||
validator.messagesProvider = new SimpleMessagesProvider(this.messages);
|
||||
await request.validateUsing(validator);
|
||||
await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) });
|
||||
} catch (error) {
|
||||
// Step 3 - Handle errors
|
||||
// return response.badRequest(error.messages);
|
||||
throw error;
|
||||
}
|
||||
const input = request.only(['name', 'enabled', 'file_extension']);
|
||||
const input = request.only(['name', 'enabled', 'file_extension', 'alternate_mimetype']);
|
||||
// Concatenate the file_extensions array into a string with '|' as the separator
|
||||
if (Array.isArray(input.file_extension)) {
|
||||
input.file_extension = input.file_extension.join('|');
|
||||
}
|
||||
// Concatenate the alias_mimetype array into a string with '|' as the separator
|
||||
if (Array.isArray(input.alternate_mimetype)) {
|
||||
input.alternate_mimetype = input.alternate_mimetype.join('|');
|
||||
}
|
||||
await MimeType.create(input);
|
||||
// if (request.input('roles')) {
|
||||
// const roles: Array<number> = request.input('roles');
|
||||
|
|
|
@ -16,9 +16,14 @@ export default class MimeType extends BaseModel {
|
|||
@column({})
|
||||
public name: string;
|
||||
|
||||
// 1 : n file_extensions are separated by '|' in the database
|
||||
@column({})
|
||||
public file_extension: string;
|
||||
|
||||
// 1 : n alternate_mimetype are separated by '|' in the database
|
||||
@column({})
|
||||
public alternate_mimetype: string;
|
||||
|
||||
@column({})
|
||||
public enabled: boolean;
|
||||
|
||||
|
|
|
@ -20,10 +20,13 @@ const viteBackendConfig = defineConfig({
|
|||
*/
|
||||
assetsUrl: '/assets',
|
||||
|
||||
// neu
|
||||
// scriptAttributes: {
|
||||
// defer: true,
|
||||
// },
|
||||
/**
|
||||
* Add defer attribute to scripts for better performance.
|
||||
*/
|
||||
scriptAttributes: {
|
||||
defer: true,
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default viteBackendConfig;
|
||||
|
|
18
database/migrations/update_1_to_mime_types.ts
Normal file
18
database/migrations/update_1_to_mime_types.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { BaseSchema } from "@adonisjs/lucid/schema";
|
||||
|
||||
export default class AddAlternateMimetypeToMimeTypes extends BaseSchema {
|
||||
protected tableName = 'mime_types';
|
||||
|
||||
public async up () {
|
||||
this.schema.alterTable(this.tableName, (table) => {
|
||||
table.string('alternate_mimetype').nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public async down () {
|
||||
this.schema.alterTable(this.tableName, (table) => {
|
||||
table.dropColumn('alternate_mimetype');
|
||||
});
|
||||
}
|
||||
}
|
||||
// ALTER TABLE "mime_types" ADD COLUMN "alternate_mimetype" VARCHAR(255) NULL
|
792
package-lock.json
generated
792
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
138
resources/js/Components/MimetypeInput.vue
Normal file
138
resources/js/Components/MimetypeInput.vue
Normal 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>
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, Ref, reactive } from 'vue';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { mdiAccountKey, mdiArrowLeftBoldOutline } from '@mdi/js';
|
||||
import { mdiAccountKey, mdiArrowLeftBoldOutline, mdiTrashCan, mdiImageText, mdiPlus } from '@mdi/js';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||
|
@ -16,12 +16,9 @@ import FormControl from '@/Components/FormControl.vue';
|
|||
import standardTypes from 'mime/types/standard.js';
|
||||
import otherTypes from 'mime/types/other.js';
|
||||
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
|
||||
import MimetypeInput from '@/Components/MimetypeInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
permissions: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
defineProps({
|
||||
borderless: Boolean,
|
||||
transparent: Boolean,
|
||||
ctrlKFocus: Boolean,
|
||||
|
@ -36,88 +33,61 @@ const file_extensions = reactive<Record<string, string>>({});
|
|||
interface FormData {
|
||||
name: string;
|
||||
file_extension: string[];
|
||||
alternate_mimetype: string[];
|
||||
enabled: boolean;
|
||||
[key: string]: string | string[] | boolean;
|
||||
}
|
||||
const form = useForm<FormData>({
|
||||
name: '',
|
||||
file_extension: [],
|
||||
alternate_mimetype: [],
|
||||
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',
|
||||
});
|
||||
}
|
||||
});
|
||||
// const newExtension = ref<string>('');
|
||||
// const addFileExtension = () => {
|
||||
// if (newExtension.value && !file_extensions[newExtension.value]) {
|
||||
// file_extensions[newExtension.value] = newExtension.value;
|
||||
// newExtension.value = '';
|
||||
// }
|
||||
// };
|
||||
const addAlternateMimetype = () => {
|
||||
form.alternate_mimetype.push("");
|
||||
};
|
||||
const removeAliasMimetype = (index: number) => {
|
||||
form.alternate_mimetype.splice(index, 1);
|
||||
};
|
||||
|
||||
// Function to reset the object
|
||||
function resetFileExtensions() {
|
||||
// Reset to an empty object
|
||||
Object.keys(file_extensions).forEach(key => {
|
||||
delete file_extensions[key];
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
];
|
||||
// if (props.icon) {
|
||||
base.push('pl-10');
|
||||
// }
|
||||
return base;
|
||||
});
|
||||
|
||||
// Check if the MIME type is valid
|
||||
const isValidMimeType = (mimeType: string): boolean => {
|
||||
let extensions = mime.getExtension(mimeType)
|
||||
return extensions !== null;
|
||||
};
|
||||
|
||||
async function handleInputChange(e: Event) {
|
||||
const target = <HTMLInputElement>e.target;
|
||||
newExtension.value = target.value;
|
||||
const clearInput = () => {
|
||||
// newExtension.value = '';
|
||||
// showDropdown.value = false;
|
||||
form.name = '';
|
||||
resetFileExtensions();
|
||||
};
|
||||
|
||||
if (newExtension.value.length >= 2) {
|
||||
showDropdown.value = true;
|
||||
filteredMimetypes.value = mimeTypes.filter(mimeType =>
|
||||
mimeType.toLowerCase().includes(newExtension.value.toLowerCase())
|
||||
);
|
||||
} else {
|
||||
// data.results = [];
|
||||
showDropdown.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MIME type selection from the dropdown
|
||||
const selectResult = (mimeType: string) => {
|
||||
form.name = mimeType;
|
||||
// file_extensions.values = [];
|
||||
resetFileExtensions();
|
||||
showDropdown.value = false;
|
||||
newExtension.value = ''; // Reset the input
|
||||
selectedIndex.value = -1;
|
||||
// showDropdown.value = false;
|
||||
// newExtension.value = '';
|
||||
// selectedIndex.value = -1;
|
||||
|
||||
if (form.name && isValidMimeType(form.name)) {
|
||||
const extensions = mime.getAllExtensions(form.name) as Set<string>;
|
||||
// Iterate over each extension and set both key and value to the extension
|
||||
Array.from(extensions).forEach(extension => {
|
||||
file_extensions[extension] = extension;
|
||||
});
|
||||
|
@ -126,61 +96,16 @@ const selectResult = (mimeType: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
function onArrowDown() {
|
||||
if (filteredMimetypes.value.length > 0) {
|
||||
selectedIndex.value = selectedIndex.value === filteredMimetypes.value.length - 1 ? 0 : selectedIndex.value + 1;
|
||||
// const currentElement: HTMLLIElement = ul.value[selectedIndex.value];
|
||||
}
|
||||
}
|
||||
|
||||
function onArrowUp() {
|
||||
if (filteredMimetypes.value.length > 0) {
|
||||
selectedIndex.value = selectedIndex.value == 0 || selectedIndex.value == -1 ? filteredMimetypes.value.length - 1 : selectedIndex.value - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function onEnter() {
|
||||
if (Array.isArray(filteredMimetypes.value) && filteredMimetypes.value.length && selectedIndex.value !== -1 && selectedIndex.value < filteredMimetypes.value.length) {
|
||||
const mimeType = filteredMimetypes.value[selectedIndex.value];
|
||||
// this.$emit('person', person);
|
||||
form.name = mimeType;
|
||||
// reset form file extensions
|
||||
// file_extensions.values = [];
|
||||
resetFileExtensions();
|
||||
showDropdown.value = false;
|
||||
newExtension.value = ''; // Reset the input
|
||||
selectedIndex.value = -1;
|
||||
if (form.name) {
|
||||
// clear all loaded file extensions
|
||||
// file_extensions.values = [];
|
||||
resetFileExtensions();
|
||||
if (isValidMimeType(form.name)) {
|
||||
let extensions = mime.getAllExtensions(form.name) as Set<string>;
|
||||
// Convert the Set to an array of objects
|
||||
// Convert the Set to an object
|
||||
Array.from(extensions).forEach(extension => {
|
||||
file_extensions[extension] = extension;
|
||||
});
|
||||
|
||||
// file_extensions.push(...formattedExtensions);
|
||||
} else {
|
||||
mimetypeError.value = 'Invalid MIME type.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.';
|
||||
|
@ -190,6 +115,7 @@ const isValidForm = (): boolean => {
|
|||
}
|
||||
if (!form.file_extension.length) {
|
||||
form.errors.file_extension = 'At least one file extension is required.';
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -205,59 +131,15 @@ const isValidForm = (): boolean => {
|
|||
<BaseButton :route-name="stardust.route('settings.mimetype.index')" :icon="mdiArrowLeftBoldOutline"
|
||||
label="Back" color="white" rounded-full small />
|
||||
</SectionTitleLineWithButton>
|
||||
<!-- <CardBox form @submit.prevent="form.post(stardust.route('role.store'))"> -->
|
||||
<CardBox form @submit.prevent="submit()">
|
||||
|
||||
<!-- MIME Type Input Field with Autocomplete -->
|
||||
<div class="relative mb-4">
|
||||
|
||||
<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="() => {
|
||||
newExtension = '';
|
||||
showDropdown = false;
|
||||
form.name = '';
|
||||
resetFileExtensions();
|
||||
}
|
||||
">
|
||||
<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>
|
||||
<MimetypeInput @on-select-result="selectResult" @on-clear-input="clearInput" :transparent="transparent"
|
||||
:borderless="borderless" :mimeTypes="mimeTypes" :isValidMimeType="isValidMimeType" />
|
||||
<div v-if="mimetypeError" class="text-red-400 text-sm mt-1">
|
||||
{{ mimetypeError }}
|
||||
</div>
|
||||
|
||||
<BaseDivider v-if="form.name" />
|
||||
|
||||
<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>
|
||||
|
@ -266,29 +148,88 @@ const isValidForm = (): boolean => {
|
|||
</div>
|
||||
</FormControl>
|
||||
</FormField>
|
||||
|
||||
|
||||
<FormField v-if="form.name" help="Activate mimetype immediately?" wrap-body
|
||||
<!-- <FormField v-if="form.name" help="Activate mimetype immediately?" wrap-body
|
||||
class="mt-8 w-full mx-2 flex-1 flex-col">
|
||||
<label for="rights" class="checkbox mr-6 mb-3 last:mr-0">
|
||||
<input type="checkbox" id="rights" required v-model="form.enabled" />
|
||||
<span class="check" />
|
||||
<a class="pl-2" target="_blank">Enable mimetype immediately </a>
|
||||
|
||||
</label>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Extensions" wrap-body>
|
||||
</FormField> -->
|
||||
<FormField v-if="Object.keys(file_extensions).length > 0" label="File Extensions" wrap-body>
|
||||
<!-- <div class="flex items-center mt-2">
|
||||
<FormControl v-model="newExtension" placeholder="Enter file extension" class="mr-2" />
|
||||
<BaseButton color="info" @click="addFileExtension" label="Add" />
|
||||
</div> -->
|
||||
<FormCheckRadioGroup v-model="form.file_extension" :options="file_extensions" name="file_extensions"
|
||||
is-column />
|
||||
is-column allow-manual-adding manual-adding-placeholder="Enter file extension manually"/>
|
||||
|
||||
</FormField>
|
||||
<div class="text-red-400 text-sm"
|
||||
v-if="form.errors.file_extension && Array.isArray(form.errors.file_extension)">
|
||||
{{ form.errors.file_extension.join(', ') }}
|
||||
<div class="text-red-400 text-sm" v-if="form.errors.file_extension">
|
||||
{{ form.errors.file_extension }}
|
||||
</div>
|
||||
|
||||
<BaseDivider />
|
||||
<!-- <FormField label="Add File Extension" wrap-body>
|
||||
<FormControl v-model="newExtension" placeholder="Enter file extension" />
|
||||
<BaseButton color="info" @click="addFileExtension" label="Add" />
|
||||
</FormField> -->
|
||||
|
||||
<BaseDivider v-if="Object.keys(file_extensions).length > 0" />
|
||||
|
||||
<CardBox v-if="form.name" class="mb-6 shadow" has-table :icon="mdiImageText" title="Alternate Mimetypes"
|
||||
:header-icon="mdiPlus" @header-icon-click="addAlternateMimetype">
|
||||
<div v-if="form.alternate_mimetype.length === 0" class="text-center py-4">
|
||||
<p class="text-gray-600">No alternate mimetypes added yet.</p>
|
||||
<p class="text-gray-400">
|
||||
Click the plus icon above to add a new alternate mimetype.
|
||||
<br>
|
||||
An alternate mimetype is needed to ensure compatibility across different systems and
|
||||
software.
|
||||
For example, the GeoPackage standard mimetype is
|
||||
'application/vnd.opengeospatial.geopackage+sqlite3', but most software stores it as
|
||||
'application/x-sqlite3'. Therefore, 'application/x-sqlite3' must be added as an alternate
|
||||
mimetype.
|
||||
</p>
|
||||
<!-- <FormField label="Alias Mimetype" wrap-body>
|
||||
<FormControl v-model="aliasMimetype" name="alias_mimetype" :error="form.errors.alias_mimetype"
|
||||
:is-read-only=isReadOnly>
|
||||
<div class="text-red-400 text-sm" v-if="form.errors.alias_mimetype">
|
||||
{{ form.errors.alias_mimetype }}
|
||||
</div>
|
||||
</FormControl>
|
||||
<BaseButton color="info" :icon="mdiEarthPlus" small @click.prevent="addAliasMimetype" />
|
||||
</FormField> -->
|
||||
</div>
|
||||
<table class="table-fixed border-green-900" v-if="form.alternate_mimetype.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-10/12">Alias Mimetype</th>
|
||||
<th class="w-2/12"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(alias, index) in form.alternate_mimetype" :key="index">
|
||||
<td>
|
||||
<FormControl required v-model="form.alternate_mimetype[index]"
|
||||
placeholder="[alternate mimetype]">
|
||||
<div class="text-red-400 text-sm"
|
||||
v-if="Array.isArray(form.errors[`alternate_mimetype.${index}`])">
|
||||
{{ form.errors[`alternate_mimetype.${index}`]?.join(', ') }}
|
||||
</div>
|
||||
</FormControl>
|
||||
</td>
|
||||
<td class="before:hidden lg:w-1 whitespace-nowrap">
|
||||
<BaseButton color="danger" :icon="mdiTrashCan" small
|
||||
@click.prevent="removeAliasMimetype(index)" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="text-red-400 text-sm"
|
||||
v-if="form.errors.alternate_mimetype && Array.isArray(form.errors.alternate_mimetype)">
|
||||
{{ form.errors.alternate_mimetype.join(', ') }}
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<template #footer>
|
||||
<BaseButtons>
|
||||
|
|
|
@ -32,7 +32,7 @@ const form = useForm({
|
|||
// isPreferationRequired: false,
|
||||
});
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
await form.delete(stardust.route('settings.mimetype.deleteStore', [props.mimetype.id]));
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { Head, usePage } from '@inertiajs/vue3';
|
||||
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus, mdiTrashCan } from '@mdi/js';
|
||||
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus, mdiTrashCan, mdiCheckCircle, mdiCloseCircle } from '@mdi/js';
|
||||
import { computed, ComputedRef } from 'vue';
|
||||
import type { PropType } from "vue";
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
|
@ -16,6 +16,7 @@ interface MimeType {
|
|||
id: number;
|
||||
name: string;
|
||||
file_extension: string;
|
||||
alternate_mimetype: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
@ -54,24 +55,41 @@ const flash: ComputedRef<any> = computed(() => {
|
|||
{{ flash.message }}
|
||||
</NotificationBar>
|
||||
<CardBox class="mb-6" has-table>
|
||||
<table>
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th v-if="can.edit">Actions</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Mimetype</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Alternate Mime Types</th>
|
||||
<th v-if="can.edit" class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="mimetype in mimetypes" :key="mimetype.id">
|
||||
<td data-label="Name">
|
||||
{{ mimetype.name }} ({{ mimetype.file_extension }})
|
||||
<td class="px-4 py-2 whitespace-nowrap text-left text-sm" data-label="Name">
|
||||
<span class="flex items-center">
|
||||
<svg viewBox="0 0 24 24" v-if="mimetype.enabled" :class="{'text-green-500': mimetype.enabled}" class="w-4 h-4 mr-2">
|
||||
<path fill="currentColor" :d="mdiCheckCircle" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" :class="{'text-red-500': !mimetype.enabled}" class="w-4 h-4 mr-2">
|
||||
<path fill="currentColor" :d="mdiCloseCircle" />
|
||||
</svg>
|
||||
<br>
|
||||
<span class="truncate block max-w-xs">{{ mimetype.name }}</span>
|
||||
</span>
|
||||
<ul class="list-none pl-0">
|
||||
<li v-for="ext in mimetype.file_extension?.split('|')" :key="ext" class="flex items-center truncate block max-w-xs">- .{{ ext }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-left text-sm" data-label="Alternate Mime Types">
|
||||
<ul class="list-none pl-0">
|
||||
<li v-for="alt in mimetype.alternate_mimetype?.split('|')" :key="alt" class="flex items-center truncate block max-w-xs">- {{ alt }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
<!-- <td class="px-4 py-2 whitespace-nowrap" data-label="Status">
|
||||
<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">
|
||||
</td> -->
|
||||
<td v-if="can.edit" class="before:hidden lg:w-1 whitespace-nowrap px-4 py-2 whitespace-nowrap text-left text-sm">
|
||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||
<BaseButton v-if="mimetype.enabled"
|
||||
:route-name="stardust.route('settings.mimetype.down', [mimetype.id])"
|
||||
|
|
|
@ -69,10 +69,10 @@ const datasets = computed(() => mainService.datasets);
|
|||
<SectionMain>
|
||||
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Overview" main>
|
||||
<BaseButton
|
||||
href="https://gitea.geologie.ac.at/geolba/tethys"
|
||||
href="https://gitea.geosphere.at/geolba/tethys.backend"
|
||||
target="_blank"
|
||||
:icon="mdiGithub"
|
||||
label="Star on Gitea"
|
||||
label="Star on GeoSphere Forgejo"
|
||||
color="contrast"
|
||||
rounded-full
|
||||
small
|
||||
|
|
|
@ -14,10 +14,6 @@ import MimeType from '#models/mime_type';
|
|||
/**
|
||||
* Options accepted by the unique rule
|
||||
*/
|
||||
// type Options = {
|
||||
// mainLanguageField: string;
|
||||
// typeField: string;
|
||||
// };
|
||||
type Options = {
|
||||
// size: string | number;
|
||||
// extnames: string[];
|
||||
|
@ -26,13 +22,8 @@ type Options = {
|
|||
allowedMimeTypes: string[];
|
||||
};
|
||||
|
||||
|
||||
// async function allowedMimetypeExtensions(file: VineMultipartFile | unknown, options: Options | unknown, field: FieldContext) {
|
||||
async function allowedMimetypeExtensions(file: VineMultipartFile | unknown, options: Options, field: FieldContext) {
|
||||
// if (typeof value !== 'string' && typeof value != 'number') {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!isBodyParserFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
@ -41,10 +32,18 @@ async function allowedMimetypeExtensions(file: VineMultipartFile | unknown, opti
|
|||
const fileExtension = validatedFile?.extname?.toLocaleLowerCase() as string; // Get file extension from the file
|
||||
|
||||
// validate if file extension is allowed in combination with mimetype
|
||||
const mimeRecord = await MimeType.query().select('file_extension').where('name', mimeType).andWhere('enabled', true).first();
|
||||
let mimeRecord = await MimeType.query().select('file_extension').where('name', mimeType).andWhere('enabled', true).first();
|
||||
|
||||
if (!mimeRecord) {
|
||||
const allowedMimetypes = await MimeType.query().select('name').where('enabled', true)
|
||||
mimeRecord = await MimeType.query()
|
||||
.select('file_extension')
|
||||
.whereRaw("? = ANY (string_to_array(alternate_mimetype, '|'))", [mimeType])
|
||||
.andWhere('enabled', true)
|
||||
.first();
|
||||
}
|
||||
|
||||
if (!mimeRecord) {
|
||||
const allowedMimetypes = await MimeType.query().select('name').where('enabled', true);
|
||||
// Transform allowed MIME types to a concatenated string
|
||||
const allowedMimetypesString = allowedMimetypes.map((mime) => mime.name).join(', ');
|
||||
// throw new Error('Invalid MIME type');
|
||||
|
|
|
@ -76,7 +76,8 @@ async function validateReference(value: unknown, options: Options, field: FieldC
|
|||
}
|
||||
}
|
||||
} else if (type === ReferenceIdentifierTypes.ISBN) {
|
||||
if (!/^(?=(?:[^0-9]*[0-9]){10}(?:(?:[^0-9]*[0-9]){3})?$)[\d-]+$/.test(value)) {
|
||||
const isbnRegex = /^(?:\d{1,5}-\d{1,7}-\d{1,7}-[\dX]$|97[89]-\d{1,5}-\d{1,7}-\d{1,7}-\d)$/;
|
||||
if (!isbnRegex.test(value)) {
|
||||
field.report('The {{ field }} must be a valid ISBN', 'validateReference', field);
|
||||
} else {
|
||||
try {
|
||||
|
|
29
start/rules/valid_mimetype.ts
Normal file
29
start/rules/valid_mimetype.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { FieldContext } from '@vinejs/vine/types';
|
||||
import vine from '@vinejs/vine';
|
||||
import { VineString } from '@vinejs/vine';
|
||||
|
||||
async function isValidMimetype(value: unknown, options: unknown, field: FieldContext) {
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Regex pattern to match valid mimetypes (e.g., "application/json", "text/html")
|
||||
const mimetypePattern = /^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/;
|
||||
if (!mimetypePattern.test(value)) {
|
||||
field.report('The given value is not a valid mimetype', 'isValidMimetype', field);
|
||||
}
|
||||
}
|
||||
|
||||
export const isValidMimetypeRule = vine.createRule(isValidMimetype);
|
||||
|
||||
declare module '@vinejs/vine' {
|
||||
interface VineString {
|
||||
isValidMimetype(): this;
|
||||
}
|
||||
}
|
||||
|
||||
VineString.macro('isValidMimetype', function (this: VineString) {
|
||||
return this.use(isValidMimetypeRule());
|
||||
});
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ test.group('ReferenceValidation', () => {
|
|||
// reference: '978-3-85316-090-9',
|
||||
// reference: '9783853160909',
|
||||
// reference: '978-3-900312-64-0', // Geologische Karte der Republik Österreich 1 : 50.000
|
||||
reference: '3900312648', // Geologische Karte der Republik Österreich 1 : 50.000
|
||||
reference: '3-90031-264-8', // Geologische Karte der Republik Österreich 1 : 50.000
|
||||
type: ReferenceIdentifierTypes.ISBN,
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue