diff --git a/.eslintrc.js b/.eslintrc.js index ba46a82a308290fb6bcaae957b77f0f22cb63498..5fea712a1c54b99da56a1ef7e496477520919bc2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,7 +24,8 @@ module.exports = { "error", { "allowWholeFile": true } ], - "@typescript-eslint/no-empty-interface": 1, // empty Interfaces will be only warnings for now. + // ToDo: REMOVE ONCE error AND pid MODULE'S STORE STATES ARE IMPLEMENTED + "@typescript-eslint/no-empty-interface": 0, // Empty Interfaces error/warning will be ignored for now. "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], // will only ignore variables that start with an underscore _ "vue/multi-word-component-names": "off" }, diff --git a/src/App.vue b/src/App.vue index 6d591d9f8587e3f0194b984206d94ecf2cd86cb2..318fd7e27dbe536a452f4597cef7da30a40cc05e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -75,4 +75,12 @@ export default defineComponent({ }); </script> -<style scoped></style> +<style> +.h-divider { + margin-top: 5px; + margin-bottom: 10px; + height: 1px; + width: 100%; + border-top: 1px solid #bebbbb; +} +</style> diff --git a/src/modules/admin/AdminModule.vue b/src/modules/admin/AdminModule.vue index b58932377881d2930f291f7748ee316f5cd7565b..9ac2177af6e705852d0418d2a2ae7dcdcd7a3c4f 100644 --- a/src/modules/admin/AdminModule.vue +++ b/src/modules/admin/AdminModule.vue @@ -1,6 +1,6 @@ <template> <div> - <router-view v-if="moduleIsReady" /> + <router-view v-if="true" /> </div> </template> @@ -22,7 +22,7 @@ export default defineComponent({ computed: { moduleIsReady(): boolean { - return true; + return this.adminStore.project !== null; }, }, @@ -32,8 +32,9 @@ export default defineComponent({ methods: { async initialize() { - // do initialization stuff (e.g. API calls, element loading, etc.) - // ... + // await Promise.all([ + // //this.adminStore.retrieveProjectQuotas(this.adminStore.project.projectGuid), + // ]) }, }, }); diff --git a/src/modules/admin/components/QuotaTable.vue b/src/modules/admin/components/QuotaTable.vue new file mode 100644 index 0000000000000000000000000000000000000000..d5bc6e84b88b0f42c7494f8a98bc051891356e75 --- /dev/null +++ b/src/modules/admin/components/QuotaTable.vue @@ -0,0 +1,271 @@ +<template> + <div id="quotatable"> + <CoscineHeadline :headline="$t('page.admin.projectQuotaHeadline')" /> + + <!-- Filter Hidden Resources --> + <b-form-checkbox + v-if="project" + v-model="showEnabledResources" + class="mt-3 mb-1" + switch + > + {{ $t("page.admin.displayHiddenResources") }} + </b-form-checkbox> + + <!-- QuotaTable --> + <b-table + id="project_quota_table" + :fields="headers" + :items="filteredProjectQuotas" + :busy="!project || isWaitingForResponse" + :locale="$i18n.locale" + :sort-by.sync="sortBy" + :sort-desc="false" + :show-empty="true" + :empty-text="$t('page.admin.projectNotSelected')" + fixed + sticky-header="100%" + no-border-collapse + sort-icon-right + striped + bordered + outlined + hover + head-variant="dark" + > + <!-- Quota Table - Resource Type Column --> + <template #cell(resourceType)="row"> + {{ + row.item.iDisplayName ? row.item.iDisplayName : row.item.resourceType + }} + </template> + + <!-- Quota Table - Reserved Quota Column --> + <template #cell(currentQuota)="row"> + {{ + row.item.isQuotaAvailable === false + ? $t("default.none") + : $t("page.admin.gb", { number: row.item.quota }) + }} + </template> + + <!-- Quota Table - Resource Type Column --> + <template #cell(allocatedQuota)="row"> + {{ + row.item.isQuotaAvailable === false + ? $t("default.none") + : $t("page.admin.gb", { number: row.item.allocated }) + }} + </template> + + <!-- Quota Table - New Quota Column --> + <template #cell(newQuota)="row"> + <b-form-input + v-if="row.item.isQuotaAvailable !== false" + v-model.number="newQuotas[row.item.quotaId]" + type="number" + aria-describedby="projectHelp" + :placeholder="$t('page.admin.newQuotaInputPlaceHolder')" + > + </b-form-input> + </template> + + <!-- Quota Table - Action Column --> + <template #cell(action)="row"> + <div class="text-center"> + <b-button + v-if="row.item.isQuotaAvailable !== false" + :id="`action${row.index}`" + variant="primary" + :disabled=" + !newQuotas[row.item.quotaId] || newQuotas[row.item.quotaId] < 0 + " + @click.stop.prevent=" + saveNewQuota(row.item.quotaId, row.item.iDisplayName) + " + > + {{ $t("buttons.save") }} + </b-button> + </div> + </template> + + <!-- Quota Table - Header Row --> + <template #head()="header"> + <span :id="header.label"> + {{ header.label }} + <b-icon v-if="header.field.hint" icon="info-circle" /> + </span> + <!-- Hint Tooltip --> + <b-tooltip + v-if="header.field.hint" + :target="header.label" + triggers="hover" + boundary="viewport" + placement="bottom" + variant="light" + > + {{ header.field.hint }} + </b-tooltip> + </template> + </b-table> + </div> +</template> + +<script lang="ts"> +import { defineComponent } from "vue-demi"; +// import the store for current module +import useAdminStore from "../store"; +// import the main store + +import type { ProjectObject } from "@coscine/api-client/dist/types/Coscine.Api.Admin"; +import type { UpdateQuotaParameterObject } from "@coscine/api-client/dist/types/Coscine.Api.Admin"; +import useResourceStore from "@/modules/resource/store"; +import type { ExtendedProjectQuotaObject } from "../types"; +import { isNumber } from "lodash"; +import useNotificationStore from "@/store/notification"; + +export default defineComponent({ + setup() { + const adminStore = useAdminStore(); + const notificationStore = useNotificationStore(); + const resourceStore = useResourceStore(); + + return { adminStore, notificationStore, resourceStore }; + }, + data() { + return { + newQuotas: {} as { [id: string]: number }, + sortBy: "iDisplayName", + isWaitingForResponse: false, + showEnabledResources: true, + }; + }, + + computed: { + project(): ProjectObject | null { + return this.adminStore.project; + }, + projectQuotas(): ExtendedProjectQuotaObject[] | null { + if (this.project) { + // Retrieve all project quotas (incl. for hidden resources) + const projectQuotas = this.project.quotas; + // Get only enabled resource types + const enabledResourceTypes = this.resourceStore.resourceTypes; + if (projectQuotas && enabledResourceTypes) { + let projectQuotasExtended = [] as ExtendedProjectQuotaObject[]; + projectQuotas.forEach((entry) => { + // Find corresponding entry from enabled resource types + const enabledResource = enabledResourceTypes.find( + (q) => q.displayName === entry.resourceType + ); + let extended = { iDisplayName: "" } as ExtendedProjectQuotaObject; + Object.assign(extended, entry); + // Replace name with pretty resource name from i18n + extended.iDisplayName = this.$t( + `resourceTypes.${entry.resourceType}.displayName` + ).toString(); + extended.isEnabled = enabledResource + ? enabledResource.isEnabled + : undefined; + extended.isQuotaAvailable = enabledResource + ? enabledResource.isQuotaAvailable + : undefined; + // Push to the newly filtered array + projectQuotasExtended.push(extended); + }); + return projectQuotasExtended; + } else { + return projectQuotas as ExtendedProjectQuotaObject[]; + } + } else { + return null; + } + }, + filteredProjectQuotas(): ExtendedProjectQuotaObject[] | null { + if (this.showEnabledResources && this.projectQuotas) { + return this.projectQuotas.filter((entry) => entry.isEnabled); + } else { + return this.projectQuotas; + } + }, + headers() { + // Define as computed property to have table + // header text react on language changes. + return [ + { + label: this.$t("page.admin.headers.resourceType"), + key: "resourceType", + sortable: true, + }, + { + label: this.$t("page.admin.headers.currentQuota"), + key: "currentQuota", + sortable: true, + hint: this.$t("page.admin.headers.currentQuotaHint"), + }, + { + label: this.$t("page.admin.headers.allocatedQuota"), + key: "allocatedQuota", + sortable: true, + hint: this.$t("page.admin.headers.allocatedQuotaHint"), + }, + { + label: this.$t("page.admin.headers.newQuota"), + key: "newQuota", + sortable: false, + hint: this.$t("page.admin.headers.newQuotaHint"), + }, + { + label: this.$t("page.admin.headers.action"), + key: "action", + sortable: false, + }, + ]; + }, + }, + + created() { + // Load list of Enabled Resource Types if not present + if (this.resourceStore.resourceTypes === null) { + this.resourceStore.retrieveResourceTypes(); + } + }, + + methods: { + async saveNewQuota(quotaId: string, resourceDisplayName: string) { + const updatedQuota: UpdateQuotaParameterObject = { + quotaId: quotaId, + quota: this.newQuotas[quotaId], + }; + if (isNumber(updatedQuota.quota) && updatedQuota.quota >= 0) { + this.isWaitingForResponse = true; + const success = await this.adminStore.updateProjectQuota(updatedQuota); + if (success) { + // On Success + delete this.newQuotas[quotaId]; + if (this.project && this.project.guid) { + // Refresh the quota values + await this.adminStore.retrieveProjectQuotas(this.project.guid); + } + this.notificationStore.postNotification({ + title: this.$t("page.admin.toast.success.title").toString(), + body: this.$t("page.admin.toast.success.body", { + resourceType: resourceDisplayName, + projectName: this.project?.name, + newQuota: updatedQuota.quota, + }).toString(), + }); + } else { + // On Failure + this.notificationStore.postNotification({ + title: this.$t("page.admin.toast.fail.title").toString(), + body: this.$t("page.admin.toast.fail.body").toString(), + variant: "warning", + }); + } + this.isWaitingForResponse = false; + } + }, + }, +}); +</script> diff --git a/src/modules/admin/i18n/de.ts b/src/modules/admin/i18n/de.ts index 3b562529948ccd64874130f0624ea6c24903ce35..036e6a1003a60d6329b82de83b93aeec6718b04e 100644 --- a/src/modules/admin/i18n/de.ts +++ b/src/modules/admin/i18n/de.ts @@ -1,15 +1,58 @@ import VueI18n from "vue-i18n"; export default { - /* - -------------------------------------------------------------------------------------- - GERMAN STRINGS - -------------------------------------------------------------------------------------- - */ page: { admin: { - title: "Adminseite", - description: "Das ist die @:page.admin.title des Coscine UIv2 Apps", + headline: "Adminseite", + + projectInputPlaceholder: "Projekt-GUID oder -Slug eingeben", + + projectFound: "Projekt gefunden", + projectNotSelected: "Kein Projekt ausgewählt", + + form: { + labelSymbol: ":", + + projectName: "Projektname", + projectNameLabel: + "@:(page.admin.form.projectName)@:(page.admin.form.labelSymbol)", + + projectShortName: "Anzeigename", + projectShortNameLabel: + "@:(page.admin.form.projectShortName)@:(page.admin.form.labelSymbol)", + + projectGuid: "GUID", + projectGuidLabel: + "@:(page.admin.form.projectGuid)@:(page.admin.form.labelSymbol)", + }, + projectQuotaHeadline: "Quota", + displayHiddenResources: "Nur aktivierte Ressourcentypen anzeigen", + + headers: { + resourceType: "Ressourcentyp", + currentQuota: "Aktuelles Projekt Reserviertes Quota", + currentQuotaHint: + "Dieser Wert gibt die Speicherobergrenze an, die für den ausgewählten Ressourcentyp im Projekt verfügbar ist.", + allocatedQuota: "Reservierte Gesamtquota für Ressourcen", + allocatedQuotaHint: + "Dieser Wert gibt den aktuellen Speicherplatz an, der von allen vorhandenen Ressourcen desselben Ressourcentyps reserviert wird.", + newQuota: "Neue Projektquota", + newQuotaHint: + 'Mit diesem Wert wird "@:(page.admin.headers.currentQuota)" angepasst', + action: "Aktion", + }, + newQuotaInputPlaceHolder: "Quota in GB angeben", + gb: "{number} GB", + toast: { + success: { + title: "Quota erfolgreich geändert", + body: "{resourceType} Quota für Projekt {projectName} gesetzt auf {newQuota} GB", + }, + fail: { + title: "Aktualisierung fehlgeschlagen", + body: "Aktualisierung der Quota fehlgeschlagen.", + }, + }, }, }, } as VueI18n.LocaleMessageObject; diff --git a/src/modules/admin/i18n/en.ts b/src/modules/admin/i18n/en.ts index 55dc51af3220b6d6a3b28a8c29226fe6ad33fd74..c43f68768933308975a952f22a259e106f05513d 100644 --- a/src/modules/admin/i18n/en.ts +++ b/src/modules/admin/i18n/en.ts @@ -1,15 +1,58 @@ import VueI18n from "vue-i18n"; export default { - /* - -------------------------------------------------------------------------------------- - ENGLISH STRINGS - -------------------------------------------------------------------------------------- - */ page: { admin: { - title: "Admin Page", - description: "This is the @:page.admin.title for the Coscine UIv2 App", + headline: "Quota Admin Panel", + + projectInputPlaceholder: "Enter a project's Slug or GUID", + + projectFound: "Project found", + projectNotSelected: "No project selected", + + form: { + labelSymbol: ":", + + projectName: "Project Name", + projectNameLabel: + "@:(page.admin.form.projectName)@:(page.admin.form.labelSymbol)", + + projectShortName: "Display Name", + projectShortNameLabel: + "@:(page.admin.form.projectShortName)@:(page.admin.form.labelSymbol)", + + projectGuid: "GUID", + projectGuidLabel: + "@:(page.admin.form.projectGuid)@:(page.admin.form.labelSymbol)", + }, + projectQuotaHeadline: "Quotas", + displayHiddenResources: "Display enabled Resource Types only", + + headers: { + resourceType: "Resource Type", + currentQuota: "Current Project Reserved Quota", + currentQuotaHint: + "This value indicates the storage upper limit, that is available for the selected resource type in the project.", + allocatedQuota: "Total Resource Reserved Quota", + allocatedQuotaHint: + "This value indicates the current storage reserved by all existing resources of the same resource type.", + newQuota: "New Project Quota", + newQuotaHint: + 'This value will change the "@:(page.admin.headers.currentQuota)"', + action: "Action", + }, + newQuotaInputPlaceHolder: "Enter Quota in GB", + gb: "{number} GB", + toast: { + success: { + title: "Quota successfully changed", + body: "{resourceType} quota for project {projectName} set to {newQuota} GB", + }, + fail: { + title: "Quota update failed", + body: "Updating quota for the selected resource type failed.", + }, + }, }, }, } as VueI18n.LocaleMessageObject; diff --git a/src/modules/admin/pages/Admin.vue b/src/modules/admin/pages/Admin.vue index 37f4a8604764916be343e65fae3959832935a40d..dbc73a84659720a9f48484aa822d7318b5f2dfa7 100644 --- a/src/modules/admin/pages/Admin.vue +++ b/src/modules/admin/pages/Admin.vue @@ -1,16 +1,75 @@ <template> - <div> - <section - class="container flex flex-col items-center px-5 py-12 mx-auto text-gray-600 body-font md:flex-row" + <div id="admin"> + <CoscineHeadline :headline="$t('page.admin.headline')" /> + <!-- Project input Tooltip --> + <b-form id="project_request_form" @submit.stop.prevent="queryProject()"> + <b-input-group class="mt-3"> + <b-form-input + v-model="projectString" + :placeholder="$t('page.admin.projectInputPlaceholder')" + > + </b-form-input> + <!-- Select Button --> + <b-input-group-append> + <b-button variant="primary" @click="queryProject()"> + {{ $t("buttons.submit") }} + </b-button> + </b-input-group-append> + </b-input-group> + </b-form> + <!-- Project Query Status --> + <span class="d-block h6 my-3"> + {{ + project && project.guid + ? $t("page.admin.projectFound") + : $t("page.admin.projectNotSelected") + }} + </span> + <!-- Project Container --> + <!-- Project Name --> + <CoscineFormGroup + label-for="ProjectName" + :label="$t('page.admin.form.projectNameLabel')" + label-cols-sm="2" + label-align-sm="left" > - <div> - <CoscineHeadline :headline="$t('page.admin.title')" /> - <p class="mb-8 leading-relaxed dark:text-white"> - {{ $t("page.admin.description") }} - </p> - <img alt="From Coscine Old" src="@/assets/images/Admin.png" /> - </div> - </section> + <b-form-input + id="project_name" + :value="project ? project.name : ''" + readonly + /> + </CoscineFormGroup> + <!-- Project Short Name --> + <CoscineFormGroup + label-for="ProjectShortName" + :label="$t('page.admin.form.projectShortNameLabel')" + label-cols-sm="2" + label-align-sm="left" + > + <b-form-input + id="project_name" + :value="project ? project.name : ''" + readonly + /> + </CoscineFormGroup> + <!-- Project Guid --> + <CoscineFormGroup + label-for="ProjectGuid" + :label="$t('page.admin.form.projectGuidLabel')" + label-cols-sm="2" + label-align-sm="left" + > + <b-form-input + id="project_guid" + :value="project ? project.guid : ''" + readonly + /> + </CoscineFormGroup> + + <div class="h-divider" /> + + <!-- Quota Table --> + <QuotaTable /> </div> </template> @@ -22,16 +81,42 @@ import CoscineHeadline from "@/components/coscine/CoscineHeadline.vue"; import useAdminStore from "../store"; // import the main store import useMainStore from "@/store/index"; +import type { ProjectObject } from "@coscine/api-client/dist/types/Coscine.Api.Admin"; +import QuotaTable from "../components/QuotaTable.vue"; export default defineComponent({ components: { CoscineHeadline, + QuotaTable, }, + setup() { const mainStore = useMainStore(); const adminStore = useAdminStore(); return { mainStore, adminStore }; }, + + data() { + return { + projectString: "", + }; + }, + + computed: { + project(): ProjectObject | null { + return this.adminStore.project; + }, + }, + + methods: { + async queryProject() { + if (!this.projectString.trim()) { + this.adminStore.project = null; + } else { + await this.adminStore.retrieveProjectQuotas(this.projectString); + } + }, + }, }); </script> diff --git a/src/modules/admin/routes.ts b/src/modules/admin/routes.ts index 5c4893ea379a2188de13dfd6f63fc5dc4fe6207f..cf880d24af2bd8db63acda199d18168f0cdaa8cc 100644 --- a/src/modules/admin/routes.ts +++ b/src/modules/admin/routes.ts @@ -11,7 +11,6 @@ export const AdminRoutes: RouteConfig[] = [ component: AdminModule, // only authenticated users can access admin meta: { - breadCrumb: "admin", requiresAdmin: true, requiresAuth: true, i18n: AdminI18nMessages, @@ -21,6 +20,9 @@ export const AdminRoutes: RouteConfig[] = [ path: "/", name: "admin", component: Admin, + meta: { + breadCrumb: "admin", + }, }, ], }, diff --git a/src/modules/admin/store.ts b/src/modules/admin/store.ts index 63c995bba484d081e0b541d20e94424b738d25ee..c07f3ccfaa98ca511253ddf38e281999f9d166c9 100644 --- a/src/modules/admin/store.ts +++ b/src/modules/admin/store.ts @@ -1,5 +1,10 @@ import { defineStore } from "pinia"; import { AdminState } from "./types"; +import { AdminApi } from "@coscine/api-client"; +import type { UpdateQuotaParameterObject } from "@coscine/api-client/dist/types/Coscine.Api.Admin"; + +import useNotificationStore from "@/store/notification"; +import type { AxiosError } from "axios"; /* Store variable name is "this.<id>Store" @@ -13,29 +18,45 @@ export const useAdminStore = defineStore({ STATES -------------------------------------------------------------------------------------- */ - state: (): AdminState => ({}), - + state: (): AdminState => ({ + project: null, + }), /* -------------------------------------------------------------------------------------- GETTERS -------------------------------------------------------------------------------------- Synchronous code only. - + In a component use as e.g.: - :label = "this.adminStore.<getter_name>; + :label = "this.projectStore.<getter_name>;" */ getters: {}, - /* - -------------------------------------------------------------------------------------- - ACTIONS - -------------------------------------------------------------------------------------- - Asynchronous & Synchronous code comes here (e.g. API calls and VueX mutations). - To change a state use an action. - In a component use as e.g.: - @click = "this.adminStore.<action_name>(); - */ - actions: {}, + actions: { + async retrieveProjectQuotas(projectString: string) { + const notificationStore = useNotificationStore(); + try { + const apiResponse = await AdminApi.adminGetProject(projectString); + this.project = apiResponse.data; + } catch (error) { + // Handle other Status Codes + notificationStore.postApiErrorNotification(error as AxiosError); + } + }, + async updateProjectQuota( + quota: UpdateQuotaParameterObject + ): Promise<boolean> { + const notificationStore = useNotificationStore(); + try { + await AdminApi.adminUpdateQuota(quota); + return true; + } catch (error) { + // Handle other Status Codes + notificationStore.postApiErrorNotification(error as AxiosError); + return false; + } + }, + }, }); export default useAdminStore; diff --git a/src/modules/admin/types.ts b/src/modules/admin/types.ts index 7d6b0eaf0584c97fbfd557228abbc61d6ebad86d..9dc68e1da0d914fd1a93f0ebf859359917105cd1 100644 --- a/src/modules/admin/types.ts +++ b/src/modules/admin/types.ts @@ -1,7 +1,14 @@ +import type { + ProjectObject, + ProjectQuotaObject, +} from "@coscine/api-client/dist/types/Coscine.Api.Admin"; + +export interface ExtendedProjectQuotaObject extends ProjectQuotaObject { + iDisplayName?: string; + isQuotaAvailable?: boolean; + isEnabled?: boolean; +} + export interface AdminState { - /* - -------------------------------------------------------------------------------------- - STATE TYPE DEFINITION - -------------------------------------------------------------------------------------- - */ + project: ProjectObject | null; } diff --git a/src/modules/project/ProjectModule.vue b/src/modules/project/ProjectModule.vue index fa56c2859ac0b828a45ec672bb173717fcbc551c..1faf7ff58ef07370fbf0633c0f44a067241e7e03 100644 --- a/src/modules/project/ProjectModule.vue +++ b/src/modules/project/ProjectModule.vue @@ -90,9 +90,11 @@ export default defineComponent({ if (this.projectStore.currentQuotas === null) { this.projectStore.retrieveQuotas(this.project); } + // Load list of Enabled Resource Types if not present if (this.resourceStore.resourceTypes === null) { this.resourceStore.retrieveResourceTypes(); } + // Load list of all Project Role Types if not present if (this.projectStore.roles === null) { this.projectStore.retrieveRoles(); } diff --git a/src/modules/project/i18n/de.ts b/src/modules/project/i18n/de.ts index 56ee0ebf5c1e85573b8ad0900a3ccd5b668c00a7..130615a221000c53f620d92367e81e476d54f93b 100644 --- a/src/modules/project/i18n/de.ts +++ b/src/modules/project/i18n/de.ts @@ -54,23 +54,21 @@ export default { pleaseTypeSomething: "Bitte geben Sie einen Namen oder eine E-Mail Adresse ein", removeSelectedInvitation: - "Sind Sie sicher, dass Sie die Einladung von Benutzer <strong>{user}</strong> zum Projekt <strong>{projectName}</strong> zurückziehen möchten?", + "Sind Sie sicher, dass Sie die Einladung von Benutzer {user} zum Projekt {projectName} zurückziehen möchten?", removeSelectedUser: - "Sind Sie sicher, dass Sie den Benutzer <strong>{user}</strong> aus dem Projekt <strong>{projectName}</strong> entfernen möchten?", + "Sind Sie sicher, dass Sie den Benutzer {user} aus dem Projekt {projectName} entfernen möchten?", deleteInvitationTitle: "Einladung Zurückziehen", deleteUserTitle: "Benutzer entfernen", inviteUser: "Einladung abschicken", reInviteUser: "Erneut senden", inviteUserText: - "Sind Sie sicher, dass Sie den Benutzer <strong>{email}</strong> mit einer Rolle als <strong>{role}</strong> zum Projekt <strong>{projectName}</strong> einladen wollen?", + "Sind Sie sicher, dass Sie den Benutzer {email} mit einer Rolle als {role} zum Projekt {projectName} einladen wollen?", inviteUserTitle: "Benutzer einladen", inviteUserCaption: "Wählen Sie {displayName} aus", invitedUserText: "Eine Einladung wurde an {email} mit einer Rolle als {role} für das Projekt {projectName} verschickt.", - invitationPendingTextTop: - "Es wurde bereits eine Einladung an <strong>{email}</strong> gesendet.", - invitationPendingTextBottom: - "Wenn Sie eine neue Einladungsemail an diesen Benutzer schicken möchten, müssen Sie die vorherige Einladung über den Tab Eingeladene Benutzer löschen.", + invitationPendingText: + "Es wurde bereits eine Einladung an {email} gesendet. {br}Wenn Sie eine neue Einladungsemail an diesen Benutzer schicken möchten, müssen Sie die vorherige Einladung über den Tab Eingeladene Benutzer löschen.", invitedUserError: "Ein Fehler ist beim Einladen von {email} aufgetreten. Eingaben sind fehlerhaft oder der Benutzer wurde bereits eingeladen.", deleteExternalUserError: @@ -88,7 +86,7 @@ export default { removeUser: "Entfernen", searchProjectPlaceholder: "Wählen Sie ein Projekt aus...", existingEmailInvitation: - "Es wurde bereits eine Einladung an <strong>{email}</strong> versendet. Möchten Sie die Einladung erneut versenden?", + "Es wurde bereits eine Einladung an {email} versendet. Möchten Sie die Einladung erneut versenden?", }, // ProjectPage.vue diff --git a/src/modules/project/i18n/en.ts b/src/modules/project/i18n/en.ts index 007f58402d301a7d8a53392c1a344f60e3717f08..91c1a032f0d2bfb126859a2bc685c409f425a696 100644 --- a/src/modules/project/i18n/en.ts +++ b/src/modules/project/i18n/en.ts @@ -50,23 +50,21 @@ export default { userManagement: "@:(page.members.title)", pleaseTypeSomething: "Please enter a name or an email address", removeSelectedInvitation: - "Are you sure you want to revoke the invitation for <strong>{user}</strong> to the project <strong>{projectName}</strong>?", + "Are you sure you want to revoke the invitation for {user} to the project {projectName}?", removeSelectedUser: - "Are you sure you want to remove <strong>{user}</strong> from the project <strong>{projectName}</strong>?", + "Are you sure you want to remove {user} from the project {projectName}?", deleteInvitationTitle: "Revoke Invitation", deleteUserTitle: "Remove User", inviteUser: "Send Invitation", reInviteUser: "Resend Invitation", inviteUserText: - "Are you sure you want to invite <strong>{email}</strong> with a role as <strong>{role}</strong> to the project <strong>{projectName}</strong>?", + "Are you sure you want to invite {email} with a role as {role} to the project {projectName}?", inviteUserTitle: "Invite User", inviteUserCaption: "Choose to invite {displayName}", invitedUserText: "An invitation has been sent to {email} with a role as {role} for project {projectName}.", - invitationPendingTextTop: - "An invitation has already been sent to <strong>{email}</strong>.", - invitationPendingTextBottom: - "If you would like to resend an invitation email to this user, you have to cancel the previous invitation via the Invited Users tab.", + invitationPendingText: + "An invitation has already been sent to {email}. {br}If you would like to resend an invitation email to this user, you have to cancel the previous invitation via the Invited Users tab.", invitedUserError: "An error ocurred while trying to invite {email}. Invalid input was provided or the user has already been invited.", deleteExternalUserError: @@ -83,7 +81,7 @@ export default { removeUser: "Remove", searchProjectPlaceholder: "Select a project...", existingEmailInvitation: - "An invitation has already been sent to <strong>{email}</strong>. Do you want to send the invitation again?", + "An invitation has already been sent to {email}. Do you want to send the invitation again?", }, // ProjectPage.vue diff --git a/src/modules/project/pages/CreateProject.vue b/src/modules/project/pages/CreateProject.vue index 9b7f302c2bf1b245f5f50527104f507d87ef3c6c..51f6b2a7324c36f47c34a6c098cd79962d526059 100644 --- a/src/modules/project/pages/CreateProject.vue +++ b/src/modules/project/pages/CreateProject.vue @@ -195,12 +195,4 @@ export default defineComponent({ }); </script> -<style scoped> -.h-divider { - margin-top: 5px; - margin-bottom: 10px; - height: 1px; - width: 100%; - border-top: 1px solid #bebbbb; -} -</style> +<style scoped></style> diff --git a/src/modules/project/pages/Quota.vue b/src/modules/project/pages/Quota.vue index 990276430c31a8ef4a35177a7bf8675e7f905f7b..aaad654cd749e5f0b8f608ba3bd04518efefe287 100644 --- a/src/modules/project/pages/Quota.vue +++ b/src/modules/project/pages/Quota.vue @@ -439,13 +439,6 @@ input[type="range"] { width: calc(100% - 8em); padding-top: 0.6rem; } -.h-divider { - margin-top: 5px; - margin-bottom: 10px; - height: 1px; - width: 100%; - border-top: 1px solid #bebbbb; -} #quotaManagement >>> .b-table-empty-row { background-color: #f5f5f5; } diff --git a/src/modules/project/pages/Settings.vue b/src/modules/project/pages/Settings.vue index a6923f79f8987f63f595e18e663f88c8dea12788..7d67d117d7d792c275d32415eb935a9b4c86e1bf 100644 --- a/src/modules/project/pages/Settings.vue +++ b/src/modules/project/pages/Settings.vue @@ -259,12 +259,4 @@ export default defineComponent({ }); </script> -<style scoped> -.h-divider { - margin-top: 5px; - margin-bottom: 10px; - height: 1px; - width: 100%; - border-top: 1px solid #bebbbb; -} -</style> +<style scoped></style> diff --git a/src/modules/project/pages/components/modals/DeleteModal.vue b/src/modules/project/pages/components/modals/DeleteModal.vue index 75518c90d7c26595c40809850bc864d74080dbf5..6657aafecdfe0c23b5e0242eb795fe0082963d22 100644 --- a/src/modules/project/pages/components/modals/DeleteModal.vue +++ b/src/modules/project/pages/components/modals/DeleteModal.vue @@ -1,25 +1,23 @@ <template> - <div> - <b-modal - :visible="visible" - :title="$t(titleKey)" - ok-variant="danger" - :ok-title="$t('buttons.delete')" - :cancel-title="$t('buttons.cancel')" - @hidden="$emit('close', $event.target.value)" - @ok="$emit('ok', $event.target.value)" - @cancel="$emit('close', $event.target.value)" - > - <div - v-html=" - $t(descriptionKey, { - user: selectedUser, - projectName: selectedProject, - }) - " - /> - </b-modal> - </div> + <b-modal + :visible="visible" + :title="$t(titleKey)" + ok-variant="danger" + :ok-title="$t('buttons.delete')" + :cancel-title="$t('buttons.cancel')" + @hidden="$emit('close', $event.target.value)" + @ok="$emit('ok', $event.target.value)" + @cancel="$emit('close', $event.target.value)" + > + <i18n :path="descriptionKey" tag="span"> + <template #user> + <b>{{ selectedUser }}</b> + </template> + <template #projectName> + <b>{{ selectedProject }}</b> + </template> + </i18n> + </b-modal> </template> <script lang="ts"> diff --git a/src/modules/project/pages/components/modals/InvitationPendingModal.vue b/src/modules/project/pages/components/modals/InvitationPendingModal.vue index 74493cec37b047e37e4905a75885b31cc6c068c7..ecdf31bdd9bb92d26a665e2c46ea51677d61dce8 100644 --- a/src/modules/project/pages/components/modals/InvitationPendingModal.vue +++ b/src/modules/project/pages/components/modals/InvitationPendingModal.vue @@ -4,22 +4,18 @@ :title="$t('page.members.inviteUserTitle')" :hide-footer="true" > - <ul> - <li - v-html=" - $t('page.members.invitationPendingTextTop', { - email: candidateForInvitation.email, - }) - " - ></li> - <li> - {{ $t("page.members.invitationPendingTextBottom") }} - </li> - </ul> + <i18n path="page.members.invitationPendingText" tag="span"> + <template #email> + <b>{{ candidateForInvitation.email }}</b> + </template> + <template #br> + <br /> + </template> + </i18n> <br /> - <b-button @click="$bvModal.hide('invitationPendingModal')">{{ - $t("buttons.cancel") - }}</b-button> + <b-button class="mt-3" @click="$bvModal.hide('invitationPendingModal')"> + {{ $t("buttons.cancel") }} + </b-button> </b-modal> </template> diff --git a/src/modules/project/pages/components/modals/InviteUserModal.vue b/src/modules/project/pages/components/modals/InviteUserModal.vue index 6368dac6f28037be25ff6c76f701a60a465526a7..d0552576eba87d7cc6a9244698fb55563e72aabb 100644 --- a/src/modules/project/pages/components/modals/InviteUserModal.vue +++ b/src/modules/project/pages/components/modals/InviteUserModal.vue @@ -5,30 +5,35 @@ :hide-footer="true" > <!-- Body Text - New Invitation --> - <div + <i18n v-if="candidateForInvitation && !candidateForInvitation.invited" - v-html=" - $t('page.members.inviteUserText', { - email: candidateForInvitation.email, - role: getRoleNameFromId(candidateForInvitation.role), - projectName: projectName, - }) - " - /> + path="page.members.inviteUserText" + tag="span" + > + <template #email> + <b>{{ candidateForInvitation.email }}</b> + </template> + <template #role> + <b>{{ getRoleNameFromId(candidateForInvitation.role) }}</b> + </template> + <template #projectName> + <b>{{ projectName }}</b> + </template> + </i18n> <!-- Body Text - Existing Invitation --> - <div + <i18n v-else-if="candidateForInvitation && candidateForInvitation.invited" - v-html=" - $t('page.members.existingEmailInvitation', { - email: candidateForInvitation.email, - }) - " - /> - <br /> + path="page.members.existingEmailInvitation" + tag="span" + > + <template #email> + <b>{{ candidateForInvitation.email }}</b> + </template> + </i18n> <!-- Buttons --> - <div> + <div class="mt-3"> <!-- Invite --> <b-button class="inviteModalRightBtn" diff --git a/src/modules/user/pages/UserProfile.vue b/src/modules/user/pages/UserProfile.vue index 53d07a4e2e329d4ec8094dc39e973c991545863c..f12aa1c9d4d27697290e5de41cb03136818631c3 100644 --- a/src/modules/user/pages/UserProfile.vue +++ b/src/modules/user/pages/UserProfile.vue @@ -242,7 +242,7 @@ <AccessToken /> <!-- User Preferences --> - <div class="h-divider"></div> + <div class="h-divider" /> <CoscineHeadline :headline="$t('page.userprofile.form.userPreferences.header')" /> diff --git a/src/modules/user/pages/components/AccessToken.vue b/src/modules/user/pages/components/AccessToken.vue index 4d4f77d5cdbbce2c4ba7ead21426fca82ef4c590..f935e6bb80403e06224869d7d4b95eef500298de 100644 --- a/src/modules/user/pages/components/AccessToken.vue +++ b/src/modules/user/pages/components/AccessToken.vue @@ -355,13 +355,6 @@ export default defineComponent({ #accesstoken .table .b-table-empty-row { background-color: #f5f5f5; } -#accesstoken .h-divider { - margin-top: 5px; - margin-bottom: 10px; - height: 1px; - width: 100%; - border-top: 1px solid #bebbbb; -} #accesstoken .btn-danger:focus { outline: none; box-shadow: none; diff --git a/yarn.lock-workspace b/yarn.lock-workspace index 6fbaf58a66a3a03b334f0073de96216e73df84ec..7f49708d04965efa9320dfbddb90f01de0444349 100644 --- a/yarn.lock-workspace +++ b/yarn.lock-workspace @@ -3093,7 +3093,7 @@ __metadata: languageName: node linkType: hard -"cacache@npm:*, cacache@npm:^16.0.0, cacache@npm:^16.0.2, cacache@npm:^16.0.6": +"cacache@npm:*, cacache@npm:^16.0.0, cacache@npm:^16.0.2, cacache@npm:^16.0.6, cacache@npm:^16.0.7": version: 16.0.7 resolution: "cacache@npm:16.0.7" dependencies: @@ -8648,8 +8648,8 @@ __metadata: linkType: hard "npm@npm:^8.3.0": - version: 8.8.0 - resolution: "npm@npm:8.8.0" + version: 8.9.0 + resolution: "npm@npm:8.9.0" dependencies: "@isaacs/string-locale-compare": ^1.1.0 "@npmcli/arborist": ^5.0.4 @@ -8661,7 +8661,7 @@ __metadata: "@npmcli/run-script": ^3.0.1 abbrev: ~1.1.1 archy: ~1.0.0 - cacache: ^16.0.6 + cacache: ^16.0.7 chalk: ^4.1.2 chownr: ^2.0.0 cli-columns: ^4.0.0 @@ -8703,7 +8703,7 @@ __metadata: npm-user-validate: ^1.0.1 npmlog: ^6.0.2 opener: ^1.5.2 - pacote: ^13.1.1 + pacote: ^13.3.0 parse-conflict-json: ^2.0.2 proc-log: ^2.0.1 qrcode-terminal: ^0.12.0 @@ -8724,7 +8724,7 @@ __metadata: bin: npm: bin/npm-cli.js npx: bin/npx-cli.js - checksum: ece9941f5e3fe1cdeeb80030bd17512ee3f6ea3fd4c9efd7cc682e0170356cd39350de814cb0a6ba4dd3ed993b1e51c3edbcf16a440b3719994e510c3eba654b + checksum: ab555b93cfe6070fc5bcdbdfae87185f38e9b30909f0121b6330cbba1ee0c22ba71629431163f37e079d0502b8cc88ef2c58508073c9addb356089fca1706e79 languageName: node linkType: hard @@ -9052,7 +9052,7 @@ __metadata: languageName: node linkType: hard -"pacote@npm:*, pacote@npm:^13.0.3, pacote@npm:^13.0.5, pacote@npm:^13.1.1": +"pacote@npm:*, pacote@npm:^13.0.3, pacote@npm:^13.0.5": version: 13.2.0 resolution: "pacote@npm:13.2.0" dependencies: @@ -9083,6 +9083,37 @@ __metadata: languageName: node linkType: hard +"pacote@npm:^13.3.0": + version: 13.3.0 + resolution: "pacote@npm:13.3.0" + dependencies: + "@npmcli/git": ^3.0.0 + "@npmcli/installed-package-contents": ^1.0.7 + "@npmcli/promise-spawn": ^3.0.0 + "@npmcli/run-script": ^3.0.1 + cacache: ^16.0.0 + chownr: ^2.0.0 + fs-minipass: ^2.1.0 + infer-owner: ^1.0.4 + minipass: ^3.1.6 + mkdirp: ^1.0.4 + npm-package-arg: ^9.0.0 + npm-packlist: ^5.0.0 + npm-pick-manifest: ^7.0.0 + npm-registry-fetch: ^13.0.1 + proc-log: ^2.0.0 + promise-retry: ^2.0.1 + read-package-json: ^5.0.0 + read-package-json-fast: ^2.0.3 + rimraf: ^3.0.2 + ssri: ^9.0.0 + tar: ^6.1.11 + bin: + pacote: lib/bin.js + checksum: 49badbafac64e7cd9c87aa14342424ee90946e0ac729faeffb489a94ed6cdab815bd7ffec7673b4863e20e113ee678b8038ace95ee2fc1f58206bc4482ff0bf5 + languageName: node + linkType: hard + "pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" @@ -10346,8 +10377,8 @@ __metadata: linkType: hard "rollup@npm:^2.58.0, rollup@npm:^2.59.0": - version: 2.71.1 - resolution: "rollup@npm:2.71.1" + version: 2.72.0 + resolution: "rollup@npm:2.72.0" dependencies: fsevents: ~2.3.2 dependenciesMeta: @@ -10355,7 +10386,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: fe2b2fda7bf53c86e970f3b026b784c00e2237089b802755b3e43725db88f5d1869c1f81f8c5257e9b68b0fd1840dcbd3897d2f19768cce97a37c70e1a563dce + checksum: d4c213d4250a0455e0ee40664db3339a9e3ca5affa09c348019b4895e7100cdbedeba881a239a7a80c407b4055dca4e97d10a740d99c09b17651efbfedd61a4b languageName: node linkType: hard @@ -12047,8 +12078,8 @@ __metadata: linkType: hard "vite@npm:^2.7.10, vite@npm:^2.8.6": - version: 2.9.7 - resolution: "vite@npm:2.9.7" + version: 2.9.8 + resolution: "vite@npm:2.9.8" dependencies: esbuild: ^0.14.27 fsevents: ~2.3.2 @@ -12071,7 +12102,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: d3d2a86855709e037d244c3066e785d7b700e41bf59ceb776818ea06ee2ed579055c10596ca883dd56de46b3654ec82da53369062013a012a1a60819dbb7c0ee + checksum: 7de3450bec4caa06f4540d1d252563ef0b7e1bdd167d04abf03db72cfd8c9a93879e18861283bc7d075e1d094b78b71770ca36f9b965bdf28c66665eafdc29dc languageName: node linkType: hard