From 2e92b271867119ebbffe4e83762be9f126e8cb3c Mon Sep 17 00:00:00 2001 From: Antonin Cezard Date: Wed, 14 Aug 2024 15:26:01 +0200 Subject: [PATCH] feat: handle shortcuttiles --- react/AppIcon/index.jsx | 9 +++ react/AppSections/Sections.jsx | 49 ++++++++++++++- .../__snapshots__/index.spec.jsx.snap | 10 +++ react/AppSections/categories.js | 46 ++++++++++---- react/AppSections/categories.spec.js | 6 ++ .../__snapshots__/AppsSection.spec.jsx.snap | 2 +- react/AppSections/constants.js | 7 ++- react/AppSections/generateI18nConfig.ts | 23 +++++++ react/AppSections/helpers.js | 8 +++ react/AppSections/locales/en.json | 6 +- react/AppSections/locales/fr.json | 6 +- react/AppTile/AppTile.spec.jsx | 7 ++- react/AppTile/index.jsx | 43 +++++++++---- react/AppTile/locales/en.json | 3 +- react/AppTile/locales/fr.json | 3 +- react/ShortcutTile/index.tsx | 63 +++++++++++++++++++ react/types.d.ts | 4 ++ 17 files changed, 261 insertions(+), 34 deletions(-) create mode 100644 react/AppSections/generateI18nConfig.ts create mode 100644 react/ShortcutTile/index.tsx create mode 100644 react/types.d.ts diff --git a/react/AppIcon/index.jsx b/react/AppIcon/index.jsx index aa13b6efee..54a1388342 100644 --- a/react/AppIcon/index.jsx +++ b/react/AppIcon/index.jsx @@ -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' @@ -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, @@ -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 + } + switch (status) { case FETCHING: return ( diff --git a/react/AppSections/Sections.jsx b/react/AppSections/Sections.jsx index 3e15fae4a3..fe46081b8d 100644 --- a/react/AppSections/Sections.jsx +++ b/react/AppSections/Sections.jsx @@ -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' @@ -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 @@ -154,6 +166,33 @@ export class Sections extends Component { })} )} + {!!shortcutsCategories.length && ( +
+ {showSubTitles && ( + {t('sections.shortcuts')} + )} + + {shortcutsCategories.map(cat => { + return ( + {cat.label} + ) : null + } + IconComponent={IconComponent} + onAppClick={onAppClick} + displaySpecificMaintenanceStyle={ + displaySpecificMaintenanceStyle + } + /> + ) + })} +
+ )} {!!konnectorsCategories.length && (
{showSubTitles && ( @@ -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.propTypes = { t: PropTypes.func.isRequired, @@ -230,6 +277,6 @@ Sections.defaultProps = { }) } -export const Untranslated = withBreakpoints()(Sections) +export const Untranslated = withBreakpoints()(SectionsWrapper) export default withLocales(locales)(translate()(Untranslated)) diff --git a/react/AppSections/__snapshots__/index.spec.jsx.snap b/react/AppSections/__snapshots__/index.spec.jsx.snap index 97758b875a..67e6d4e4d9 100644 --- a/react/AppSections/__snapshots__/index.spec.jsx.snap +++ b/react/AppSections/__snapshots__/index.spec.jsx.snap @@ -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, @@ -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, diff --git a/react/AppSections/categories.js b/react/AppSections/categories.js index a4b95ce091..3b5ba43b32 100644 --- a/react/AppSections/categories.js +++ b/react/AppSections/categories.js @@ -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) => ({ @@ -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) { @@ -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 }) }) diff --git a/react/AppSections/categories.spec.js b/react/AppSections/categories.spec.js index ae7b235d78..93c61b4f1d 100644 --- a/react/AppSections/categories.spec.js +++ b/react/AppSections/categories.spec.js @@ -104,6 +104,7 @@ describe('generateOptionsFromApps', () => { type: 'webapp', value: 'others' }, + { label: 'Shortcuts', secondary: false, value: 'shortcuts' }, { label: 'Services', secondary: false, @@ -156,6 +157,11 @@ describe('generateOptionsFromApps', () => { type: 'webapp', value: 'others' }, + { + label: 'Shortcuts', + secondary: false, + value: 'shortcuts' + }, { label: 'Services', secondary: false, diff --git a/react/AppSections/components/__snapshots__/AppsSection.spec.jsx.snap b/react/AppSections/components/__snapshots__/AppsSection.spec.jsx.snap index ec14d0b8ef..e2c6fa593c 100644 --- a/react/AppSections/components/__snapshots__/AppsSection.spec.jsx.snap +++ b/react/AppSections/components/__snapshots__/AppsSection.spec.jsx.snap @@ -28,7 +28,7 @@ Array [ "title": "Cozy Photos", }, Object { - "developer": null, + "developer": "By undefined", "status": "Update available", "title": "Tasky", }, diff --git a/react/AppSections/constants.js b/react/AppSections/constants.js index 7ae0aef3c3..577e939a7f 100644 --- a/react/AppSections/constants.js +++ b/react/AppSections/constants.js @@ -1,4 +1,9 @@ export const APP_TYPE = { KONNECTOR: 'konnector', - WEBAPP: 'webapp' + WEBAPP: 'webapp', + FILE: 'file' +} + +export const APP_CLASS = { + SHORTCUT: 'shortcut' } diff --git a/react/AppSections/generateI18nConfig.ts b/react/AppSections/generateI18nConfig.ts new file mode 100644 index 0000000000..f4b338a145 --- /dev/null +++ b/react/AppSections/generateI18nConfig.ts @@ -0,0 +1,23 @@ +export const generateI18nConfig = (categories?: { + [key: string]: string +}): { + en: Record + fr: Record +} => { + if (!categories) return { en: {}, fr: {} } + + const i18nConfig: Record = {} + + 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 + } +} diff --git a/react/AppSections/helpers.js b/react/AppSections/helpers.js index 110a56c455..048b4580c8 100644 --- a/react/AppSections/helpers.js +++ b/react/AppSections/helpers.js @@ -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 +} diff --git a/react/AppSections/locales/en.json b/react/AppSections/locales/en.json index 198cb7eb71..694c629d54 100644 --- a/react/AppSections/locales/en.json +++ b/react/AppSections/locales/en.json @@ -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" } } diff --git a/react/AppSections/locales/fr.json b/react/AppSections/locales/fr.json index 996bbf6b84..764ea63aea 100644 --- a/react/AppSections/locales/fr.json +++ b/react/AppSections/locales/fr.json @@ -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" } } diff --git a/react/AppTile/AppTile.spec.jsx b/react/AppTile/AppTile.spec.jsx index 85369d8795..7c06f3869a 100644 --- a/react/AppTile/AppTile.spec.jsx +++ b/react/AppTile/AppTile.spec.jsx @@ -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 = { @@ -41,11 +42,11 @@ const appMock2 = { const client = new CozyClient({}) const Wrapper = props => { return ( - + en} lang="en"> - + ) } diff --git a/react/AppTile/index.jsx b/react/AppTile/index.jsx index 84af82609b..6596208496 100644 --- a/react/AppTile/index.jsx +++ b/react/AppTile/index.jsx @@ -7,8 +7,10 @@ import en from './locales/en.json' import fr from './locales/fr.json' import styles from './styles.styl' import AppIcon from '../AppIcon' +import { isShortcutFile } from '../AppSections/helpers.js' import Icon from '../Icon' import WrenchCircleIcon from '../Icons/WrenchCircle' +import { ShortcutTile } from '../ShortcutTile' import Tile, { TileTitle, TileSubtitle, @@ -18,6 +20,7 @@ import Tile, { } from '../Tile' import palette from '../palette' import { AppDoctype } from '../proptypes' +import useBreakpoints from '../providers/Breakpoints' import { createUseI18n } from '../providers/I18n' const locales = { en, fr } @@ -47,16 +50,30 @@ export const AppTile = ({ IconComponent: IconComponentProp, displaySpecificMaintenanceStyle }) => { - const name = nameProp || app.name const { t } = useI18n() const { developer = {} } = app + const { isMobile } = useBreakpoints() + + const name = nameProp || app.name + const statusLabel = getCurrentStatusLabel(app) - const statusToDisplay = Array.isArray(showStatus) - ? showStatus.indexOf(statusLabel) > -1 && statusLabel - : showStatus && statusLabel + + const isStatusArray = Array.isArray(showStatus) + + const statusToDisplay = + isShortcutFile(app) && statusLabel === APP_STATUS.installed && isMobile + ? 'favorite' + : isStatusArray + ? showStatus.indexOf(statusLabel) > -1 && statusLabel + : showStatus && statusLabel + const IconComponent = IconComponentProp || AppIcon + const isInMaintenanceWithSpecificDisplay = displaySpecificMaintenanceStyle && statusLabel === APP_STATUS.maintenance + const tileSubtitle = isShortcutFile(app) + ? app.metadata?.source + : developer.name return ( - + {isShortcutFile(app) ? ( + + ) : ( + + )} {isInMaintenanceWithSpecificDisplay && ( {namePrefix ? `${namePrefix} ${name}` : name} - {developer.name && showDeveloper && ( - {`${t('app_item.by')} ${developer.name}`} + {showDeveloper && ( + {`${t('app_item.by')} ${tileSubtitle}`} )} {statusToDisplay && ( diff --git a/react/AppTile/locales/en.json b/react/AppTile/locales/en.json index f42122281c..e47c19e692 100644 --- a/react/AppTile/locales/en.json +++ b/react/AppTile/locales/en.json @@ -3,6 +3,7 @@ "by": "By", "installed": "Installed", "maintenance": "In maintenance", - "update": "Update available" + "update": "Update available", + "favorite": "Added to home page" } } diff --git a/react/AppTile/locales/fr.json b/react/AppTile/locales/fr.json index 223598be09..0d9a5e6857 100644 --- a/react/AppTile/locales/fr.json +++ b/react/AppTile/locales/fr.json @@ -3,6 +3,7 @@ "by": "Par", "installed": "Installée", "maintenance": "En maintenance", - "update": "Mise à jour dispo." + "update": "Mise à jour dispo.", + "favorite": "Ajouté sur la page d'accueil" } } diff --git a/react/ShortcutTile/index.tsx b/react/ShortcutTile/index.tsx new file mode 100644 index 0000000000..a5452d733c --- /dev/null +++ b/react/ShortcutTile/index.tsx @@ -0,0 +1,63 @@ +import React from 'react' + +import { IOCozyFile } from 'cozy-client/types/types' +// @ts-expect-error +import { nameToColor } from 'cozy-ui/react/Avatar/helpers' +// @ts-expect-error +import Typography from 'cozy-ui/react/Typography' + +import styles from '../AppTile/styles.styl' +import { TileIcon } from '../Tile' +import { makeStyles } from '../styles' + +interface ShortcutTileProps { + file: Partial & { + name: string + attributes?: { metadata?: { icon?: string; iconMimeType?: string } } + } +} + +const useStyles = makeStyles({ + letter: { + color: 'white', + margin: 'auto' + }, + letterWrapper: { + backgroundColor: ({ name }: { name: string }) => + (nameToColor as (name: string) => string)(name) + } +}) + +export const ShortcutTile = ({ file }: ShortcutTileProps): JSX.Element => { + const classes = useStyles({ name: file.name }) + const icon = file.attributes?.metadata?.icon + const iconMimeType = file.attributes?.metadata?.iconMimeType + + if (icon) { + return ( + + {file.name} + + ) + } + + return ( + +
+ + {file.name[0].toUpperCase()} + +
+
+ ) +} diff --git a/react/types.d.ts b/react/types.d.ts new file mode 100644 index 0000000000..3c830dd422 --- /dev/null +++ b/react/types.d.ts @@ -0,0 +1,4 @@ +declare module '*.styl' { + const classes: Record + export default classes +}