Files
cloudron-box/dashboard/src/components/ApplinkDialog.vue
2026-03-05 11:40:50 +01:00

213 lines
7.4 KiB
Vue

<script setup>
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
import { ref, useTemplateRef } from 'vue';
import { Dialog, FormGroup, InputDialog, MultiSelect, Radiobutton, TagInput, TextInput } from '@cloudron/pankow';
import { API_ORIGIN } from '../constants.js';
import { getDataURLFromFile } from '../utils.js';
import ImagePicker from './ImagePicker.vue';
import ApplinksModel from '../models/ApplinksModel.js';
import UsersModel from '../models/UsersModel.js';
import GroupsModel from '../models/GroupsModel.js';
const applinksModel = ApplinksModel.create();
const usersModel = UsersModel.create();
const groupsModel = GroupsModel.create();
const emits = defineEmits(['success']);
const imagePicker = useTemplateRef('imagePicker');
const applinkDialog = useTemplateRef('applinkDialog');
const inputDialog = useTemplateRef('inputDialog');
const users = ref([]);
const groups = ref([]);
const busy = ref(false);
const error = ref({});
const mode = ref('');
const id = ref('');
const upstreamUri = ref('');
const label = ref('');
const tags = ref([]);
const iconUrl = ref('');
const accessRestrictionOption = ref('');
const accessRestriction = ref({
users: [],
groups: [],
});
let iconFile = 'src';
function onIconChanged(file) {
iconFile = file;
}
const form = useTemplateRef('form');
const isFormValid = ref(false);
function checkValidity() {
isFormValid.value = form.value ? form.value.checkValidity() : false;
if (isFormValid.value) {
try {
new URL(upstreamUri.value);
// eslint-disable-next-line no-unused-vars
} catch (e) {
isFormValid.value = false;
}
}
}
async function onSubmit() {
if (!form.value.reportValidity()) return;
busy.value = true;
const data = {
upstreamUri: upstreamUri.value,
tags: tags.value,
};
if (label.value) data.label = label.value;
data.accessRestriction = null;
if (accessRestrictionOption.value === 'groups') {
data.accessRestriction = { users: [], groups: [] };
data.accessRestriction.users = accessRestriction.value.users;
data.accessRestriction.groups = accessRestriction.value.groups;
}
if (iconFile === 'fallback') { // user reset the icon
data.icon = '';
} else if (iconFile !== 'src') { // user loaded custom icon
data.icon = (await getDataURLFromFile(iconFile)).replace(/^data:image\/[a-z]+;base64,/, '');
}
let error;
if (mode.value === 'edit') [error] = await applinksModel.update(id.value, data);
else [error] = await applinksModel.add(data);
if (error) {
busy.value = false;
return console.error(error);
}
emits('success');
applinkDialog.value.close();
busy.value = false;
}
async function onRemove() {
const yes = await inputDialog.value.confirm({
message: `Really remove applink?`,
confirmStyle: 'danger',
confirmLabel: t('main.action.remove'),
rejectLabel: t('main.dialog.cancel'),
rejectStyle: 'secondary'
});
if (!yes) return;
const [error] = await applinksModel.remove(id.value);
if (error) return console.error(error);
emits('success');
applinkDialog.value.close();
}
defineExpose({
open: async function (applink) {
mode.value = applink ? 'edit' : 'new';
id.value = applink ? applink.id : '';
upstreamUri.value = applink ? applink.upstreamUri : '';
label.value = applink ? applink.label : '';
iconUrl.value = applink ? applink.iconUrl : '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: [] };
// fetch users and groups
let [error, result] = await usersModel.list();
if (error) return console.error(error);
result.forEach(u => { u.label = u.username || u.email; });
users.value = result;
[error, result] = await groupsModel.list();
if (error) return console.error(error);
groups.value = result;
applinkDialog.value.open();
setTimeout(checkValidity, 100); // update state of the confirm button
}
});
</script>
<template>
<Dialog ref="applinkDialog"
:title="mode === 'edit' ? $t('app.editApplinkDialog.title') : $t('app.addApplinkDialog.title')"
:alternate-label="mode === 'edit' ? 'Delete' : ''"
alternate-style="danger"
:reject-label="$t('main.dialog.cancel')"
reject-style="secondary"
:confirm-label="mode === 'edit' ? $t('main.dialog.save') : $t('main.action.add')"
:confirm-active="!busy && isFormValid"
:confirm-busy="busy"
@confirm="onSubmit()"
@alternate="onRemove()"
>
<InputDialog ref="inputDialog" />
<form @submit.prevent="onSubmit()" autocomplete="off" ref="form" @input="checkValidity()">
<fieldset :disabled="busy">
<input style="display: none;" type="submit" />
<p class="has-error" v-show="error.generic">{{ error.generic }}</p>
<FormGroup :class="{ 'has-error': error.upstreamUri }">
<label for="applinkUpstreamUri">{{ $t('app.applinks.upstreamUri') }}</label>
<TextInput id="applinkUpstreamUri" v-model="upstreamUri" required />
<span class="text-danger" v-show="error.upstreamUri">{{ error.upstreamUri }}</span>
</FormGroup>
<FormGroup>
<label for="applinkLabel">{{ $t('app.applinks.label') }}</label>
<TextInput id="applinkLabel" v-model="label" />
</FormGroup>
<div>
<label>{{ $t('app.display.icon') }}</label>
<ImagePicker ref="imagePicker" mode="simple" :src="iconUrl" :fallback-src="`${API_ORIGIN}/img/appicon_fallback.png`" @changed="onIconChanged" :size="512" display-height="80px" style="width: 80px"/>
</div>
<FormGroup>
<label for="applinkTags">{{ $t('app.display.tags') }}</label>
<TagInput id="applinkTags" v-model="tags" v-tooltip="$t('app.display.tagsTooltip')" />
<small class="helper-text">{{ $t('app.display.tagsPlaceholder') }}</small>
</FormGroup>
<FormGroup>
<label>{{ $t('app.accessControl.userManagement.dashboardVisibility') }} <sup><a href="https://docs.cloudron.io/apps/#no-sso" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<Radiobutton v-model="accessRestrictionOption" value="any" :label="$t('app.accessControl.userManagement.visibleForAllUsers')"/>
<Radiobutton v-model="accessRestrictionOption" value="groups" :label="$t('app.accessControl.userManagement.visibleForSelected')"/>
<!-- <span class="label label-danger"v-show="accessRestrictionOption === 'groups' && !isAccessRestrictionValid(applinkDialogData)">{{ $t('appstore.installDialog.errorUserManagementSelectAtLeastOne') }}</span> -->
</FormGroup>
<div v-if="accessRestrictionOption === 'groups'">
<div style="margin-left: 20px; display: flex;">
<div>
{{ $t('appstore.installDialog.users') }}: <MultiSelect v-model="accessRestriction.users" :options="users" option-key="id" option-label="label" :search-threshold="20" />
</div>
<div>
{{ $t('appstore.installDialog.groups') }}: <MultiSelect v-model="accessRestriction.groups" :options="groups" option-key="id" option-label="name" :search-threshold="20" />
</div>
</div>
</div>
</fieldset>
</form>
</Dialog>
</template>