diff --git a/adonisrc.ts b/adonisrc.ts index 97e4528..19e946c 100644 --- a/adonisrc.ts +++ b/adonisrc.ts @@ -35,6 +35,7 @@ export default defineConfig({ () => import('#start/rules/dependent_array_min_length'), () => import('#start/rules/referenceValidation'), () => import('#start/rules/valid_mimetype'), + () => import('#start/rules/array_contains_types'), ], /* |-------------------------------------------------------------------------- diff --git a/app/Controllers/Http/Submitter/DatasetController.ts b/app/Controllers/Http/Submitter/DatasetController.ts index d39d6d2..42ae5c3 100644 --- a/app/Controllers/Http/Submitter/DatasetController.ts +++ b/app/Controllers/Http/Submitter/DatasetController.ts @@ -207,7 +207,8 @@ export default class DatasetController { .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) - .minLength(1), + // .minLength(2) + .arrayContainsTypes({ typeA: 'main', typeB: 'translated' }), descriptions: vine .array( vine.object({ @@ -221,7 +222,8 @@ export default class DatasetController { .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) - .minLength(1), + // .minLength(1), + .arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }), authors: vine .array( vine.object({ @@ -270,7 +272,6 @@ export default class DatasetController { } return response.redirect().back(); } - public async thirdStep({ request, response }: HttpContext) { const newDatasetSchema = vine.object({ // first step @@ -296,7 +297,8 @@ export default class DatasetController { .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) - .minLength(1), + // .minLength(2) + .arrayContainsTypes({ typeA: 'main', typeB: 'translated' }), descriptions: vine .array( vine.object({ @@ -310,7 +312,8 @@ export default class DatasetController { .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) - .minLength(1), + // .minLength(1), + .arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }), authors: vine .array( vine.object({ @@ -539,11 +542,6 @@ export default class DatasetController { try { await multipart.process(); // // 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) { // This is where you'd expect to catch any errors. session.flash('errors', error.messages); diff --git a/app/validators/dataset.ts b/app/validators/dataset.ts index cdd1fbf..864f1ba 100644 --- a/app/validators/dataset.ts +++ b/app/validators/dataset.ts @@ -40,7 +40,8 @@ export const createDatasetValidator = vine.compile( .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) - .minLength(1), + // .minLength(2) + .arrayContainsTypes({ typeA: 'main', typeB: 'translated' }), descriptions: vine .array( vine.object({ @@ -54,7 +55,8 @@ export const createDatasetValidator = vine.compile( .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) - .minLength(1), + // .minLength(1), + .arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }), authors: vine .array( vine.object({ @@ -156,8 +158,7 @@ export const createDatasetValidator = vine.compile( .fileScan({ removeInfected: true }), ) .minLength(1), - }), -); + }),); /** * Validates the dataset's update action @@ -187,7 +188,8 @@ export const updateDatasetValidator = vine.compile( .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) - .minLength(1), + // .minLength(2) + .arrayContainsTypes({ typeA: 'main', typeB: 'translated' }), descriptions: vine .array( vine.object({ @@ -201,7 +203,7 @@ export const updateDatasetValidator = vine.compile( .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) - .minLength(1), + .arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }), authors: vine .array( vine.object({ diff --git a/database/migrations/dataset_7_collections.ts b/database/migrations/dataset_7_collections.ts index 4fe4b8b..1e970c5 100644 --- a/database/migrations/dataset_7_collections.ts +++ b/database/migrations/dataset_7_collections.ts @@ -5,7 +5,7 @@ export default class Collections extends BaseSchema { public async up() { 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 .foreign('role_id', 'collections_role_id_foreign') diff --git a/resources/js/Components/FormCheckRadio.vue b/resources/js/Components/FormCheckRadio.vue index 8cda06e..ca27675 100644 --- a/resources/js/Components/FormCheckRadio.vue +++ b/resources/js/Components/FormCheckRadio.vue @@ -56,27 +56,15 @@ const isChecked = computed(() => { return Array.isArray(computedValue.value) && computedValue.value.length > 0 && 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; }); - - - diff --git a/resources/js/Components/FormCheckRadioGroup.vue b/resources/js/Components/FormCheckRadioGroup.vue index ef86f4b..c995cab 100644 --- a/resources/js/Components/FormCheckRadioGroup.vue +++ b/resources/js/Components/FormCheckRadioGroup.vue @@ -47,7 +47,7 @@ const computedValue = computed({ if (props.modelValue.every((item) => typeof item === 'number')) { return props.modelValue; } 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 props.modelValue; @@ -109,6 +109,6 @@ const inputElClass = computed(() => { + :name="name" :input-value="Number(key)" :label="value" :class="componentClass" /> diff --git a/start/rules/array_contains_types.ts b/start/rules/array_contains_types.ts new file mode 100644 index 0000000..30c2363 --- /dev/null +++ b/start/rules/array_contains_types.ts @@ -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 { + arrayContainsTypes(options: Options): this; + } +} + +VineArray.macro('arrayContainsTypes', function (this: VineArray, options: Options) { + return this.use(arrayContainsMainAndTranslatedRule(options)); +}); diff --git a/tailwind.config.js b/tailwind.config.js index af6d6b2..e41b62d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -14,6 +14,7 @@ module.exports = { extend: { 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\")", + '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: { 'primary': '#22C55E',