users: fix avatar handling and various translations
This commit is contained in:
@@ -31,7 +31,6 @@ const id = ref('');
|
||||
const upstreamUri = ref('');
|
||||
const label = ref('');
|
||||
const tags = ref([]);
|
||||
const iconFile = ref(null);
|
||||
const iconUrl = ref('');
|
||||
const accessRestrictionOption = ref('');
|
||||
const accessRestriction = ref({
|
||||
@@ -53,8 +52,9 @@ const isValid = computed(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
let iconFile = null;
|
||||
function onIconChanged(file) {
|
||||
iconFile.value = file;
|
||||
iconFile = file;
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
@@ -73,10 +73,10 @@ async function onSubmit() {
|
||||
data.accessRestriction.groups = accessRestriction.value.groups.map(function (g) { return g.id; });
|
||||
}
|
||||
|
||||
if (iconFile.value === 'fallback') { // user reset the icon
|
||||
if (iconFile === 'fallback') { // user reset the icon
|
||||
data.icon = '';
|
||||
} else if (iconFile.value !== 'src') { // user loaded custom icon
|
||||
data.icon = (await getDataURLFromFile(iconFile.value)).replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
} else if (iconFile !== 'src') { // user loaded custom icon
|
||||
data.icon = (await getDataURLFromFile(iconFile)).replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
}
|
||||
|
||||
let error;
|
||||
@@ -118,7 +118,7 @@ defineExpose({
|
||||
upstreamUri.value = applink ? applink.upstreamUri : '';
|
||||
label.value = applink ? applink.label : '';
|
||||
iconUrl.value = applink ? applink.iconUrl : 'fallback';
|
||||
iconFile.value = applink?.iconUrl ? 'src' : 'fallback';
|
||||
iconFile = applink?.iconUrl ? 'src' : 'fallback';
|
||||
tags.value = applink ? applink.tags : [];
|
||||
accessRestrictionOption.value = applink && applink.accessRestriction ? 'groups' : 'any';
|
||||
accessRestriction.value = applink && applink.accessRestriction ? applink.accessRestriction : { users: [], groups: [] };
|
||||
|
||||
@@ -26,9 +26,15 @@ const internalSrc = ref('');
|
||||
const isChanged = ref(false);
|
||||
const busy = ref(false);
|
||||
|
||||
function reset() {
|
||||
isChanged.value = false;
|
||||
internalSrc.value = props.src;
|
||||
}
|
||||
|
||||
defineExpose({ reset });
|
||||
|
||||
watchEffect(() => {
|
||||
internalSrc.value = props.src;
|
||||
isChanged.value = false;
|
||||
});
|
||||
|
||||
function dataURLtoFile(dataURL, filename) {
|
||||
@@ -147,6 +153,8 @@ function onChanged(event) {
|
||||
|
||||
internalSrc.value = canvas.toDataURL('image/png');
|
||||
isChanged.value = true;
|
||||
|
||||
console.log('internalSrc is now some data url');
|
||||
emit('changed', file);
|
||||
};
|
||||
|
||||
@@ -180,6 +188,7 @@ function onError() {
|
||||
<Button @click.stop="onUnset" tool small icon="fa fa-trash" :loading="busy" :disabled="busy" v-if="unsetHandler && internalSrc !== fallbackSrc"/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Simple mode -->
|
||||
<template v-else-if="mode === 'simple'">
|
||||
<div class="image-picker-actions">
|
||||
<Button @click.stop="onEdit" tool small icon="fa fa-pencil-alt" :disabled="busy"/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { ref, useTemplateRef, inject } from 'vue';
|
||||
import { Dialog, TextInput, FormGroup, Checkbox, MultiSelect, SingleSelect } from '@cloudron/pankow';
|
||||
import { ROLES } from '../constants.js';
|
||||
import ImagePicker from '../components/ImagePicker.vue';
|
||||
@@ -21,8 +21,11 @@ const groupsModel = GroupsModel.create();
|
||||
const emit = defineEmits([ 'success' ]);
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
const imagePicker = useTemplateRef('imagePicker');
|
||||
const form = useTemplateRef('form');
|
||||
|
||||
const refreshProfile = inject('refreshProfile');
|
||||
|
||||
// also determines if new or edit mode
|
||||
const user = ref(null);
|
||||
const roles = ref([]);
|
||||
@@ -58,9 +61,9 @@ async function onReset2FA() {
|
||||
reset2FABusy.value = false;
|
||||
}
|
||||
|
||||
let newAvatarFile = null;
|
||||
function onAvatarSet(file) {
|
||||
newAvatarFile = file;
|
||||
let avatarFile = null;
|
||||
function onAvatarChanged(file) {
|
||||
avatarFile = file;
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
@@ -79,7 +82,7 @@ async function onSubmit() {
|
||||
let userId = user.value ? user.value.id : null;
|
||||
|
||||
// can only be set not updated
|
||||
if (!user.value) data.username = username.value || null;
|
||||
if (!user.value || !user.value.username) data.username = username.value || null;
|
||||
|
||||
const isExternal = user.value && user.value.source;
|
||||
|
||||
@@ -154,16 +157,22 @@ async function onSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
if (newAvatarFile) {
|
||||
const [error] = await usersModel.setAvatar(userId, newAvatarFile);
|
||||
if (error) {
|
||||
formError.value.generic = error.body ? error.body.message : 'Internal error';
|
||||
let avatarError = null;
|
||||
if (avatarFile === 'fallback') { // user reset the icon
|
||||
[avatarError] = await usersModel.unsetAvatar(userId);
|
||||
if (isSelf.value) await refreshProfile();
|
||||
} else if (avatarFile !== 'src') { // user loaded custom icon
|
||||
[avatarError] = await usersModel.setAvatar(userId, avatarFile);
|
||||
if (isSelf.value) await refreshProfile();
|
||||
}
|
||||
if (avatarError) {
|
||||
formError.value.generic = avatarError.body ? avatarError.body.message : 'Internal error';
|
||||
busy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
return console.error(avatarError);
|
||||
}
|
||||
|
||||
emit('success');
|
||||
|
||||
dialog.value.close();
|
||||
busy.value = false;
|
||||
}
|
||||
@@ -180,8 +189,8 @@ defineExpose({
|
||||
role.value = u ? u.role : ROLES.USER;
|
||||
sendInvite.value = false;
|
||||
active.value = u ? u.active : true;
|
||||
avatarUrl.value = u ? u.avatarUrl : '';
|
||||
newAvatarFile = null;
|
||||
avatarUrl.value = u ? `${u.avatarUrl}&ts=${Date.now()}` : ''; // there is already access token in query param
|
||||
avatarFile = u?.avatarUrl ? 'src' : 'fallback';
|
||||
|
||||
let [error, result] = await groupsModel.list();
|
||||
if (error) return console.error(error);
|
||||
@@ -209,6 +218,7 @@ defineExpose({
|
||||
profileLocked.value = result.profileLocked;
|
||||
external2FA.value = result.external2FA;
|
||||
|
||||
imagePicker.value.reset();
|
||||
dialog.value.open();
|
||||
}
|
||||
});
|
||||
@@ -237,15 +247,10 @@ defineExpose({
|
||||
<fieldset :disabled="busy">
|
||||
<input type="submit" style="display: none;" />
|
||||
|
||||
<div style="display: flex; gap: 20px">
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<div style="width: 80px;">
|
||||
<ImagePicker mode="simple" :src="avatarUrl" fallback-src="/img/avatar-default-symbolic.svg" :size="512" :save-handler="onAvatarSet" display-width="80px"/>
|
||||
<ImagePicker ref="imagePicker" mode="simple" :src="avatarUrl" fallback-src="/img/avatar-default-symbolic.svg" :size="512" @changed="onAvatarChanged" display-width="80px"/>
|
||||
</div>
|
||||
|
||||
<FormGroup style="flex-grow: 1">
|
||||
<label for="displayNameInput">{{ $t('users.user.fullName') }}</label>
|
||||
<TextInput id="displayNameInput" v-model="displayName" :disabled="(user && user.source) ? true : null" :placeholder="$t('users.user.displayNamePlaceholder')"/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
@@ -254,16 +259,24 @@ defineExpose({
|
||||
<div class="text-danger" v-if="formError.email">{{ formError.email }}</div>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="fallbackEmailInput">{{ $t('users.user.recoveryEmail') }} <sup><a href="https://docs.cloudron.io/profile/#password-recovery-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<TextInput id="fallbackEmailInput" v-model="fallbackEmail" :placeholder="$t('users.user.fallbackEmailPlaceholder')" />
|
||||
<!-- if profile edit is locked a user has to be set here . username is editable until one is set -->
|
||||
<FormGroup v-if="!user || !user.username" :has-error="formError.username">
|
||||
<label for="usernameInput">{{ $t('users.user.username') }}</label>
|
||||
<TextInput id="usernameInput" v-model="username" :required="profileLocked ? true : null" />
|
||||
<small class="helper-text">{{ profileLocked ? '' : $t('users.user.usernamePlaceholder') }}</small>
|
||||
<div class="text-danger" v-if="formError.username">{{ formError.username }}</div>
|
||||
</FormGroup>
|
||||
|
||||
<!-- if profile edit is locked a user has to be set here -->
|
||||
<FormGroup v-if="!user" :has-error="formError.username">
|
||||
<label for="usernameInput">{{ $t('users.user.username') }}</label>
|
||||
<TextInput id="usernameInput" v-model="username" :required="profileLocked ? true : null" :placeholder="profileLocked ? '' : $t('users.user.usernamePlaceholder')" />
|
||||
<div class="text-danger" v-if="formError.username">{{ formError.username }}</div>
|
||||
<FormGroup style="flex-grow: 1">
|
||||
<label for="displayNameInput">{{ $t('users.user.fullName') }}</label>
|
||||
<TextInput id="displayNameInput" v-model="displayName" :disabled="(user && user.source) ? true : null"/>
|
||||
<small v-if="!user || !user.username" class="helper-text">{{ $t('users.user.displayNamePlaceholder') }}</small> <!-- don't show if user has already signed up -->
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="fallbackEmailInput">{{ $t('users.user.recoveryEmail') }} <sup><a href="https://docs.cloudron.io/profile/#password-recovery-email" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<TextInput id="fallbackEmailInput" v-model="fallbackEmail" />
|
||||
<small class="helper-text">{{ $t('users.user.fallbackEmailPlaceholder') }}</small>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="profile.isAtLeastAdmin" :has-error="formError.role">
|
||||
|
||||
Reference in New Issue
Block a user