]> source.dussan.org Git - nextcloud-server.git/commitdiff
refactor(app-store): Split `AppItemName` component from `AppItem` for better maintain...
authorFerdinand Thiessen <opensource@fthiessen.de>
Mon, 21 Oct 2024 10:26:24 +0000 (12:26 +0200)
committerFerdinand Thiessen <opensource@fthiessen.de>
Wed, 23 Oct 2024 10:58:52 +0000 (12:58 +0200)
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
apps/settings/src/components/AppList/AppItem.vue [deleted file]
apps/settings/src/components/AppStore/AppItem/AppItem.vue [new file with mode: 0644]
apps/settings/src/components/AppStore/AppItem/AppItemName.vue [new file with mode: 0644]

diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue
deleted file mode 100644 (file)
index 32c0631..0000000
+++ /dev/null
@@ -1,370 +0,0 @@
-<!--
-  - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
-  - SPDX-License-Identifier: AGPL-3.0-or-later
--->
-<template>
-       <component :is="listView ? 'tr' : (inline ? 'article' : 'li')"
-               class="app-item"
-               :class="{
-                       'app-item--list-view': listView,
-                       'app-item--store-view': !listView,
-                       'app-item--selected': isSelected,
-                       'app-item--with-sidebar': withSidebar,
-               }">
-               <AppItemIcon :app="app"
-                       :list-view="listView"
-                       :headers="useBundleView ? `${headers} app-table-col-icon` : undefined" />
-               <component :is="dataItemTag"
-                       class="app-name"
-                       :headers="getDataItemHeaders(`app-table-col-name`)">
-                       <router-link class="app-name--link"
-                               :to="{
-                                       name: 'apps-details',
-                                       params: {
-                                               category: category,
-                                               id: app.id
-                                       },
-                               }"
-                               :aria-label="t('settings', 'Show details for {appName} app', { appName:app.name })">
-                               {{ app.name }}
-                       </router-link>
-               </component>
-               <component :is="dataItemTag"
-                       v-if="!listView"
-                       class="app-summary"
-                       :headers="getDataItemHeaders(`app-version`)">
-                       {{ app.summary }}
-               </component>
-               <component :is="dataItemTag"
-                       v-if="listView"
-                       class="app-version"
-                       :headers="getDataItemHeaders(`app-table-col-version`)">
-                       <span v-if="app.version">{{ app.version }}</span>
-                       <span v-else-if="app.appstoreData.releases[0].version">{{ app.appstoreData.releases[0].version }}</span>
-               </component>
-
-               <component :is="dataItemTag" :headers="getDataItemHeaders(`app-table-col-level`)" class="app-level">
-                       <AppLevelBadge :level="app.level" />
-                       <AppScore v-if="hasRating && !listView" :score="app.score" />
-               </component>
-               <component :is="dataItemTag"
-                       v-if="!inline"
-                       :headers="getDataItemHeaders(`app-table-col-actions`)"
-                       class="app-actions">
-                       <div v-if="app.error" class="warning">
-                               {{ app.error }}
-                       </div>
-                       <div v-if="isLoading" class="icon icon-loading-small" />
-                       <NcButton v-if="app.update"
-                               type="primary"
-                               :disabled="installing || isLoading"
-                               @click.stop="update(app.id)">
-                               {{ t('settings', 'Update to {update}', {update:app.update}) }}
-                       </NcButton>
-                       <NcButton v-if="app.canUnInstall"
-                               class="uninstall"
-                               type="tertiary"
-                               :disabled="installing || isLoading"
-                               @click.stop="remove(app.id)">
-                               {{ t('settings', 'Remove') }}
-                       </NcButton>
-                       <NcButton v-if="app.active"
-                               :disabled="installing || isLoading"
-                               @click.stop="disable(app.id)">
-                               {{ t('settings','Disable') }}
-                       </NcButton>
-                       <NcButton v-if="!app.active && (app.canInstall || app.isCompatible)"
-                               :title="enableButtonTooltip"
-                               :aria-label="enableButtonTooltip"
-                               type="primary"
-                               :disabled="!app.canInstall || installing || isLoading"
-                               @click.stop="enable(app.id)">
-                               {{ enableButtonText }}
-                       </NcButton>
-                       <NcButton v-else-if="!app.active"
-                               :title="forceEnableButtonTooltip"
-                               :aria-label="forceEnableButtonTooltip"
-                               type="secondary"
-                               :disabled="installing || isLoading"
-                               @click.stop="forceEnable(app.id)">
-                               {{ forceEnableButtonText }}
-                       </NcButton>
-               </component>
-       </component>
-</template>
-
-<script>
-import AppScore from './AppScore.vue'
-import AppLevelBadge from './AppLevelBadge.vue'
-import AppManagement from '../../mixins/AppManagement.js'
-import SvgFilterMixin from '../SvgFilterMixin.vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-
-export default {
-       name: 'AppItem',
-       components: {
-               AppLevelBadge,
-               AppScore,
-               NcButton,
-       },
-       mixins: [AppManagement, SvgFilterMixin],
-       props: {
-               app: {
-                       type: Object,
-                       required: true,
-               },
-               category: {
-                       type: String,
-                       required: true,
-               },
-               listView: {
-                       type: Boolean,
-                       default: true,
-               },
-               useBundleView: {
-                       type: Boolean,
-                       default: false,
-               },
-               headers: {
-                       type: String,
-                       default: null,
-               },
-               inline: {
-                       type: Boolean,
-                       default: false,
-               },
-       },
-       data() {
-               return {
-                       isSelected: false,
-                       scrolled: false,
-                       screenshotLoaded: false,
-               }
-       },
-       computed: {
-               hasRating() {
-                       return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
-               },
-               dataItemTag() {
-                       return this.listView ? 'td' : 'div'
-               },
-               withSidebar() {
-                       return !!this.$route.params.id
-               },
-       },
-       watch: {
-               '$route.params.id'(id) {
-                       this.isSelected = (this.app.id === id)
-               },
-       },
-       mounted() {
-               this.isSelected = (this.app.id === this.$route.params.id)
-               if (this.app.releases && this.app.screenshot) {
-                       const image = new Image()
-                       image.onload = () => {
-                               this.screenshotLoaded = true
-                       }
-                       image.src = this.app.screenshot
-               }
-       },
-       watchers: {
-
-       },
-       methods: {
-               prefix(prefix, content) {
-                       return prefix + '_' + content
-               },
-
-               getDataItemHeaders(columnName) {
-                       return this.useBundleView ? [this.headers, columnName].join(' ') : null
-               },
-       },
-}
-</script>
-
-<style scoped lang="scss">
-@use '../../../../../core/css/variables.scss' as variables;
-@use 'sass:math';
-
-.app-item {
-       position: relative;
-
-       &:hover {
-               background-color: var(--color-background-dark);
-       }
-
-       &--list-view {
-               --app-item-padding: calc(var(--default-grid-baseline) * 2);
-               --app-item-height: calc(var(--default-clickable-area) + var(--app-item-padding) * 2);
-
-               &.app-item--selected {
-                       background-color: var(--color-background-dark);
-               }
-
-               > * {
-                       vertical-align: middle;
-                       border-bottom: 1px solid var(--color-border);
-                       padding: var(--app-item-padding);
-                       height: var(--app-item-height);
-               }
-
-               .app-image {
-                       width: var(--default-clickable-area);
-                       height: auto;
-                       text-align: end;
-               }
-
-               .app-image-icon svg,
-               .app-image-icon .icon-settings-dark {
-                       margin-top: 5px;
-                       width: 20px;
-                       height: 20px;
-                       opacity: .5;
-                       background-size: cover;
-                       display: inline-block;
-               }
-
-               .app-name {
-                       padding: 0 var(--app-item-padding);
-               }
-
-               .app-name--link {
-                       height: var(--app-item-height);
-                       display: flex;
-                       align-items: center;
-               }
-
-               // Note: because of Safari bug, we cannot position link overlay relative to the table row
-               // So we need to manually position it relative to the table container and cell
-               // See: https://bugs.webkit.org/show_bug.cgi?id=240961
-               .app-name--link::after {
-                       content: '';
-                       position: absolute;
-                       inset-inline: 0;
-                       height: var(--app-item-height);
-               }
-
-               .app-actions {
-                       display: flex;
-                       gap: var(--app-item-padding);
-                       flex-wrap: wrap;
-                       justify-content: end;
-
-                       .icon-loading-small {
-                               display: inline-block;
-                               top: 4px;
-                               margin-inline-end: 10px;
-                       }
-               }
-
-               /* hide app version and level on narrower screens */
-               @media only screen and (max-width: 900px) {
-                       .app-version,
-                       .app-level {
-                               display: none;
-                       }
-               }
-
-               /* Hide actions on a small screen. Click on app opens fill-screen sidebar with the buttons */
-               @media only screen and (max-width: math.div(variables.$breakpoint-mobile, 2)) {
-                       .app-actions {
-                               display: none;
-                       }
-               }
-       }
-
-       &--store-view {
-               padding: 30px;
-
-               .app-image-icon .icon-settings-dark {
-                       width: 100%;
-                       height: 150px;
-                       background-size: 45px;
-                       opacity: 0.5;
-               }
-
-               .app-image-icon svg {
-                       position: absolute;
-                       bottom: 43px;
-                       /* position halfway vertically */
-                       width: 64px;
-                       height: 64px;
-                       opacity: .1;
-               }
-
-               .app-name {
-                       margin: 5px 0;
-               }
-
-               .app-name--link::after {
-                       content: '';
-                       position: absolute;
-                       inset-block: 0;
-                       inset-inline: 0;
-               }
-
-               .app-actions {
-                       margin: 10px 0;
-               }
-
-               @media only screen and (min-width: 1601px) {
-                       width: 25%;
-
-                       &.app-item--with-sidebar {
-                               width: 33%;
-                       }
-               }
-
-               @media only screen and (max-width: 1600px) {
-                       width: 25%;
-
-                       &.app-item--with-sidebar {
-                               width: 33%;
-                       }
-               }
-
-               @media only screen and (max-width: 1400px) {
-                       width: 33%;
-
-                       &.app-item--with-sidebar {
-                               width: 50%;
-                       }
-               }
-
-               @media only screen and (max-width: 900px) {
-                       width: 50%;
-
-                       &.app-item--with-sidebar {
-                               width: 100%;
-                       }
-               }
-
-               @media only screen and (max-width: variables.$breakpoint-mobile) {
-                       width: 50%;
-               }
-
-               @media only screen and (max-width: 480px) {
-                       width: 100%;
-               }
-       }
-}
-
-.app-icon {
-       filter: var(--background-invert-if-bright);
-}
-
-.app-image {
-       position: relative;
-       height: 150px;
-       opacity: 1;
-       overflow: hidden;
-
-       img {
-               width: 100%;
-       }
-}
-
-.app-version {
-       color: var(--color-text-maxcontrast);
-}
-</style>
diff --git a/apps/settings/src/components/AppStore/AppItem/AppItem.vue b/apps/settings/src/components/AppStore/AppItem/AppItem.vue
new file mode 100644 (file)
index 0000000..09dc8f8
--- /dev/null
@@ -0,0 +1,235 @@
+<!--
+  - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+  - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+       <component :is="listView ? 'tr' : (inline ? 'article' : 'li')"
+               class="app-item"
+               :class="{
+                       'app-item--list-view': listView,
+                       'app-item--store-view': !listView,
+                       'app-item--selected': isSelected,
+                       'app-item--with-sidebar': withSidebar,
+               }">
+               <AppItemIcon :app="app"
+                       :list-view="listView"
+                       :headers="getDataItemHeaders('app-table-col-icon')" />
+
+               <AppItemName :app="app"
+                       :category="category"
+                       class="app-item__name"
+                       :list-view="listView"
+                       :headers="getDataItemHeaders('app-table-col-name')" />
+
+               <component :is="dataItemTag"
+                       v-if="!listView"
+                       class="app-summary"
+                       :headers="getDataItemHeaders(`app-version`)">
+                       {{ app.summary }}
+               </component>
+
+               <component :is="dataItemTag"
+                       v-if="listView"
+                       class="app-version"
+                       :headers="getDataItemHeaders(`app-table-col-version`)">
+                       <span v-if="app.version">{{ app.version }}</span>
+                       <span v-else-if="app.appstoreData.releases[0].version">{{ app.appstoreData.releases[0].version }}</span>
+               </component>
+
+               <component :is="dataItemTag" :headers="getDataItemHeaders(`app-table-col-level`)" class="app-level">
+                       <AppLevelBadge :level="app.level" />
+                       <AppScore v-if="hasRating && !listView" :score="app.score" />
+               </component>
+
+               <AppItemActions v-if="!inline"
+                       v-show="!isSmallMobile"
+                       :app="app"
+                       :headers="getDataItemHeaders('app-table-col-actions')" />
+       </component>
+</template>
+
+<script setup lang="ts">
+import type { PropType } from 'vue'
+import type { IAppstoreApp } from '../../../app-types'
+
+import { useIsSmallMobile } from '@nextcloud/vue/dist/Composables/useIsMobile.js'
+import { useRoute } from 'vue-router/composables'
+import { computed } from 'vue'
+
+import AppItemActions from './AppItemActions.vue'
+import AppItemIcon from './AppItemIcon.vue'
+import AppItemName from './AppItemName.vue'
+import AppScore from '../AppScore.vue'
+import AppLevelBadge from '../AppLevelBadge.vue'
+
+const props = defineProps({
+       /**
+        * The app to show
+        */
+       app: {
+               type: Object as PropType<IAppstoreApp>,
+               required: true,
+       },
+
+       /**
+        * The category the app is shown in
+        */
+       category: {
+               type: String,
+               required: true,
+       },
+
+       /**
+        * If list view is used (table vs grid)
+        */
+       listView: {
+               type: Boolean,
+               default: false,
+       },
+
+       /**
+        * If this app is shown as part of a bundle
+        */
+       useBundleView: {
+               type: Boolean,
+               default: false,
+       },
+
+       /**
+        * Headers table cell -> table header mapping to be set on every cell
+        */
+       headers: {
+               type: String,
+               default: null,
+       },
+
+       /**
+        * To render the app item as inline content without any wrapper list / grid.
+        * E.g. for the app discover section.
+        */
+       inline: {
+               type: Boolean,
+               default: false,
+       },
+})
+
+const route = useRoute()
+const isSmallMobile = useIsSmallMobile()
+
+/**
+ * The HTML tag to use.
+ */
+const dataItemTag = computed(() => props.listView ? 'td' : 'div')
+
+/**
+ * If the rating should be shown.
+ * This is true if at least five rating were sent.
+ */
+const hasRating = computed(() => props.app.appstoreData?.ratingNumOverall > 5)
+
+/**
+ * Is the sidebar shown.
+ * The sidebar is always shown if an app is selected.
+ */
+const withSidebar = computed(() => Boolean(route.params.id))
+
+/**
+ * Is this app is the currently selected app
+ */
+const isSelected = computed(() => route.params.id === props.app.id)
+
+/**
+ * Set table header association to a table cell.
+ * If the list view is not used this returns `undefined`.
+ *
+ * @param columnName The column name
+ */
+function getDataItemHeaders(columnName: string) {
+       return props.useBundleView ? [props.headers, columnName].join(' ') : undefined
+}
+</script>
+
+<style scoped lang="scss">
+@use '../../../../../../core/css/variables.scss' as variables;
+@use 'sass:math';
+
+.app-item {
+       position: relative;
+
+       &:hover {
+               background-color: var(--color-background-dark);
+       }
+
+       &--list-view {
+               --app-item-padding: calc(var(--default-grid-baseline) * 2);
+               --app-item-height: calc(var(--default-clickable-area) + var(--app-item-padding) * 2);
+
+               &.app-item--selected {
+                       background-color: var(--color-background-dark);
+               }
+
+               > * {
+                       vertical-align: middle;
+                       border-bottom: 1px solid var(--color-border);
+                       padding: var(--app-item-padding);
+                       height: var(--app-item-height);
+               }
+
+               /* hide app version and level on narrower screens */
+               @media only screen and (max-width: 900px) {
+                       .app-version,
+                       .app-level {
+                               display: none;
+                       }
+               }
+       }
+
+       &--store-view {
+               padding: 30px;
+
+               @media only screen and (min-width: 1601px) {
+                       width: 25%;
+
+                       &.app-item--with-sidebar {
+                               width: 33%;
+                       }
+               }
+
+               @media only screen and (max-width: 1600px) {
+                       width: 25%;
+
+                       &.app-item--with-sidebar {
+                               width: 33%;
+                       }
+               }
+
+               @media only screen and (max-width: 1400px) {
+                       width: 33%;
+
+                       &.app-item--with-sidebar {
+                               width: 50%;
+                       }
+               }
+
+               @media only screen and (max-width: 900px) {
+                       width: 50%;
+
+                       &.app-item--with-sidebar {
+                               width: 100%;
+                       }
+               }
+
+               @media only screen and (max-width: variables.$breakpoint-mobile) {
+                       width: 50%;
+               }
+
+               @media only screen and (max-width: 480px) {
+                       width: 100%;
+               }
+       }
+}
+
+.app-version {
+       color: var(--color-text-maxcontrast);
+}
+</style>
diff --git a/apps/settings/src/components/AppStore/AppItem/AppItemName.vue b/apps/settings/src/components/AppStore/AppItem/AppItemName.vue
new file mode 100644 (file)
index 0000000..8270cc1
--- /dev/null
@@ -0,0 +1,67 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<script setup lang="ts">
+import type { IAppstoreApp } from '../../../app-types'
+import { computed } from 'vue'
+
+const props = defineProps<{
+       app: IAppstoreApp
+       category: string
+       listView: boolean
+}>()
+
+/**
+ * The HTML tag to use - depending on the list vs grid view
+ */
+const tag = computed(() => props.listView ? 'td' : 'div')
+</script>
+
+<template>
+       <component :is="tag" class="app-item-name">
+               <router-link class="app-item-name__link"
+                       :to="{
+                               name: 'apps-details',
+                               params: {
+                                       category: category,
+                                       id: app.id
+                               },
+                       }">
+                       {{ app.name }}
+               </router-link>
+       </component>
+</template>
+
+<style scoped lang="scss">
+.app-item-name {
+       margin: calc(2 * var(--default-grid-baseline)) 0;
+
+       &__link::after {
+               content: '';
+               position: absolute;
+               inset-block: 0;
+               inset-inline: 0;
+       }
+
+       // The list view
+       &--list-view {
+               margin: 0;
+               padding: 0 var(--app-item-padding);
+
+               .app-item-name__link {
+                       height: var(--app-item-height);
+                       display: flex;
+                       align-items: center;
+
+                       // Note: because of Safari bug, we cannot position link overlay relative to the table row
+                       // So we need to manually position it relative to the table container and cell
+                       // See: https://bugs.webkit.org/show_bug.cgi?id=240961
+                       &::after {
+                               height: var(--app-item-height);
+                       }
+               }
+       }
+}
+</style>