tethys.backend/resources/js/Components/FormControl.vue
Arno Kaimbacher 36cd7a757b
All checks were successful
CI / container-job (push) Successful in 41s
feat: Integrate official drive_provider, update user profile features & UI improvements
- adonisrc.ts: Load official drive_provider and unload custom driver_provider.
- packages.json: Add @headlessui/vue dependency for tab components.
- AvatarController.ts: Rewrite avatar generation logic to always return the same avatar per user.
- auth/UserController.ts: Add profile and profileUpdate methods to support user profile editing.
- Submitter/datasetController.ts & app/models/file.ts: Adapt code to use the official drive_provider.
- app/models/user.ts: Introduce “isAdmin” getter.
- config/drive.ts: Create new configuration for the official drive_provider.
- providers/vinejs_provider.ts: Adapt allowedExtensions control to use provided options or database enabled extensions.
- resource/js/app.ts: Load default Head and Link components.
- resources/js/menu.ts: Add settings-profile.edit menu point.
- resources/js/Components/action-message.vue: Add new component for improved user feedback after form submissions.
- New avatar-input.vue component: Enable profile picture selection.
- Components/CardBox.vue: Alter layout to optionally show HeaderIcon in title bar.
- FormControl.vue: Define a readonly prop for textareas.
- Improve overall UI with updates to NavBar.vue, UserAvatar.vue, UserAvatarCurrentUser.vue, and add v-model support to password-meter.vue.
- Remove profile editing logic from AccountInfo.vue and introduce new profile components (show.vue, update-password-form.vue, update-profile-information.vue).
- app.edge: Modify page (add @inertiaHead tag) for better meta management.
- routes.ts: Add new routes for editing user profiles.
- General npm updates.
2025-02-27 16:24:25 +01:00

150 lines
4.7 KiB
Vue

<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from 'vue';
import { MainService } from '@/Stores/main';
import FormControlIcon from '@/Components/FormControlIcon.vue';
const props = defineProps({
name: {
type: String,
default: null,
},
id: {
type: String,
default: null,
},
autocomplete: {
type: String,
default: null,
},
placeholder: {
type: String,
default: null,
},
inputmode: {
type: String,
default: null,
},
icon: {
type: String,
default: null,
},
options: {
type: [Array, Object],
default: null,
},
type: {
type: String,
default: 'text',
},
isReadOnly: {
type: Boolean,
default: false,
},
modelValue: {
type: [String, Number, Boolean, Array, Object],
default: '',
},
showCharCount: {
type: Boolean,
default: false,
},
maxInputLength: {
type: Number,
default: null,
},
extraHigh: {
type: Boolean,
default: false,
},
required: Boolean,
borderless: Boolean,
transparent: Boolean,
ctrlKFocus: Boolean,
});
const emit = defineEmits(['update:modelValue', 'setRef']);
const computedValue = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:modelValue', value);
},
});
const inputElClass = computed(() => {
const base = [
'px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full',
'dark:placeholder-gray-400',
props.extraHigh ? 'h-80' : (computedType.value === 'textarea' ? 'h-44' : 'h-12'),
props.borderless ? 'border-0' : 'border',
// props.transparent && !props.isReadOnly ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
];
if (props.icon) {
base.push('pl-10', 'pr-10');
}
return base;
});
const computedType = computed(() => (props.options ? 'select' : props.type));
const controlIconH = computed(() => (props.type === 'textarea' ? 'h-full' : 'h-12'));
const mainService = MainService();
const selectEl = ref(null);
const textareaEl = ref(null);
const inputEl = ref(null);
onMounted(() => {
if (selectEl.value) {
emit('setRef', selectEl.value);
} else if (textareaEl.value) {
emit('setRef', textareaEl.value);
} else {
emit('setRef', inputEl.value);
}
});
if (props.ctrlKFocus) {
const fieldFocusHook = (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
inputEl.value.focus();
} else if (e.key === 'Escape') {
inputEl.value.blur();
}
};
onMounted(() => {
if (!mainService.isFieldFocusRegistered) {
window.addEventListener('keydown', fieldFocusHook);
mainService.isFieldFocusRegistered = true;
} else {
// console.error('Duplicate field focus event')
}
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', fieldFocusHook);
mainService.isFieldFocusRegistered = false;
});
}
const focus = () => {
inputEl?.value.focus();
};
</script>
<template>
<div class="relative">
<select v-if="computedType === 'select'" :id="id" v-model="computedValue" :name="name" :class="inputElClass"
:disabled="isReadOnly">
<option v-if="placeholder" class="text-opacity-25" value="" disabled selected>{{ placeholder }}</option>
<option v-for="(option, index) in options" :key="index" :value="option.value ?? index">
{{ option.label ?? option }}
</option>
</select>
<textarea v-else-if="computedType === 'textarea'" :id="id" v-model="computedValue" :class="inputElClass"
:name="name" :placeholder="placeholder" :required="required" :readonly="isReadOnly"/>
<input v-else :id="id" ref="inputEl" v-model="computedValue" :name="name" :inputmode="inputmode"
:autocomplete="autocomplete" :required="required" :placeholder="placeholder" :type="computedType"
:class="inputElClass" :readonly="isReadOnly" />
<FormControlIcon v-if="icon" :icon="icon" :h="controlIconH" />
<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 }">
{{ computedValue.length }}
<template v-if="maxInputLength">
/ {{ maxInputLength }}
</template>
</span>
</div>
</template>