2025-03-22 10:19:55 +01:00
|
|
|
<script setup>
|
2025-01-03 15:06:41 +01:00
|
|
|
|
2025-03-22 10:19:55 +01:00
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
const i18n = useI18n();
|
|
|
|
|
const t = i18n.t;
|
|
|
|
|
|
|
|
|
|
import { computed, ref, useTemplateRef } from 'vue';
|
2025-01-09 18:29:00 +01:00
|
|
|
import { Dialog, FormGroup, InputDialog, MultiSelect, Radiobutton, TagInput, TextInput } from 'pankow';
|
2025-03-03 11:22:56 +01:00
|
|
|
import { API_ORIGIN } from '../constants.js';
|
2025-03-22 10:19:55 +01:00
|
|
|
import ImagePicker from './ImagePicker.vue';
|
2025-01-03 15:06:41 +01:00
|
|
|
import ApplinksModel from '../models/ApplinksModel.js';
|
|
|
|
|
import UsersModel from '../models/UsersModel.js';
|
|
|
|
|
import GroupsModel from '../models/GroupsModel.js';
|
|
|
|
|
|
2025-01-31 21:02:48 +01:00
|
|
|
const applinksModel = ApplinksModel.create();
|
|
|
|
|
const usersModel = UsersModel.create();
|
|
|
|
|
const groupsModel = GroupsModel.create();
|
2025-01-03 15:06:41 +01:00
|
|
|
|
2025-03-22 10:19:55 +01:00
|
|
|
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 iconFile = ref(null); // if set to '' we will reset
|
|
|
|
|
const iconUrl = ref('');
|
|
|
|
|
const accessRestrictionOption = ref('');
|
|
|
|
|
const accessRestriction = ref({
|
|
|
|
|
users: [],
|
|
|
|
|
groups: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const isValid = computed(() => {
|
|
|
|
|
if (busy.value) return false;
|
|
|
|
|
if (!upstreamUri.value) return false;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
new URL(upstreamUri.value);
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function getDataURLFromFile(file) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
|
|
|
|
reader.onload = function(event) {
|
|
|
|
|
resolve(event.target.result);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
reader.onerror = function(event) {
|
|
|
|
|
reject(event.target.error);
|
2025-01-03 15:06:41 +01:00
|
|
|
};
|
2025-03-22 10:19:55 +01:00
|
|
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onIconChanged(file) {
|
|
|
|
|
iconFile.value = file;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onResetIcon() {
|
|
|
|
|
iconFile.value = '';
|
|
|
|
|
imagePicker.value.clear(`${API_ORIGIN}/img/appicon_fallback.png`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onSubmit() {
|
|
|
|
|
busy.value = true;
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
label: label.value,
|
|
|
|
|
upstreamUri: upstreamUri.value,
|
|
|
|
|
tags: tags.value,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
data.accessRestriction = null;
|
|
|
|
|
if (accessRestrictionOption.value === 'groups') {
|
|
|
|
|
data.accessRestriction = { users: [], groups: [] };
|
|
|
|
|
data.accessRestriction.users = accessRestriction.value.users.map(function (u) { return u.id; });
|
|
|
|
|
data.accessRestriction.groups = accessRestriction.value.groups.map(function (g) { return g.id; });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (iconFile.value === '') { // user reset the icon
|
|
|
|
|
data.icon = '';
|
|
|
|
|
} else if (iconFile.value) { // user loaded custom icon
|
|
|
|
|
data.icon = (await getDataURLFromFile(iconFile.value)).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();
|
|
|
|
|
|
|
|
|
|
// clear this to retrigger ImagePicker loading
|
|
|
|
|
iconUrl.value = '';
|
|
|
|
|
busy.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onRemove() {
|
|
|
|
|
const yes = await inputDialog.value.confirm({
|
|
|
|
|
message: `Really remove applink?`,
|
|
|
|
|
confirmStyle: 'danger',
|
|
|
|
|
confirmLabel: t('main.dialog.yes'),
|
|
|
|
|
rejectLabel: t('main.dialog.cancel')
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 : '';
|
2025-03-22 10:22:42 +01:00
|
|
|
iconUrl.value = applink ? applink.iconUrl : 'fallback';
|
2025-03-22 10:19:55 +01:00
|
|
|
iconFile.value = null;
|
|
|
|
|
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);
|
|
|
|
|
users.value = result;
|
|
|
|
|
|
|
|
|
|
[error, result] = await groupsModel.list();
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
groups.value = result;
|
|
|
|
|
|
|
|
|
|
applinkDialog.value.open();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-01-03 15:06:41 +01:00
|
|
|
|
|
|
|
|
</script>
|
2025-01-19 12:00:22 +01:00
|
|
|
|
|
|
|
|
<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')"
|
2025-03-02 17:49:17 +01:00
|
|
|
reject-style="secondary"
|
2025-01-19 12:00:22 +01:00
|
|
|
:confirm-label="$t('main.dialog.save')"
|
|
|
|
|
:confirm-active="isValid"
|
|
|
|
|
:confirm-busy="busy"
|
|
|
|
|
@confirm="onSubmit()"
|
|
|
|
|
@alternate="onRemove()"
|
|
|
|
|
>
|
|
|
|
|
<InputDialog ref="inputDialog" />
|
|
|
|
|
|
2025-03-02 17:17:56 +01:00
|
|
|
<form @submit.prevent="onSubmit()" autocomplete="off">
|
2025-01-19 12:00:22 +01:00
|
|
|
<fieldset :disabled="busy">
|
|
|
|
|
<input style="display: none;" type="submit" :disabled="!isValid" />
|
|
|
|
|
|
|
|
|
|
<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>
|
2025-03-22 10:19:55 +01:00
|
|
|
<label for="previewIcon">{{ $t('app.display.icon') }}</label>
|
|
|
|
|
<ImagePicker ref="imagePicker" v-if="iconUrl" :src="iconUrl" :fallback-src="`${API_ORIGIN}/img/appicon_fallback.png`" @changed="onIconChanged" size="512" display-height="80px"/>
|
|
|
|
|
<span class="actionable" @click="onResetIcon()">{{ $t('app.applinks.clearIconAction') }}</span> - <span class="text-small">{{ $t('app.applinks.clearIconDescription') }}</span>
|
2025-01-19 12:00:22 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label for="applinkTags">{{ $t('app.display.tags') }}</label>
|
|
|
|
|
<TagInput id="applinkTags" :placeholder="$t('app.display.tagsPlaceholder')" v-model="tags" v-tooltip="$t('app.display.tagsTooltip')" />
|
|
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label>{{ $t('app.accessControl.userManagement.dashboardVisibility') }} <sup><a href="https://docs.cloudron.io/apps/#dashboard-visibility" 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-label="username" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
{{ $t('appstore.installDialog.groups') }}: <MultiSelect v-model="accessRestriction.groups" :options="groups" option-label="name" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</fieldset>
|
|
|
|
|
</form>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</template>
|