hotfix(dataset): enhance radio button and checkbox components and add arrayContainsTypes validation

- Added checkbox support to the `FormCheckRadio` component.
- Updated the styling of the radio button and checkbox components.
- Added the `arrayContainsTypes` validation rule to ensure that arrays contain specific types.
- Updated the `dataset` validators and controllers to use the new validation rule.
- Updated the `FormCheckRadioGroup` component to correctly handle the `input-value` as a number.
- Removed the default value from the `id` column in the `collections` migration.
- Added the `array_contains_types` rule to the `adonisrc.ts` file.
This commit is contained in:
Kaimbacher 2025-03-28 17:34:46 +01:00
parent 9823364670
commit 09f65359f9
8 changed files with 116 additions and 41 deletions

View file

@ -35,6 +35,7 @@ export default defineConfig({
() => import('#start/rules/dependent_array_min_length'), () => import('#start/rules/dependent_array_min_length'),
() => import('#start/rules/referenceValidation'), () => import('#start/rules/referenceValidation'),
() => import('#start/rules/valid_mimetype'), () => import('#start/rules/valid_mimetype'),
() => import('#start/rules/array_contains_types'),
], ],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -207,7 +207,8 @@ export default class DatasetController {
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}), }),
) )
.minLength(1), // .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
descriptions: vine descriptions: vine
.array( .array(
vine.object({ vine.object({
@ -221,7 +222,8 @@ export default class DatasetController {
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}), }),
) )
.minLength(1), // .minLength(1),
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
authors: vine authors: vine
.array( .array(
vine.object({ vine.object({
@ -270,7 +272,6 @@ export default class DatasetController {
} }
return response.redirect().back(); return response.redirect().back();
} }
public async thirdStep({ request, response }: HttpContext) { public async thirdStep({ request, response }: HttpContext) {
const newDatasetSchema = vine.object({ const newDatasetSchema = vine.object({
// first step // first step
@ -296,7 +297,8 @@ export default class DatasetController {
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}), }),
) )
.minLength(1), // .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
descriptions: vine descriptions: vine
.array( .array(
vine.object({ vine.object({
@ -310,7 +312,8 @@ export default class DatasetController {
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}), }),
) )
.minLength(1), // .minLength(1),
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
authors: vine authors: vine
.array( .array(
vine.object({ vine.object({
@ -539,11 +542,6 @@ export default class DatasetController {
try { try {
await multipart.process(); await multipart.process();
// // Instead of letting an error abort the controller, check if any error occurred // // Instead of letting an error abort the controller, check if any error occurred
// if (fileUploadError) {
// // Flash the error and return an inertia view that shows the error message.
// session.flash('errors', { 'upload error': [fileUploadError.message] });
// return response.redirect().back();
// }
} catch (error) { } catch (error) {
// This is where you'd expect to catch any errors. // This is where you'd expect to catch any errors.
session.flash('errors', error.messages); session.flash('errors', error.messages);

View file

@ -40,7 +40,8 @@ export const createDatasetValidator = vine.compile(
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}), }),
) )
.minLength(1), // .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
descriptions: vine descriptions: vine
.array( .array(
vine.object({ vine.object({
@ -54,7 +55,8 @@ export const createDatasetValidator = vine.compile(
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}), }),
) )
.minLength(1), // .minLength(1),
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
authors: vine authors: vine
.array( .array(
vine.object({ vine.object({
@ -156,8 +158,7 @@ export const createDatasetValidator = vine.compile(
.fileScan({ removeInfected: true }), .fileScan({ removeInfected: true }),
) )
.minLength(1), .minLength(1),
}), }),);
);
/** /**
* Validates the dataset's update action * Validates the dataset's update action
@ -187,7 +188,8 @@ export const updateDatasetValidator = vine.compile(
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}), }),
) )
.minLength(1), // .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
descriptions: vine descriptions: vine
.array( .array(
vine.object({ vine.object({
@ -201,7 +203,7 @@ export const updateDatasetValidator = vine.compile(
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}), }),
) )
.minLength(1), .arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
authors: vine authors: vine
.array( .array(
vine.object({ vine.object({

View file

@ -5,7 +5,7 @@ export default class Collections extends BaseSchema {
public async up() { public async up() {
this.schema.createTable(this.tableName, (table) => { this.schema.createTable(this.tableName, (table) => {
table.increments('id').defaultTo("nextval('collections_id_seq')"); table.increments('id');//.defaultTo("nextval('collections_id_seq')");
table.integer('role_id').unsigned(); table.integer('role_id').unsigned();
table table
.foreign('role_id', 'collections_role_id_foreign') .foreign('role_id', 'collections_role_id_foreign')

View file

@ -56,27 +56,15 @@ const isChecked = computed(() => {
return Array.isArray(computedValue.value) && return Array.isArray(computedValue.value) &&
computedValue.value.length > 0 && computedValue.value.length > 0 &&
computedValue.value[0] === props.inputValue; computedValue.value[0] === props.inputValue;
} else if (props.type === 'checkbox') {
return Array.isArray(computedValue.value) &&
computedValue.value.length > 0 &&
computedValue.value.includes(props.inputValue);
} }
return computedValue.value === props.inputValue; return computedValue.value === props.inputValue;
}); });
</script> </script>
<!-- <template>
<label v-if="type == 'radio'" :class="[type, 'mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative']">
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue" class="absolute left-0 opacity-0 -z-1 focus:outline-none focus:ring-0" />
<span
class="check border-gray-700 border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded-full"
:class="{ 'bg-no-repeat bg-center bg-blue-600 border-blue-600 border-4': isChecked }"/>
<span class="pl-2">{{ label }}</span>
</label>
<label v-else :class="[type, 'mr-6 mb-3 last:mr-0']">
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue" />
<span class="check" />
<span class="pl-2">{{ label }}</span>
</label>
</template> -->
<template> <template>
<label v-if="type === 'radio'" :class="[type]" <label v-if="type === 'radio'" :class="[type]"
class="mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative"> class="mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative">
@ -85,14 +73,19 @@ const isChecked = computed(() => {
:checked="isChecked" /> :checked="isChecked" />
<span class="check border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded-full" :class="{ <span class="check border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded-full" :class="{
'border-gray-700': !isChecked, 'border-gray-700': !isChecked,
'bg-radio-checked bg-no-repeat bg-center bg-blue-600 border-blue-600 border-4': isChecked 'bg-radio-checked bg-no-repeat bg-center bg-lime-600 border-lime-600 border-4': isChecked
}" /> }" />
<span class="pl-2">{{ label }}</span> <span class="pl-2 control-label">{{ label }}</span>
</label> </label>
<label v-else class="mr-6 mb-3 last:mr-0" :class="[type]"> <label v-else-if="type === 'checkbox'" :class="[type]"
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue" /> class="mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative">
<span class="check" /> <input v-model="computedValue" :type="inputType" :name="name" :value="inputValue"
<span class="pl-2">{{ label }}</span> class="absolute left-0 opacity-0 -z-1 focus:outline-none focus:ring-0" />
<span class="check border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded" :class="{
'border-gray-700': !isChecked,
'bg-checkbox-checked bg-no-repeat bg-center bg-lime-600 border-lime-600 border-4': isChecked
}" />
<span class="pl-2 control-label">{{ label }}</span>
</label> </label>
</template> </template>

View file

@ -47,7 +47,7 @@ const computedValue = computed({
if (props.modelValue.every((item) => typeof item === 'number')) { if (props.modelValue.every((item) => typeof item === 'number')) {
return props.modelValue; return props.modelValue;
} else if (props.modelValue.every((item) => hasIdAttribute(item))) { } else if (props.modelValue.every((item) => hasIdAttribute(item))) {
const ids = props.modelValue.map((obj) => obj.id.toString()); const ids = props.modelValue.map((obj) => obj.id);
return ids; return ids;
} }
return props.modelValue; return props.modelValue;
@ -109,6 +109,6 @@ const inputElClass = computed(() => {
</svg> </svg>
</div> </div>
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type" <FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type"
:name="name" :input-value="key" :label="value" :class="componentClass" /> :name="name" :input-value="Number(key)" :label="value" :class="componentClass" />
</div> </div>
</template> </template>

View file

@ -0,0 +1,80 @@
import { FieldContext } from '@vinejs/vine/types';
import vine, { VineArray } from '@vinejs/vine';
import { SchemaTypes } from '@vinejs/vine/types';
type Options = {
typeA: string;
typeB: string;
};
/**
* Custom rule to validate an array of titles contains at least one title
* with type 'main' and one with type 'translated'.
*
* This rule expects the validated value to be an array of objects,
* where each object has a "type" property.
*/
async function arrayContainsTypes(value: unknown, options: Options, field: FieldContext) {
if (!Array.isArray(value)) {
field.report(`The {{field}} must be an array of titles.`, 'array.titlesContainsMainAndTranslated', field);
return false;
}
const typeAExpected = options.typeA.toLowerCase();
const typeBExpected = options.typeB.toLowerCase();
// const hasMain = value.some((title: any) => {
// return typeof title === 'object' && title !== null && String(title.type).toLowerCase() === 'main';
// });
// const hasTranslated = value.some((title: any) => {
// return typeof title === 'object' && title !== null && String(title.type).toLowerCase() === 'translated';
// });
const hasTypeA = value.some((item: any) => {
return typeof item === 'object' && item !== null && String(item.type).toLowerCase() === typeAExpected;
});
const hasTypeB = value.some((item: any) => {
return typeof item === 'object' && item !== null && String(item.type).toLowerCase() === typeBExpected;
});
if (!hasTypeA || !hasTypeB) {
let errorMessage = `The ${field.getFieldPath()} array must have at least one '${options.typeA}' item and one '${options.typeB}' item.`;
// Check for specific field names to produce a more readable message.
if (field.getFieldPath() === 'titles') {
// For titles we expect one main and minimum one translated title.
if (!hasTypeA && !hasTypeB) {
errorMessage = 'For titles, define one main title and minimum one translated title.';
} else if (!hasTypeA) {
errorMessage = 'For titles, define one main title.';
} else if (!hasTypeB) {
errorMessage = 'For titles, define minimum one translated title.';
}
} else if (field.getFieldPath() === 'descriptions') {
// For descriptions we expect one abstracts description and minimum one translated description.
if (!hasTypeA && !hasTypeB) {
errorMessage = 'For descriptions, define one abstracts description and minimum one translated description.';
} else if (!hasTypeA) {
errorMessage = 'For descriptions, define one abstracts description.';
} else if (!hasTypeB) {
errorMessage = 'For descriptions, define minimum one translated description.';
}
}
field.report(errorMessage, 'array.containsTypes', field, options);
return false;
}
return true;
}
export const arrayContainsMainAndTranslatedRule = vine.createRule(arrayContainsTypes);
declare module '@vinejs/vine' {
interface VineArray<Schema extends SchemaTypes> {
arrayContainsTypes(options: Options): this;
}
}
VineArray.macro('arrayContainsTypes', function <Schema extends SchemaTypes>(this: VineArray<Schema>, options: Options) {
return this.use(arrayContainsMainAndTranslatedRule(options));
});

View file

@ -14,6 +14,7 @@ module.exports = {
extend: { extend: {
backgroundImage: { backgroundImage: {
'radio-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E\")", 'radio-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E\")",
'checkbox-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E\")",
}, },
colors: { colors: {
'primary': '#22C55E', 'primary': '#22C55E',