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 && (
@@ -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..13d78b570e
--- /dev/null
+++ b/react/ShortcutTile/index.tsx
@@ -0,0 +1,61 @@
+import React from 'react'
+
+import { IOCozyFile } from 'cozy-client/types/types'
+import { nameToColor } from 'cozy-ui/react/Avatar/helpers'
+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 (
+
+
+
+ )
+ }
+
+ 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
+}