Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle shortcuts in appsections VO-777 #2680

Merged
merged 1 commit into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 9 additions & 0 deletions react/AppIcon/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import React, { Component } from 'react'
import { withClient } from 'cozy-client'

import styles from './styles.styl'
import { isShortcutFile } from '../AppSections/helpers'
import Icon, { iconPropType } from '../Icon'
import CubeIcon from '../Icons/Cube'
import { ShortcutTile } from '../ShortcutTile'
import palette from '../palette'
import { AppDoctype } from '../proptypes'

Expand Down Expand Up @@ -45,6 +47,9 @@ export class AppIcon extends Component {
fetchIcon() {
const { app, type, priority, client } = this.props

// Shortcut files used in cozy-store have their own icon in their doctype metadata
if (isShortcutFile(app)) return

return client.getStackClient().getIconURL({
type,
slug: app.slug || app,
Expand Down Expand Up @@ -93,6 +98,10 @@ export class AppIcon extends Component {
const { alt, className, fallbackIcon } = this.props
const { icon, status } = this.state

if (isShortcutFile(this.props.app)) {
return <ShortcutTile file={this.props.app} />
}

switch (status) {
case FETCHING:
return (
Expand Down
49 changes: 48 additions & 1 deletion react/AppSections/Sections.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import cx from 'classnames'
import PropTypes from 'prop-types'
import React, { Component } from 'react'

import flag from 'cozy-flags'
import { useExtendI18n } from 'cozy-ui/transpiled/react/providers/I18n'

import styles from './Sections.styl'
import * as catUtils from './categories'
import AppsSection from './components/AppsSection'
import DropdownFilter from './components/DropdownFilter'
import { APP_TYPE } from './constants'
import { generateI18nConfig } from './generateI18nConfig'
import { isShortcutFile } from './helpers'
import en from './locales/en.json'
import fr from './locales/fr.json'
import * as searchUtils from './search'
Expand Down Expand Up @@ -105,12 +110,19 @@ export class Sections extends Component {
const webAppGroups = catUtils.groupApps(
filteredApps.filter(a => a.type === APP_TYPE.WEBAPP)
)
const shortcutsGroups = catUtils.groupApps(
filteredApps.filter(a => isShortcutFile(a))
)

const webAppsCategories = Object.keys(webAppGroups)
.map(cat => catUtils.addLabel({ value: cat }, t))
.sort(catUtils.sorter)
const konnectorsCategories = Object.keys(konnectorGroups)
.map(cat => catUtils.addLabel({ value: cat }, t))
.sort(catUtils.sorter)
const shortcutsCategories = Object.keys(shortcutsGroups)
.map(cat => catUtils.addLabel({ value: cat }, t))
.sort(catUtils.sorter)

const dropdownDisplayed =
hasNav && (isMobile || isTablet) && showFilterDropdown
Expand Down Expand Up @@ -154,6 +166,33 @@ export class Sections extends Component {
})}
</div>
)}
{!!shortcutsCategories.length && (
<div>
{showSubTitles && (
<SectionSubtitle>{t('sections.shortcuts')}</SectionSubtitle>
)}

{shortcutsCategories.map(cat => {
return (
<AppsSection
key={cat.value}
{...componentsProps?.appsSection}
appsList={shortcutsGroups[cat.value]}
subtitle={
showSubSubTitles ? (
<SectionSubSubtitle>{cat.label}</SectionSubSubtitle>
) : null
}
IconComponent={IconComponent}
onAppClick={onAppClick}
displaySpecificMaintenanceStyle={
displaySpecificMaintenanceStyle
}
/>
)
})}
</div>
)}
{!!konnectorsCategories.length && (
<div>
{showSubTitles && (
Expand Down Expand Up @@ -186,6 +225,14 @@ export class Sections extends Component {
}
}

const SectionsWrapper = props => {
const config = flag('store.alternative-source')
const i18nConfig = generateI18nConfig(config?.categories)
useExtendI18n(i18nConfig)

return <Sections {...props} />
}

Sections.propTypes = {
t: PropTypes.func.isRequired,

Expand Down Expand Up @@ -230,6 +277,6 @@ Sections.defaultProps = {
})
}

export const Untranslated = withBreakpoints()(Sections)
export const Untranslated = withBreakpoints()(SectionsWrapper)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on pourrait remplacer withBreakpoints par useBreakpoints au passage ?

Pourquoi utiliser useExtendI18n en plus de withLocales ? Quel problème on résout grâce à generateI18nConfig ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on reçoit les localisations au runtime, on ne les a pas au moment du build dans cette feature

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vu ensemble, les locales sont dans les flags. Ici on récupère la technique employée sur Store. Il serait plus judicieux de faire un getI18nFromFlag ou un useI18nFromFlag (qui utiliserait useExtendI18n) pour gérer ça plus globalement. Vu l'échéance, on décale ce refacto à une autre PR @acezard


export default withLocales(locales)(translate()(Untranslated))
10 changes: 10 additions & 0 deletions react/AppSections/__snapshots__/index.spec.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,11 @@ exports[`AppsSection component should render dropdown filter on mobile if no nav
"type": "webapp",
"value": "partners",
},
Object {
"label": "Shortcuts",
"secondary": false,
"value": "shortcuts",
},
Object {
"label": "Services",
"secondary": false,
Expand Down Expand Up @@ -1476,6 +1481,11 @@ exports[`AppsSection component should render dropdown filter on tablet if no nav
"type": "webapp",
"value": "partners",
},
Object {
"label": "Shortcuts",
"secondary": false,
"value": "shortcuts",
},
Object {
"label": "Services",
"secondary": false,
Expand Down
46 changes: 35 additions & 11 deletions react/AppSections/categories.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,37 @@ export const groupApps = apps => multiGroupBy(apps, getAppCategory)
* Alphabetical sort on label except for
* - 'all' value always at the beginning
* - 'others' value always at the end
* - 'cozy' value should be near the beginning, right after 'all'
* - items of type 'file' should appear alphabetically between 'webapp' and 'konnector'
*
* @param {CategoryOption} categoryA
* @param {CategoryOption} categoryB
* @return {Number}
*/
export const sorter = (categoryA, categoryB) => {
return (
(categoryA.value === 'all' && -1) ||
(categoryB.value === 'all' && 1) ||
(categoryA.value === 'others' && 1) ||
(categoryB.value === 'others' && -1) ||
(categoryA.value === 'cozy' && -1) ||
(categoryB.value === 'cozy' && 1) ||
categoryA.label.localeCompare(categoryB.label)
)
// Always keep 'all' at the beginning
if (categoryA.value === 'all') return -1
if (categoryB.value === 'all') return 1

// Always keep 'others' at the end
if (categoryA.value === 'others') return 1
if (categoryB.value === 'others') return -1

// Keep 'cozy' near the beginning, right after 'all'
if (categoryA.value === 'cozy') return -1
if (categoryB.value === 'cozy') return 1

// Sort by type order: webapp < file < konnector
const typeOrder = ['webapp', 'file', 'konnector']
const typeAIndex = typeOrder.indexOf(categoryA.type)
const typeBIndex = typeOrder.indexOf(categoryB.type)

if (typeAIndex !== typeBIndex) {
return typeAIndex - typeBIndex
}

// Alphabetical sort on label for the rest
return categoryA.label.localeCompare(categoryB.label)
}

export const addLabel = (cat, t) => ({
Expand Down Expand Up @@ -82,7 +98,7 @@ export const generateOptionsFromApps = (apps, options = {}) => {
]
: []

for (const type of [APP_TYPE.WEBAPP, APP_TYPE.KONNECTOR]) {
for (const type of [APP_TYPE.WEBAPP, APP_TYPE.FILE, APP_TYPE.KONNECTOR]) {
const catApps = groupApps(apps.filter(a => a.type === type))
// Add an entry to filter by all konnectors
if (type === APP_TYPE.KONNECTOR) {
Expand All @@ -93,11 +109,19 @@ export const generateOptionsFromApps = (apps, options = {}) => {
})
)
}
if (type === APP_TYPE.FILE) {
allCategoryOptions.push(
addLabel({
value: 'shortcuts',
secondary: false
})
)
}
const categoryOptions = Object.keys(catApps).map(cat => {
return addLabel({
value: cat,
type: type,
secondary: type === APP_TYPE.KONNECTOR
secondary: type === APP_TYPE.KONNECTOR || type === APP_TYPE.FILE
})
})

Expand Down
6 changes: 6 additions & 0 deletions react/AppSections/categories.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ describe('generateOptionsFromApps', () => {
type: 'webapp',
value: 'others'
},
{ label: 'Shortcuts', secondary: false, value: 'shortcuts' },
{
label: 'Services',
secondary: false,
Expand Down Expand Up @@ -156,6 +157,11 @@ describe('generateOptionsFromApps', () => {
type: 'webapp',
value: 'others'
},
{
label: 'Shortcuts',
secondary: false,
value: 'shortcuts'
},
{
label: 'Services',
secondary: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Array [
"title": "Cozy Photos",
},
Object {
"developer": null,
"developer": "By undefined",
"status": "Update available",
"title": "Tasky",
},
Expand Down
7 changes: 6 additions & 1 deletion react/AppSections/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export const APP_TYPE = {
KONNECTOR: 'konnector',
WEBAPP: 'webapp'
WEBAPP: 'webapp',
FILE: 'file'
}

export const APP_CLASS = {
SHORTCUT: 'shortcut'
}
23 changes: 23 additions & 0 deletions react/AppSections/generateI18nConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const generateI18nConfig = (categories?: {
[key: string]: string
}): {
en: Record<string, string>
fr: Record<string, string>
} => {
if (!categories) return { en: {}, fr: {} }

const i18nConfig: Record<string, string> = {}

for (const [key, value] of Object.entries(categories)) {
// Extract the final part of the path as the display name
const displayName =
value?.split('/').pop() ?? ''.replace(/([A-Z])/g, ' $1').trim()

i18nConfig[`app_categories.${key}`] = displayName
}

return {
en: i18nConfig,
fr: i18nConfig
}
}
8 changes: 8 additions & 0 deletions react/AppSections/helpers.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import _get from 'lodash/get'

import { APP_CLASS, APP_TYPE } from './constants'

export const getTranslatedManifestProperty = (app, path, t) => {
if (!t || !app || !path) return _get(app, path, '')
return t(`apps.${app.slug}.${path}`, {
_: _get(app, path, '')
})
}

export const isShortcutFile = app => {
if (!app) return false

return app.type === APP_TYPE.FILE && app.class === APP_CLASS.SHORTCUT
}
6 changes: 4 additions & 2 deletions react/AppSections/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
"tech": "Tech",
"telecom": "Telecom",
"transport": "Transportation",
"pro": "Work"
"pro": "Work",
"shortcuts": "Shortcuts"
},
"sections": {
"applications": "Applications",
"konnectors": "Services"
"konnectors": "Services",
"shortcuts": "Shortcuts"
}
}
6 changes: 4 additions & 2 deletions react/AppSections/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
"tech": "Tech",
"telecom": "Mobile",
"transport": "Voyage et transport",
"pro": "Travail"
"pro": "Travail",
"shortcuts": "Raccourcis"
},
"sections": {
"applications": "Applications",
"konnectors": "Services"
"konnectors": "Services",
"shortcuts": "Raccourcis"
}
}
7 changes: 4 additions & 3 deletions react/AppTile/AppTile.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import { render } from '@testing-library/react'
import React from 'react'

import CozyClient, { CozyProvider } from 'cozy-client'
import CozyClient from 'cozy-client'

import AppTile from '.'
import en from '../AppSections/locales/en.json'
import DemoProvider from '../providers/DemoProvider'
import I18n from '../providers/I18n'

const appMock = {
Expand Down Expand Up @@ -41,11 +42,11 @@ const appMock2 = {
const client = new CozyClient({})
const Wrapper = props => {
return (
<CozyProvider client={client}>
<DemoProvider client={client}>
<I18n dictRequire={() => en} lang="en">
<AppTile {...props} />
</I18n>
</CozyProvider>
</DemoProvider>
)
}

Expand Down
Loading