diff --git a/en/cozy-home/src/components/KonnectorHelpers.ts b/en/cozy-home/src/components/KonnectorHelpers.ts new file mode 100644 index 000000000..7c674a0bc --- /dev/null +++ b/en/cozy-home/src/components/KonnectorHelpers.ts @@ -0,0 +1,49 @@ +import { + IOCozyAccount, + IOCozyTrigger, + IOCozyKonnector +} from 'cozy-client/types/types' + +export const STATUS = { + OK: 0, + MAINTENANCE: 2, + ERROR: 3, + NO_ACCOUNT: 4, + LOADING: 5 +} as const + +/** + * Get accounts from triggers + * @param {IOCozyAccount[]} accounts + * @param {IOCozyTrigger[]} triggers + * @returns {IOCozyAccount[]} + */ +export const getAccountsFromTrigger = ( + accounts: IOCozyAccount[], + triggers: IOCozyTrigger[] +): IOCozyAccount[] => { + const triggerAccountIds = triggers.map(trigger => trigger.message.account) + const matchingAccounts = Object.values(accounts).filter(account => + triggerAccountIds.includes(account._id) + ) + return matchingAccounts +} + +/** + * Get triggers by slug + * @param {IOCozyTrigger[]} triggers + * @param {IOCozyKonnector['slug']} slug + * @returns {IOCozyTrigger[]} + */ +export function getTriggersBySlug( + triggers: IOCozyTrigger[], + slug: IOCozyKonnector['slug'] +): IOCozyTrigger[] { + return Object.values(triggers).filter(trigger => { + return ( + trigger.message && + trigger.message.konnector && + trigger.message.konnector === slug + ) + }) +} diff --git a/en/cozy-home/src/components/KonnectorTile.jsx b/en/cozy-home/src/components/KonnectorTile.jsx index 958a7777f..28a94e0ec 100644 --- a/en/cozy-home/src/components/KonnectorTile.jsx +++ b/en/cozy-home/src/components/KonnectorTile.jsx @@ -8,6 +8,12 @@ import { getErrorLocaleBound, KonnectorJobError } from 'cozy-harvest-lib' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import { generateWebLink, useClient } from 'cozy-client' +import { + STATUS, + getAccountsFromTrigger, + getTriggersBySlug +} from 'components/KonnectorHelpers' + /** * * @param {object} param @@ -25,20 +31,25 @@ const getKonnectorError = ({ error, lang, konnector }) => { return getErrorLocaleBound(konnError, konnector, lang, 'title') } -export const STATUS = { - OK: 0, - MAINTENANCE: 2, - ERROR: 3, - NO_ACCOUNT: 4, - LOADING: 5 -} +/** + * @typedef {Object} StatusMap + * @property {string} [STATUS.NO_ACCOUNT] - The status when there is no account, represented as 'ghost'. + * @property {string} [STATUS.MAINTENANCE] - The status when under maintenance, represented as 'maintenance'. + * @property {string} [STATUS.ERROR] - The status when there is an error, represented as 'error'. + * @property {string} [STATUS.LOADING] - The status when loading, represented as 'loading'. + */ +/** + * A mapping of status constants to their corresponding string representations. + * @type {StatusMap} + */ const statusMap = { [STATUS.NO_ACCOUNT]: 'ghost', [STATUS.MAINTENANCE]: 'maintenance', [STATUS.ERROR]: 'error', [STATUS.LOADING]: 'loading' } + /** * * @param {object} props @@ -63,21 +74,6 @@ export const getKonnectorStatus = ({ else return STATUS.OK } -/** - * @param {object} triggers - * @param {import('cozy-client/types/types').IOCozyTrigger} triggers.trigger - io.cozy.triggers object - * @param {import('cozy-client/types/types').IOCozyKonnector['slug']} slug - * @returns - */ -function getTriggersBySlug(triggers, slug) { - return Object.values(triggers).filter(trigger => { - return ( - trigger.message && - trigger.message.konnector && - trigger.message.konnector === slug - ) - }) -} /** * @param {import('cozy-client/types/types').IOCozyTrigger[]} triggers - io.cozy.triggers object * @param {object} jobs @@ -99,19 +95,7 @@ function getErrorForTriggers(triggers, jobs) { } return null } -/** - * - * @param {import('cozy-client/types/types').IOCozyAccount[]} accounts - * @param {import('cozy-client/types/types').IOCozyTrigger[]} triggers - * @returns - */ -const getAccountsFromTrigger = (accounts, triggers) => { - const triggerAccountIds = triggers.map(trigger => trigger.message.account) - const matchingAccounts = Object.values(accounts).filter(account => - triggerAccountIds.includes(account.id) - ) - return matchingAccounts -} + /** * * @param {import('cozy-client/types/types').IOCozyTrigger[]} triggers diff --git a/en/cozy-home/src/components/KonnectorTile.spec.jsx b/en/cozy-home/src/components/KonnectorTile.spec.jsx index 463e34a67..b247ce652 100644 --- a/en/cozy-home/src/components/KonnectorTile.spec.jsx +++ b/en/cozy-home/src/components/KonnectorTile.spec.jsx @@ -7,11 +7,8 @@ import { render } from '@testing-library/react' import CozyTheme from 'cozy-ui/transpiled/react/providers/CozyTheme' import { createMockClient } from 'cozy-client/dist/mock' -import { - KonnectorTile, - getKonnectorStatus, - STATUS -} from 'components/KonnectorTile' +import { KonnectorTile, getKonnectorStatus } from 'components/KonnectorTile' +import { STATUS } from 'components/KonnectorHelpers' import AppLike from 'test/AppLike' jest.mock('react-router-dom', () => ({ diff --git a/en/cozy-home/src/components/Sections/SectionAppGroup.tsx b/en/cozy-home/src/components/Sections/SectionAppGroup.tsx index 7a9a34736..3f8c0b071 100644 --- a/en/cozy-home/src/components/Sections/SectionAppGroup.tsx +++ b/en/cozy-home/src/components/Sections/SectionAppGroup.tsx @@ -1,18 +1,22 @@ import React from 'react' import get from 'lodash/get' +import cx from 'classnames' import type { IOCozyFile, IOCozyKonnector } from 'cozy-client/types/types' import { nameToColor } from 'cozy-ui/react/Avatar/helpers' import Typography from 'cozy-ui/transpiled/react/Typography' import Grid from 'cozy-ui/transpiled/react/Grid' import AppIcon from 'cozy-ui/transpiled/react/AppIcon' +import { STATUS } from 'components/KonnectorHelpers' interface SectionAppGroupProps { items: IOCozyFile[] | IOCozyKonnector[] } interface SectionAppTileProps { - item: IOCozyFile | IOCozyKonnector + item: (IOCozyFile | IOCozyKonnector) & { + status?: number + } } const typedNameToColor = nameToColor as (name: string) => string @@ -20,14 +24,15 @@ const typedNameToColor = nameToColor as (name: string) => string const SectionAppTile = ({ item }: SectionAppTileProps): JSX.Element => { const icon = get(item, 'attributes.metadata.icon') as string const iconMimeType = get(item, 'attributes.metadata.iconMimeType') as string - return ( {item.type === 'konnector' ? ( ) : icon ? ( { - const installedKonnectorNames = new Set(installedKonnectors.map(k => k.name)) - const maintenanceSlugs = new Set( - appsAndKonnectorsInMaintenance.map(k => k.slug) - ) - - const sections = Object.keys(data).map(key => { - const allItems = data[key] || [] - const availableItems = allItems.filter( - item => !maintenanceSlugs.has(item.slug) - ) - const installedItems = availableItems.filter(item => - installedKonnectorNames.has(item.name) - ) - const suggestedItems = availableItems.filter(item => - suggestedKonnectors.some(k => k.slug === item.slug) - ) - const items = - installedItems.length > 0 - ? [...installedItems, ...suggestedItems] - : availableItems - - return { - name: key, - items, - id: key, - type: 'category', - layout: { - originalName: key, - createdByApp: '', - mobile: { detailedLines: false, grouped: true }, - desktop: { detailedLines: false, grouped: true }, - order: 0 - }, - pristine: installedItems.length === 0 - } - }) - - sections.sort((a, b) => { - if (!a.pristine && b.pristine) { - return -1 - } - if (a.pristine && !b.pristine) { - return 1 - } - return a.name.localeCompare(b.name) - }) +import { + makeAccountsQuery, + konnectorsConn, + suggestedKonnectorsConn, + makeTriggersQuery +} from 'queries' +import { formatServicesSections } from 'components/Sections/lib/formatServicesSections' + +const _makeTriggersQuery = makeTriggersQuery as { + definition: () => QueryDefinition + options: Record +} - return sections +const _makeAccountsQuery = makeAccountsQuery as { + definition: () => QueryDefinition + options: Record } export const useKonnectorsByCat = (): Section[] => { const client = useClient() - const [groupedData, setGroupedData] = - useState<{ [key: string]: IOCozyKonnector[] }>() - const konnectors: IOCozyKonnector[] = - useSelector( - getInstalledKonnectors as ( - state: Record - ) => IOCozyKonnector[] - ) || [] + const { t } = useI18n() + + const { data: allTriggers } = useQuery( + _makeTriggersQuery.definition(), + _makeTriggersQuery.options + ) as { + data: IOCozyTrigger[] + } + const { data: accounts } = useQuery( + _makeAccountsQuery.definition(), + _makeAccountsQuery.options + ) as { + data: IOCozyAccount[] + } + + const { data: konnectors } = useQuery( + konnectorsConn.query, + konnectorsConn + ) as { + data: IOCozyKonnector[] + } const appsAndKonnectorsInMaintenance = ( useAppsInMaintenance as unknown as () => IOCozyApp[] )() - const installedKonnectors = sortBy(konnectors, konnector => - konnector.name.toLowerCase() + const installedKonnectors = useMemo( + () => sortBy(konnectors, konnector => konnector.name.toLowerCase()), + [konnectors] ) const suggestedKonnectorsQuery = useQuery( suggestedKonnectorsConn.query, suggestedKonnectorsConn - ) as { - data: IOCozyKonnector[] - } + ) as { data: IOCozyKonnector[] } - const candidatesSlugBlacklist = appsAndKonnectorsInMaintenance - .map(({ slug }) => slug) - .concat(installedKonnectors.map(({ slug }) => slug)) + const candidatesSlugBlacklist = useMemo( + () => + appsAndKonnectorsInMaintenance + .map(({ slug }) => slug) + .concat(installedKonnectors.map(({ slug }) => slug)), + [appsAndKonnectorsInMaintenance, installedKonnectors] + ) const suggestedKonnectors = useMemo(() => { return suggestedKonnectorsQuery.data @@ -104,33 +89,41 @@ export const useKonnectorsByCat = (): Section[] => { : [] }, [suggestedKonnectorsQuery.data, candidatesSlugBlacklist]) + const [groupedData, setGroupedData] = + useState<{ [key: string]: IOCozyKonnector[] }>() + useEffect(() => { const fetchData = async (): Promise => { if (!client) return const grouped = await fetchAllKonnectors(client) setGroupedData(grouped) } - void fetchData() }, [client]) - const sortedData = useMemo( - () => + const sortedData = useCallback( + (): Section[] => groupedData - ? transformAndSortData( + ? formatServicesSections( groupedData, installedKonnectors, suggestedKonnectors, - appsAndKonnectorsInMaintenance + appsAndKonnectorsInMaintenance, + t, + allTriggers, + accounts ) : [], [ - appsAndKonnectorsInMaintenance, groupedData, installedKonnectors, - suggestedKonnectors + suggestedKonnectors, + appsAndKonnectorsInMaintenance, + t, + allTriggers, + accounts ] ) - return sortedData + return sortedData() } diff --git a/en/cozy-home/src/components/Sections/lib/formatServicesSections.ts b/en/cozy-home/src/components/Sections/lib/formatServicesSections.ts new file mode 100644 index 000000000..221587ec5 --- /dev/null +++ b/en/cozy-home/src/components/Sections/lib/formatServicesSections.ts @@ -0,0 +1,123 @@ +import { + IOCozyKonnector, + IOCozyTrigger, + IOCozyAccount +} from 'cozy-client/types/types' + +import { + STATUS, + getAccountsFromTrigger, + getTriggersBySlug +} from 'components/KonnectorHelpers' +import { Section } from 'components/Sections/SectionsTypes' +import config from 'components/Sections/config.json' + +// Helper functions +const isItemInMaintenance = ( + maintenanceSlugs: Set, + item: IOCozyKonnector +): boolean => !maintenanceSlugs.has(item.slug) + +const getItemStatus = (accountsForKonnector: IOCozyAccount[]): number => + accountsForKonnector && accountsForKonnector.length > 0 + ? STATUS.OK + : STATUS.NO_ACCOUNT + +const sortItemsByStatusAndName = ( + a: IOCozyKonnector & { status?: number }, + b: IOCozyKonnector & { status?: number } +): number => { + if (a.status === STATUS.OK && b.status !== STATUS.OK) return -1 + if (a.status !== STATUS.OK && b.status === STATUS.OK) return 1 + if (a.status === STATUS.NO_ACCOUNT && b.status !== STATUS.NO_ACCOUNT) + return -1 + if (a.status !== STATUS.NO_ACCOUNT && b.status === STATUS.NO_ACCOUNT) return 1 + return a.name.localeCompare(b.name) +} + +const shouldIncludeSection = (section: Section, whitelist: string[]): boolean => + whitelist.includes(section.name) || !section.pristine + +const sortSections = ( + a: Section, + b: Section, + t: (key: string) => string +): number => { + if (!a.pristine && b.pristine) return -1 + if (a.pristine && !b.pristine) return 1 + return t(`category.${a.name}`).localeCompare(t(`category.${b.name}`)) +} + +// New helper function for processing and sorting items within a category +const processAndSortItems = ( + items: IOCozyKonnector[], + allTriggers: IOCozyTrigger[], + accounts: IOCozyAccount[] +): IOCozyKonnector[] => { + return items + .map(item => { + const triggers = getTriggersBySlug(allTriggers, item.slug) + const accountsForKonnector = getAccountsFromTrigger(accounts, triggers) + return { + ...item, + status: getItemStatus(accountsForKonnector) + } + }) + .sort(sortItemsByStatusAndName) +} + +// Main transformation function +export const formatServicesSections = ( + data: { [key: string]: (IOCozyKonnector & { status?: string })[] }, + installedKonnectors: IOCozyKonnector[], + suggestedKonnectors: IOCozyKonnector[], + appsAndKonnectorsInMaintenance: IOCozyKonnector[], + t: (key: string) => string, + allTriggers: IOCozyTrigger[], + accounts: IOCozyAccount[] +): Section[] => { + const installedKonnectorNames = new Set(installedKonnectors.map(k => k.name)) + const maintenanceSlugs = new Set( + appsAndKonnectorsInMaintenance.map(k => k.slug) + ) + + const sections: Section[] = Object.keys(data).map(key => { + const allItems = data[key] || [] + const availableItems = allItems.filter(item => + isItemInMaintenance(maintenanceSlugs, item) + ) + const installedItems = availableItems.filter(item => + installedKonnectorNames.has(item.name) + ) + const suggestedItems = availableItems.filter(item => + suggestedKonnectors.some(k => k.slug === item.slug) + ) + const itemsToSort = + installedItems.length > 0 + ? [...installedItems, ...suggestedItems] + : availableItems + + const sortedItems = processAndSortItems(itemsToSort, allTriggers, accounts) + + return { + name: key, + items: sortedItems, + id: key, + type: 'category', + layout: { + originalName: key, + createdByApp: '', + mobile: { detailedLines: false, grouped: true }, + desktop: { detailedLines: false, grouped: true }, + order: 0 + }, + pristine: installedItems.length === 0 + } + }) + + return sections + .filter(section => + shouldIncludeSection(section, config.categoriesWhitelist) + ) + .sort((a, b) => sortSections(a, b, t)) +} diff --git a/en/cozy-home/src/queries.js b/en/cozy-home/src/queries.js index 7bd122a68..1bdecae9a 100644 --- a/en/cozy-home/src/queries.js +++ b/en/cozy-home/src/queries.js @@ -14,6 +14,22 @@ export const konnectorsConn = { fetchPolicy: defaultFetchPolicy } +export const makeTriggersQuery = { + definition: () => Q('io.cozy.triggers'), + options: { + as: 'io.cozy.triggers', + fetchPolicy: defaultFetchPolicy + } +} + +export const makeAccountsQuery = { + definition: () => Q('io.cozy.accounts'), + options: { + as: 'io.cozy.accounts', + fetchPolicy: defaultFetchPolicy + } +} + export const instanceSettingsConn = { query: Q('io.cozy.settings').getById('io.cozy.settings.instance'), as: 'io.cozy.settings/io.cozy.settings.instance', diff --git a/en/cozy-home/src/styles/lists.styl b/en/cozy-home/src/styles/lists.styl index bb8d56b3b..a2107d793 100644 --- a/en/cozy-home/src/styles/lists.styl +++ b/en/cozy-home/src/styles/lists.styl @@ -39,6 +39,9 @@ width rem(12) height rem(12) + &.ghost + filter saturate(0) + a text-decoration none -webkit-tap-highlight-color transparent // https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color diff --git a/en/cozy-konnector-libs/cli/index.html b/en/cozy-konnector-libs/cli/index.html index a2feb3314..2d223b194 100644 --- a/en/cozy-konnector-libs/cli/index.html +++ b/en/cozy-konnector-libs/cli/index.html @@ -1961,26 +1961,6 @@