Replace local form controls with 2fauth/formcontrols components

This commit is contained in:
Bubka 2025-06-19 07:55:14 +02:00
parent 7213cec998
commit b1b8e71cea
40 changed files with 143 additions and 988 deletions

25
resources/js/app.js vendored
View File

@ -49,17 +49,20 @@ import ResponsiveWidthWrapper from '@/layouts/ResponsiveWidthWrapper.vue'
import FormWrapper from '@/layouts/FormWrapper.vue'
import Footer from '@/layouts/Footer.vue'
import Modal from '@/layouts/Modal.vue'
import VueButton from '@/components/formElements/Button.vue'
import ButtonBackCloseCancel from '@/components/formElements/ButtonBackCloseCancel.vue'
import FieldError from '@/components/formElements/FieldError.vue'
import FormField from '@/components/formElements/FormField.vue'
import FormPasswordField from '@/components/formElements/FormPasswordField.vue'
import FormSelect from '@/components/formElements/FormSelect.vue'
import FormToggle from '@/components/formElements/FormToggle.vue'
import FormCheckbox from '@/components/formElements/FormCheckbox.vue'
import FormButtons from '@/components/formElements/FormButtons.vue'
import Kicker from '@/components/Kicker.vue'
import {
FormField,
FormPasswordField,
FormFieldError,
FormCheckbox,
FormSelect,
FormToggle,
FormButtons,
NavigationButton,
VueButton
} from '@2fauth/formcontrols'
app
.component('FontAwesomeIcon', FontAwesomeIcon)
.component('ResponsiveWidthWrapper', ResponsiveWidthWrapper)
@ -67,8 +70,8 @@ app
.component('VueFooter', Footer)
.component('Modal', Modal)
.component('VueButton', VueButton)
.component('ButtonBackCloseCancel', ButtonBackCloseCancel)
.component('FieldError', FieldError)
.component('NavigationButton', NavigationButton)
.component('FormFieldError', FormFieldError)
.component('FormField', FormField)
.component('FormPasswordField', FormPasswordField)
.component('FormSelect', FormSelect)

View File

@ -62,7 +62,7 @@
<p class="control">
<button type="button" class="button is-link is-rounded" @click="moveAccounts">{{ $t('commons.move') }}</button>
</p>
<ButtonBackCloseCancel action="cancel" :useLinkTag="false" @canceled="$emit('update:showDestinationGroupSelector', false)" />
<NavigationButton action="cancel" :useLinkTag="false" @canceled="$emit('update:showDestinationGroupSelector', false)" />
</VueFooter>
</div>
</template>

View File

@ -45,7 +45,7 @@
</div>
</div>
<VueFooter :showButtons="true">
<ButtonBackCloseCancel action="close" :useLinkTag="false" @closed="$emit('update:showGroupSwitch', false)" />
<NavigationButton action="close" :useLinkTag="false" @closed="$emit('update:showGroupSwitch', false)" />
</VueFooter>
</div>
</template>

View File

@ -1,33 +0,0 @@
<script setup>
const props = defineProps({
color: {
type: String,
default: 'is-link'
},
nativeType: {
type: String,
default: 'submit'
},
isLoading: {
type: Boolean,
default: false
},
isDisabled: {
type: Boolean,
default: false
}
})
</script>
<template>
<button
:type="nativeType"
:disabled="isLoading || isDisabled"
:class="{
'button': true,
[`${color}`]: true,
'is-loading': isLoading,
}">
<slot />
</button>
</template>

View File

@ -1,94 +0,0 @@
<script setup>
import { useColorMode } from '@vueuse/core'
const router = useRouter()
const route = useRoute()
const mode = useColorMode()
const props = defineProps({
returnTo: {
type: Object,
default: { name: 'accounts' }
},
action: {
type: String,
default: 'close'
},
useLinkTag: {
type: Boolean,
default: true
},
isText: {
type: Boolean,
default: false
},
isCapture: {
type: Boolean,
default: false
},
isRounded: {
type: Boolean,
default: true
},
})
const classes = 'button'
+ (mode.value === 'dark' && ! props.isText && ! props.isCapture ? ' is-dark' : '')
+ (props.isText ? ' is-text' : '')
+ (props.isCapture ? ' is-large is-warning' : '')
+ (props.isRounded ? ' is-rounded' : '')
</script>
<template>
<!-- back / close / cancel button -->
<p v-if="useLinkTag" class="control">
<RouterLink
v-if="action == 'close'"
id="btnClose"
:to="returnTo"
:class="classes"
tabindex="0"
role="button"
:aria-label="$t('commons.close_the_x_page', { pagetitle: $route.meta.title })"
>
{{ $t('commons.close') }}
</RouterLink>
<RouterLink
v-else-if="action == 'back'"
id="lnkBack"
:to="returnTo"
:class="classes"
:aria-label="$t('commons.close_the_x_page', { pagetitle: $route.meta.title })"
>
{{ $t('commons.back') }}
</RouterLink>
<RouterLink
v-else-if="action == 'cancel'"
id="btnCancel"
:to="returnTo"
:class="classes"
>
{{ $t('commons.cancel') }}
</RouterLink>
</p>
<p v-else class="control">
<button
v-if="action == 'close'"
id="btnClose"
:class="classes"
@click="$emit('closed')"
type="button"
>
{{ $t('commons.close') }}
</button>
<button
v-if="action == 'cancel'"
id="btnCancel"
:class="classes"
@click="$emit('canceled')"
type="button"
>
{{ $t('commons.cancel') }}
</button>
</p>
</template>

View File

@ -1,29 +0,0 @@
<script setup>
import { useValidationErrorIdGenerator } from '@/composables/helpers'
const props = defineProps({
error: {
type: String,
required: true
},
field: {
type: String,
required: true
},
alertType: {
type: String,
default: 'is-danger'
}
})
const { valErrorId } = useValidationErrorIdGenerator(props.field)
</script>
<template>
<div role="alert">
<p :id="valErrorId"
class="help"
:class="alertType"
v-html="error" />
</div>
</template>

View File

@ -1,51 +0,0 @@
<script setup>
const props = defineProps({
showCancelButton: {
type: Boolean,
default: false
},
isBusy: {
type: Boolean,
default: false
},
isDisabled: {
type: Boolean,
default: false
},
caption: {
type: String,
default: 'commons.submit'
},
cancelLandingView: {
type: String,
default: ''
},
color: {
type: String,
default: 'is-link'
},
submitId: {
type: String,
default: 'btnSubmit'
},
cancelId: {
type: String,
default: 'btnCancel'
},
})
</script>
<template>
<div class="field is-grouped">
<div class="control">
<VueButton :id="submitId" :color="color" :isLoading="isBusy" :disabled="isDisabled" >
{{ $t(caption) }}
</VueButton>
</div>
<div class="control" v-if="showCancelButton">
<RouterLink :id="cancelId" :to="{ name: cancelLandingView }" class="button is-text">
{{ $t('commons.cancel') }}
</RouterLink>
</div>
</div>
</template>

View File

@ -1,64 +0,0 @@
<script setup>
import { useIdGenerator } from '@/composables/helpers'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: Boolean,
fieldName: {
type: String,
default: '',
required: true
},
label: {
type: String,
default: ''
},
labelClass: {
type: String,
default: ''
},
help: {
type: String,
default: ''
},
isIndented: Boolean,
isDisabled: Boolean,
isLocked: Boolean,
})
const emit = defineEmits(['update:modelValue'])
const legendId = useIdGenerator('legend', props.fieldName).inputId
const attrs = useAttrs()
const model = computed({
get() {
return props.modelValue;
},
set(value) {
emit("update:modelValue", value);
},
})
function toggleModel() {
if (attrs['disabled'] != true) {
model.value = !model.value
}
}
</script>
<template>
<div class="field is-flex">
<div v-if="isIndented" class="mx-2 pr-1" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
</div>
<div>
<input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="model" :disabled="isDisabled || isLocked" :aria-describedby="help ? legendId : undefined" />
<label tabindex="0" :for="fieldName" class="label" :class="labelClass" v-on:keypress.space.prevent="toggleModel">
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
</label>
<p :id="legendId" class="help" v-html="$t(help)" v-if="help" />
</div>
</div>
</template>

View File

@ -1,95 +0,0 @@
<script setup>
import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: [String, Number, Boolean],
label: {
type: String,
default: ''
},
fieldName: {
type: String,
default: '',
required: true
},
fieldError: [String],
inputType: {
type: String,
default: 'text'
},
placeholder: {
type: String,
default: ''
},
help: {
type: String,
default: ''
},
hasOffset: {
type: Boolean,
default: false
},
isDisabled: {
type: Boolean,
default: false
},
maxLength: {
type: Number,
default: null
},
isIndented: Boolean,
isLocked: Boolean,
leftIcon: '',
rightIcon: '',
idSuffix: {
type: String,
default: ''
},
})
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
const legendId = useIdGenerator('legend', props.fieldName).inputId
</script>
<template>
<div class="mb-3" :class="{ 'pt-3' : hasOffset, 'is-flex' : isIndented }">
<div v-if="isIndented" class="mx-2 pr-1" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
</div>
<div class="field" :class="{ 'is-flex-grow-5' : isIndented }">
<label :for="inputId" class="label" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
</label>
<div class="control" :class="{ 'has-icons-left' : leftIcon, 'has-icons-right': rightIcon }">
<input
:disabled="isDisabled || isLocked"
:id="inputId"
:type="inputType"
class="input"
:value="modelValue"
:placeholder="placeholder"
v-bind="$attrs"
v-on:input="$emit('update:modelValue', $event.target.value)"
v-on:change="$emit('change:modelValue', $event.target.value)"
:maxlength="maxLength"
:aria-describedby="help ? legendId : undefined"
:aria-invalid="fieldError != undefined"
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
/>
<span v-if="leftIcon" class="icon is-small is-left">
<FontAwesomeIcon :icon="['fas', leftIcon]" transform="rotate-75" size="xs" />
</span>
<span v-if="rightIcon" class="icon is-small is-right">
<FontAwesomeIcon :icon="['fas', rightIcon]" transform="rotate-75" size="xs" />
</span>
</div>
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
<p :id="legendId" class="help" v-html="$t(help)" v-if="help"></p>
</div>
</div>
</template>

View File

@ -1,143 +0,0 @@
<script setup>
import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers'
import { UseColorMode } from '@vueuse/components'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) },
isEditMode: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
fieldName: {
type: String,
default: '',
required: true
},
fieldError: [String],
inputType: {
type: String,
default: 'text'
},
placeholder: {
type: String,
default: ''
},
help: {
type: String,
default: ''
},
hasOffset: {
type: Boolean,
default: false
},
isDisabled: {
type: Boolean,
default: false
},
isExpanded: {
type: Boolean,
default: true
},
maxLength: {
type: Number,
default: null
},
idSuffix: {
type: String,
default: ''
},
isLocked: Boolean,
})
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
const legendId = useIdGenerator('legend', props.fieldName).inputId
const fieldIsLocked = ref(props.isDisabled || props.isEditMode || props.isLocked)
const hasBeenTrimmed = ref(false)
const componentKey = ref(0);
const emit = defineEmits(['update:modelValue'])
/**
* Removes spaces from the input string
*/
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.trimAll) {
value = value.replace(/\s+/g, '')
}
emit('update:modelValue', value)
}
function alertOnSpace(e) {
let value = e.target.value
hasBeenTrimmed.value = value.includes(' ')
emit('update:modelValue', value)
}
function forceRefresh(e) {
hasBeenTrimmed.value = e.target.value.includes(' ')
componentKey.value += 1
}
</script>
<template>
<label :for="inputId" class="label">
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
</label>
<div class="field has-addons mb-0" :class="{ 'pt-3' : hasOffset }">
<div class="control" :class="{ 'is-expanded': isExpanded }">
<input
:key="componentKey"
:disabled="fieldIsLocked"
:id="inputId"
:type="inputType"
class="input"
:value="modelValue"
:placeholder="placeholder"
v-bind="$attrs"
v-on:input="alertOnSpace"
v-on:change="emitValue"
v-on:blur="forceRefresh"
:maxlength="maxLength"
:aria-describedby="help ? legendId : undefined"
:aria-invalid="fieldError != undefined"
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
/>
</div>
<UseColorMode v-slot="{ mode }" v-if="isEditMode">
<div class="control" v-if="fieldIsLocked">
<button type="button" class="button field-lock" :class="{'is-dark' : mode == 'dark'}" @click.stop="fieldIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
<span class="icon">
<FontAwesomeIcon :icon="['fas', 'lock']" />
</span>
</button>
</div>
<div class="control" v-else>
<button type="button" class="button field-unlock" :class="{'is-dark' : mode == 'dark'}" @click.stop="fieldIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
<span class="icon has-text-danger">
<FontAwesomeIcon :icon="['fas', 'lock-open']" />
</span>
</button>
</div>
</UseColorMode>
</div>
<FieldError v-if="hasBeenTrimmed" :error="$t('twofaccounts.forms.spaces_are_ignored')" :field="'spaces'" :alertType="'is-warning'" />
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
<p :id="legendId" class="help" v-html="$t(help)" v-if="help"></p>
</template>

View File

@ -1,130 +0,0 @@
<script setup>
import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers'
defineOptions({
inheritAttrs: true
})
const props = defineProps({
modelValue: [String],
label: {
type: String,
default: ''
},
fieldName: {
type: String,
default: '',
required: true
},
fieldError: [String],
inputType: {
type: String,
default: 'password'
},
placeholder: {
type: String,
default: ''
},
help: {
type: String,
default: ''
},
hasOffset: {
type: Boolean,
default: false
},
isDisabled: {
type: Boolean,
default: false
},
showRules: {
type: Boolean,
default: false
},
idSuffix: {
type: String,
default: ''
},
isLocked: Boolean,
})
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
const legendId = useIdGenerator('legend', props.fieldName).inputId
const currentType = ref(props.inputType)
const hasCapsLockOn = ref(false)
const hasLowerCase = computed(() => {
return /[a-z]/.test(props.modelValue)
})
const hasUpperCase = computed(() => {
return /[A-Z]/.test(props.modelValue)
})
const hasNumber = computed(() => {
return /[0-9]/.test(props.modelValue)
})
const hasSpecialChar = computed(() => {
return /[^A-Za-z0-9]/.test(props.modelValue)
})
const IsLongEnough = computed(() => {
return props.modelValue.length >= 8
})
function checkCapsLock(event) {
if (typeof event.getModifierState === 'function') {
hasCapsLockOn.value = event.getModifierState('CapsLock') ? true : false
}
}
function setFieldType(event) {
if (currentType.value != event) {
currentType.value = event
}
}
</script>
<template>
<div class="field" :class="{ 'pt-3' : hasOffset }">
<label :for="inputId" class="label">
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
</label>
<div class="control has-icons-right">
<input
:disabled="isDisabled || isLocked"
:id="inputId"
:type="currentType"
class="input"
:value="modelValue"
:placeholder="placeholder"
v-bind="$attrs"
v-on:input="$emit('update:modelValue', $event.target.value)"
v-on:keyup="checkCapsLock"
:aria-describedby="help ? legendId : undefined"
:aria-invalid="fieldError != undefined"
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
/>
<span v-if="currentType == 'password'" role="button" id="btnTogglePassword" tabindex="0" class="icon is-small is-right is-clickable" @keyup.enter="setFieldType('text')" @click="setFieldType('text')" :title="$t('auth.forms.reveal_password')">
<font-awesome-icon :icon="['fas', 'eye-slash']" />
</span>
<span v-else role="button" id="btnTogglePassword" tabindex="0" class="icon is-small is-right is-clickable" @keyup.enter="setFieldType('password')" @click="setFieldType('password')" :title="$t('auth.forms.hide_password')">
<font-awesome-icon :icon="['fas', 'eye']" />
</span>
</div>
<p class="help is-warning" v-if="hasCapsLockOn" v-html="$t('auth.forms.caps_lock_is_on')" />
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
<p class="help" v-html="$t(help)" v-if="help" />
<div v-if="showRules" :id="legendId" class="columns is-mobile is-size-7 mt-0">
<div class="column is-one-third">
<span class="has-text-weight-semibold">{{ $t("auth.forms.mandatory_rules") }}</span><br />
<span class="is-underscored" id="valPwdIsLongEnough" :class="{'is-dot' : IsLongEnough}"></span>{{ $t('auth.forms.is_long_enough') }}<br/>
</div>
<div class="column">
<span class="has-text-weight-semibold">{{ $t("auth.forms.optional_rules_you_should_follow") }}</span><br />
<span class="is-underscored" id="valPwdHasLowerCase" :class="{'is-dot' : hasLowerCase}"></span>{{ $t('auth.forms.has_lower_case') }}<br/>
<span class="is-underscored" id="valPwdHasUpperCase" :class="{'is-dot' : hasUpperCase}"></span>{{ $t('auth.forms.has_upper_case') }}<br/>
<span class="is-underscored" id="valPwdHasSpecialChar" :class="{'is-dot' : hasSpecialChar}"></span>{{ $t('auth.forms.has_special_char') }}<br/>
<span class="is-underscored" id="valPwdHasNumber" :class="{'is-dot' : hasNumber}"></span>{{ $t('auth.forms.has_number') }}
</div>
</div>
</div>
</template>

View File

@ -1,66 +0,0 @@
<script setup>
import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers'
const model = defineModel()
const props = defineProps({
label: {
type: String,
default: ''
},
fieldName: {
type: String,
default: '',
required: true
},
fieldError: [String],
options: {
type: Array,
required: true
},
help: {
type: String,
default: ''
},
isIndented: Boolean,
isDisabled: Boolean,
isLocked: Boolean,
idSuffix: {
type: String,
default: ''
},
})
const { inputId } = useIdGenerator('select', props.fieldName + props.idSuffix)
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
const legendId = useIdGenerator('legend', props.fieldName + props.idSuffix).inputId
</script>
<template>
<div class="field is-flex">
<div v-if="isIndented" class="mx-2 pr-1" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
</div>
<div>
<label :for="inputId" class="label" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
</label>
<div class="control">
<div class="select">
<select
:id="inputId"
v-model="model"
:disabled="isDisabled || isLocked"
:aria-describedby="help ? legendId : undefined"
:aria-invalid="fieldError != undefined"
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
>
<option v-for="option in options" :key="option.value" :value="option.value">{{ $t(option.text) }}</option>
</select>
</div>
<slot></slot>
</div>
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
<p :id="legendId" class="help" v-html="$t(help)" v-if="help"></p>
</div>
</div>
</template>

View File

@ -1,89 +0,0 @@
<script setup>
import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: [String, Number, Boolean],
label: {
type: String,
default: ''
},
fieldName: {
type: String,
default: '',
required: true
},
fieldError: [String],
placeholder: {
type: String,
default: ''
},
help: {
type: String,
default: ''
},
size: {
type: String,
default: ''
},
hasOffset: {
type: Boolean,
default: false
},
isDisabled: {
type: Boolean,
default: false
},
maxLength: {
type: Number,
default: null
},
isIndented: Boolean,
isLocked: Boolean,
leftIcon: '',
rightIcon: '',
idSuffix: {
type: String,
default: ''
}
})
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
const legendId = useIdGenerator('legend', props.fieldName).inputId
</script>
<template>
<div class="mb-3" :class="{ 'pt-3' : hasOffset, 'is-flex' : isIndented }">
<div v-if="isIndented" class="mx-2 pr-1" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
</div>
<div class="field" :class="{ 'is-flex-grow-5' : isIndented }">
<label v-if="label" :for="inputId" class="label">
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
</label>
<div class="control" :class="{ 'has-icons-left' : leftIcon, 'has-icons-right': rightIcon }">
<textarea
:disabled="isDisabled || isLocked"
:id="inputId"
class="textarea"
:class="size"
:value="modelValue"
:placeholder="placeholder"
v-bind="$attrs"
v-on:input="$emit('update:modelValue', $event.target.value)"
v-on:change="$emit('change:modelValue', $event.target.value)"
:maxlength="maxLength"
:aria-describedby="help ? legendId : undefined"
:aria-invalid="fieldError != undefined"
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
/>
</div>
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
<p :id="legendId" class="help" v-html="$t(help)" v-if="help"></p>
</div>
</div>
</template>

View File

@ -1,88 +0,0 @@
<script setup>
import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers'
import { UseColorMode } from '@vueuse/components'
const props = defineProps({
modelValue: [String, Number, Boolean],
choices: {
type: Array,
required: true
},
fieldName: {
type: String,
required: true
},
fieldError: [String],
hasOffset: Boolean,
isDisabled: Boolean,
isLocked: Boolean,
label: {
type: String,
default: ''
},
help: {
type: String,
default: ''
},
})
// defines what events our component emits
const emit = defineEmits(['update:modelValue'])
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
const legendId = useIdGenerator('legend', props.fieldName).inputId
function setRadio(event) {
emit('update:modelValue', event)
}
</script>
<template>
<div class="field" :class="{ 'pt-3': hasOffset }">
<span v-if="label" class="label" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
</span>
<div
id="rdoGroup"
role="radiogroup"
:aria-describedby="help ? legendId : undefined"
:aria-invalid="fieldError != undefined"
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
class="is-toggle buttons"
>
<UseColorMode v-slot="{ mode }">
<button
v-for="choice in choices"
:key="choice.value"
:id="useIdGenerator('button',fieldName+choice.value).inputId"
role="radio"
type="button"
class="button"
:aria-checked="modelValue===choice.value"
:disabled="isDisabled || isLocked"
:class="{
'is-link': modelValue===choice.value,
'is-dark': mode==='dark',
'is-multiline': choice.legend,
}"
v-on:click.stop="setRadio(choice.value)">
<input
:id="useIdGenerator('radio',choice.value).inputId"
type="radio"
class="is-hidden"
:checked="modelValue===choice.value"
:value="choice.value"
:disabled="isDisabled || isLocked"
/>
<span v-if="choice.legend" v-html="$t(choice.legend)" class="is-block is-size-7" />
<FontAwesomeIcon :icon="['fas',choice.icon]" v-if="choice.icon" class="mr-2" />
<label :for="useIdGenerator('button',fieldName+choice.value).inputId" class="is-clickable">
{{ $t(choice.text) }}
</label>
</button>
</UseColorMode>
</div>
<FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" />
<p :id="legendId" class="help" v-html="$t(help)" v-if="help" />
</div>
</template>

View File

@ -50,7 +50,7 @@
</section>
</div>
<VueFooter v-if="props.closable" :showButtons="true" :internalFooterType="'modal'">
<ButtonBackCloseCancel action="close" :useLinkTag="false" @closed="closeModal" />
<NavigationButton action="close" :useLinkTag="false" @closed="closeModal" />
</VueFooter>
</div>
</template>

View File

@ -63,7 +63,7 @@
</ul>
<!-- footer -->
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ path: returnTo }" action="back" />
<NavigationButton action="back" @goback="router.push({ name: returnTo })" :previous-page-title="$t('title.' + returnTo)" />
</VueFooter>
</UseColorMode>
</ResponsiveWidthWrapper>

View File

@ -80,7 +80,7 @@
{{ $t('twofaccounts.forms.scan_qrcode') }}
</button>
</div>
<FieldError v-if="form.errors.hasAny('qrcode')" :error="form.errors.get('qrcode')" :field="'qrcode'" />
<FormFieldError v-if="form.errors.hasAny('qrcode')" :error="form.errors.get('qrcode')" :field="'qrcode'" />
</div>
<!-- alternative methods -->
<div class="column is-full">
@ -108,7 +108,7 @@
</div>
<!-- Footer -->
<VueFooter :showButtons="true" >
<ButtonBackCloseCancel :returnTo="{ name: 'accounts' }" action="back" v-if="!twofaccounts.isEmpty" />
<NavigationButton v-if="!twofaccounts.isEmpty" action="back" @goback="router.push({ name: 'accounts' })" :previous-page-title="$t('title.accounts')" />
</VueFooter>
</div>
</template>

View File

@ -140,7 +140,7 @@
</FormWrapper>
</div>
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: returnTo }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: returnTo })" :current-page-title="$t('title.admin.appSetup')" />
</VueFooter>
</div>
</template>

View File

@ -86,9 +86,9 @@
<!-- restrict registration -->
<FormCheckbox v-model="appSettings.restrictRegistration" @update:model-value="val => useAppSettingsUpdater('restrictRegistration', val)" fieldName="restrictRegistration" :isDisabled="appSettings.disableRegistration" label="admin.forms.restrict_registration.label" help="admin.forms.restrict_registration.help" />
<!-- restrict list -->
<FormField v-model="appSettings.restrictList" @change:model-value="val => saveOrDeleteSetting('restrictList', val)" :fieldError="fieldErrors.restrictList" fieldName="restrictList" :isDisabled="!appSettings.restrictRegistration || appSettings.disableRegistration" label="admin.forms.restrict_list.label" help="admin.forms.restrict_list.help" :isIndented="true" />
<FormField v-model="appSettings.restrictList" @change:model-value="val => saveOrDeleteSetting('restrictList', val)" :errorMessage="fieldErrors.restrictList" fieldName="restrictList" :isDisabled="!appSettings.restrictRegistration || appSettings.disableRegistration" label="admin.forms.restrict_list.label" help="admin.forms.restrict_list.help" :isIndented="true" />
<!-- restrict rule -->
<FormField v-model="appSettings.restrictRule" @change:model-value="val => saveOrDeleteSetting('restrictRule', val)" :fieldError="fieldErrors.restrictRule" fieldName="restrictRule" :isDisabled="!appSettings.restrictRegistration || appSettings.disableRegistration" label="admin.forms.restrict_rule.label" help="admin.forms.restrict_rule.help" :isIndented="true" leftIcon="slash" rightIcon="slash" />
<FormField v-model="appSettings.restrictRule" @change:model-value="val => saveOrDeleteSetting('restrictRule', val)" :errorMessage="fieldErrors.restrictRule" fieldName="restrictRule" :isDisabled="!appSettings.restrictRegistration || appSettings.disableRegistration" label="admin.forms.restrict_rule.label" help="admin.forms.restrict_rule.help" :isIndented="true" leftIcon="Slash" rightIcon="Slash" />
<!-- disable registration -->
<FormCheckbox v-model="appSettings.disableRegistration" @update:model-value="val => useAppSettingsUpdater('disableRegistration', val)" fieldName="disableRegistration" label="admin.forms.disable_registration.label" help="admin.forms.disable_registration.help" />
<!-- keep sso registration -->
@ -97,7 +97,7 @@
</FormWrapper>
</div>
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: returnTo }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: returnTo })" :current-page-title="$t('title.admin.auth')" />
</VueFooter>
</div>
</template>

View File

@ -171,7 +171,7 @@
<Spinner :isVisible="isFetching && users.length === 0" />
<!-- footer -->
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: returnTo }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: returnTo })" :current-page-title="$t('title.admin.users')" />
</VueFooter>
</FormWrapper>
</div>

View File

@ -40,8 +40,8 @@
<AccessLogViewer :userId="props.userId" :lastOnly="false" :showSearch="true" :period="1" />
<!-- footer -->
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: 'admin.manageUser', params: { userId: props.userId }}" action="back" />
<ButtonBackCloseCancel :returnTo="{ name: 'accounts' }" action="close" />
<NavigationButton action="back" @goback="router.push({ name: 'admin.manageUser', params: { userId: props.userId }})" :previous-page-title="$t('title.admin.manageUser')" />
<NavigationButton action="close" @closed="router.push({ name: 'accounts' })" :current-page-title="$t('title.admin.logs.access')" />
</VueFooter>
</ResponsiveWidthWrapper>
</template>

View File

@ -31,11 +31,11 @@
<div>
<FormWrapper title="admin.new_user">
<form @submit.prevent="createUser" @keydown="registerForm.onKeydown($event)">
<FormField v-model="registerForm.name" fieldName="name" :fieldError="registerForm.errors.get('name')" inputType="text" label="auth.forms.name" autocomplete="username" :maxLength="255" autofocus />
<FormField v-model="registerForm.email" fieldName="email" :fieldError="registerForm.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" />
<FormPasswordField v-model="registerForm.password" fieldName="password" :fieldError="registerForm.errors.get('password')" :showRules="true" label="auth.forms.password" autocomplete="new-password" />
<FormField v-model="registerForm.name" fieldName="name" :errorMessage="registerForm.errors.get('name')" inputType="text" label="auth.forms.name" autocomplete="username" :maxLength="255" autofocus />
<FormField v-model="registerForm.email" fieldName="email" :errorMessage="registerForm.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" />
<FormPasswordField v-model="registerForm.password" fieldName="password" :errorMessage="registerForm.errors.get('password')" :showRules="true" label="auth.forms.password" autocomplete="new-password" />
<FormCheckbox v-model="registerForm.is_admin" fieldName="is_admin" label="admin.forms.is_admin.label" help="admin.forms.is_admin.help" />
<FormButtons :isBusy="registerForm.isBusy" :isDisabled="registerForm.isDisabled" :showCancelButton="true" :cancelLandingView="'admin.users'" caption="commons.create" submitId="btnCreateUser" />
<FormButtons :isBusy="registerForm.isBusy" :isDisabled="registerForm.isDisabled" :showCancelButton="true" @cancel="router.push({ name: 'admin.users' })" submitLabel="commons.create" submitId="btnCreateUser" />
</form>
</FormWrapper>
<!-- footer -->

View File

@ -316,8 +316,8 @@
</div>
<!-- footer -->
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: 'admin.users' }" action="back" />
<ButtonBackCloseCancel :returnTo="{ name: 'accounts' }" action="close" />
<NavigationButton action="back" @goback="router.push({ name: 'admin.users' })" :previous-page-title="$t('title.admin.users')" />
<NavigationButton action="close" @closed="router.push({ name: 'accounts' })" :current-page-title="$t('title.admin.manageUser')" />
</VueFooter>
</ResponsiveWidthWrapper>
</UseColorMode>

View File

@ -126,8 +126,8 @@
{{ $t('auth.webauthn.use_security_device_to_sign_in') }}
</div>
<form id="frmWebauthnLogin" @submit.prevent="webauthnLogin" @keydown="form.onKeydown($event)">
<FormField v-model="form.email" fieldName="email" :fieldError="form.errors.get('email')" inputType="email" label="auth.forms.email" autofocus />
<FormButtons :isBusy="isBusy" caption="commons.continue" submitId="btnContinue"/>
<FormField v-model="form.email" fieldName="email" :errorMessage="form.errors.get('email')" inputType="email" label="auth.forms.email" autofocus />
<FormButtons :isBusy="isBusy" submitLabel="commons.continue" submitId="btnContinue"/>
</form>
<div class="nav-links">
<p>
@ -195,9 +195,9 @@
<div v-if="$2fauth.isTestingApp" class="notification is-warning has-text-centered is-radiusless" v-html="$t('auth.forms.welcome_to_testing_app_use_those_credentials')" />
<div v-if="appSettings.enableSso == true && appSettings.useSsoOnly == true" class="notification is-warning has-text-centered" v-html="$t('auth.forms.sso_only_form_restricted_to_admin')" />
<form id="frmLegacyLogin" @submit.prevent="LegacysignIn" @keydown="form.onKeydown($event)">
<FormField v-model="form.email" fieldName="email" :fieldError="form.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="username" autofocus />
<FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" label="auth.forms.password" autocomplete="current-password" />
<FormButtons :isBusy="isBusy" caption="auth.sign_in" submitId="btnSignIn"/>
<FormField v-model="form.email" fieldName="email" :errorMessage="form.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="username" autofocus />
<FormPasswordField v-model="form.password" fieldName="password" :errorMessage="form.errors.get('password')" label="auth.forms.password" autocomplete="current-password" />
<FormButtons :isBusy="isBusy" submitLabel="auth.sign_in" submitId="btnSignIn"/>
</form>
<div class="nav-links">
<p>{{ $t('auth.forms.forgot_your_password') }}&nbsp;

View File

@ -83,8 +83,8 @@
<div v-if="deviceId" class="field">
<label id="lblDeviceRegistrationSuccess" class="label mb-5">{{ $t('auth.webauthn.device_successfully_registered') }}&nbsp;<font-awesome-icon :icon="['fas', 'check']" /></label>
<form @submit.prevent="RenameDevice" @keydown="renameDeviceForm.onKeydown($event)">
<FormField v-model="renameDeviceForm.name" fieldName="name" :fieldError="renameDeviceForm.errors.get('name')" inputType="text" placeholder="iPhone 12, TouchID, Yubikey 5C" label="auth.forms.name_this_device" />
<FormButtons :isBusy="renameDeviceForm.isBusy" :isDisabled="renameDeviceForm.isDisabled" caption="commons.continue" />
<FormField v-model="renameDeviceForm.name" fieldName="name" :errorMessage="renameDeviceForm.errors.get('name')" inputType="text" placeholder="iPhone 12, TouchID, Yubikey 5C" label="auth.forms.name_this_device" />
<FormButtons :isBusy="renameDeviceForm.isBusy" :isDisabled="renameDeviceForm.isDisabled" submitLabel="commons.continue" />
</form>
</div>
<div v-else class="field is-grouped">
@ -101,10 +101,10 @@
<!-- User registration form -->
<FormWrapper v-else title="auth.register" punchline="auth.forms.register_punchline">
<form @submit.prevent="register" @keydown="registerForm.onKeydown($event)">
<FormField v-model="registerForm.name" fieldName="name" :fieldError="registerForm.errors.get('name')" inputType="text" label="auth.forms.name" autocomplete="username" :maxLength="255" autofocus />
<FormField v-model="registerForm.email" fieldName="email" :fieldError="registerForm.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" />
<FormPasswordField v-model="registerForm.password" fieldName="password" :fieldError="registerForm.errors.get('password')" :showRules="true" autocomplete="new-password" label="auth.forms.password" />
<FormButtons :isBusy="registerForm.isBusy" :isDisabled="registerForm.isDisabled" caption="auth.register" submitId="btnRegister" />
<FormField v-model="registerForm.name" fieldName="name" :errorMessage="registerForm.errors.get('name')" inputType="text" label="auth.forms.name" autocomplete="username" :maxLength="255" autofocus />
<FormField v-model="registerForm.email" fieldName="email" :errorMessage="registerForm.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" />
<FormPasswordField v-model="registerForm.password" fieldName="password" :errorMessage="registerForm.errors.get('password')" :showRules="true" autocomplete="new-password" label="auth.forms.password" />
<FormButtons :isBusy="registerForm.isBusy" :isDisabled="registerForm.isDisabled" submitLabel="auth.register" submitId="btnRegister" />
</form>
<div class="nav-links">
<p>{{ $t('auth.forms.already_register') }}&nbsp;<RouterLink id="lnkSignIn" :to="{ name: 'login' }" class="is-link">{{ $t('auth.sign_in') }}</RouterLink></p>

View File

@ -38,13 +38,13 @@
<template>
<FormWrapper :title="$t(isWebauthnReset ? 'auth.webauthn.account_recovery' : 'auth.forms.reset_password')" :punchline="$t(isWebauthnReset ? 'auth.webauthn.recovery_punchline' : 'auth.forms.reset_punchline')">
<form @submit.prevent="requestPasswordReset" @keydown="form.onKeydown($event)">
<FormField v-model="form.email" fieldName="email" :fieldError="form.errors.get('email')" label="auth.forms.email" autofocus />
<FormField v-model="form.email" fieldName="email" :errorMessage="form.errors.get('email')" label="auth.forms.email" autofocus />
<FormButtons
:submitId="'btnSendResetPwd'"
:isBusy="form.isBusy"
:caption="$t(isWebauthnReset ? 'auth.webauthn.send_recovery_link' : 'auth.forms.send_password_reset_link')"
:submitLabel="isWebauthnReset ? 'auth.webauthn.send_recovery_link' : 'auth.forms.send_password_reset_link'"
:showCancelButton="true"
cancelLandingView="login" />
@cancel="router.push({ name: 'login' })" />
</form>
<VueFooter />
</FormWrapper>

View File

@ -45,16 +45,16 @@
<template>
<FormWrapper :title="$t('auth.forms.new_password')">
<form @submit.prevent="resetPassword" @keydown="form.onKeydown($event)">
<FormField v-model="form.email" :isDisabled="true" fieldName="email" :fieldError="form.errors.get('email')" label="auth.forms.email" autofocus />
<FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" autocomplete="new-password" :showRules="true" label="auth.forms.new_password" />
<FieldError v-if="form.errors.get('token') != undefined" :error="form.errors.get('token')" :field="form.token" />
<FormField v-model="form.email" :isDisabled="true" fieldName="email" :errorMessage="form.errors.get('email')" label="auth.forms.email" autofocus />
<FormPasswordField v-model="form.password" fieldName="password" :errorMessage="form.errors.get('password')" autocomplete="new-password" :showRules="true" label="auth.forms.new_password" />
<FormFieldError v-if="form.errors.get('token') != undefined" :error="form.errors.get('token')" :field="form.token" />
<FormButtons
v-if="isPending"
:submitId="'btnResetPwd'"
:isBusy="form.isBusy"
:caption="$t('auth.forms.change_password')"
submitLabel="auth.forms.change_password"
:showCancelButton="true"
cancelLandingView="login" />
@cancel="router.push({ name: 'login' })" />
<RouterLink v-if="!isPending" id="btnContinue" :to="{ name: 'accounts' }" class="button is-link">{{ $t('commons.continue') }}</RouterLink>
</form>
<VueFooter />

View File

@ -48,7 +48,7 @@
<div>
<form @submit.prevent="recover" @keydown="form.onKeydown($event)">
<FormCheckbox v-model="form.revokeAll" fieldName="revokeAll" label="auth.webauthn.disable_all_security_devices" help="auth.webauthn.disable_all_security_devices_help" />
<FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" autocomplete="current-password" :showRules="false" label="auth.forms.current_password.label" help="auth.forms.current_password.help" />
<FormPasswordField v-model="form.password" fieldName="password" :errorMessage="form.errors.get('password')" autocomplete="current-password" :showRules="false" label="auth.forms.current_password.label" help="auth.forms.current_password.help" />
<div class="field">
<p>
{{ $t('auth.forms.forgot_your_password') }}&nbsp;
@ -61,9 +61,9 @@
:submitId="'btnRecover'"
:isBusy="form.isBusy"
:isDisabled="form.isDisabled"
:caption="$t('commons.continue')"
submitLabel="commons.continue"
:showCancelButton="true"
cancelLandingView="login" />
@cancel="router.push({ name: 'login' })" />
</form>
</div>
<VueFooter />

View File

@ -68,13 +68,13 @@
<template>
<FormWrapper :title="isEditMode ? $t('groups.forms.rename_group') : $t('groups.forms.new_group')">
<form @submit.prevent="handleSubmit" @keydown="form.onKeydown($event)">
<FormField v-model="form.name" fieldName="name" :fieldError="form.errors.get('name')" label="commons.name" autofocus />
<FormField v-model="form.name" fieldName="name" :errorMessage="form.errors.get('name')" label="commons.name" autofocus />
<FormButtons
:submitId="isEditMode ? 'btnEditGroup' : 'btnCreateGroup'"
:isBusy="form.isBusy"
:caption="isEditMode ? $t('commons.save') : $t('commons.create')"
:submitLabel="isEditMode ? 'commons.save' : 'commons.create'"
:showCancelButton="true"
cancelLandingView="groups" />
@cancel="router.push({ name: 'groups' })" />
</form>
</FormWrapper>
</template>

View File

@ -70,7 +70,7 @@
</div>
<!-- footer -->
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: 'accounts' }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: 'accounts' })" :current-page-title="$t('title.groups')" />
</VueFooter>
</ResponsiveWidthWrapper>
</template>

View File

@ -114,10 +114,10 @@
<div v-if="$2fauth.config.proxyAuth" class="notification is-warning has-text-centered" v-html="$t('auth.user_account_controlled_by_proxy')" />
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.profile') }}</h4>
<fieldset :disabled="$2fauth.config.proxyAuth || user.oauth_provider">
<FormField v-model="formProfile.name" fieldName="name" :fieldError="formProfile.errors.get('name')" label="auth.forms.name" :maxLength="255" autocomplete="username" autofocus />
<FormField v-model="formProfile.email" fieldName="email" :fieldError="formProfile.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" autofocus />
<FormField v-model="formProfile.password" fieldName="password" :fieldError="formProfile.errors.get('password')" inputType="password" label="auth.forms.current_password.label" autocomplete="current-password" help="auth.forms.current_password.help" />
<FormButtons :isBusy="formProfile.isBusy" caption="commons.update" />
<FormField v-model="formProfile.name" fieldName="name" :errorMessage="formProfile.errors.get('name')" label="auth.forms.name" :maxLength="255" autocomplete="username" autofocus />
<FormField v-model="formProfile.email" fieldName="email" :errorMessage="formProfile.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" autofocus />
<FormField v-model="formProfile.password" fieldName="password" :errorMessage="formProfile.errors.get('password')" inputType="password" label="auth.forms.current_password.label" autocomplete="current-password" help="auth.forms.current_password.help" />
<FormButtons :isBusy="formProfile.isBusy" submitLabel="commons.update" />
</fieldset>
</form>
<form @submit.prevent="submitPassword" @keydown="formPassword.onKeydown($event)">
@ -125,10 +125,10 @@
<input hidden type="text" name="email" :value="formProfile.email" autocomplete="email" />
<h4 class="title is-4 pt-6 has-text-grey-light">{{ $t('settings.change_password') }}</h4>
<fieldset :disabled="$2fauth.config.proxyAuth || user.oauth_provider">
<FormPasswordField v-model="formPassword.password" fieldName="password" :fieldError="formPassword.errors.get('password')" idSuffix="ForUpdate" autocomplete="new-password" :showRules="true" label="auth.forms.new_password" />
<FormPasswordField v-model="formPassword.password_confirmation" :showRules="false" fieldName="password_confirmation" :fieldError="formPassword.errors.get('password_confirmation')" inputType="password" autocomplete="new-password" label="auth.forms.confirm_new_password" />
<FormField v-model="formPassword.currentPassword" fieldName="currentPassword" :fieldError="formPassword.errors.get('currentPassword')" inputType="password" label="auth.forms.current_password.label" autocomplete="current-password" help="auth.forms.current_password.help" />
<FormButtons :isBusy="formPassword.isBusy" submitId="btnSubmitChangePwd" caption="auth.forms.change_password" />
<FormPasswordField v-model="formPassword.password" fieldName="password" :errorMessage="formPassword.errors.get('password')" idSuffix="ForUpdate" autocomplete="new-password" :showRules="true" label="auth.forms.new_password" />
<FormPasswordField v-model="formPassword.password_confirmation" :showRules="false" fieldName="password_confirmation" :errorMessage="formPassword.errors.get('password_confirmation')" inputType="password" autocomplete="new-password" label="auth.forms.confirm_new_password" />
<FormField v-model="formPassword.currentPassword" fieldName="currentPassword" :errorMessage="formPassword.errors.get('currentPassword')" inputType="password" label="auth.forms.current_password.label" autocomplete="current-password" help="auth.forms.current_password.help" />
<FormButtons :isBusy="formPassword.isBusy" submitId="btnSubmitChangePwd" submitLabel="auth.forms.change_password" />
</fieldset>
</form>
<form id="frmDeleteAccount" @submit.prevent="submitDelete" @keydown="formDelete.onKeydown($event)">
@ -141,14 +141,14 @@
<p>{{ $t('auth.forms.deleting_2fauth_account_does_not_impact_provider') }}</p>
</div>
<fieldset :disabled="$2fauth.config.proxyAuth">
<FormField v-model="formDelete.password" fieldName="password" :fieldError="formDelete.errors.get('password')" inputType="password" idSuffix="ForDelete" autocomplete="new-password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" />
<FormButtons :isBusy="formDelete.isBusy" caption="auth.forms.delete_your_account" submitId="btnDeleteAccount" color="is-danger" />
<FormField v-model="formDelete.password" fieldName="password" :errorMessage="formDelete.errors.get('password')" inputType="password" idSuffix="ForDelete" autocomplete="new-password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" />
<FormButtons :isBusy="formDelete.isBusy" submitLabel="auth.forms.delete_your_account" submitId="btnDeleteAccount" color="is-danger" />
</fieldset>
</form>
</FormWrapper>
</div>
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: returnTo }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: returnTo })" :current-page-title="$t('title.settings.account')" />
</VueFooter>
</div>
</template>

View File

@ -28,13 +28,13 @@
<template>
<FormWrapper title="auth.webauthn.rename_device">
<form @submit.prevent="updateCredential" @keydown="form.onKeydown($event)">
<FormField v-model="form.name" fieldName="name" :fieldError="form.errors.get('name')" inputType="text" label="commons.new_name" autofocus />
<FormField v-model="form.name" fieldName="name" :errorMessage="form.errors.get('name')" inputType="text" label="commons.new_name" autofocus />
<FormButtons
:submitId="'btnEditCredential'"
:isBusy="form.isBusy"
:caption="$t('commons.save')"
submitLabel="commons.save"
:showCancelButton="true"
cancelLandingView="settings.webauthn.devices"
@cancel="router.push({ name: 'settings.webauthn.devices' })"
/>
</form>
</FormWrapper>

View File

@ -191,7 +191,7 @@
<Spinner :isVisible="isFetching && tokens.length === 0" />
<!-- footer -->
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: returnTo }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: returnTo })" :current-page-title="$t('title.settings.oauth.tokens')" />
</VueFooter>
</FormWrapper>
</div>
@ -199,15 +199,15 @@
<main class="main-section">
<FormWrapper title="settings.forms.new_token">
<form @submit.prevent="generatePAToken" @keydown="form.onKeydown($event)">
<FormField v-model="form.name" fieldName="name" :fieldError="form.errors.get('name')" inputType="text" label="commons.name" autofocus />
<FormField v-model="form.name" fieldName="name" :errorMessage="form.errors.get('name')" inputType="text" label="commons.name" autofocus />
<div class="field is-grouped">
<div class="control">
<VueButton :id="'btnGenerateToken'" :isLoading="form.isBusy" >
<VueButton nativeType="submit" :id="'btnGenerateToken'" :isLoading="form.isBusy" >
{{ $t('commons.generate') }}
</VueButton>
</div>
<div class="control">
<VueButton @click="cancelPATcreation" :nativeType="'button'" id="btnCancel" :color="'is-text'">
<VueButton @click="cancelPATcreation" nativeType="button" id="btnCancel" :color="'is-text'">
{{ $t('commons.cancel') }}
</VueButton>
</div>

View File

@ -15,13 +15,13 @@
const returnTo = useStorage($2fauth.prefix + 'returnTo', 'accounts')
const layouts = [
{ text: 'settings.forms.grid', value: 'grid', icon: 'th' },
{ text: 'settings.forms.list', value: 'list', icon: 'list' },
{ text: 'settings.forms.grid', value: 'grid', icon: 'Grid3X3' },
{ text: 'settings.forms.list', value: 'list', icon: 'List' },
]
const themes = [
{ text: 'settings.forms.light', value: 'light', icon: 'sun' },
{ text: 'settings.forms.dark', value: 'dark', icon: 'moon' },
{ text: 'settings.forms.automatic', value: 'system', icon: 'desktop' },
{ text: 'settings.forms.light', value: 'light', icon: 'Sun' },
{ text: 'settings.forms.dark', value: 'dark', icon: 'Moon' },
{ text: 'settings.forms.automatic', value: 'system', icon: 'MonitorCheck' },
]
const iconCollections = [
{ text: 'selfh.st', value: 'selfh', url: 'https://selfh.st/icons/', defaultVariant: 'regular' },
@ -261,7 +261,7 @@
</FormWrapper>
</div>
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: returnTo }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: returnTo })" :current-page-title="$t('title.settings.options')" />
</VueFooter>
</div>
</template>

View File

@ -173,7 +173,7 @@
</form>
<!-- footer -->
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: returnTo }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: returnTo })" :current-page-title="$t('title.settings.webauthn.devices')" />
</VueFooter>
</FormWrapper>
</div>

View File

@ -207,7 +207,7 @@
</div>
</div>
<div class="fullscreen-footer">
<ButtonBackCloseCancel action="cancel" :isCapture="true" :useLinkTag="false" @canceled="exitStream()" />
<NavigationButton action="cancel" :isCapture="true" :useLinkTag="false" @canceled="exitStream()" />
</div>
</div>
<modal v-model="showQrContent">

View File

@ -2,7 +2,7 @@
import Form from '@/components/formElements/Form'
import OtpDisplay from '@/components/OtpDisplay.vue'
import QrContentDisplay from '@/components/QrContentDisplay.vue'
import FormLockField from '@/components/formElements/FormLockField.vue'
import { FormProtectedField } from '@2fauth/formcontrols'
import twofaccountService from '@/services/twofaccountService'
import { useUserStore } from '@/stores/user'
import { useTwofaccounts } from '@/stores/twofaccounts'
@ -450,7 +450,7 @@
<div class="container preview has-text-centered">
<div class="columns is-mobile">
<div class="column">
<FieldError v-if="iconForm.errors.hasAny('icon')" :error="iconForm.errors.get('icon')" :field="'icon'" class="help-for-file" />
<FormFieldError v-if="iconForm.errors.hasAny('icon')" :error="iconForm.errors.get('icon')" :field="'icon'" class="help-for-file" />
<label class="add-icon-button" v-if="!tempIcon">
<input inert class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
<FontAwesomeIcon :icon="['fas', 'image']" size="2x" />
@ -478,9 +478,9 @@
<div class="column quickform-footer">
<div class="field is-grouped is-grouped-centered">
<div class="control">
<VueButton :isLoading="form.isBusy" >{{ $t('commons.save') }}</VueButton>
<VueButton nativeType="submit" :isLoading="form.isBusy" >{{ $t('commons.save') }}</VueButton>
</div>
<ButtonBackCloseCancel action="cancel" :isText="true" :isRounded="false" :useLinkTag="false" @canceled="cancelCreation" />
<NavigationButton action="cancel" :isText="true" :isRounded="false" :useLinkTag="false" @canceled="cancelCreation" />
</div>
</div>
</div>
@ -505,11 +505,11 @@
</div>
</div>
</div>
<FieldError v-if="qrcodeForm.errors.hasAny('qrcode')" :error="qrcodeForm.errors.get('qrcode')" :field="'qrcode'" class="help-for-file" />
<FormFieldError v-if="qrcodeForm.errors.hasAny('qrcode')" :error="qrcodeForm.errors.get('qrcode')" :field="'qrcode'" class="help-for-file" />
<!-- service -->
<FormField v-model="form.service" fieldName="service" :fieldError="form.errors.get('email')" :isDisabled="form.otp_type === 'steamtotp'" label="twofaccounts.service" :placeholder="$t('twofaccounts.forms.service.placeholder')" autofocus />
<FormField v-model="form.service" fieldName="service" :errorMessage="form.errors.get('email')" :isDisabled="form.otp_type === 'steamtotp'" label="twofaccounts.service" :placeholder="$t('twofaccounts.forms.service.placeholder')" autofocus />
<!-- account -->
<FormField v-model="form.account" fieldName="account" :fieldError="form.errors.get('account')" label="twofaccounts.account" :placeholder="$t('twofaccounts.forms.account.placeholder')" />
<FormField v-model="form.account" fieldName="account" :errorMessage="form.errors.get('account')" label="twofaccounts.account" :placeholder="$t('twofaccounts.forms.account.placeholder')" />
<!-- icon upload -->
<label for="filUploadIcon" class="label">{{ $t('twofaccounts.icon') }}</label>
<!-- try my luck -->
@ -538,7 +538,7 @@
<div class="field is-grouped">
<!-- try my luck button -->
<div class="control">
<VueButton @click="fetchLogo" :color="mode == 'dark' ? 'is-dark' : ''" :nativeType="'button'" :is-loading="fetchingLogo" :disabled="!form.service" aria-describedby="lgdTryMyLuck">
<VueButton @click="fetchLogo" :color="mode == 'dark' ? 'is-dark' : ''" nativeType="button" :is-loading="fetchingLogo" :disabled="!form.service" aria-describedby="lgdTryMyLuck">
<span class="icon is-small">
<FontAwesomeIcon :icon="['fas', 'globe']" />
</span>
@ -565,16 +565,16 @@
</div>
</div>
<div class="field">
<FieldError v-if="iconForm.errors.hasAny('icon')" :error="iconForm.errors.get('icon')" :field="'icon'" class="help-for-file" />
<FormFieldError v-if="iconForm.errors.hasAny('icon')" :error="iconForm.errors.get('icon')" :field="'icon'" class="help-for-file" />
<p id="lgdTryMyLuck" v-if="user.preferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
</div>
<!-- group -->
<FormSelect v-if="groups.length > 0" v-model="form.group_id" :options="groups" fieldName="group_id" label="twofaccounts.forms.group.label" help="twofaccounts.forms.group.help" />
<!-- otp type -->
<FormToggle v-model="form.otp_type" :isDisabled="isEditMode" :choices="otp_types" fieldName="otp_type" :fieldError="form.errors.get('otp_type')" label="twofaccounts.forms.otp_type.label" help="twofaccounts.forms.otp_type.help" :hasOffset="true" />
<FormToggle v-model="form.otp_type" :isDisabled="isEditMode" :choices="otp_types" fieldName="otp_type" :errorMessage="form.errors.get('otp_type')" label="twofaccounts.forms.otp_type.label" help="twofaccounts.forms.otp_type.help" :hasOffset="true" />
<div v-if="form.otp_type != ''">
<!-- secret -->
<FormLockField :isEditMode="isEditMode" v-model.trimAll="form.secret" fieldName="secret" :fieldError="form.errors.get('secret')" label="twofaccounts.forms.secret.label" help="twofaccounts.forms.secret.help" />
<FormProtectedField :enableProtection="isEditMode" v-model.trimAll="form.secret" fieldName="secret" :errorMessage="form.errors.get('secret')" label="twofaccounts.forms.secret.label" help="twofaccounts.forms.secret.help" />
<!-- Options -->
<div v-if="form.otp_type !== 'steamtotp'">
<h2 class="title is-4 mt-5 mb-2">{{ $t('commons.options') }}</h2>
@ -582,23 +582,23 @@
{{ $t('twofaccounts.forms.options_help') }}
</p>
<!-- digits -->
<FormToggle v-model="form.digits" :choices="digitsChoices" fieldName="digits" :fieldError="form.errors.get('digits')" label="twofaccounts.forms.digits.label" help="twofaccounts.forms.digits.help" />
<FormToggle v-model="form.digits" :choices="digitsChoices" fieldName="digits" :errorMessage="form.errors.get('digits')" label="twofaccounts.forms.digits.label" help="twofaccounts.forms.digits.help" />
<!-- algorithm -->
<FormToggle v-model="form.algorithm" :choices="algorithms" fieldName="algorithm" :fieldError="form.errors.get('algorithm')" label="twofaccounts.forms.algorithm.label" help="twofaccounts.forms.algorithm.help" />
<FormToggle v-model="form.algorithm" :choices="algorithms" fieldName="algorithm" :errorMessage="form.errors.get('algorithm')" label="twofaccounts.forms.algorithm.label" help="twofaccounts.forms.algorithm.help" />
<!-- TOTP period -->
<FormField v-if="form.otp_type === 'totp'" pattern="[0-9]{1,4}" :class="'is-third-width-field'" v-model="form.period" fieldName="period" :fieldError="form.errors.get('period')" label="twofaccounts.forms.period.label" help="twofaccounts.forms.period.help" :placeholder="$t('twofaccounts.forms.period.placeholder')" />
<FormField v-if="form.otp_type === 'totp'" pattern="[0-9]{1,4}" :class="'is-third-width-field'" v-model="form.period" fieldName="period" :errorMessage="form.errors.get('period')" label="twofaccounts.forms.period.label" help="twofaccounts.forms.period.help" :placeholder="$t('twofaccounts.forms.period.placeholder')" />
<!-- HOTP counter -->
<FormLockField v-if="form.otp_type === 'hotp'" pattern="[0-9]{1,4}" :isEditMode="isEditMode" :isExpanded="false" v-model="form.counter" fieldName="counter" :fieldError="form.errors.get('counter')" label="twofaccounts.forms.counter.label" :placeholder="$t('twofaccounts.forms.counter.placeholder')" :help="isEditMode ? 'twofaccounts.forms.counter.help_lock' : 'twofaccounts.forms.counter.help'" />
<FormProtectedField v-if="form.otp_type === 'hotp'" pattern="[0-9]{1,4}" :enableProtection="isEditMode" :isExpanded="false" v-model="form.counter" fieldName="counter" :errorMessage="form.errors.get('counter')" label="twofaccounts.forms.counter.label" :placeholder="$t('twofaccounts.forms.counter.placeholder')" :help="isEditMode ? 'twofaccounts.forms.counter.help_lock' : 'twofaccounts.forms.counter.help'" />
</div>
</div>
<VueFooter :showButtons="true">
<p class="control">
<VueButton :id="isEditMode ? 'btnUpdate' : 'btnCreate'" :isLoading="form.isBusy" class="is-rounded" >{{ isEditMode ? $t('commons.save') : $t('commons.create') }}</VueButton>
<VueButton nativeType="submit" :id="isEditMode ? 'btnUpdate' : 'btnCreate'" :isLoading="form.isBusy" class="is-rounded" >{{ isEditMode ? $t('commons.save') : $t('commons.create') }}</VueButton>
</p>
<p class="control" v-if="form.otp_type && form.secret">
<button id="btnPreview" type="button" class="button is-success is-rounded" @click="previewOTP">{{ $t('twofaccounts.forms.test') }}</button>
</p>
<ButtonBackCloseCancel action="cancel" :useLinkTag="false" @canceled="cancelCreation" />
<NavigationButton action="cancel" :useLinkTag="false" @canceled="cancelCreation" />
</VueFooter>
</form>
<!-- modal -->

View File

@ -1,9 +1,9 @@
<script setup>
import Form from '@/components/formElements/Form'
import FormTextarea from '@/components/formElements/FormTextarea.vue'
import twofaccountService from '@/services/twofaccountService'
import OtpDisplay from '@/components/OtpDisplay.vue'
import Spinner from '@/components/Spinner.vue'
import { FormTextarea } from '@2fauth/formcontrols'
import { useNotifyStore } from '@/stores/notify'
import { useUserStore } from '@/stores/user'
import { useBusStore } from '@/stores/bus'
@ -271,7 +271,7 @@
<p class="subtitle is-6 is-size-7-mobile">{{ $t('twofaccounts.import.supported_formats_for_qrcode_upload') }}</p>
</div>
</div>
<FieldError v-if="qrcodeForm.errors.hasAny('qrcode')" :error="qrcodeForm.errors.get('qrcode')" :field="'qrcode'" />
<FormFieldError v-if="qrcodeForm.errors.hasAny('qrcode')" :error="qrcodeForm.errors.get('qrcode')" :field="'qrcode'" />
</div>
<footer class="card-footer">
<RouterLink id="btnCapture" :to="{ name: 'capture' }" class="card-footer-item">
@ -298,7 +298,7 @@
<p class="subtitle is-6 is-size-7-mobile">{{ $t('twofaccounts.import.supported_formats_for_file_upload') }}</p>
</div>
</div>
<FieldError v-if="fileForm.errors.hasAny('file')" :error="fileForm.errors.get('file')" :field="'file'" />
<FormFieldError v-if="fileForm.errors.hasAny('file')" :error="fileForm.errors.get('file')" :field="'file'" />
</div>
<footer class="card-footer">
<a role="button" tabindex="0" class="card-footer-item is-relative" @click="fileInput.click()" @keyup.enter="fileInput.click()">
@ -323,7 +323,7 @@
</div>
</div>
<div class="content">
<FormTextarea v-model="directInput" :fieldError="directInputError" fieldName="payload" rows="5" :size="'is-small'" />
<FormTextarea v-model="directInput" :errorMessage="directInputError" fieldName="payload" rows="5" :size="'is-small'" />
</div>
</div>
<footer class="card-footer">
@ -458,7 +458,8 @@
</span> -->
</button>
</p>
<ButtonBackCloseCancel :returnTo="{ name: 'accounts' }" :action="importableCount > 0 ? 'cancel' : 'close'" />
<NavigationButton v-if="importableCount > 0" action="cancel" @canceled="router.push({ name: 'accounts' })" />
<NavigationButton v-else action="close" @closed="router.push({ name: 'accounts' })" :current-page-title="$t('title.importAccounts')" />
</VueFooter>
</ResponsiveWidthWrapper>
<!-- modal -->

View File

@ -29,7 +29,7 @@
</p>
</div>
<VueFooter :showButtons="true" :internalFooterType="'modal'">
<ButtonBackCloseCancel :returnTo="{ name: 'accounts' }" action="close" />
<NavigationButton action="close" @closed="router.push({ name: 'accounts' })" :current-page-title="$t('title.showQRcode')" />
</VueFooter>
</div>
</template>

View File

@ -194,6 +194,7 @@
"test.info": "This is an info",
"test.success": "This is a success",
"test.warn": "This is a warn",
"title.about": "About",
"title.landing": "Landing",
"title.options": "Options",
@ -201,10 +202,42 @@
"title.reset_extension": "Reset extension",
"title.settings": "Settings",
"title.setup": "Setup",
"title.start": "Start",
"title.twofauth_webext": "2FAuth webext",
"title.unauthorized": "Unauthorized",
"title.unlock": "Unlock",
"title.start": "New account",
"title.capture": "Flash QR",
"title.accounts": "Accounts",
"title.createAccount": "Create account",
"title.importAccounts": "Import accounts",
"title.editAccount": "Account edit",
"title.showQRcode": "Account as QR code",
"title.groups": "Groups",
"title.createGroup": "Create group",
"title.editGroup": "Group edit",
"title.settings.options": "Options",
"title.settings.account": "User account",
"title.settings.oauth.tokens": "OAuth tokens",
"title.settings.oauth.generatePAT": "New personal token",
"title.settings.webauthn.editCredential": "Device edit",
"title.settings.webauthn.devices": "WebAuthn devices",
"title.login": "Login",
"title.register": "Register",
"title.autolock": "Auto lock",
"title.password.request": "Reset password",
"title.password.reset": "New password",
"title.webauthn.lost": "Account recovery",
"title.webauthn.recover": "Register a new device",
"title.flooded": "Flood",
"title.genericError": "Error",
"title.404": "Item not found",
"title.admin.appSetup": "App setup",
"title.admin.auth": "Authentication",
"title.admin.users": "Users management",
"title.admin.createUser": "Create user",
"title.admin.manageUser": "Manage user",
"title.admin.logs.access": "Access log",
"tooltip.hide_password": "Hide password",
"tooltip.lock": "Lock it",
"tooltip.reveal_password": "Reveal password",