Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(settings): Implement new app discover section for app management
Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux authored and Altahrim committed Mar 14, 2024
commit 4cadb828502dce74f8ce41f85c21fceb15954cf6
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<template>
<div class="app-discover">
<NcEmptyContent v-if="hasError"
:name="t('settings', 'Nothing to show')"
:description="t('settings', 'Could not load section content from app store.')">
<template #icon>
<NcIconSvgWrapper :path="mdiEyeOff" :size="64" />
</template>
</NcEmptyContent>
<NcEmptyContent v-else-if="elements.length === 0"
:name="t('settings', 'Loading')"
:description="t('settings', 'Fetching the latest news…')">
<template #icon>
<NcLoadingIcon :size="64" />
</template>
</NcEmptyContent>
<template v-else>
<component :is="getComponent(entry.type)"
v-for="entry, index in elements"
:key="entry.id ?? index"
v-bind="entry" />
</template>
</div>
</template>

<script setup lang="ts">
import type { IAppDiscoverElements } from '../../constants/AppDiscoverTypes.ts'

import { mdiEyeOff } from '@mdi/js'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { defineAsyncComponent, defineComponent, onBeforeMount, ref } from 'vue'

import axios from '@nextcloud/axios'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'

import logger from '../../logger'

const PostType = defineAsyncComponent(() => import('./PostType.vue'))

const hasError = ref(false)
const elements = ref<IAppDiscoverElements[]>([])

/**
* Shuffle using the Fisher-Yates algorithm
* @param array The array to shuffle (in place)
*/
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]]
}
return array
}

/**
* Load the app discover section information
*/
onBeforeMount(async () => {
try {
const { data } = await axios.get<IAppDiscoverElements[]>(generateUrl('/settings/api/apps/discover'))
elements.value = shuffleArray(data)
} catch (error) {
hasError.value = true
logger.error(error as Error)
showError(t('settings', 'Could not load app discover section'))
}
})

const getComponent = (type) => {
if (type === 'post') {
return PostType
}
return defineComponent({
mounted: () => logger.error('Unknown component requested ', type),
render: (h) => h('div', t('settings', 'Could not render element')),
})
}
</script>

<style scoped lang="scss">
.app-discover {
max-width: 1008px; /* 900px + 2x 54px padding for the carousel controls */
margin-inline: auto;
padding-inline: 54px;
/* Padding required to make last element not bound to the bottom */
padding-block-end: var(--default-clickable-area, 44px);

display: flex;
flex-direction: column;
gap: var(--default-clickable-area, 44px);
}
</style>
81 changes: 81 additions & 0 deletions apps/settings/src/components/AppStoreDiscover/PostType.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<article class="app-discover-post"
:class="{ 'app-discover-post--reverse': media && media.alignment === 'start' }">
<div v-if="headline || text" class="app-discover-post__text">
<h3>{{ translatedHeadline }}</h3>
<p>{{ translatedText }}</p>
</div>
<div v-if="media">
<img class="app-discover-post__media" :alt="mediaAlt" :src="mediaSource">
</div>
</article>
</template>

<script setup lang="ts">
import { getLanguage } from '@nextcloud/l10n'
import { computed } from 'vue'

type ILocalizedValue<T> = Record<string, T | undefined> & { en: T }

const props = defineProps<{
type: string

headline: ILocalizedValue<string>
text: ILocalizedValue<string>
link?: string
media: {
alignment: 'start'|'end'
content: ILocalizedValue<{ src: string, alt: string}>
}
}>()

const language = getLanguage()

const getLocalizedValue = <T, >(dict: ILocalizedValue<T>) => dict[language] ?? dict[language.split('_')[0]] ?? dict.en

const translatedText = computed(() => getLocalizedValue(props.text))
const translatedHeadline = computed(() => getLocalizedValue(props.headline))

const localizedMedia = computed(() => getLocalizedValue(props.media.content))

const mediaSource = computed(() => localizedMedia.value?.src)
const mediaAlt = ''
</script>

<style scoped lang="scss">
.app-discover-post {
width: 100%;
background-color: var(--color-primary-element-light);
border-radius: var(--border-radius-rounded);

display: flex;
flex-direction: row;
&--reverse {
flex-direction: row-reverse;
}

h3 {
font-size: 24px;
font-weight: 600;
margin-block: 0 1em;
}

&__text {
padding: var(--border-radius-rounded);
}

&__media {
max-height: 300px;
max-width: 450px;
border-radius: var(--border-radius-rounded);
border-end-start-radius: 0;
border-start-start-radius: 0;
}

&--reverse &__media {
border-radius: var(--border-radius-rounded);
border-end-end-radius: 0;
border-start-end-radius: 0;
}
}
</style>
1 change: 1 addition & 0 deletions apps/settings/src/constants/AppsConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { translate as t } from '@nextcloud/l10n'

/** Enum of verification constants, according to Apps */
export const APPS_SECTION_ENUM = Object.freeze({
discover: t('settings', 'Discover'),
installed: t('settings', 'Your apps'),
enabled: t('settings', 'Active apps'),
disabled: t('settings', 'Disabled apps'),
Expand Down
2 changes: 2 additions & 0 deletions apps/settings/src/constants/AppstoreCategoryIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
mdiOpenInApp,
mdiSecurity,
mdiStar,
mdiStarCircleOutline,
mdiStarShooting,
mdiTools,
mdiViewDashboard,
Expand All @@ -49,6 +50,7 @@ import {
*/
export default Object.freeze({
// system special categories
discover: mdiStarCircleOutline,
installed: mdiAccount,
enabled: mdiCheck,
disabled: mdiClose,
Expand Down
42 changes: 24 additions & 18 deletions apps/settings/src/views/AppStore.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
<template>
<!-- Apps list -->
<NcAppContent class="app-settings-content"
:page-heading="pageHeading">
<NcEmptyContent v-if="isLoading"
:page-heading="appStoreLabel">
<h2 class="app-settings-content__label" v-text="viewLabel" />

<AppStoreDiscoverSection v-if="currentCategory === 'discover'" />
<NcEmptyContent v-else-if="isLoading"
class="empty-content__loading"
:name="t('settings', 'Loading app list')">
<template #icon>
Expand All @@ -38,36 +41,31 @@

<script setup lang="ts">
import { translate as t } from '@nextcloud/l10n'
import { computed, getCurrentInstance, onBeforeMount, watch } from 'vue'
import { computed, getCurrentInstance, onBeforeMount, watchEffect } from 'vue'
import { useRoute } from 'vue-router/composables'
import { APPS_SECTION_ENUM } from '../constants/AppsConstants.js'

import { useAppsStore } from '../store/apps-store'
import { APPS_SECTION_ENUM } from '../constants/AppsConstants'

import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import AppList from '../components/AppList.vue'
import AppStoreDiscoverSection from '../components/AppStoreDiscover/AppStoreDiscoverSection.vue'

const route = useRoute()
const store = useAppsStore()

/**
* ID of the current active category, default is `installed`
* ID of the current active category, default is `discover`
*/
const currentCategory = computed(() => route.params?.category ?? 'installed')
const currentCategory = computed(() => route.params?.category ?? 'discover')

/**
* The H1 to be used on the website
*/
const pageHeading = computed(() => {
if (currentCategory.value in APPS_SECTION_ENUM) {
return APPS_SECTION_ENUM[currentCategory.value]
}
const category = store.getCategoryById(currentCategory.value)
return category?.displayName ?? t('settings', 'Apps')
})
watch([pageHeading], () => {
window.document.title = `${pageHeading.value} - Apps - Nextcloud`
const appStoreLabel = t('settings', 'App Store')
const viewLabel = computed(() => APPS_SECTION_ENUM[currentCategory.value] ?? store.getCategoryById(currentCategory.value)?.displayName ?? appStoreLabel)

watchEffect(() => {
window.document.title = `${viewLabel.value} - ${appStoreLabel} - Nextcloud`
})

// TODO this part should be migrated to pinia
Expand All @@ -87,4 +85,12 @@ onBeforeMount(() => {
.empty-content__loading {
height: 100%;
}

.app-settings-content__label {
margin-block-start: var(--app-navigation-padding);
margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2);
min-height: var(--default-clickable-area);
line-height: var(--default-clickable-area);
vertical-align: center;
}
</style>
10 changes: 9 additions & 1 deletion apps/settings/src/views/AppStoreNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
<!-- Categories & filters -->
<NcAppNavigation :aria-label="t('settings', 'Apps')">
<template #list>
<NcAppNavigationItem id="app-category-your-apps"
<NcAppNavigationItem id="app-category-discover"
:to="{ name: 'apps' }"
:exact="true"
:name="APPS_SECTION_ENUM.discover">
<template #icon>
<NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.discover" />
</template>
</NcAppNavigationItem>
<NcAppNavigationItem id="app-category-installed"
:to="{ name: 'apps-category', params: { category: 'installed'} }"
:exact="true"
:name="APPS_SECTION_ENUM.installed">
<template #icon>
<NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.installed" />
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/settings/apps.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('Settings: App management', { testIsolation: true }, () => {
// I am logged in as the admin
cy.login(admin)
// I open the Apps management
cy.visit('/settings/apps')
cy.visit('/settings/apps/installed')
})

it('Can enable an installed app', () => {
Expand Down