feat: enhanced dataset management and UI improvements
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:
Kaimbacher 2025-01-08 11:45:03 +01:00
parent f67b736a88
commit d1480b1240
17 changed files with 2682 additions and 1446 deletions

View File

@ -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.',

View File

@ -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(
@ -278,14 +300,13 @@ export const updateDatasetValidator = vine.compile(
.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,
}) }),
), ),
}), }),
); );

View File

@ -128,7 +128,7 @@ 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'
@ -142,13 +142,15 @@ export class VanillaErrorReporter implements ErrorReporterContract {
// } // }
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,9 +161,17 @@ export class VanillaErrorReporter implements ErrorReporterContract {
} }
} }
} else { } else {
// normal field if (this.errors[error.field]) {
this.errors[error.field]?.push(message);
} else {
this.errors[error.field] = [message]; 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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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">

View File

@ -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);

View File

@ -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;
};

View File

@ -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);
// Watch localPassword and emit changes back to the parent
watch(localPassword, (newValue) => {
emit('update:password', newValue);
});
// const score = (event) => { type PasswordMetrics = {
// emit('score', event, payload: { score; strength: string }); score: number;
// }; scoreLabel: string | null;
hints: string[];
isSecure: boolean;
};
// Computed property for password class // Combined computed property for password strength metrics
const passwordClass = computed(() => { const passwordMetrics = computed<PasswordMetrics>(() => {
if (!props.password) { if (!localPassword.value) {
return null;
}
// const scoreLabel = checkStrength(props.password);
// const score = scorePassword(props.password);
const { score, scoreLabel } = checkStrength(props.password);
emit('score', score);
return { return {
[scoreLabel]: true, score: 0,
// scored: true, 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%;
} }

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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,10 +974,61 @@ 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>

View File

@ -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>

View File

@ -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({