feat: enhanced dataset management and UI improvements
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m10s
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m10s
- Submitter/DatasetController.ts: improved validations for time_absolute, time_min, and time_max. - validators/dataset.ts: enhanced validations for time_absolute, time_min, and time_max. - Added new favicon.ico for better branding. - Improved password-meter.vue component with clearer hint messages. - Updated checkStrength.ts: enhanced checkStrength() method for password strength validation. - submitter/Dataset/Create.vue: added form controls for time_min, time_max, and/or time_absolute fields. - submitter/Dataset/Edit.vue: introduced a loading spinner during file upload for better UX.
This commit is contained in:
parent
f67b736a88
commit
d1480b1240
|
@ -115,16 +115,6 @@ export default class DatasetController {
|
||||||
|
|
||||||
const projects = await Project.query().pluck('label', 'id');
|
const projects = await Project.query().pluck('label', 'id');
|
||||||
|
|
||||||
// const doctypes = {
|
|
||||||
// analysisdata: { label: 'Analysis', value: 'analysisdata' },
|
|
||||||
// measurementdata: { label: 'Measurements', value: 'measurementdata' },
|
|
||||||
// monitoring: 'Monitoring',
|
|
||||||
// remotesensing: 'Remote Sensing',
|
|
||||||
// gis: 'GIS',
|
|
||||||
// models: 'Models',
|
|
||||||
// mixedtype: 'Mixed Type',
|
|
||||||
// vocabulary: 'Vocabulary',
|
|
||||||
// };
|
|
||||||
return inertia.render('Submitter/Dataset/Create', {
|
return inertia.render('Submitter/Dataset/Create', {
|
||||||
licenses: licenses,
|
licenses: licenses,
|
||||||
doctypes: DatasetTypes,
|
doctypes: DatasetTypes,
|
||||||
|
@ -358,6 +348,17 @@ export default class DatasetController {
|
||||||
depth_absolut: vine.number().negative().optional(),
|
depth_absolut: vine.number().negative().optional(),
|
||||||
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
||||||
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
||||||
|
time_abolute: vine.date({ formats: { utc: true } }).optional(),
|
||||||
|
time_min: vine
|
||||||
|
.date({ formats: { utc: true } })
|
||||||
|
.beforeField('time_max')
|
||||||
|
.optional()
|
||||||
|
.requiredIfExists('time_max'),
|
||||||
|
time_max: vine
|
||||||
|
.date({ formats: { utc: true } })
|
||||||
|
.afterField('time_min')
|
||||||
|
.optional()
|
||||||
|
.requiredIfExists('time_min'),
|
||||||
}),
|
}),
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
|
@ -647,11 +648,13 @@ export default class DatasetController {
|
||||||
'required': '{{ field }} is required',
|
'required': '{{ field }} is required',
|
||||||
'unique': '{{ field }} must be unique, and this value is already taken',
|
'unique': '{{ field }} must be unique, and this value is already taken',
|
||||||
// 'confirmed': '{{ field }} is not correct',
|
// 'confirmed': '{{ field }} is not correct',
|
||||||
'licenses.minLength': 'at least {{ min }} permission must be defined',
|
'licenses.array.minLength': 'at least {{ min }} licence must be defined',
|
||||||
'licenses.*.number': 'Define roles as valid numbers',
|
'licenses.*.number': 'Define roles as valid numbers',
|
||||||
'rights.in': 'you must agree to continue',
|
'rights.in': 'you must agree to continue',
|
||||||
|
|
||||||
|
// 'titles.array.minLength': 'Main Title is required',
|
||||||
'titles.0.value.minLength': 'Main Title must be at least {{ min }} characters long',
|
'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.0.value.required': 'Main Title is required',
|
||||||
'titles.*.value.required': 'Additional title is required, if defined',
|
'titles.*.value.required': 'Additional title is required, if defined',
|
||||||
'titles.*.type.required': 'Additional title type is required',
|
'titles.*.type.required': 'Additional title type is required',
|
||||||
|
@ -668,7 +671,7 @@ export default class DatasetController {
|
||||||
'The language of the translated description must be different from the language of the dataset',
|
'The language of the translated description must be different from the language of the dataset',
|
||||||
|
|
||||||
'authors.array.minLength': 'at least {{ min }} author must be defined',
|
'authors.array.minLength': 'at least {{ min }} author must be defined',
|
||||||
'authors.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.',
|
'authors.array.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.',
|
||||||
'authors.*.email.isUnique': 'the email of the new creator already exists in the database',
|
'authors.*.email.isUnique': 'the email of the new creator already exists in the database',
|
||||||
'contributors.*.pivot_contributor_type.required': 'contributor type is required, if defined',
|
'contributors.*.pivot_contributor_type.required': 'contributor type is required, if defined',
|
||||||
'contributors.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.',
|
'contributors.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.',
|
||||||
|
|
|
@ -110,6 +110,17 @@ export const createDatasetValidator = vine.compile(
|
||||||
depth_absolut: vine.number().negative().optional(),
|
depth_absolut: vine.number().negative().optional(),
|
||||||
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
||||||
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
||||||
|
time_abolute: vine.date({ formats: { utc: true } }).optional(),
|
||||||
|
time_min: vine
|
||||||
|
.date({ formats: { utc: true } })
|
||||||
|
.beforeField('time_max')
|
||||||
|
.optional()
|
||||||
|
.requiredIfExists('time_max'),
|
||||||
|
time_max: vine
|
||||||
|
.date({ formats: { utc: true } })
|
||||||
|
.afterField('time_min')
|
||||||
|
.optional()
|
||||||
|
.requiredIfExists('time_min'),
|
||||||
}),
|
}),
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
|
@ -246,6 +257,17 @@ export const updateDatasetValidator = vine.compile(
|
||||||
depth_absolut: vine.number().negative().optional(),
|
depth_absolut: vine.number().negative().optional(),
|
||||||
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
||||||
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
||||||
|
time_abolute: vine.date({ formats: { utc: true } }).optional(),
|
||||||
|
time_min: vine
|
||||||
|
.date({ formats: { utc: true } })
|
||||||
|
.beforeField('time_max')
|
||||||
|
.optional()
|
||||||
|
.requiredIfExists('time_max'),
|
||||||
|
time_max: vine
|
||||||
|
.date({ formats: { utc: true } })
|
||||||
|
.afterField('time_min')
|
||||||
|
.optional()
|
||||||
|
.requiredIfExists('time_min'),
|
||||||
}),
|
}),
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
|
@ -269,23 +291,22 @@ export const updateDatasetValidator = vine.compile(
|
||||||
.distinct('value'),
|
.distinct('value'),
|
||||||
// last step
|
// last step
|
||||||
files: vine
|
files: vine
|
||||||
.array(
|
.array(
|
||||||
vine
|
vine
|
||||||
.myfile({
|
.myfile({
|
||||||
size: '512mb',
|
size: '512mb',
|
||||||
//extnames: extensions,
|
//extnames: extensions,
|
||||||
})
|
})
|
||||||
.allowedMimetypeExtensions()
|
.allowedMimetypeExtensions()
|
||||||
.filenameLength({ clientNameSizeLimit: 100 })
|
.filenameLength({ clientNameSizeLimit: 100 })
|
||||||
.fileScan({ removeInfected: true }),
|
.fileScan({ removeInfected: true }),
|
||||||
).dependentArrayMinLength({ dependentArray: 'fileInputs', min: 1}),
|
)
|
||||||
fileInputs: vine
|
.dependentArrayMinLength({ dependentArray: 'fileInputs', min: 1 }),
|
||||||
.array(
|
fileInputs: vine.array(
|
||||||
vine
|
vine.object({
|
||||||
.object({
|
label: vine.string().trim().maxLength(100),
|
||||||
label: vine.string().trim().maxLength(100),
|
//extnames: extensions,
|
||||||
//extnames: extensions,
|
}),
|
||||||
})
|
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -128,12 +128,12 @@ export class VanillaErrorReporter implements ErrorReporterContract {
|
||||||
const error: SimpleError = {
|
const error: SimpleError = {
|
||||||
message,
|
message,
|
||||||
rule,
|
rule,
|
||||||
field: field.wildCardPath ?field.wildCardPath.split('.')[0] : field.getFieldPath(),
|
field: field.getFieldPath(), // ?field.wildCardPath.split('.')[0] : field.getFieldPath(),
|
||||||
};
|
};
|
||||||
// field: 'titles.0.value'
|
// field: 'titles.0.value'
|
||||||
// message: 'Main Title is required'
|
// message: 'Main Title is required'
|
||||||
// rule: 'required' "required"
|
// rule: 'required' "required"
|
||||||
|
|
||||||
if (meta) {
|
if (meta) {
|
||||||
error.meta = meta;
|
error.meta = meta;
|
||||||
}
|
}
|
||||||
|
@ -141,14 +141,16 @@ export class VanillaErrorReporter implements ErrorReporterContract {
|
||||||
// error.index = field.name;
|
// error.index = field.name;
|
||||||
// }
|
// }
|
||||||
this.hasErrors = true;
|
this.hasErrors = true;
|
||||||
|
|
||||||
|
var test = field.getFieldPath();
|
||||||
|
|
||||||
// this.errors.push(error);
|
// this.errors.push(error);
|
||||||
// if (this.errors[error.field]) {
|
// if (this.errors[error.field]) {
|
||||||
// this.errors[error.field]?.push(message);
|
// this.errors[error.field]?.push(message);
|
||||||
// }
|
// }
|
||||||
if (field.isArrayMember) {
|
if (field.isArrayMember) {
|
||||||
// Check if the field has wildCardPath and if the error field already exists
|
// Check if the field has wildCardPath and if the error field already exists
|
||||||
if (this.errors[error.field] && field.wildCardPath) {
|
if (this.errors[error.field]) {
|
||||||
// Do nothing, as we don't want to push further messages
|
// Do nothing, as we don't want to push further messages
|
||||||
} else {
|
} else {
|
||||||
// If the error field already exists, push the message
|
// If the error field already exists, push the message
|
||||||
|
@ -159,10 +161,18 @@ export class VanillaErrorReporter implements ErrorReporterContract {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// normal field
|
if (this.errors[error.field]) {
|
||||||
this.errors[error.field] = [message];
|
this.errors[error.field]?.push(message);
|
||||||
|
} else {
|
||||||
|
this.errors[error.field] = [message];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// } else {
|
||||||
|
// // normal field
|
||||||
|
// this.errors[field.field] = [message];
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collecting errors as per the JSONAPI spec
|
* Collecting errors as per the JSONAPI spec
|
||||||
*/
|
*/
|
||||||
|
|
3641
package-lock.json
generated
3641
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -97,6 +97,5 @@
|
||||||
"assets/images/marker-icon.png": "http://localhost:8080/assets/images/marker-icon.2b3e1faf.png",
|
"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-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/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"
|
|
||||||
}
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -77,7 +77,7 @@ const inputElClass = computed(() => {
|
||||||
props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
||||||
];
|
];
|
||||||
if (props.icon) {
|
if (props.icon) {
|
||||||
base.push('pl-10');
|
base.push('pl-10', 'pr-10');
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
});
|
});
|
||||||
|
@ -136,6 +136,7 @@ if (props.ctrlKFocus) {
|
||||||
:class="inputElClass" :readonly="isReadOnly" />
|
:class="inputElClass" :readonly="isReadOnly" />
|
||||||
<FormControlIcon v-if="icon" :icon="icon" :h="controlIconH" />
|
<FormControlIcon v-if="icon" :icon="icon" :h="controlIconH" />
|
||||||
<slot />
|
<slot />
|
||||||
|
<slot name="right" /> <!-- Add slot for right-side content -->
|
||||||
<span v-if="showCharCount" class="message-counter" :class="{ 'text-red-500': maxInputLength && maxInputLength < computedValue.length }">
|
<span v-if="showCharCount" class="message-counter" :class="{ 'text-red-500': maxInputLength && maxInputLength < computedValue.length }">
|
||||||
{{ computedValue.length }}
|
{{ computedValue.length }}
|
||||||
<template v-if="maxInputLength">
|
<template v-if="maxInputLength">
|
||||||
|
|
|
@ -269,9 +269,8 @@ const ENDPOINT = 'https://resource.geolba.ac.at/PoolParty/sparql/keyword';
|
||||||
// });
|
// });
|
||||||
|
|
||||||
async function handleInput(e: Event) {
|
async function handleInput(e: Event) {
|
||||||
const target = <HTMLInputElement>e.target;
|
// const target = <HTMLInputElement>e.target;
|
||||||
|
// console.log(target.value);
|
||||||
console.log(target.value);
|
|
||||||
if (computedValue.value.length >= 2) {
|
if (computedValue.value.length >= 2) {
|
||||||
data.isOpen = true;
|
data.isOpen = true;
|
||||||
return await request(ENDPOINT, computedValue.value);
|
return await request(ENDPOINT, computedValue.value);
|
||||||
|
|
|
@ -1,53 +1,54 @@
|
||||||
import commonPasswords from '../data/commonPasswords';
|
import commonPasswords from '../data/commonPasswords';
|
||||||
import Trie from './Trie';
|
import Trie from './Trie';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const checkStrength = (pass: string) => {
|
const checkStrength = (pass: string) => {
|
||||||
const score = scorePassword(pass);
|
const score = scorePassword(pass);
|
||||||
const scoreLabel = mapScoreToLabel(score);
|
const scoreLabel = mapScoreToLabel(score);
|
||||||
|
const hints = getImprovementHints(pass);
|
||||||
return {
|
return {
|
||||||
score,
|
score,
|
||||||
scoreLabel
|
scoreLabel,
|
||||||
}
|
hints,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default checkStrength;
|
export default checkStrength;
|
||||||
|
|
||||||
// Function to score the password based on different criteria
|
// Function to score the password based on different criteria
|
||||||
const scorePassword = (password: string): number => {
|
const scorePassword = (password: string): number => {
|
||||||
if (password.length <= 6) return 0;
|
if (password.length <= 8 || isCommonPassword(password)) return 0;
|
||||||
if (isCommonPassword(password)) return 0;
|
|
||||||
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
score += getLengthScore(password);
|
score += getLengthScore(password); //3
|
||||||
score += getSpecialCharScore(password);
|
score += getSpecialCharScore(password); //1
|
||||||
score += getCaseMixScore(password);
|
score += getCaseMixScore(password); //1
|
||||||
score += getNumberMixScore(password);
|
score += getNumberMixScore(password); //1
|
||||||
|
|
||||||
return Math.min(score, 4); // Maximum score is 4
|
return Math.min(score, 6); // Maximum score is 6
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the Trie with common passwords
|
// Initialize the Trie with common passwords
|
||||||
const trie = new Trie();
|
const trie = new Trie();
|
||||||
commonPasswords.forEach(password => trie.insert(password));
|
commonPasswords.forEach((password) => trie.insert(password));
|
||||||
const isCommonPassword = (password: string): boolean => {
|
const isCommonPassword = (password: string): boolean => {
|
||||||
// return commonPasswords.includes(password);
|
// return commonPasswords.includes(password);
|
||||||
return trie.search(password);
|
return trie.search(password);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to get the score based on password length
|
// Length-based scoring
|
||||||
const getLengthScore = (password: string): number => {
|
const getLengthScore = (password: string): number => {
|
||||||
if (password.length > 20 && !hasRepeatChars(password)) return 3;
|
if (password.length > 20 && !hasRepeatChars(password)) return 3;
|
||||||
if (password.length > 12 && !hasRepeatChars(password)) return 2;
|
if (password.length > 12 && !hasRepeatChars(password)) return 2;
|
||||||
if (password.length > 8) return 1;
|
if (password.length > 8) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
// Function to check if the password contains repeated characters
|
|
||||||
const hasRepeatChars = (password: string): boolean => {
|
// const hasRepeatChars = (password: string): boolean => {
|
||||||
const repeatCharRegex = /(\w)(\1+\1+\1+\1+)/g;
|
// const repeatCharRegex = /(\w)(\1+\1+\1+\1+)/g;
|
||||||
return repeatCharRegex.test(password);
|
// return repeatCharRegex.test(password);
|
||||||
};
|
// };
|
||||||
|
// Check for repeated characters
|
||||||
|
const hasRepeatChars = (password: string): boolean => /(\w)\1{3,}/.test(password);
|
||||||
|
|
||||||
// Function to get the score based on the presence of special characters
|
// Function to get the score based on the presence of special characters
|
||||||
const getSpecialCharScore = (password: string): number => {
|
const getSpecialCharScore = (password: string): number => {
|
||||||
|
@ -71,23 +72,45 @@ const getNumberMixScore = (password: string): number => {
|
||||||
|
|
||||||
// Function to map the score to a corresponding label
|
// Function to map the score to a corresponding label
|
||||||
const mapScoreToLabel = (score: number): string => {
|
const mapScoreToLabel = (score: number): string => {
|
||||||
const labels = ['risky', 'guessable', 'weak', 'safe', 'secure'];
|
const labels = ['risky', 'guessable', 'weak', 'safe', 'secure', 'safe-secure', 'optimal'];
|
||||||
return labels[score] || '';
|
return labels[score] || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// const nameScore = (score: number): string => {
|
// Function to get improvement hints
|
||||||
// switch (score) {
|
const getImprovementHints = (password: string): string[] => {
|
||||||
// case 0:
|
const hints = [];
|
||||||
// return 'risky';
|
|
||||||
// case 1:
|
if (password.length <= 8) {
|
||||||
// return 'guessable';
|
hints.push('Increase your password length to more than 8 characters for 1 scoring point.');
|
||||||
// case 2:
|
} else {
|
||||||
// return 'weak';
|
if (password.length > 8 && password.length <= 12) {
|
||||||
// case 3:
|
hints.push('Increase your password length to more than 12 characters for additional scoring point.');
|
||||||
// return 'safe';
|
} else if (password.length > 12 && password.length <= 20) {
|
||||||
// case 4:
|
hints.push('Increase your password length to more than 20 characters for additional scoring point.');
|
||||||
// return 'secure';
|
}
|
||||||
// default:
|
|
||||||
// return '';
|
// Check for special character score
|
||||||
// }
|
if (!getSpecialCharScore(password)) {
|
||||||
// };
|
hints.push('Include at least one special character for 1 point.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for case mix score
|
||||||
|
if (!getCaseMixScore(password)) {
|
||||||
|
hints.push('Mix uppercase and lowercase letters for 1 point.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for number mix score
|
||||||
|
if (!getNumberMixScore(password)) {
|
||||||
|
hints.push('Add numbers to your password for 1 point.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRepeatChars(password) && password.length < 20) {
|
||||||
|
hints.push('Remove repeated characters for 1 point.');
|
||||||
|
}
|
||||||
|
if (hasRepeatChars(password) && password.length > 20) {
|
||||||
|
hints.push('Remove repeated characters for 2 points.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hints;
|
||||||
|
};
|
||||||
|
|
|
@ -1,63 +1,101 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { checkStrength } from './logic/index';
|
import { checkStrength } from './logic/index';
|
||||||
|
import { mdiFormTextboxPassword } from '@mdi/js';
|
||||||
|
import FormField from '@/Components/FormField.vue';
|
||||||
|
import FormControl from '@/Components/FormControl.vue';
|
||||||
|
|
||||||
// Define props
|
// Define props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
password: string;
|
password: string;
|
||||||
|
errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Define emits
|
const emit = defineEmits(['update:password', 'score']);
|
||||||
// const emit = defineEmits<{
|
|
||||||
// (event: 'score', payload: { score: number; strength: string }): void;
|
|
||||||
// }>();
|
|
||||||
|
|
||||||
const emit = defineEmits(['score']);
|
// A local reactive variable for password input
|
||||||
|
const localPassword = ref(props.password);
|
||||||
// const score = (event) => {
|
// Watch localPassword and emit changes back to the parent
|
||||||
// emit('score', event, payload: { score; strength: string });
|
watch(localPassword, (newValue) => {
|
||||||
// };
|
emit('update:password', newValue);
|
||||||
|
});
|
||||||
// Computed property for password class
|
|
||||||
const passwordClass = computed(() => {
|
type PasswordMetrics = {
|
||||||
if (!props.password) {
|
score: number;
|
||||||
return null;
|
scoreLabel: string | null;
|
||||||
}
|
hints: string[];
|
||||||
// const scoreLabel = checkStrength(props.password);
|
isSecure: boolean;
|
||||||
// const score = scorePassword(props.password);
|
};
|
||||||
const { score, scoreLabel } = checkStrength(props.password);
|
|
||||||
emit('score', score);
|
// Combined computed property for password strength metrics
|
||||||
return {
|
const passwordMetrics = computed<PasswordMetrics>(() => {
|
||||||
[scoreLabel]: true,
|
if (!localPassword.value) {
|
||||||
// scored: true,
|
return {
|
||||||
};
|
score: 0,
|
||||||
|
scoreLabel: null,
|
||||||
|
hints: [],
|
||||||
|
isSecure: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { score, scoreLabel, hints } = checkStrength(localPassword.value);
|
||||||
|
|
||||||
|
emit('score', score);
|
||||||
|
|
||||||
|
return {
|
||||||
|
score,
|
||||||
|
scoreLabel,
|
||||||
|
hints,
|
||||||
|
isSecure: score >= 4
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// export default {
|
|
||||||
// name: 'PasswordMeter',
|
|
||||||
// props: {
|
|
||||||
// password: String,
|
|
||||||
// },
|
|
||||||
// emits: ['score'],
|
|
||||||
// computed: {
|
|
||||||
// passwordClass(): object | null {
|
|
||||||
// if (!this.password) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// const strength = checkStrength(this.password);
|
|
||||||
// const score = scorePassword(this.password);
|
|
||||||
// this.$emit('score', { score, strength });
|
|
||||||
// return {
|
|
||||||
// [strength]: true,
|
|
||||||
// scored: true,
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="po-password-strength-bar" :class="passwordClass" />
|
<!-- Password input Form -->
|
||||||
|
<FormField label="New password" help="Required. New password" :class="{ 'text-red-400': errors.new_password }">
|
||||||
|
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" required
|
||||||
|
:error="errors.new_password">
|
||||||
|
<!-- Secure Icon -->
|
||||||
|
<template #right>
|
||||||
|
<span v-if="passwordMetrics.isSecure"
|
||||||
|
class="inline-flex justify-center items-center w-10 h-full absolute inset-y-0 right-2 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20"
|
||||||
|
fill="currentColor">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-10.707a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="text-red-400 text-sm" v-if="errors.new_password">
|
||||||
|
{{ errors.new_password }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<!-- Password Strength Bar -->
|
||||||
|
<div class="po-password-strength-bar w-full h-2 rounded transition-all duration-200 mb-4"
|
||||||
|
:class="passwordMetrics.scoreLabel" :style="{ width: `${(passwordMetrics.score / 6) * 100}%` }"
|
||||||
|
role="progressbar" :aria-valuenow="passwordMetrics.score" aria-valuemin="0" aria-valuemax="6"
|
||||||
|
:aria-label="`Password strength: ${passwordMetrics.scoreLabel || 'unknown'}`">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hint Message -->
|
||||||
|
<div v-if="passwordMetrics.hints.length > 0"
|
||||||
|
class="hint-message bg-gray-50 border-l-4 border-gray-300 text-gray-600 p-3 mb-4 rounded-md shadow-sm">
|
||||||
|
<p class="font-medium text-sm mb-2">To improve your password strength:</p>
|
||||||
|
<ul class="list-disc list-inside text-xs space-y-1">
|
||||||
|
<li v-for="(hint, index) in passwordMetrics.hints" :key="index"
|
||||||
|
class="hover:text-gray-800 transition-colors">
|
||||||
|
{{ hint }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!-- Score Display -->
|
||||||
|
<div class="text-gray-700 text-sm">
|
||||||
|
{{ passwordMetrics.score }} / 6 points max
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
@ -88,7 +126,9 @@ const { score, scoreLabel } = checkStrength(props.password);
|
||||||
width: 77.5%;
|
width: 77.5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.po-password-strength-bar.secure {
|
.po-password-strength-bar.secure,
|
||||||
|
.po-password-strength-bar.safe-secure,
|
||||||
|
.po-password-strength-bar.optimal {
|
||||||
background-color: #35cc62;
|
background-color: #35cc62;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -365,7 +365,7 @@ function onEnter() {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div> -->
|
</div> -->
|
||||||
<FormField label="Permissions" wrap-body>
|
<FormField label="Extensions" wrap-body>
|
||||||
<FormCheckRadioGroup v-model="form.file_extension" :options="file_extensions" name="file_extensions"
|
<FormCheckRadioGroup v-model="form.file_extension" :options="file_extensions" name="file_extensions"
|
||||||
is-column />
|
is-column />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
|
@ -14,11 +14,11 @@ import BaseButton from '@/Components/BaseButton.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
// import { Inertia } from '@inertiajs/inertia';
|
// import { Inertia } from '@inertiajs/inertia';
|
||||||
import passwordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
|
import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
|
||||||
|
|
||||||
const enabled = ref(false);
|
const enabled = ref(false);
|
||||||
const handleScore = (score: number) => {
|
const handleScore = (score: number) => {
|
||||||
if (score == 4){
|
if (score >= 4){
|
||||||
enabled.value = true;
|
enabled.value = true;
|
||||||
} else {
|
} else {
|
||||||
enabled.value = false;
|
enabled.value = false;
|
||||||
|
@ -101,15 +101,15 @@ const submit = async () => {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="Password" :class="{ 'text-red-400': errors.password }">
|
<!-- <FormField label="Password" :class="{ 'text-red-400': errors.password }">
|
||||||
<FormControl v-model="form.password" type="password" placeholder="Enter Password" :errors="errors.password">
|
<FormControl v-model="form.password" type="password" placeholder="Enter Password" :errors="errors.password">
|
||||||
<div class="text-red-400 text-sm" v-if="errors.password && Array.isArray(errors.password)">
|
<div class="text-red-400 text-sm" v-if="errors.password && Array.isArray(errors.password)">
|
||||||
<!-- {{ errors.password }} -->
|
|
||||||
{{ errors.password.join(', ') }}
|
{{ errors.password.join(', ') }}
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormField>
|
</FormField>
|
||||||
<password-meter :password="form.password" @score="handleScore" />
|
<password-meter :password="form.password" @score="handleScore" /> -->
|
||||||
|
<PasswordMeter v-model:password="form.password" :errors="form.errors" @score="handleScore" />
|
||||||
|
|
||||||
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
|
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|
|
@ -14,7 +14,7 @@ import BaseButton from '@/Components/BaseButton.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
// import { Inertia } from '@inertiajs/inertia';
|
// import { Inertia } from '@inertiajs/inertia';
|
||||||
import passwordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
|
import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
|
||||||
|
|
||||||
const enabled = ref(false);
|
const enabled = ref(false);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -52,7 +52,7 @@ const submit = async () => {
|
||||||
await router.put(stardust.route('settings.user.update', [props.user.id]), form);
|
await router.put(stardust.route('settings.user.update', [props.user.id]), form);
|
||||||
};
|
};
|
||||||
const handleScore = (score: number) => {
|
const handleScore = (score: number) => {
|
||||||
if (score == 4){
|
if (score >= 4){
|
||||||
enabled.value = true;
|
enabled.value = true;
|
||||||
} else {
|
} else {
|
||||||
enabled.value = false;
|
enabled.value = false;
|
||||||
|
@ -116,7 +116,7 @@ const handleScore = (score: number) => {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<password-meter :password="form.password" @score="handleScore" />
|
<PasswordMeter v-model:password="form.password" :errors="form.errors" @score="handleScore" />
|
||||||
|
|
||||||
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
|
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|
|
@ -25,11 +25,11 @@ import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
import passwordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
|
|
||||||
import { computed, Ref } from 'vue';
|
import { computed, Ref } from 'vue';
|
||||||
import { usePage } from '@inertiajs/vue3';
|
import { usePage } from '@inertiajs/vue3';
|
||||||
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
|
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
|
||||||
import PersonalTotpSettings from '@/Components/PersonalTotpSettings.vue';
|
import PersonalTotpSettings from '@/Components/PersonalTotpSettings.vue';
|
||||||
|
import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
|
||||||
// import PersonalSettings from '@/Components/PersonalSettings.vue';
|
// import PersonalSettings from '@/Components/PersonalSettings.vue';
|
||||||
// import { MainService } from '@/Stores/main';
|
// import { MainService } from '@/Stores/main';
|
||||||
// const mainService = MainService();
|
// const mainService = MainService();
|
||||||
|
@ -38,7 +38,7 @@ const emit = defineEmits(['confirm', 'update:confirmation'])
|
||||||
|
|
||||||
const enabled = ref(false);
|
const enabled = ref(false);
|
||||||
const handleScore = (score: number) => {
|
const handleScore = (score: number) => {
|
||||||
if (score == 4){
|
if (score >= 4){
|
||||||
enabled.value = true;
|
enabled.value = true;
|
||||||
} else {
|
} else {
|
||||||
enabled.value = false;
|
enabled.value = false;
|
||||||
|
@ -186,7 +186,7 @@ const flash: Ref<any> = computed(() => {
|
||||||
</FormField>
|
</FormField>
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
|
||||||
<FormField label="New password" help="Required. New password"
|
<!-- <FormField label="New password" help="Required. New password"
|
||||||
:class="{ 'text-red-400': passwordForm.errors.new_password }">
|
:class="{ 'text-red-400': passwordForm.errors.new_password }">
|
||||||
<FormControl v-model="passwordForm.new_password" :icon="mdiFormTextboxPassword" name="new_password"
|
<FormControl v-model="passwordForm.new_password" :icon="mdiFormTextboxPassword" name="new_password"
|
||||||
type="password" required :error="passwordForm.errors.new_password">
|
type="password" required :error="passwordForm.errors.new_password">
|
||||||
|
@ -194,8 +194,8 @@ const flash: Ref<any> = computed(() => {
|
||||||
{{ passwordForm.errors.new_password }}
|
{{ passwordForm.errors.new_password }}
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormField>
|
</FormField> -->
|
||||||
<password-meter :password="passwordForm.new_password" @score="handleScore" />
|
<PasswordMeter v-model:password="passwordForm.new_password" :errors="passwordForm.errors" @score="handleScore" />
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Confirm password" help="Required. New password one more time"
|
<FormField label="Confirm password" help="Required. New password one more time"
|
||||||
|
|
|
@ -252,6 +252,19 @@ watch(depth, (currentValue) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// let time= "no_time";
|
// let time= "no_time";
|
||||||
|
let time = ref('no_time');
|
||||||
|
watch(time, (currentValue) => {
|
||||||
|
if (currentValue == 'absolut') {
|
||||||
|
form.coverage.time_min = undefined;
|
||||||
|
form.coverage.time_max = undefined;
|
||||||
|
} else if (currentValue == 'range') {
|
||||||
|
form.coverage.time_absolut = undefined;
|
||||||
|
} else {
|
||||||
|
form.coverage.time_absolut = undefined;
|
||||||
|
form.coverage.time_min = undefined;
|
||||||
|
form.coverage.time_max = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const isModalActive = ref(false);
|
const isModalActive = ref(false);
|
||||||
const formStep = ref(1);
|
const formStep = ref(1);
|
||||||
|
@ -860,16 +873,16 @@ Removes a selected keyword
|
||||||
|
|
||||||
<CardBox class="mb-6 shadow" has-table title="Coverage Information" :icon="mdiEarthPlus">
|
<CardBox class="mb-6 shadow" has-table title="Coverage Information" :icon="mdiEarthPlus">
|
||||||
<!-- elevation menu -->
|
<!-- elevation menu -->
|
||||||
<div class="lex flex-col md:flex-row mb-3">
|
<div class="flex flex-col md:flex-row mb-3 space-y-2 md:space-y-0 md:space-x-4">
|
||||||
<label for="elevation-option-one" class="pure-radio">
|
<label for="elevation-option-one" class="pure-radio mb-2 md:mb-0">
|
||||||
<input id="elevation-option-one" type="radio" v-model="elevation" value="absolut" />
|
<input id="elevation-option-one" type="radio" v-model="elevation" value="absolut" />
|
||||||
absolut elevation (m)
|
absolut elevation (m)
|
||||||
</label>
|
</label>
|
||||||
<label for="elevation-option-two" class="pure-radio">
|
<label for="elevation-option-two" class="pure-radio mb-2 md:mb-0">
|
||||||
<input id="elevation-option-two" type="radio" v-model="elevation" value="range" />
|
<input id="elevation-option-two" type="radio" v-model="elevation" value="range" />
|
||||||
elevation range (m)
|
elevation range (m)
|
||||||
</label>
|
</label>
|
||||||
<label for="elevation-option-three" class="pure-radio">
|
<label for="elevation-option-three" class="pure-radio mb-2 md:mb-0">
|
||||||
<input id="elevation-option-three" type="radio" v-model="elevation"
|
<input id="elevation-option-three" type="radio" v-model="elevation"
|
||||||
value="no_elevation" />
|
value="no_elevation" />
|
||||||
no elevation
|
no elevation
|
||||||
|
@ -912,16 +925,16 @@ Removes a selected keyword
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- depth menu -->
|
<!-- depth menu -->
|
||||||
<div class="lex flex-col md:flex-row mb-3">
|
<div class="flex flex-col md:flex-row mb-3 space-y-2 md:space-y-0 md:space-x-4">
|
||||||
<label for="depth-option-one" class="pure-radio">
|
<label for="depth-option-one" class="pure-radio mb-2 md:mb-0">
|
||||||
<input id="depth-option-one" type="radio" v-model="depth" value="absolut" />
|
<input id="depth-option-one" type="radio" v-model="depth" value="absolut" />
|
||||||
absolut depth (m)
|
absolut depth (m)
|
||||||
</label>
|
</label>
|
||||||
<label for="depth-option-two" class="pure-radio">
|
<label for="depth-option-two" class="pure-radio mb-2 md:mb-0">
|
||||||
<input id="depth-option-two" type="radio" v-model="depth" value="range" />
|
<input id="depth-option-two" type="radio" v-model="depth" value="range" />
|
||||||
depth range (m)
|
depth range (m)
|
||||||
</label>
|
</label>
|
||||||
<label for="depth-option-three" class="pure-radio">
|
<label for="depth-option-three" class="pure-radio mb-2 md:mb-0">
|
||||||
<input id="depth-option-three" type="radio" v-model="depth" value="no_depth" />
|
<input id="depth-option-three" type="radio" v-model="depth" value="no_depth" />
|
||||||
no depth
|
no depth
|
||||||
</label>
|
</label>
|
||||||
|
@ -961,11 +974,62 @@ Removes a selected keyword
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- time menu -->
|
||||||
|
<div class="flex flex-col md:flex-row mb-3 space-y-2 md:space-y-0 md:space-x-4">
|
||||||
|
<label for="time-option-one" class="pure-radio mb-2 md:mb-0">
|
||||||
|
<input id="time-option-one" type="radio" v-model="time" value="absolut" />
|
||||||
|
absolut time (yyyy-MM-dd HH:mm:ss)
|
||||||
|
</label>
|
||||||
|
<label for="time-option-two" class="pure-radio mb-2 md:mb-0">
|
||||||
|
<input id="time-option-two" type="radio" v-model="time" value="range" />
|
||||||
|
time range (yyyy-MM-dd HH:mm:ss)
|
||||||
|
</label>
|
||||||
|
<label for="time-option-three" class="pure-radio mb-2 md:mb-0">
|
||||||
|
<input id="time-option-three" type="radio" v-model="time" value="no_time" />
|
||||||
|
no time
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row">
|
||||||
|
<FormField v-if="time === 'absolut'" label="time absolut"
|
||||||
|
:class="{ 'text-red-400': form.errors['coverage.time_absolut'] }"
|
||||||
|
class="w-full mx-2 flex-1">
|
||||||
|
<FormControl required v-model="form.coverage.time_absolut" type="datetime-local"
|
||||||
|
placeholder="[enter time_absolut]">
|
||||||
|
<div class="text-red-400 text-sm"
|
||||||
|
v-if="Array.isArray(form.errors['coverage.time_absolut'])">
|
||||||
|
{{ form.errors['coverage.time_absolut'].join(', ') }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
<FormField v-if="time === 'range'" label="time min"
|
||||||
|
:class="{ 'text-red-400': form.errors['coverage.time_min'] }"
|
||||||
|
class="w-full mx-2 flex-1">
|
||||||
|
<FormControl required v-model="form.coverage.time_min" type="datetime-local"
|
||||||
|
placeholder="[enter time_min]">
|
||||||
|
<div class="text-red-400 text-sm"
|
||||||
|
v-if="Array.isArray(form.errors['coverage.time_min'])">
|
||||||
|
{{ form.errors['coverage.time_min'].join(', ') }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
<FormField v-if="time === 'range'" label="time max"
|
||||||
|
:class="{ 'text-red-400': form.errors['coverage.time_max'] }"
|
||||||
|
class="w-full mx-2 flex-1">
|
||||||
|
<FormControl required v-model="form.coverage.time_max" type="datetime-local"
|
||||||
|
placeholder="[enter time_max]">
|
||||||
|
<div class="text-red-400 text-sm"
|
||||||
|
v-if="Array.isArray(form.errors['coverage.time_max'])">
|
||||||
|
{{ form.errors['coverage.time_max'].join(', ') }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
<CardBox class="mb-6 shadow" has-table title="Dataset References" :header-icon="mdiPlusCircle"
|
<CardBox class="mb-6 shadow" has-table title="Dataset References" :icon="mdiEarthPlus"
|
||||||
v-on:header-icon-click="addReference">
|
:header-icon="mdiPlusCircle" v-on:header-icon-click="addReference">
|
||||||
<!-- Message when no references exist -->
|
<!-- Message when no references exist -->
|
||||||
<div v-if="form.references.length === 0" class="text-center py-4">
|
<div v-if="form.references.length === 0" class="text-center py-4">
|
||||||
<p class="text-gray-600">No references added yet.</p>
|
<p class="text-gray-600">No references added yet.</p>
|
||||||
<p class="text-gray-400">Click the plus icon above to add a new reference.</p>
|
<p class="text-gray-400">Click the plus icon above to add a new reference.</p>
|
||||||
|
|
|
@ -334,8 +334,14 @@
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardBox class="mb-6 shadow" has-table title="Dataset References" :header-icon="mdiPlusCircle"
|
<CardBox class="mb-6 shadow" has-table title="Dataset References" :icon="mdiEarthPlus" :header-icon="mdiPlusCircle"
|
||||||
v-on:header-icon-click="addReference">
|
v-on:header-icon-click="addReference">
|
||||||
|
<!-- Message when no references exist -->
|
||||||
|
<div v-if="form.references.length === 0" class="text-center py-4">
|
||||||
|
<p class="text-gray-600">No references added yet.</p>
|
||||||
|
<p class="text-gray-400">Click the plus icon above to add a new reference.</p>
|
||||||
|
</div>
|
||||||
|
<!-- Reference form -->
|
||||||
<table class="table-fixed border-green-900" v-if="form.references.length">
|
<table class="table-fixed border-green-900" v-if="form.references.length">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -388,8 +394,9 @@
|
||||||
<FormControl required v-model="form.references[index].label" type="text"
|
<FormControl required v-model="form.references[index].label" type="text"
|
||||||
placeholder="[reference label]">
|
placeholder="[reference label]">
|
||||||
<div class="text-red-400 text-sm"
|
<div class="text-red-400 text-sm"
|
||||||
v-if="form.errors[`references.${index}.label`] && Array.isArray(form.errors[`references.${index}.label`])">
|
v-if="form.errors[`references.${index}.label`]">
|
||||||
{{ form.errors[`references.${index}.label`].join(', ') }}
|
{{ form.errors[`references.${index}.label`].join(', ') }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</td>
|
</td>
|
||||||
|
@ -468,7 +475,15 @@
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</template>
|
</template>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
<!-- </div> -->
|
<!-- 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>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -12,8 +12,6 @@ import vine from '@vinejs/vine';
|
||||||
// import { FieldContext } from '@vinejs/vine/types';
|
// import { FieldContext } from '@vinejs/vine/types';
|
||||||
// import db from '@adonisjs/lucid/services/db';
|
// import db from '@adonisjs/lucid/services/db';
|
||||||
import { VanillaErrorReporter } from '#validators/vanilla_error_reporter';
|
import { VanillaErrorReporter } from '#validators/vanilla_error_reporter';
|
||||||
// import { SimpleErrorReporter } from '@vinejs/vine';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// vine.messagesProvider = new SimpleMessagesProvider({
|
// vine.messagesProvider = new SimpleMessagesProvider({
|
||||||
|
|
Loading…
Reference in New Issue
Block a user