feat: enhance user management, mimetype creation, and validation
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m8s

- **AdminuserController.ts**: enable editing `first_name` and `last_name` for user creation and updates
- **MimetypeController.ts**: add creation support for mimetypes with selectable extensions
- **Models**: add `Mimetype` model (mime_type.ts); add `SnakeCaseNamingStrategy` for User model
- **Validators**:
  - **updateDatasetValidator**: increase title length to 255 and description length to 2500
  - **User Validators**: refine `createUserValidator` and `updateUserValidator` to include `first_name` and `last_name`
- **vanilla_error_reporter**: improve error reporting for wildcard fields
- **SKOS Query**: refine keyword request in `SearchCategoryAutocomplete.vue`
- **UI Enhancements**:
  - improve icon design in wizard (Wizard.vue)
  - add components for mimetype creation (Create.vue and button in Index.vue)
- **Routes**: update `routes.ts` to include new AdonisJS routes
This commit is contained in:
Kaimbacher 2024-10-31 11:02:36 +01:00
parent 2235f3905a
commit 49bd96ee77
24 changed files with 1548 additions and 945 deletions

View File

@ -85,7 +85,7 @@ export default class AdminuserController {
// return response.badRequest(error.messages);
throw error;
}
const input = request.only(['login', 'email', 'password']);
const input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
const user = await User.create(input);
if (request.input('roles')) {
const roles: Array<number> = request.input('roles');
@ -141,9 +141,9 @@ export default class AdminuserController {
// password is optional
let input;
if (request.input('password')) {
input = request.only(['login', 'email', 'password']);
input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
} else {
input = request.only(['login', 'email']);
input = request.only(['login', 'email', 'first_name', 'last_name']);
}
await user.merge(input).save();
// await user.save();

View File

@ -1,5 +1,6 @@
import type { HttpContext } from '@adonisjs/core/http';
import MimeType from '#models/mime_type';
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
export default class MimetypeController {
public async index({ auth, inertia }: HttpContext) {
@ -9,26 +10,115 @@ export default class MimetypeController {
return inertia.render('Admin/Mimetype/Index', {
mimetypes: mimetypes,
can: {
create: await auth.user?.can(['settings']),
edit: await auth.user?.can(['settings']),
},
});
}
public async create({ inertia }: HttpContext) {
// const permissions = await Permission.query().select('id', 'name').pluck('name', 'id');
return inertia.render('Admin/Mimetype/Create', {});
}
public async store({ request, response, session }: HttpContext) {
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
enabled: vine.boolean(),
});
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
try {
// Step 2 - Validate request body against the schema
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
const validator = vine.compile(newDatasetSchema);
validator.messagesProvider = new SimpleMessagesProvider(this.messages);
await request.validateUsing(validator);
} catch (error) {
// Step 3 - Handle errors
// return response.badRequest(error.messages);
throw error;
}
const input = request.only(['name', 'enabled', 'file_extension']);
// 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('|');
}
await MimeType.create(input);
// if (request.input('roles')) {
// const roles: Array<number> = request.input('roles');
// await user.related('roles').attach(roles);
// }
session.flash('message', 'MimeType has been created successfully');
return response.redirect().toRoute('settings.mimetype.index');
}
public messages = {
'minLength': '{{ field }} must be at least {{ min }} characters long',
'maxLength': '{{ field }} must be less then {{ max }} characters long',
'isUnique': '{{ field }} must be unique, and this value is already taken',
'required': '{{ field }} is required',
'file_extension.minLength': 'at least {{ min }} mimetypes must be defined',
'file_extension.*.string': 'Each file extension must be a valid string', // Adjusted to match the type
};
public async edit({ request, inertia }: HttpContext) {
const id = request.param('id');
const mimetype = await MimeType.query().where('id', id).firstOrFail();
// const permissions = await Permission.query().pluck('name', 'id');
// // const userHasRoles = user.roles;
// const rolerHasPermissions = await role.related('permissions').query().orderBy('name').pluck('id');
return inertia.render('Admin/Mimetype/Edit', {
mimetype: mimetype,
});
}
// public async update({ request, response, session }: HttpContext) {
// // node ace make:validator UpdateUser
// const id = request.param('id');
// const role = await Role.query().where('id', id).firstOrFail();
// // validate update form
// // await request.validate(UpdateRoleValidator);
// await request.validateUsing(updateRoleValidator, {
// meta: {
// roleId: role.id,
// },
// });
// // password is optional
// const input = request.only(['name', 'description']);
// await role.merge(input).save();
// // await user.save();
// if (request.input('permissions')) {
// const permissions: Array<number> = request.input('permissions');
// await role.related('permissions').sync(permissions);
// }
// session.flash('message', 'Role has been updated successfully');
// return response.redirect().toRoute('settings.role.index');
// }
public async down({ request, response }: HttpContext) {
const id = request.param('id');
const mimetype = await MimeType.findOrFail(id);
mimetype.enabled = false;
await mimetype .save();
await mimetype.save();
// session.flash({ message: 'person has been deactivated!' });
return response.flash('mimetype has been deactivated!', 'message').toRoute('settings.mimetype.index')
return response.flash('mimetype has been deactivated!', 'message').toRoute('settings.mimetype.index');
}
public async up({ request, response }: HttpContext) {
const id = request.param('id');
const mimetype = await MimeType.findOrFail(id);
mimetype.enabled = true;
await mimetype .save();
await mimetype.save();
// session.flash({ message: 'person has been activated!' });
return response.flash('mimetype has been activated!', 'message').toRoute('settings.mimetype.index');

View File

@ -1,5 +1,6 @@
import { column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import BaseModel from './base_model.js';
import { DateTime } from 'luxon';
export default class MimeType extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
@ -21,11 +22,16 @@ export default class MimeType extends BaseModel {
@column({})
public enabled: boolean;
@column({})
public visible_frontdoor: boolean;
@column.dateTime({
autoCreate: true,
})
public created_at: DateTime;
public visible_oai: boolean;
@column.dateTime({
autoCreate: true,
autoUpdate: true,
})
public updated_at: DateTime;
// @hasMany(() => Collection, {
// foreignKey: 'role_id',

View File

@ -1,6 +1,6 @@
import { DateTime } from 'luxon';
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid';
import { column, manyToMany, hasMany } from '@adonisjs/lucid/orm';
import { column, manyToMany, hasMany, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import hash from '@adonisjs/core/services/hash';
import Role from './role.js';
import db from '@adonisjs/lucid/services/db';
@ -40,6 +40,7 @@ const AuthFinder = withAuthFinder(() => hash.use('laravel'), {
export default class User extends compose(BaseModel, AuthFinder) {
// export default class User extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
public static table = 'accounts';
@column({ isPrimary: true })
@ -48,6 +49,13 @@ export default class User extends compose(BaseModel, AuthFinder) {
@column()
public login: string;
@column()
public firstName: string;
@column()
public lastName: string;
@column()
public email: string;
@ -113,8 +121,6 @@ export default class User extends compose(BaseModel, AuthFinder) {
})
public backupcodes: HasMany<typeof BackupCode>;
public async getBackupCodes(this: User): Promise<BackupCode[]> {
const test = await this.related('backupcodes').query();
// return test.map((role) => role.code);

View File

@ -164,7 +164,7 @@ export const updateDatasetValidator = vine.compile(
titles: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(2500),
value: vine.string().trim().minLength(3).maxLength(255),
type: vine.enum(Object.values(TitleTypes)),
language: vine
.string()
@ -178,7 +178,7 @@ export const updateDatasetValidator = vine.compile(
descriptions: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
value: vine.string().trim().minLength(3).maxLength(2500),
type: vine.enum(Object.values(DescriptionTypes)),
language: vine
.string()
@ -295,6 +295,7 @@ let messagesProvider = new SimpleMessagesProvider({
'rights.in': 'you must agree to continue',
'titles.0.value.minLength': 'Main Title must be at least {{ min }} characters long',
'titles.0.value.maxLength': 'Main Title must be less than {{ max }} characters long',
'titles.0.value.required': 'Main Title is required',
'titles.*.value.required': 'Additional title is required, if defined',
'titles.*.type.required': 'Additional title type is required',

View File

@ -56,7 +56,7 @@ let messagesProvider = new SimpleMessagesProvider({
// 'contacts.0.email.required': 'The primary email of the contact is required',
// 'contacts.*.email.required': 'Contact email is required',
'permissions.minLength': 'at least {{ options.minLength }} permission must be defined',
'permissions.minLength': 'at least {{min }} permission must be defined',
'permissions.*.number': 'Define permissions as valid numbers',
});

View File

@ -13,6 +13,8 @@ export const createUserValidator = vine.compile(
.maxLength(20)
.isUnique({ table: 'accounts', column: 'login' })
.regex(/^[a-zA-Z0-9]+$/), //Must be alphanumeric with hyphens or underscores
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
email: vine.string().maxLength(255).email().normalizeEmail().isUnique({ table: 'accounts', column: 'email' }),
password: vine.string().confirmed().trim().minLength(3).maxLength(60),
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
@ -30,8 +32,10 @@ export const updateUserValidator = vine.withMetaData<{ objId: number }>().compil
.trim()
.minLength(3)
.maxLength(20)
.isUnique({ table: 'accounts', column: 'login', whereNot: (field) => field.meta.objId })
.regex(/^[a-zA-Z0-9]+$/), //Must be alphanumeric with hyphens or underscores
.isUnique({ table: 'accounts', column: 'login', whereNot: (field) => field.meta.objId }),
// .regex(/^[a-zA-Z0-9]+$/), //Must be alphanumeric with hyphens or underscores
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
email: vine
.string()
.maxLength(255)
@ -49,10 +53,10 @@ let messagesProvider = new SimpleMessagesProvider({
'unique': '{{ field }} must be unique, and this value is already taken',
'string': 'The value of {{ field }} field must be a string',
'email': 'The value is not a valid email address',
'minLength': '{{ field }} must be at least {{ options.minLength }} characters long',
'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long',
'minLength': '{{ field }} must be at least {{ min }} characters long',
'maxLength': '{{ field }} must be less then {{ max }} characters long',
'confirmed': 'Oops! The confirmation of {{ field }} is not correct. Please double-check and ensure they match.',
'roles.minLength': 'at least {{ options.minLength }} role must be defined',
'roles.minLength': 'at least {{ min }} role must be defined',
'roles.*.number': 'Define roles as valid numbers',
});

View File

@ -128,11 +128,12 @@ export class VanillaErrorReporter implements ErrorReporterContract {
const error: SimpleError = {
message,
rule,
field: field.getFieldPath(),
field: field.wildCardPath ?field.wildCardPath.split('.')[0] : field.getFieldPath(),
};
// field: 'titles.0.value'
// message: 'Main Title is required'
// rule: 'required' "required"
if (meta) {
error.meta = meta;
}
@ -140,10 +141,25 @@ export class VanillaErrorReporter implements ErrorReporterContract {
// error.index = field.name;
// }
this.hasErrors = true;
// this.errors.push(error);
if (this.errors[error.field]) {
this.errors[error.field]?.push(message);
// if (this.errors[error.field]) {
// this.errors[error.field]?.push(message);
// }
if (field.isArrayMember) {
// Check if the field has wildCardPath and if the error field already exists
if (this.errors[error.field] && field.wildCardPath) {
// Do nothing, as we don't want to push further messages
} else {
// If the error field already exists, push the message
if (this.errors[error.field]) {
this.errors[error.field].push(message);
} else {
this.errors[error.field] = [message];
}
}
} else {
// normal field
this.errors[error.field] = [message];
}

1564
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -107,6 +107,7 @@
"http-status-codes": "^2.2.0",
"leaflet": "^1.9.3",
"luxon": "^3.2.1",
"mime": "^4.0.4",
"node-2fa": "^2.0.3",
"node-exceptions": "^4.0.1",
"notiwind": "^2.0.0",

View File

@ -4,6 +4,7 @@
"assets/resources_js_apps_settings_l18n_de_js.js": "http://localhost:8080/assets/resources_js_apps_settings_l18n_de_js.js",
"assets/resources_js_apps_settings_l18n_en_js.js": "http://localhost:8080/assets/resources_js_apps_settings_l18n_en_js.js",
"assets/resources_js_Pages_Admin_License_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_License_Index_vue.js",
"assets/resources_js_Pages_Admin_Mimetype_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Mimetype_Create_vue.js",
"assets/resources_js_Pages_Admin_Mimetype_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Mimetype_Index_vue.js",
"assets/resources_js_Pages_Admin_Permission_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Create_vue.js",
"assets/resources_js_Pages_Admin_Permission_Edit_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Edit_vue.js",
@ -47,6 +48,7 @@
"assets/resources_js_Pages_Submitter_Dataset_Delete_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Delete_vue.js",
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-b6a1eb.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-b6a1eb.css",
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-b6a1eb.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-b6a1eb.js",
"assets/resources_js_Pages_Submitter_Dataset_Index_vue.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Index_vue.css",
"assets/resources_js_Pages_Submitter_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Index_vue.js",
"assets/resources_js_Pages_Submitter_Dataset_Release_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Release_vue.js",
"assets/resources_js_Pages_Submitter_Person_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Person_Index_vue.js",
@ -57,6 +59,7 @@
"assets/vendors-node_modules_toastify-js_src_toastify_js.js": "http://localhost:8080/assets/vendors-node_modules_toastify-js_src_toastify_js.js",
"assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-adabdc.js": "http://localhost:8080/assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-adabdc.js",
"assets/vendors-node_modules_buffer_index_js-node_modules_vuedraggable_dist_vuedraggable_umd_js.js": "http://localhost:8080/assets/vendors-node_modules_buffer_index_js-node_modules_vuedraggable_dist_vuedraggable_umd_js.js",
"assets/vendors-node_modules_mime_dist_src_index_js.js": "http://localhost:8080/assets/vendors-node_modules_mime_dist_src_index_js.js",
"assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js": "http://localhost:8080/assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js",
"assets/resources_js_Components_BaseButton_vue.js": "http://localhost:8080/assets/resources_js_Components_BaseButton_vue.js",
"assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js": "http://localhost:8080/assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js",
@ -67,10 +70,10 @@
"assets/resources_js_utils_toast_ts-resources_js_Components_NotificationBar_vue.js": "http://localhost:8080/assets/resources_js_utils_toast_ts-resources_js_Components_NotificationBar_vue.js",
"assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-058bcc.js": "http://localhost:8080/assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-058bcc.js",
"assets/resources_js_Components_SectionMain_vue-resources_js_Components_SectionTitleLineWithButton_vu-764dfe.js": "http://localhost:8080/assets/resources_js_Components_SectionMain_vue-resources_js_Components_SectionTitleLineWithButton_vu-764dfe.js",
"assets/resources_js_Components_BaseButtons_vue-resources_js_Components_NotificationBar_vue-resources-7e06d8.js": "http://localhost:8080/assets/resources_js_Components_BaseButtons_vue-resources_js_Components_NotificationBar_vue-resources-7e06d8.js",
"assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js": "http://localhost:8080/assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js",
"assets/resources_js_Components_CardBoxModal_vue.js": "http://localhost:8080/assets/resources_js_Components_CardBoxModal_vue.js",
"assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-25e686.js": "http://localhost:8080/assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-25e686.js",
"assets/resources_js_Components_BaseButtons_vue-resources_js_Components_NotificationBar_vue-resources-7e06d8.js": "http://localhost:8080/assets/resources_js_Components_BaseButtons_vue-resources_js_Components_NotificationBar_vue-resources-7e06d8.js",
"assets/fonts/inter-latin-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.1c20f7dc.woff",
"assets/fonts/inter-latin-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-400-normal.b0c8fe9d.woff",
"assets/fonts/inter-latin-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.3d10c85f.woff2",
@ -92,5 +95,6 @@
"assets/images/marker-icon.png": "http://localhost:8080/assets/images/marker-icon.2b3e1faf.png",
"assets/images/layers-2x.png": "http://localhost:8080/assets/images/layers-2x.8f2c4d11.png",
"assets/images/layers.png": "http://localhost:8080/assets/images/layers.416d9136.png",
"assets/images/Close.svg": "http://localhost:8080/assets/images/Close.e4887675.svg"
"assets/images/Close.svg": "http://localhost:8080/assets/images/Close.e4887675.svg",
"assets/vendors-node_modules_vue-facing-decorator_dist_esm_index_js-node_modules_vue-facing-decorator-818045.js": "http://localhost:8080/assets/vendors-node_modules_vue-facing-decorator_dist_esm_index_js-node_modules_vue-facing-decorator-818045.js"
}

View File

@ -1,50 +1,50 @@
<template>
<div class="flex items-center relative">
<!-- v-bind:class="{ 'text-white bg-teal-600 border-teal-600': isCurrent, 'border-teal-600': isChecked }" -->
<div
class="text-gray-500 rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2"
<!-- The main circular icon with dynamic classes based on the state of the step -->
<div class="text-gray-500 rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2"
:class="[
isCurrent ? 'text-white bg-teal-600 border-teal-600' : 'border-gray-300',
isChecked && 'text-teal-600 border-teal-600',
]"
>
<!-- <svg class="my-svg-component" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z"
/>
</svg> -->
]">
<!-- Slot for custom content inside the circle -->
<slot></slot>
<div class="absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase invisible sm:visible">
<!-- Label displayed above the icon, visibility controlled by isCurrent and isDark -->
<div class="absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase invisible sm:visible"
:class="[!isDark && isCurrent ? 'font-black text-green-600' : '']">
{{ label }}
</div>
</div>
</div>
<div
v-if="!isLastStep"
class="flex-auto border-t-2 transition duration-500 ease-in-out invisible sm:visible"
:class="[isChecked ? 'border-teal-600' : 'border-gray-300']"
></div>
<!-- Divider line between steps, only visible if this is not the last step -->
<div v-if="!isLastStep" class="flex-auto border-t-2 transition duration-500 ease-in-out invisible sm:visible"
:class="[isChecked ? 'border-teal-600' : 'border-gray-300']"></div>
</template>
<script>
<script lang="ts">
import { StyleService } from '@/Stores/style.service';
const styleService = StyleService();
export default {
name: 'Icon_Multistep',
props: {
isCurrent: Boolean,
isChecked: Boolean,
isLastStep: Boolean,
label: String,
isCurrent: Boolean, // Indicates if this step is the current one
isChecked: Boolean, // Indicates if this step has been checked
isLastStep: Boolean, // Indicates if this step is the last one
label: String, // Label to display above the icon
},
data() {
return {
mode: 'light',
checkedClass: 'border-teal-600',
uncheckedClass: 'border-gray-300',
mode: 'light', // Light mode setting
checkedClass: 'border-teal-600', // Class for checked state
uncheckedClass: 'border-gray-300', // Class for unchecked state
};
},
computed: {
// Computed property to determine if dark mode is enabled
isDark() {
return styleService.darkMode === true; // Return true if dark mode is active
}
},
};
</script>

View File

@ -181,17 +181,6 @@ let computedValue = computed({
},
});
// const dropDownClass = computed(() => {
// const base = [
// 'z-10 bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700',
// props.borderless ? 'border-0' : 'border',
// props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
// // props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
// statesToggle.value == true ? 'block' : 'hidden',
// ];
// return base;
// });
const inputElClass = computed(() => {
// class="block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg border-l-gray-50 border-l-2 border border-gray-300
// focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-l-gray-700 dark:border-gray-600
@ -272,7 +261,7 @@ const setLanguage = (item) => {
data.isOpen = false;
};
const ENDPOINT = 'https://resource.geolba.ac.at/PoolParty/sparql/geoera';
const ENDPOINT = 'https://resource.geolba.ac.at/PoolParty/sparql/keyword';
// const USER_LANG = 'de';
// watch(search, async () => {
@ -306,7 +295,34 @@ async function handleInput(e: Event) {
// }
// }
async function request(url, param) {
async function request(url: string, param: string) {
try {
let query = encodeURIComponent(`
PREFIX skos:<http://www.w3.org/2004/02/skos/core#>
select distinct (?label as ?title) ?s (strlen(str(?label)) as ?strlen)
where
{
VALUES ?n {"${sparqlEncode(param.toLowerCase())}"}
?s skos:prefLabel ?label .
filter(lang(?label)='${language.value}')
filter(regex(?label, ?n, "i")) # Case-insensitive regex match
}
order by ?label ?strlen
`);
let response = await searchTerm(url + '?query=' + query + '&format=application/json');
error.value = '';
data.results = getResults(response);
// // this.results = res.data;
// // this.loading = false;
} catch (error) {
error.value = error.message;
// this.loading = false;
}
}
async function requestOriginal(url: string, param: string) {
try {
let query = encodeURIComponent(`
PREFIX dcterms:<http://purl.org/dc/terms/>
@ -316,8 +332,15 @@ async function request(url, param) {
VALUES ?n {"${sparqlEncode(param.toLowerCase())}"}
VALUES ?p { skos:prefLabel skos:altLabel }
?s a skos:Concept; ?p ?lEN . FILTER((lang(?lEN)="en"))
# FILTER(regex(str(?s), 'ncl/geoera/keyword'))
OPTIONAL{?s ?p ?l . FILTER(lang(?l)="${language.value}")}
BIND(COALESCE(?l, ?lEN) AS ?L) . FILTER(regex(?L,?n,"i"))
FILTER(!regex(?L, "\(category\)", "i"))
FILTER(!regex(?L, "\(kategorie\)", "i"))
?s skos:prefLabel ?plEN . FILTER((lang(?plEN)="en"))
OPTIONAL{?s skos:prefLabel ?pl . FILTER(lang(?pl)="${language.value}")}
BIND(COALESCE(?pl, ?plEN) AS ?title)
@ -327,6 +350,7 @@ async function request(url, param) {
ORDER BY ?sort
LIMIT 100`);
let response = await searchTerm(url + '?query=' + query + '&format=application/json');
error.value = '';
data.results = getResults(response);
@ -351,7 +375,7 @@ function getResults(response) {
return [];
}
function sparqlEncode(str) {
function sparqlEncode(str: string) {
var hex, i;
str = str.toLowerCase();
var result = '';
@ -383,14 +407,4 @@ function clear() {
// this.$emit("clear");
}
// function onEnter() {
// if (Array.isArray(data.results) && data.results.length && selectedIndex.value !== -1 && selectedIndex.value < data.results.length) {
// //this.display = this.results[this.selectedIndex];
// const person = data.results[selectedIndex.value];
// // this.$emit('person', person);
// emit('person', person);
// clear();
// selectedIndex.value = -1;
// }
// }
</script>

View File

@ -61,25 +61,6 @@ const removeItem = (key) => {
items.value.splice(key, 1);
};
// const remove = (arr, cb) => {
// const newArr = [];
// arr.forEach((item) => {
// if (!cb(item)) {
// newArr.push(item);
// }
// });
// return newArr;
// };
// const checked = (isChecked, client) => {
// if (isChecked) {
// checkedRows.value.push(client);
// } else {
// checkedRows.value = remove(checkedRows.value, (row) => row.id === client.id);
// }
// };
</script>
<template>
@ -114,10 +95,7 @@ const removeItem = (key) => {
</thead>
<tbody>
<tr v-for="(item, index) in itemsPaginated" :key="index">
<!-- <TableCheckboxCell v-if="checkable" @checked="checked($event, client)" /> -->
<!-- <td class="border-b-0 lg:w-6 before:hidden hidden lg:table-cell">
<UserAvatar :username="client.value" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</td> -->
<td data-label="Type" scope="row">
<FormControl required v-model="item.type" @update:modelValue="() => {item.external_key = undefined; item.value= '';}" :type="'select'" placeholder="[Enter Language]" :options="props.subjectTypes">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.type`]">

View File

@ -0,0 +1,390 @@
<script lang="ts" setup>
import { ref, watch, computed, Ref, reactive } from 'vue';
import { Head, useForm } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline } from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import CardBox from '@/Components/CardBox.vue';
import BaseDivider from '@/Components/BaseDivider.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import mime from 'mime';
import FormField from '@/Components/FormField.vue';
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';
const props = defineProps({
permissions: {
type: Object,
default: () => ({}),
},
borderless: Boolean,
transparent: Boolean,
ctrlKFocus: Boolean,
});
const isReadOnly = true;
// Get keys from standardTypes and otherTypes
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({
name: '',
file_extension: [] as Array<string>,
enabled: true,
});
// 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;
});
// 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
// };
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;
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;
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.';
}
};
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) {
//this.display = this.results[this.selectedIndex];
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.';
}
}
}
}
</script>
<template>
<LayoutAuthenticated>
<Head title="Add mimetype" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Add Mimetype" main>
<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>
<!-- <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)"
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: HTMLLIElement) => {
ul[index] = el;
}
">
<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>
<div v-if="mimetypeError" class="text-red-400 text-sm mt-1">
{{ 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">
<div class="text-red-400 text-sm" v-if="form.errors.name">
{{ form.errors.name }}
</div>
</FormControl>
</FormField>
<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>
<!-- 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="Permissions" 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 />
<template #footer>
<BaseButtons>
<BaseButton type="submit" color="info" label="Create" :class="{ 'opacity-25': form.processing }"
:disabled="form.processing" />
</BaseButtons>
</template>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>

View File

@ -1,7 +1,8 @@
<script lang="ts" setup>
import { Head, usePage } from '@inertiajs/vue3';
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline } from '@mdi/js';
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus } from '@mdi/js';
import { computed, ComputedRef } from 'vue';
import type { PropType } from "vue";
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
@ -9,23 +10,20 @@ import BaseButton from '@/Components/BaseButton.vue';
import CardBox from '@/Components/CardBox.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import NotificationBar from '@/Components/NotificationBar.vue';
// import Pagination from '@/Components/Admin/Pagination.vue';
// import Sort from '@/Components/Admin/Sort.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
// import CardBoxModal from '@/Components/CardBoxModal.vue';
// const isModalDangerActive = ref(false);
// const deleteId = ref();
interface MimeType {
id: number;
name: string;
file_extension: string;
enabled: boolean;
}
defineProps({
mimetypes: {
type: Object,
type: Array as PropType<MimeType[]>,
default: () => ({}),
},
// filters: {
// type: Object,
// default: () => ({}),
// },
can: {
type: Object,
default: () => ({}),
@ -33,8 +31,6 @@ defineProps({
});
const flash: ComputedRef<any> = computed(() => {
// let test = usePage();
// console.log(test);
return usePage().props.flash;
});
@ -47,16 +43,19 @@ const flash: ComputedRef<any> = computed(() => {
<Head title="Mime Types" />
<SectionMain>
<!--
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Mime Types" main>
<!-- <BaseButton
</SectionTitleLineWithButton> -->
<SectionTitleLineWithButton :icon="mdiAccountKey" title="Mime Types" main>
<BaseButton
v-if="can.create"
:route-name="stardust.route('settings.role.create')"
:route-name="stardust.route('settings.mimetype.create')"
:icon="mdiPlus"
label="Add"
color="info"
rounded-full
small
/> -->
/>
</SectionTitleLineWithButton>
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
@ -83,13 +82,7 @@ const flash: ComputedRef<any> = computed(() => {
<tbody>
<tr v-for="mimetype in mimetypes" :key="mimetype.id">
<td data-label="Name">
<!-- <Link
:href="stardust.route('settings.role.show', [role.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400"
>
{{ license.name }}
</Link> -->
{{ mimetype.name }}
{{ mimetype.name }} ({{ mimetype.file_extension }})
</td>
<td data-label="Status">
<template v-if="mimetype.enabled">Active</template>
@ -102,16 +95,13 @@ const flash: ComputedRef<any> = computed(() => {
:route-name="stardust.route('settings.mimetype.down', [mimetype.id])"
color="warning" :icon="mdiSquareEditOutline" label="disable" small />
<BaseButton v-else
:route-name="stardust.route('settings.mimetype.up', [mimetype.id])" color="success"
:icon="mdiSquareEditOutline" label="enable" small />
:route-name="stardust.route('settings.mimetype.up', [mimetype.id])"
color="success" :icon="mdiSquareEditOutline" label="enable" small />
</BaseButtons>
</td>
</tr>
</tbody>
</table>
<!-- <div class="py-4">
<Pagination v-bind:data="roles.meta" />
</div> -->
</CardBox>
</SectionMain>
</LayoutAuthenticated>

View File

@ -38,6 +38,8 @@ const props = defineProps({
const form = useForm({
login: '',
first_name: '',
last_name: '',
email: '',
password: '',
password_confirmation: '',
@ -74,6 +76,22 @@ const submit = async () => {
</FormControl>
</FormField>
<FormField label="First Name" :class="{ 'text-red-400': errors.first_name }">
<FormControl v-model="form.first_name" type="text" placeholder="Enter First Name" :errors="errors.first_name">
<div class="text-red-400 text-sm" v-if="errors.first_name && Array.isArray(errors.first_name)">
{{ errors.first_name.join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Last Name" :class="{ 'text-red-400': errors.last_name }">
<FormControl v-model="form.last_name" type="text" placeholder="Enter Last Name" :errors="errors.last_name">
<div class="text-red-400 text-sm" v-if="errors.last_name && Array.isArray(errors.last_name)">
{{ errors.last_name.join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Email" :class="{ 'text-red-400': errors.email }">
<FormControl v-model="form.email" type="text" placeholder="Enter Email" :errors="errors.email">
<div class="text-red-400 text-sm" v-if="errors.email && Array.isArray(errors.email)">

View File

@ -39,6 +39,8 @@ const props = defineProps({
const form = useForm({
_method: 'put',
login: props.user.login,
first_name: props.user.first_name,
last_name: props.user.last_name,
email: props.user.email,
password: '',
password_confirmation: '',
@ -82,6 +84,22 @@ const handleScore = (score: number) => {
</FormControl>
</FormField>
<FormField label="First Name" :class="{ 'text-red-400': errors.first_name }">
<FormControl v-model="form.first_name" type="text" placeholder="Enter First Name" :errors="errors.first_name">
<div class="text-red-400 text-sm" v-if="errors.first_name && Array.isArray(errors.first_name)">
{{ errors.first_name.join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Last Name" :class="{ 'text-red-400': errors.last_name }">
<FormControl v-model="form.last_name" type="text" placeholder="Enter Last Name" :errors="errors.last_name">
<div class="text-red-400 text-sm" v-if="errors.last_name && Array.isArray(errors.last_name)">
{{ errors.last_name.join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Enter Email" :class="{ 'text-red-400': errors.email }">
<FormControl v-model="form.email" type="text" placeholder="Email" :errors="errors.email">
<div class="text-red-400 text-sm" v-if="errors.email && Array.isArray(errors.email)">

View File

@ -11,7 +11,7 @@ import BaseButton from '@/Components/BaseButton.vue';
import CardBox from '@/Components/CardBox.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import NotificationBar from '@/Components/NotificationBar.vue';
import Pagination from '@/Components/Admin/Pagination.vue';
import Pagination from '@/Components/Pagination.vue';
import Sort from '@/Components/Admin/Sort.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
// import { Vue } from 'vue-facing-decorator';

View File

@ -39,7 +39,8 @@ import { MainService } from '@/Stores/main';
import { notify } from '@/notiwind';
import MapComponent from '@/Components/Map/map.component.vue';
import { MapOptions } from '@/Components/Map/MapOptions';
import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
// import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
import { LatLngBoundsExpression } from 'leaflet';
import { LayerOptions } from '@/Components/Map/LayerOptions';
import TableKeywords from '@/Components/TableKeywords.vue';
import NotificationBar from '@/Components/NotificationBar.vue';
@ -277,7 +278,7 @@ const mapId = 'test';
// };
const nextStep = async () => {
let route ="";
let route = "";
if (formStep.value == 1) {
route = stardust.route('dataset.first.step');
} else if (formStep.value == 2) {
@ -504,7 +505,7 @@ Removes a selected keyword
<icon-mandatory></icon-mandatory>
</icon-wizard>
<icon-wizard :is-current="formStep == 3" :is-checked="formStep > 3" :label="'Recommendet'">
<icon-wizard :is-current="formStep == 3" :is-checked="formStep > 3" :label="'Recommended'">
<icon-recommendet></icon-recommendet>
</icon-wizard>
@ -595,7 +596,8 @@ Removes a selected keyword
:class="{ 'text-red-400': form.errors['titles.0.value'] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.titles[0].value" type="textarea"
placeholder="[enter main title]" :show-char-count="true" :max-input-length="255">
placeholder="[enter main title]" :show-char-count="true"
:max-input-length="255">
<div class="text-red-400 text-sm"
v-if="form.errors['titles.0.value'] && Array.isArray(form.errors['titles.0.value'])">
{{ form.errors['titles.0.value'].join(', ') }}
@ -670,7 +672,8 @@ Removes a selected keyword
:class="{ 'text-red-400': form.errors['descriptions.0.value'] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.descriptions[0].value" type="textarea"
placeholder="[enter main abstract]" :show-char-count="true" :max-input-length="2500">
placeholder="[enter main abstract]" :show-char-count="true"
:max-input-length="2500">
<div class="text-red-400 text-sm"
v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.value'])">
{{ form.errors['descriptions.0.value'].join(', ') }}
@ -702,7 +705,8 @@ Removes a selected keyword
:class="{ 'text-red-400': form.errors[`descriptions.${index}.value`] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.descriptions[index].value" type="text"
placeholder="[enter additional description]" :show-char-count="true" :max-input-length="2500">
placeholder="[enter additional description]" :show-char-count="true"
:max-input-length="2500">
<div class="text-red-400 text-sm" v-if="form.errors[`descriptions.${index}.value`] &&
Array.isArray(form.errors[`descriptions.${index}.value`])
">
@ -744,7 +748,8 @@ Removes a selected keyword
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
<TablePersons :errors="form.errors" :persons="form.authors" :relation="'authors'" v-if="form.authors.length > 0" />
<TablePersons :errors="form.errors" :persons="form.authors" :relation="'authors'"
v-if="form.authors.length > 0" />
<div class="text-red-400 text-sm" v-if="errors.authors && Array.isArray(errors.authors)">
{{ errors.authors.join(', ') }}
</div>
@ -762,8 +767,9 @@ Removes a selected keyword
placeholder="search in person table...." v-on:person="onAddContributor">
</SearchAutocomplete>
<TablePersons :persons="form.contributors" :relation="'contributors'" v-if="form.contributors.length > 0"
:contributortypes="contributorTypes" :errors="form.errors" />
<TablePersons :persons="form.contributors" :relation="'contributors'"
v-if="form.contributors.length > 0" :contributortypes="contributorTypes"
:errors="form.errors" />
<div class="text-red-400 text-sm"
v-if="form.errors.contributors && Array.isArray(form.errors.contributors)">
{{ form.errors.contributors.join(', ') }}
@ -807,8 +813,8 @@ Removes a selected keyword
<FormField label="Coverage X Min"
:class="{ 'text-red-400': form.errors['coverage.x_min'] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.coverage.x_min" type="text" inputmode="numeric" pattern="\d*"
placeholder="[enter x_min]">
<FormControl required v-model="form.coverage.x_min" type="text" inputmode="numeric"
pattern="\d*" placeholder="[enter x_min]">
<div class="text-red-400 text-sm"
v-if="form.errors['coverage.x_min'] && Array.isArray(form.errors['coverage.x_min'])">
{{ form.errors['coverage.x_min'].join(', ') }}
@ -1066,6 +1072,7 @@ Removes a selected keyword
</div>
</div>
<template #footer>
<div class="flex p-2 mt-4">
<button v-if="formStep > 1" @click="prevStep"
@ -1087,11 +1094,30 @@ Removes a selected keyword
</button>
</div>
</div>
<progress v-if="form.progress" :value="form.progress.percentage" max="100">{{
form.progress.percentage
}}%</progress>
<progress v-if="form.progress" :value="form.progress.percentage" max="100">
{{ form.progress.percentage }}%
</progress>
</template>
</CardBox>
<!-- Loading Spinner -->
<div v-if="form.processing"
class="fixed inset-0 flex items-center justify-center bg-gray-500 bg-opacity-50 z-50">
<svg class="animate-spin h-12 w-12 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M12 2a10 10 0 0110 10h-4a6 6 0 00-6-6V2z"></path>
</svg>
</div>
<!-- <div
class="fixed inset-0 flex items-center justify-center bg-gray-500 bg-opacity-50">
<svg class="animate-spin h-10 w-10 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 0116 0 8 8 0 01-16 0zm2 0a6 6 0 0112 0 6 6 0 01-12 0z"></path>
</svg>
</div> -->
</SectionMain>
</LayoutAuthenticated>
</template>

View File

@ -79,7 +79,7 @@
<FormField label="Main Title *" help="required: main title"
:class="{ 'text-red-400': form.errors['titles.0.value'] }" class="w-full mr-1 flex-1">
<FormControl required v-model="form.titles[0].value" type="text"
placeholder="[enter main title]">
placeholder="[enter main title]" :show-char-count="true" :max-input-length="255">
<div class="text-red-400 text-sm"
v-if="form.errors['titles.0.value'] && Array.isArray(form.errors['titles.0.value'])">
{{ form.errors['titles.0.value'].join(', ') }}
@ -163,7 +163,7 @@
:class="{ 'text-red-400': form.errors['descriptions.0.value'] }"
class="w-full mr-1 flex-1">
<FormControl required v-model="form.descriptions[0].value" type="textarea"
placeholder="[enter main abstract]">
placeholder="[enter main abstract]" :show-char-count="true" :max-input-length="2500">
<div class="text-red-400 text-sm"
v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.value'])">
{{ form.errors['descriptions.0.value'].join(', ') }}

View File

@ -92,7 +92,7 @@ const formatServerState = (state: string) => {
<!-- table -->
<CardBox class="mb-6" has-table>
<table class="">
<table class="w-full table-fixed">
<thead>
<tr>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
@ -114,7 +114,7 @@ const formatServerState = (state: string) => {
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="getRowClass(dataset)">
<td data-label="Login" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<td data-label="Login" class="py-4 whitespace-nowrap text-gray-700 dark:text-white table-title">
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
{{ user.login }}
@ -156,44 +156,55 @@ const formatServerState = (state: string) => {
</LayoutAuthenticated>
</template>
<!-- <style scoped lang="css">
.pure-table tr.released {
background-color: rgb(52 211 153);
color: gray;
<style scoped lang="css">
.table-title {
max-width: 200px; /* set a maximum width */
overflow: hidden; /* hide overflow */
text-overflow: ellipsis; /* show ellipsis for overflowed text */
white-space: nowrap; /* prevent wrapping */
}
.table-fixed {
table-layout: fixed;
}
.pure-table tr.inprogress {
/* .pure-table tr.released {
background-color: rgb(52 211 153);
color: gray;
} */
/* .pure-table tr.inprogress {
padding: 0.8em;
background-color: rgb(94 234 212);
color: gray;
}
} */
.pure-table tr.editor_accepted {
/* .pure-table tr.editor_accepted {
background-color: rgb(125 211 252);
color: gray;
}
} */
.pure-table tr.rejected_reviewer {
/* .pure-table tr.rejected_reviewer {
padding: 0.8em;
background-color: orange;
color: gray;
}
} */
.pure-table tr.rejected_editor {
/* .pure-table tr.rejected_editor {
background-color: orange;
color: gray;
}
} */
.pure-table tr.reviewed {
/* .pure-table tr.reviewed {
background-color: yellow;
color: gray;
}
} */
.pure-table tr.approved {
/* .pure-table tr.approved {
background-color: rgb(86, 86, 241);
color: whitesmoke;
}
}*/
</style> -->
</style>

View File

@ -193,6 +193,9 @@ router.group(() => {
router.get('/mimetype', [MimetypeController, 'index']).as('mimetype.index');
// router.get('/mimetype/:id/edit', [MimetypeController, 'edit']).as('mimetype.edit').where('id', router.matchers.number()).use(middleware.can(['settings']));
router.get('/mimetype/create', [MimetypeController, 'create']).as('mimetype.create').use(middleware.can(['settings']));
router.post('/mimetype/store', [MimetypeController, 'store']).as('mimetype.store').use(middleware.can(['settings']));
router.get('/mimetype/:id/down', [MimetypeController, 'down']).as('mimetype.down').where('id', router.matchers.number()).use(middleware.can(['settings']));
router.get('/mimetype/:id/up', [MimetypeController, 'up']).as('mimetype.up').where('id', router.matchers.number()).use(middleware.can(['settings']));

View File

@ -36,6 +36,7 @@ async function isUnique(value: unknown, options: Options, field: FieldContext) {
if (result) {
// report that value is NOT unique
field.report('The {{ field }} field is not unique', 'isUnique', field);
// field.report(messages.unique, "isUnique", field);
}
}