/
githubmirror
/
photoprism
Обзор
Документация
Войти
/
githubmirror
/
photoprism
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/src/component/settings/apps.vue
501 строка
15 KB
Michael Mayer
Frontend: Declare emits on dialog & service components #5505
28 апр 2026, 03:01
28 апр 2026, 03:01
199cf69
Код
Авторство
О чём код?
<template> <v-dialog ref="dialog" :model-value="visible" persistent max-width="610" class="p-dialog modal-dialog p-settings-apps" @keydown.esc.exact="close" @after-enter="afterEnter" @after-leave="afterLeave" > <v-form ref="form" validate-on="invalid-input" class="form-password" accept-charset="UTF-8" tabindex="-1" @submit.prevent> <v-card> <v-card-title class="d-flex justify-start align-center ga-3"> <v-icon v-if="action === 'add'" size="28" color="primary">mdi-plus</v-icon> <v-icon v-else-if="action === 'copy'" size="28" color="primary">mdi-shield-lock</v-icon> <v-icon v-else size="28" color="primary">mdi-cellphone-link</v-icon> <h6 class="text-h6">{{ $gettext(`Apps and Devices`) }}</h6> </v-card-title> <!-- Confirm --> <template v-if="confirmAction !== ''"> <v-card-text class="dense"> <v-row align="start" dense> <v-col cols="12" class="text-body-2"> {{ $gettext(`Enter your password to confirm the action and continue:`) }} </v-col> <v-col cols="12"> <v-text-field v-model="password" :disabled="busy" name="password" :type="showPassword ? 'text' : 'password'" :label="$gettext('Password')" hide-details autofocus autocorrect="off" autocapitalize="none" autocomplete="current-password" class="input-password text-monospace text-selectable" :append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'" prepend-inner-icon="mdi-lock" @click:append-inner="showPassword = !showPassword" @keyup.enter="onConfirm" ></v-text-field> </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="secondary-light" class="action-back" @click.stop="onBack"> {{ $gettext(`Back`) }} </v-btn> <v-btn variant="flat" color="highlight" :disabled="!password || password.length < 4" class="action-confirm" @click.stop="onConfirm"> {{ $gettext(`Continue`) }} </v-btn> </v-card-actions> </template> <!-- Copy --> <template v-else-if="action === 'copy'"> <v-card-text class="dense"> <v-row align="start" dense> <v-col cols="12" class="text-body-2"> {{ $gettext(`Please copy the following randomly generated app password and keep it in a safe place, as you will not be able to see it again:`) }} </v-col> <v-col cols="12"> <v-text-field v-model="appPassword" type="text" hide-details readonly autocorrect="off" autocapitalize="none" autocomplete="off" append-inner-icon="mdi-content-copy" class="input-app-password text-selectable cursor-copy" @click.stop="onCopyAppPassword" > </v-text-field> </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="button" class="action-close" @click.stop="close"> {{ $gettext(`Close`) }} </v-btn> <v-btn v-if="appPasswordCopied" variant="flat" color="highlight" :disabled="busy" class="action-done" @click.stop="onDone"> {{ $gettext(`Done`) }} </v-btn> <v-btn v-else variant="flat" color="highlight" class="action-copy" @click.stop="onCopyAppPassword"> {{ $gettext(`Copy`) }} </v-btn> </v-card-actions> </template> <!-- Add --> <template v-else-if="action === 'add'"> <v-card-text class="dense"> <v-row align="start" dense> <v-col cols="12" class="text-body-2"> {{ $gettext( `To generate a new app-specific password, please enter the name and authorization scope of the application and select an expiration date:` ) }} </v-col> <v-col cols="12"> <v-text-field v-model="app.client_name" :disabled="busy" name="client_name" type="text" :label="$gettext('Name')" autofocus hide-details autocorrect="off" autocapitalize="none" autocomplete="off" class="input-name text-selectable" ></v-text-field> </v-col> <v-col cols="12" sm="6"> <v-select v-model="app.scope" hide-details :disabled="busy" item-title="text" item-value="value" :items="auth.ScopeOptions()" :label="$gettext('Scope')" :menu-props="{ maxHeight: 346 }" class="input-scope" ></v-select> </v-col> <v-col cols="12" sm="6"> <v-select v-model="app.expires_in" :disabled="busy" :label="$gettext('Expires')" autocomplete="off" hide-details class="input-expires" item-title="text" item-value="value" :items="options.Expires()" ></v-select> </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="button" class="action-cancel" @click.stop="onCancel"> {{ $gettext(`Cancel`) }} </v-btn> <v-btn variant="flat" color="highlight" :disabled="app.client_name === '' || app.scope === ''" class="action-generate" @click.stop="onGenerate"> {{ $gettext(`Generate`) }} </v-btn> </v-card-actions> </template> <!-- Apps --> <template v-else> <v-card-text> <v-row align="start" no-gutters> <v-col cols="12"> <v-data-table v-model="selected" :headers="listColumns" :items="results" :items-per-page-options="[]" hide-default-footer item-key="ID" :no-data-text="$gettext('Nothing was found.')" density="compact" class="elevation-0 user-results list-view" > <template #item="props"> <tr :data-name="props.item.ClientName"> <td class="text-selectable text-break text-start"> {{ props.item.ClientName }} </td> <td class="text-start text-break hidden-xs" nowrap> {{ scopeInfo(props.item.AuthScope) }} </td> <td class="text-start text-break" nowrap> {{ formatDateTime(props.item.LastActive) }} </td> <td class="text-start hidden-sm-and-down" nowrap> {{ formatDate(props.item.Expires) }} </td> <td> <div class="table-actions"> <v-btn icon="mdi-delete" color="surface-variant" density="compact" variant="plain" :ripple="false" class="action-remove action-secondary" @click.stop.prevent="onRevoke(props.item)" ></v-btn> </div> </td> </tr> </template> </v-data-table> </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="button" class="action-close" @click.stop="close"> {{ $gettext(`Close`) }} </v-btn> <v-btn variant="flat" color="highlight" class="action-add" @click.stop="onAdd"> {{ $gettext(`Add`) }} </v-btn> </v-card-actions> </template> </v-card> </v-form> <p-confirm-dialog :visible="revoke.dialog" icon="mdi-delete-outline" @close="revoke.dialog = false" @confirm="onRevoked"></p-confirm-dialog> </v-dialog> </template> <script> import User from "model/user"; import * as auth from "options/auth"; import * as options from "options/options"; import { DateTime } from "luxon"; import memoizeOne from "memoize-one"; import PConfirmDialog from "component/confirm/dialog.vue"; export default { name: "PSettingsApps", components: { PConfirmDialog, }, props: { visible: { type: Boolean, default: false, }, model: { type: Object, default: () => new User(null), }, }, emits: ["close"], data() { return { auth, options, busy: false, isDemo: this.$config.get("demo"), isPublic: this.$config.get("public"), password: "", showPassword: false, minLength: this.$config.get("passwordLength"), maxLength: 72, timeZone: this.$config.getTimeZone(), rtl: this.$isRtl, action: "", confirmAction: "", user: this.$session.getUser(), results: [], selected: [], app: { client_name: "", scope: "*", expires_in: 0, }, revoke: { token: "", dialog: false, }, appPassword: "", appPasswordCopied: false, listColumns: [ { title: this.$gettext("Name"), key: "ID", sortable: false, align: "left" }, { title: this.$gettext("Scope"), headerProps: { class: "hidden-xs", }, key: "AuthScope", sortable: false, align: "left", }, { title: this.$gettext("Last Used"), headerProps: { class: "text-no-wrap", }, key: "LastActive", sortable: false, align: "left", }, { title: this.$gettext("Expires"), headerProps: { class: "hidden-sm-and-down", }, key: "Expires", sortable: false, align: "left", mobile: false, }, { title: "", key: "", sortable: false, align: "right" }, ], }; }, watch: { visible: function (show) { if (show) { this.reset(); this.find(); } }, }, created() { if (this.isPublic && !this.isDemo) { this.$emit("close"); } }, methods: { afterEnter() { this.$view.enter(this); }, afterLeave() { this.$view.leave(this); }, onCopyAppPassword() { // Use the browser API to copy the app password to the clipboard. this.$util.copyText(this.appPassword); // Flag the password as copied to the clipboard even // if the copyText() function returns an error. this.appPasswordCopied = true; }, formatDate(d) { if (!d) { return "–"; } if (!Number.isInteger(d)) { return DateTime.fromISO(d, { zone: this.timeZone }).toLocaleString(DateTime.DATE_SHORT); } else if (d <= 0) { return "–"; } return DateTime.fromSeconds(d, { zone: this.timeZone }).toLocaleString(DateTime.DATE_SHORT); }, formatDateTime(d) { if (!d) { return "–"; } if (!Number.isInteger(d)) { return DateTime.fromISO(d, { zone: this.timeZone }).toLocaleString(DateTime.DATETIME_SHORT); } else if (d <= 0) { return "–"; } return DateTime.fromSeconds(d, { zone: this.timeZone }).toLocaleString(DateTime.DATETIME_SHORT); }, scopeInfo(s) { let info = memoizeOne(auth.Scopes)()[s]; if (info) { return info; } return s; }, reset(action) { if (!action) { action = "apps"; } this.app = { client_name: "", scope: "*", expires_in: 0, }; this.action = action; this.confirmAction = ""; this.appPasswordCopied = false; this.revoke.token = ""; this.revoke.dialog = false; }, onConfirm() { if (this.busy) { return; } switch (this.confirmAction) { case "onGenerate": this.onGenerate(); } }, onDone() { if (this.busy) { return; } this.appPassword = ""; this.reset(); this.find(); }, onCancel() { if (this.busy) { return; } this.reset(); }, onBack() { if (this.busy) { return; } this.confirmAction = ""; }, onAdd() { if (this.busy) { return; } this.action = "add"; this.confirmAction = ""; }, onRevoke(app) { if (this.busy) { return; } this.revoke.token = app.ID; this.revoke.dialog = true; }, onRevoked() { if (this.busy || !this.revoke.token) { return; } this.busy = true; this.$session .deleteApp(this.revoke.token) .then(() => { this.$notify.info(this.$gettext("Successfully deleted")); this.revoke.token = ""; this.find(); this.revoke.dialog = false; this.busy = false; }) .catch(() => { this.busy = false; }); }, onGenerate() { if (this.busy) { return; } if (this.confirmAction === "" && this.$session.provider !== "oidc") { this.confirmAction = "onGenerate"; return; } this.busy = true; this.$session .createApp(this.app.client_name, this.app.scope, this.app.expires_in, this.password) .then((app) => { this.appPassword = app.access_token; this.reset("copy"); }) .catch(() => { this.action = "add"; this.confirmAction = ""; }) .finally(() => { this.busy = false; }); }, find() { this.$notify.blockUI("busy"); this.model .findApps() .then((resp) => { this.results = resp; }) .finally(() => { this.$notify.unblockUI(); }); }, close() { if (this.busy) { return; } this.appPassword = ""; this.$emit("close"); }, }, }; </script>