diff options
Diffstat (limited to 'apps/settings/src/components/AppStoreSidebar')
5 files changed, 960 insertions, 0 deletions
diff --git a/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue new file mode 100644 index 00000000000..7c0b8ea4421 --- /dev/null +++ b/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue @@ -0,0 +1,50 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcAppSidebarTab v-if="app?.daemon" + id="daemon" + :name="t('settings', 'Daemon')" + :order="3"> + <template #icon> + <NcIconSvgWrapper :path="mdiFileChart" :size="24" /> + </template> + <div class="daemon"> + <h4>{{ t('settings', 'Deploy Daemon') }}</h4> + <p><b>{{ t('settings', 'Type') }}</b>: {{ app?.daemon.accepts_deploy_id }}</p> + <p><b>{{ t('settings', 'Name') }}</b>: {{ app?.daemon.name }}</p> + <p><b>{{ t('settings', 'Display Name') }}</b>: {{ app?.daemon.display_name }}</p> + <p><b>{{ t('settings', 'GPUs support') }}</b>: {{ gpuSupport }}</p> + <p><b>{{ t('settings', 'Compute device') }}</b>: {{ app?.daemon?.deploy_config?.computeDevice?.label }}</p> + </div> + </NcAppSidebarTab> +</template> + +<script setup lang="ts"> +import type { IAppstoreExApp } from '../../app-types' + +import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' + +import { mdiFileChart } from '@mdi/js' +import { ref } from 'vue' + +const props = defineProps<{ + app: IAppstoreExApp, +}>() + +const gpuSupport = ref(props.app?.daemon?.deploy_config?.computeDevice?.id !== 'cpu' || false) +</script> + +<style scoped lang="scss"> +.daemon { + padding: 20px; + + h4 { + font-weight: bold; + margin: 10px auto; + } +} +</style> diff --git a/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue b/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue new file mode 100644 index 00000000000..0544c3848be --- /dev/null +++ b/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue @@ -0,0 +1,320 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcDialog :open="show" + size="normal" + :name="t('settings', 'Advanced deploy options')" + @update:open="$emit('update:show', $event)"> + <div class="modal__content"> + <p class="deploy-option__hint"> + {{ configuredDeployOptions === null ? t('settings', 'Edit ExApp deploy options before installation') : t('settings', 'Configured ExApp deploy options. Can be set only during installation') }}. + <a v-if="deployOptionsDocsUrl" :href="deployOptionsDocsUrl"> + {{ t('settings', 'Learn more') }} + </a> + </p> + <h3 v-if="environmentVariables.length > 0 || (configuredDeployOptions !== null && configuredDeployOptions.environment_variables.length > 0)"> + {{ t('settings', 'Environment variables') }} + </h3> + <template v-if="configuredDeployOptions === null"> + <div v-for="envVar in environmentVariables" + :key="envVar.envName" + class="deploy-option"> + <NcTextField :label="envVar.displayName" :value.sync="deployOptions.environment_variables[envVar.envName]" /> + <p class="deploy-option__hint"> + {{ envVar.description }} + </p> + </div> + </template> + <fieldset v-else-if="Object.keys(configuredDeployOptions).length > 0" + class="envs"> + <legend class="deploy-option__hint"> + {{ t('settings', 'ExApp container environment variables') }} + </legend> + <NcTextField v-for="(value, key) in configuredDeployOptions.environment_variables" + :key="key" + :label="value.displayName ?? key" + :helper-text="value.description" + :value="value.value" + readonly /> + </fieldset> + <template v-else> + <p class="deploy-option__hint"> + {{ t('settings', 'No environment variables defined') }} + </p> + </template> + + <h3>{{ t('settings', 'Mounts') }}</h3> + <template v-if="configuredDeployOptions === null"> + <p class="deploy-option__hint"> + {{ t('settings', 'Define host folder mounts to bind to the ExApp container') }} + </p> + <NcNoteCard type="info" :text="t('settings', 'Must exist on the Deploy daemon host prior to installing the ExApp')" /> + <div v-for="mount in deployOptions.mounts" + :key="mount.hostPath" + class="deploy-option" + style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;"> + <NcTextField :label="t('settings', 'Host path')" :value.sync="mount.hostPath" /> + <NcTextField :label="t('settings', 'Container path')" :value.sync="mount.containerPath" /> + <NcCheckboxRadioSwitch :checked.sync="mount.readonly"> + {{ t('settings', 'Read-only') }} + </NcCheckboxRadioSwitch> + <NcButton :aria-label="t('settings', 'Remove mount')" + style="margin-top: 6px;" + @click="removeMount(mount)"> + <template #icon> + <NcIconSvgWrapper :path="mdiDeleteOutline" /> + </template> + </NcButton> + </div> + <div v-if="addingMount" class="deploy-option"> + <h4> + {{ t('settings', 'New mount') }} + </h4> + <div style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;"> + <NcTextField ref="newMountHostPath" + :label="t('settings', 'Host path')" + :aria-label="t('settings', 'Enter path to host folder')" + :value.sync="newMountPoint.hostPath" /> + <NcTextField :label="t('settings', 'Container path')" + :aria-label="t('settings', 'Enter path to container folder')" + :value.sync="newMountPoint.containerPath" /> + <NcCheckboxRadioSwitch :checked.sync="newMountPoint.readonly" + :aria-label="t('settings', 'Toggle read-only mode')"> + {{ t('settings', 'Read-only') }} + </NcCheckboxRadioSwitch> + </div> + <div style="display: flex; align-items: center; margin-top: 4px;"> + <NcButton :aria-label="t('settings', 'Confirm adding new mount')" + @click="addMountPoint"> + <template #icon> + <NcIconSvgWrapper :path="mdiCheck" /> + </template> + {{ t('settings', 'Confirm') }} + </NcButton> + <NcButton :aria-label="t('settings', 'Cancel adding mount')" + style="margin-left: 4px;" + @click="cancelAddMountPoint"> + <template #icon> + <NcIconSvgWrapper :path="mdiClose" /> + </template> + {{ t('settings', 'Cancel') }} + </NcButton> + </div> + </div> + <NcButton v-if="!addingMount" + :aria-label="t('settings', 'Add mount')" + style="margin-top: 5px;" + @click="startAddingMount"> + <template #icon> + <NcIconSvgWrapper :path="mdiPlus" /> + </template> + {{ t('settings', 'Add mount') }} + </NcButton> + </template> + <template v-else-if="configuredDeployOptions.mounts.length > 0"> + <p class="deploy-option__hint"> + {{ t('settings', 'ExApp container mounts') }} + </p> + <div v-for="mount in configuredDeployOptions.mounts" + :key="mount.hostPath" + class="deploy-option" + style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;"> + <NcTextField :label="t('settings', 'Host path')" :value.sync="mount.hostPath" readonly /> + <NcTextField :label="t('settings', 'Container path')" :value.sync="mount.containerPath" readonly /> + <NcCheckboxRadioSwitch :checked.sync="mount.readonly" disabled> + {{ t('settings', 'Read-only') }} + </NcCheckboxRadioSwitch> + </div> + </template> + <p v-else class="deploy-option__hint"> + {{ t('settings', 'No mounts defined') }} + </p> + </div> + + <template v-if="!app.active && (app.canInstall || app.isCompatible) && configuredDeployOptions === null" #actions> + <NcButton :title="enableButtonTooltip" + :aria-label="enableButtonTooltip" + type="primary" + :disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying" + @click.stop="submitDeployOptions"> + {{ enableButtonText }} + </NcButton> + </template> + </NcDialog> +</template> + +<script> +import { computed, ref } from 'vue' + +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' +import { emit } from '@nextcloud/event-bus' + +import NcDialog from '@nextcloud/vue/components/NcDialog' +import NcTextField from '@nextcloud/vue/components/NcTextField' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' + +import { mdiPlus, mdiCheck, mdiClose, mdiDeleteOutline } from '@mdi/js' + +import { useAppApiStore } from '../../store/app-api-store.ts' +import { useAppsStore } from '../../store/apps-store.ts' + +import AppManagement from '../../mixins/AppManagement.js' + +export default { + name: 'AppDeployOptionsModal', + components: { + NcDialog, + NcTextField, + NcButton, + NcNoteCard, + NcCheckboxRadioSwitch, + NcIconSvgWrapper, + }, + mixins: [AppManagement], + props: { + app: { + type: Object, + required: true, + }, + show: { + type: Boolean, + required: true, + }, + }, + setup(props) { + // for AppManagement mixin + const store = useAppsStore() + const appApiStore = useAppApiStore() + + const environmentVariables = computed(() => { + if (props.app?.releases?.length === 1) { + return props.app?.releases[0]?.environmentVariables || [] + } + return [] + }) + + const deployOptions = ref({ + environment_variables: environmentVariables.value.reduce((acc, envVar) => { + acc[envVar.envName] = envVar.default || '' + return acc + }, {}), + mounts: [], + }) + + return { + environmentVariables, + deployOptions, + store, + appApiStore, + mdiPlus, + mdiCheck, + mdiClose, + mdiDeleteOutline, + } + }, + data() { + return { + addingMount: false, + newMountPoint: { + hostPath: '', + containerPath: '', + readonly: false, + }, + addingPortBinding: false, + configuredDeployOptions: null, + deployOptionsDocsUrl: loadState('settings', 'deployOptionsDocsUrl', null), + } + }, + watch: { + show(newShow) { + if (newShow) { + this.fetchExAppDeployOptions() + } else { + this.configuredDeployOptions = null + } + }, + }, + methods: { + startAddingMount() { + this.addingMount = true + this.$nextTick(() => { + this.$refs.newMountHostPath.focus() + }) + }, + addMountPoint() { + this.deployOptions.mounts.push(this.newMountPoint) + this.newMountPoint = { + hostPath: '', + containerPath: '', + readonly: false, + } + this.addingMount = false + }, + cancelAddMountPoint() { + this.newMountPoint = { + hostPath: '', + containerPath: '', + readonly: false, + } + this.addingMount = false + }, + removeMount(mountToRemove) { + this.deployOptions.mounts = this.deployOptions.mounts.filter(mount => mount !== mountToRemove) + }, + async fetchExAppDeployOptions() { + return axios.get(generateUrl(`/apps/app_api/apps/deploy-options/${this.app.id}`)) + .then(response => { + this.configuredDeployOptions = response.data + }) + .catch(() => { + this.configuredDeployOptions = null + }) + }, + async submitDeployOptions() { + await this.appApiStore.fetchDockerDaemons() + if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) { + this.enable(this.app.id, this.appApiStore.dockerDaemons[0], this.deployOptions) + } else if (this.app.needsDownload) { + emit('showDaemonSelectionModal', this.deployOptions) + } else { + this.enable(this.app.id, this.app.daemon, this.deployOptions) + } + this.$emit('update:show', false) + }, + }, +} +</script> + +<style scoped> +.deploy-option { + margin: calc(var(--default-grid-baseline) * 4) 0; + display: flex; + flex-direction: column; + align-items: flex-start; + + &__hint { + margin-top: 4px; + font-size: 0.8em; + color: var(--color-text-maxcontrast); + } +} + +.envs { + width: 100%; + overflow: auto; + height: 100%; + max-height: 300px; + + li { + margin: 10px 0; + } +} +</style> diff --git a/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue new file mode 100644 index 00000000000..299d084ef9e --- /dev/null +++ b/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue @@ -0,0 +1,38 @@ +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcAppSidebarTab id="desc" + :name="t('settings', 'Description')" + :order="0"> + <template #icon> + <NcIconSvgWrapper :path="mdiTextShort" /> + </template> + <div class="app-description"> + <Markdown :text="app.description" :min-heading="4" /> + </div> + </NcAppSidebarTab> +</template> + +<script setup lang="ts"> +import type { IAppstoreApp } from '../../app-types' + +import { mdiTextShort } from '@mdi/js' +import { translate as t } from '@nextcloud/l10n' + +import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import Markdown from '../Markdown.vue' + +defineProps<{ + app: IAppstoreApp, +}>() +</script> + +<style scoped lang="scss"> +.app-description { + padding: 12px; +} +</style> diff --git a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue new file mode 100644 index 00000000000..eb66d8f3e3a --- /dev/null +++ b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue @@ -0,0 +1,495 @@ +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcAppSidebarTab id="details" + :name="t('settings', 'Details')" + :order="1"> + <template #icon> + <NcIconSvgWrapper :path="mdiTextBoxOutline" /> + </template> + <div class="app-details"> + <div class="app-details__actions"> + <div v-if="app.active && canLimitToGroups(app)" class="app-details__actions-groups"> + <input :id="`groups_enable_${app.id}`" + v-model="groupCheckedAppsData" + type="checkbox" + :value="app.id" + class="groups-enable__checkbox checkbox" + @change="setGroupLimit"> + <label :for="`groups_enable_${app.id}`">{{ t('settings', 'Limit to groups') }}</label> + <input type="hidden" + class="group_select" + :title="t('settings', 'All')" + value=""> + <br> + <label for="limitToGroups"> + <span>{{ t('settings', 'Limit app usage to groups') }}</span> + </label> + <NcSelect v-if="isLimitedToGroups(app)" + input-id="limitToGroups" + :options="groups" + :value="appGroups" + :limit="5" + label="name" + :multiple="true" + :close-on-select="false" + @option:selected="addGroupLimitation" + @option:deselected="removeGroupLimitation" + @search="asyncFindGroup"> + <span slot="noResult">{{ t('settings', 'No results') }}</span> + </NcSelect> + </div> + <div class="app-details__actions-manage"> + <input v-if="app.update" + class="update primary" + type="button" + :value="t('settings', 'Update to {version}', { version: app.update })" + :disabled="installing || isLoading || isManualInstall" + @click="update(app.id)"> + <input v-if="app.canUnInstall" + class="uninstall" + type="button" + :value="t('settings', 'Remove')" + :disabled="installing || isLoading" + @click="remove(app.id, removeData)"> + <input v-if="app.active" + class="enable" + type="button" + :value="disableButtonText" + :disabled="installing || isLoading || isInitializing || isDeploying" + @click="disable(app.id)"> + <input v-if="!app.active && (app.canInstall || app.isCompatible)" + :title="enableButtonTooltip" + :aria-label="enableButtonTooltip" + class="enable primary" + type="button" + :value="enableButtonText" + :disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying" + @click="enableButtonAction"> + <input v-else-if="!app.active && !app.canInstall" + :title="forceEnableButtonTooltip" + :aria-label="forceEnableButtonTooltip" + class="enable force" + type="button" + :value="forceEnableButtonText" + :disabled="installing || isLoading" + @click="forceEnable(app.id)"> + <NcButton v-if="app?.app_api && (app.canInstall || app.isCompatible)" + :aria-label="t('settings', 'Advanced deploy options')" + type="secondary" + @click="() => showDeployOptionsModal = true"> + <template #icon> + <NcIconSvgWrapper :path="mdiToyBrickPlusOutline" /> + </template> + {{ t('settings', 'Deploy options') }} + </NcButton> + </div> + <p v-if="!defaultDeployDaemonAccessible" class="warning"> + {{ t('settings', 'Default Deploy daemon is not accessible') }} + </p> + <NcCheckboxRadioSwitch v-if="app.canUnInstall" + :checked="removeData" + :disabled="installing || isLoading || !defaultDeployDaemonAccessible" + @update:checked="toggleRemoveData"> + {{ t('settings', 'Delete data on remove') }} + </NcCheckboxRadioSwitch> + </div> + + <ul class="app-details__dependencies"> + <li v-if="app.missingMinOwnCloudVersion"> + {{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }} + </li> + <li v-if="app.missingMaxOwnCloudVersion"> + {{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }} + </li> + <li v-if="!app.canInstall"> + {{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }} + <ul class="missing-dependencies"> + <li v-for="(dep, index) in app.missingDependencies" :key="index"> + {{ dep }} + </li> + </ul> + </li> + </ul> + + <div v-if="lastModified && !app.shipped" class="app-details__section"> + <h4> + {{ t('settings', 'Latest updated') }} + </h4> + <NcDateTime :timestamp="lastModified" /> + </div> + + <div class="app-details__section"> + <h4> + {{ t('settings', 'Author') }} + </h4> + <p class="app-details__authors"> + {{ appAuthors }} + </p> + </div> + + <div class="app-details__section"> + <h4> + {{ t('settings', 'Categories') }} + </h4> + <p> + {{ appCategories }} + </p> + </div> + + <div v-if="externalResources.length > 0" class="app-details__section"> + <h4>{{ t('settings', 'Resources') }}</h4> + <ul class="app-details__documentation" :aria-label="t('settings', 'Documentation')"> + <li v-for="resource of externalResources" :key="resource.id"> + <a class="appslink" + :href="resource.href" + target="_blank" + rel="noreferrer noopener"> + {{ resource.label }} ↗ + </a> + </li> + </ul> + </div> + + <div class="app-details__section"> + <h4>{{ t('settings', 'Interact') }}</h4> + <div class="app-details__interact"> + <NcButton :disabled="!app.bugs" + :href="app.bugs ?? '#'" + :aria-label="t('settings', 'Report a bug')" + :title="t('settings', 'Report a bug')"> + <template #icon> + <NcIconSvgWrapper :path="mdiBugOutline" /> + </template> + </NcButton> + <NcButton :disabled="!app.bugs" + :href="app.bugs ?? '#'" + :aria-label="t('settings', 'Request feature')" + :title="t('settings', 'Request feature')"> + <template #icon> + <NcIconSvgWrapper :path="mdiFeatureSearchOutline" /> + </template> + </NcButton> + <NcButton v-if="app.appstoreData?.discussion" + :href="app.appstoreData.discussion" + :aria-label="t('settings', 'Ask questions or discuss')" + :title="t('settings', 'Ask questions or discuss')"> + <template #icon> + <NcIconSvgWrapper :path="mdiTooltipQuestionOutline" /> + </template> + </NcButton> + <NcButton v-if="!app.internal" + :href="rateAppUrl" + :aria-label="t('settings', 'Rate the app')" + :title="t('settings', 'Rate')"> + <template #icon> + <NcIconSvgWrapper :path="mdiStar" /> + </template> + </NcButton> + </div> + </div> + + <AppDeployOptionsModal v-if="app?.app_api" + :show.sync="showDeployOptionsModal" + :app="app" /> + <DaemonSelectionDialog v-if="app?.app_api" + :show.sync="showSelectDaemonModal" + :app="app" + :deploy-options="deployOptions" /> + </div> + </NcAppSidebarTab> +</template> + +<script> +import { subscribe, unsubscribe } from '@nextcloud/event-bus' +import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcDateTime from '@nextcloud/vue/components/NcDateTime' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import AppDeployOptionsModal from './AppDeployOptionsModal.vue' +import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue' + +import AppManagement from '../../mixins/AppManagement.js' +import { mdiBugOutline, mdiFeatureSearchOutline, mdiStar, mdiTextBoxOutline, mdiTooltipQuestionOutline, mdiToyBrickPlusOutline } from '@mdi/js' +import { useAppsStore } from '../../store/apps-store' +import { useAppApiStore } from '../../store/app-api-store' + +export default { + name: 'AppDetailsTab', + + components: { + NcAppSidebarTab, + NcButton, + NcDateTime, + NcIconSvgWrapper, + NcSelect, + NcCheckboxRadioSwitch, + AppDeployOptionsModal, + DaemonSelectionDialog, + }, + mixins: [AppManagement], + + props: { + app: { + type: Object, + required: true, + }, + }, + + setup() { + const store = useAppsStore() + const appApiStore = useAppApiStore() + + return { + store, + appApiStore, + + mdiBugOutline, + mdiFeatureSearchOutline, + mdiStar, + mdiTextBoxOutline, + mdiTooltipQuestionOutline, + mdiToyBrickPlusOutline, + } + }, + + data() { + return { + groupCheckedAppsData: false, + removeData: false, + showDeployOptionsModal: false, + showSelectDaemonModal: false, + deployOptions: null, + } + }, + + computed: { + lastModified() { + return (this.app.appstoreData?.releases ?? []) + .map(({ lastModified }) => Date.parse(lastModified)) + .sort() + .at(0) ?? null + }, + /** + * App authors as comma separated string + */ + appAuthors() { + console.warn(this.app) + if (!this.app) { + return '' + } + + const authorName = (xmlNode) => { + if (xmlNode['@value']) { + // Complex node (with email or homepage attribute) + return xmlNode['@value'] + } + // Simple text node + return xmlNode + } + + const authors = Array.isArray(this.app.author) + ? this.app.author.map(authorName) + : [authorName(this.app.author)] + + return authors + .sort((a, b) => a.split(' ').at(-1).localeCompare(b.split(' ').at(-1))) + .join(', ') + }, + + appstoreUrl() { + return `https://apps.nextcloud.com/apps/${this.app.id}` + }, + + /** + * Further external resources (e.g. website) + */ + externalResources() { + const resources = [] + if (!this.app.internal) { + resources.push({ + id: 'appstore', + href: this.appstoreUrl, + label: t('settings', 'View in store'), + }) + } + if (this.app.website) { + resources.push({ + id: 'website', + href: this.app.website, + label: t('settings', 'Visit website'), + }) + } + if (this.app.documentation) { + if (this.app.documentation.user) { + resources.push({ + id: 'doc-user', + href: this.app.documentation.user, + label: t('settings', 'Usage documentation'), + }) + } + if (this.app.documentation.admin) { + resources.push({ + id: 'doc-admin', + href: this.app.documentation.admin, + label: t('settings', 'Admin documentation'), + }) + } + if (this.app.documentation.developer) { + resources.push({ + id: 'doc-developer', + href: this.app.documentation.developer, + label: t('settings', 'Developer documentation'), + }) + } + } + return resources + }, + + appCategories() { + return [this.app.category].flat() + .map((id) => this.store.getCategoryById(id)?.displayName ?? id) + .join(', ') + }, + + rateAppUrl() { + return `${this.appstoreUrl}#comments` + }, + appGroups() { + return this.app.groups.map(group => { return { id: group, name: group } }) + }, + groups() { + return this.$store.getters.getGroups + .filter(group => group.id !== 'disabled') + .sort((a, b) => a.name.localeCompare(b.name)) + }, + }, + watch: { + 'app.id'() { + this.removeData = false + }, + }, + beforeUnmount() { + this.deployOptions = null + unsubscribe('showDaemonSelectionModal') + }, + mounted() { + if (this.app.groups.length > 0) { + this.groupCheckedAppsData = true + } + subscribe('showDaemonSelectionModal', (deployOptions) => { + this.showSelectionModal(deployOptions) + }) + }, + methods: { + toggleRemoveData() { + this.removeData = !this.removeData + }, + showSelectionModal(deployOptions = null) { + this.deployOptions = deployOptions + this.showSelectDaemonModal = true + }, + async enableButtonAction() { + if (!this.app?.app_api) { + this.enable(this.app.id) + return + } + await this.appApiStore.fetchDockerDaemons() + if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) { + this.enable(this.app.id, this.appApiStore.dockerDaemons[0]) + } else if (this.app.needsDownload) { + this.showSelectionModal() + } else { + this.enable(this.app.id, this.app.daemon) + } + }, + }, +} +</script> + +<style scoped lang="scss"> +.app-details { + padding: 20px; + + &__actions { + // app management + &-manage { + // if too many, shrink them and ellipsis + display: flex; + align-items: center; + input { + flex: 0 1 auto; + min-width: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + } + &__authors { + color: var(--color-text-maxcontrast); + } + + &__section { + margin-top: 15px; + + h4 { + font-size: 16px; + font-weight: bold; + margin-block-end: 5px; + } + } + + &__interact { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 12px; + } + + &__documentation { + a { + text-decoration: underline; + } + li { + padding-inline-start: 20px; + + &::before { + width: 5px; + height: 5px; + border-radius: 100%; + background-color: var(--color-main-text); + content: ""; + float: inline-start; + margin-inline-start: -13px; + position: relative; + top: 10px; + } + } + } +} + +.force { + color: var(--color-error); + border-color: var(--color-error); + background: var(--color-main-background); +} + +.force:hover, +.force:active { + color: var(--color-main-background); + border-color: var(--color-error) !important; + background: var(--color-error); +} + +.missing-dependencies { + list-style: initial; + list-style-type: initial; + list-style-position: inside; +} +</style> diff --git a/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue b/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue new file mode 100644 index 00000000000..e65df0341db --- /dev/null +++ b/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue @@ -0,0 +1,57 @@ +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcAppSidebarTab v-if="hasChangelog" + id="changelog" + :name="t('settings', 'Changelog')" + :order="2"> + <template #icon> + <NcIconSvgWrapper :path="mdiClockFast" :size="24" /> + </template> + <div v-for="release in app.releases" :key="release.version" class="app-sidebar-tabs__release"> + <h2>{{ release.version }}</h2> + <Markdown class="app-sidebar-tabs__release-text" + :text="createChangelogFromRelease(release)" /> + </div> + </NcAppSidebarTab> +</template> + +<script setup lang="ts"> +import type { IAppstoreApp, IAppstoreAppRelease } from '../../app-types.ts' + +import { mdiClockFast } from '@mdi/js' +import { getLanguage, translate as t } from '@nextcloud/l10n' +import { computed } from 'vue' + +import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import Markdown from '../Markdown.vue' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const props = defineProps<{ app: IAppstoreApp }>() + +const hasChangelog = computed(() => Object.values(props.app.releases?.[0]?.translations ?? {}).some(({ changelog }) => !!changelog)) + +const createChangelogFromRelease = (release: IAppstoreAppRelease) => release.translations?.[getLanguage()]?.changelog ?? release.translations?.en?.changelog ?? '' +</script> + +<style scoped lang="scss"> +.app-sidebar-tabs__release { + h2 { + border-bottom: 1px solid var(--color-border); + font-size: 24px; + } + + &-text { + // Overwrite changelog heading styles + :deep(h3) { + font-size: 20px; + } + :deep(h4) { + font-size: 17px; + } + } +} +</style> |