diff --git a/config/webpack.config.cozy-home.js b/config/webpack.config.cozy-home.js index abb3d9f374..f28da29ef1 100644 --- a/config/webpack.config.cozy-home.js +++ b/config/webpack.config.cozy-home.js @@ -64,12 +64,6 @@ module.exports = { } }, plugins: [ - environment === 'development' - ? new webpack.ProvidePlugin({ - 'cozy.client': 'cozy-client-js/dist/cozy-client.js' - }) - : null, - new ContextReplacementPlugin( /moment[/\\]locale$/, regexpMomentDateFnsLocales diff --git a/jest.config.js b/jest.config.js index e262afe837..9c2d926862 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,6 @@ module.exports = { moduleDirectories: ['src', 'node_modules'], moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'styl'], moduleNameMapper: { - '^lib/redux-cozy-client$': '/src/lib/redux-cozy-client', '\\.(png|gif|jpe?g|svg|css)$': '/test/__mocks__/fileMock.js', '.styl$': 'identity-obj-proxy', '^cozy-client$': 'cozy-client/dist/index', diff --git a/manifest.webapp b/manifest.webapp index 222a496f79..2537cfa6da 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -113,7 +113,14 @@ { "action": "CREATE", "type": ["io.cozy.accounts"], - "href": "/intents" + "href": "/intents", + "data": ["slug"] + }, + { + "action": "VIEW", + "type": ["io.cozy.accounts"], + "href": "/intents", + "data": ["slug", "accountId"] }, { "action": "REDIRECT", diff --git a/package.json b/package.json index c14a9c7da3..888e648dfd 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "cozy-harvest-lib": "^21.0.0", "cozy-intent": "^2.3.0", "cozy-keys-lib": "^6.0.0", + "cozy-interapp": "^0.8.1", "cozy-logger": "1.10.0", "cozy-realtime": "4.2.9", "cozy-sharing": "10.0.0", @@ -68,6 +69,7 @@ "redux-logger": "3.0.6", "redux-persist": "^6.0.0", "redux-thunk": "2.3.0", + "reselect": "^4.1.8", "terser-webpack-plugin": "1.4.4" }, "devDependencies": { @@ -80,7 +82,6 @@ "babel-jest": "27.5.1", "babel-preset-cozy-app": "2.0.2", "bundlemon": "1.3.2", - "cozy-client-js": "0.20.0", "cozy-scripts": "8.1.1", "eslint": "8.9.0", "eslint-config-cozy-app": "4.0.0", diff --git a/src/components/AppWrapper.jsx b/src/components/AppWrapper.jsx index 70fb98fcf8..a410f91d2c 100644 --- a/src/components/AppWrapper.jsx +++ b/src/components/AppWrapper.jsx @@ -1,5 +1,3 @@ -/* global __DEVELOPMENT__ */ - import React, { createContext } from 'react' import { Provider as ReduxProvider } from 'react-redux' import memoize from 'lodash/memoize' @@ -12,10 +10,6 @@ import CozyTheme from 'cozy-ui/transpiled/react/providers/CozyTheme' import { BreakpointsProvider } from 'cozy-ui/transpiled/react/providers/Breakpoints' import { PersistGate } from 'redux-persist/integration/react' -import { - CozyClient as LegacyCozyClient, - CozyProvider as LegacyCozyProvider -} from 'lib/redux-cozy-client' import configureStore from 'store/configureStore' import homeConfig from 'config/home.json' import { RealtimePlugin } from 'cozy-realtime' @@ -52,23 +46,14 @@ export const setupAppContext = memoize(() => { ? true : false }) - const legacyClient = new LegacyCozyClient({ - cozyURL: `//${data.cozyDomain}`, - token: data.cozyToken, - cozyClient - }) + cozyClient.registerPlugin(flag.plugin) cozyClient.registerPlugin(RealtimePlugin) // store - const { store, persistor } = configureStore( - legacyClient, - cozyClient, - context, - { - lang, - ...homeConfig - } - ) + const { store, persistor } = configureStore(cozyClient, context, { + lang, + ...homeConfig + }) cozyClient.setStore(store) return { cozyClient, store, data, lang, context, persistor } @@ -80,6 +65,7 @@ const Inner = ({ children, lang, context }) => ( + {process.env.NODE_ENV !== 'production' ? : null} ) @@ -103,34 +89,27 @@ const ThemeProvider = ({ children }) => { */ const AppWrapper = ({ children }) => { const appContext = setupAppContext() - const { store, cozyClient, data, context, lang, persistor } = appContext + const { store, cozyClient, context, lang, persistor } = appContext return ( - - - ( - - {children} - - )} - > - + + ( + {children} - - - - + + )} + > + + {children} + + + diff --git a/src/components/AppWrapper.spec.jsx b/src/components/AppWrapper.spec.jsx index 6a14d242e4..e95119d728 100644 --- a/src/components/AppWrapper.spec.jsx +++ b/src/components/AppWrapper.spec.jsx @@ -15,10 +15,7 @@ jest.mock('cozy-client', () => ({ ), default: () => mockClient })) -jest.mock('lib/redux-cozy-client', () => ({ - CozyClient: children => children, - CozyProvider: ({ children }) => children -})) + jest.mock('store/configureStore', () => () => ({ store: { dispatch: () => jest.fn(), diff --git a/src/components/Applications.jsx b/src/components/Applications.jsx index f6c30ee246..70dbf5135a 100644 --- a/src/components/Applications.jsx +++ b/src/components/Applications.jsx @@ -1,6 +1,6 @@ import React, { memo, useEffect, useRef } from 'react' import memoize from 'lodash/memoize' - +import uniqBy from 'lodash/uniqBy' import { useQuery } from 'cozy-client' import flag from 'cozy-flags' import Divider from 'cozy-ui/transpiled/react/Divider' @@ -47,8 +47,8 @@ const getApplicationsList = memoize(data => { !homeConfig.filteredApps.includes(app.slug) && !flag(`home_hidden_apps.${app.slug.toLowerCase()}`) // can be set in the context with `home_hidden_apps: - drive - banks`for example ) - - const array = apps.map(app => ) + const dedupapps = uniqBy(apps, 'slug') + const array = dedupapps.map(app => ) array.push( diff --git a/src/components/BanksLink.jsx b/src/components/BanksLink.jsx deleted file mode 100644 index aa63b94584..0000000000 --- a/src/components/BanksLink.jsx +++ /dev/null @@ -1,46 +0,0 @@ -/* global cozy */ - -import React from 'react' - -import AppLinker from 'cozy-ui/transpiled/react/AppLinker' -import Icon from 'cozy-ui/transpiled/react/Icon' -import styles from 'styles/konnectorSuccess.styl' -import OpenwithIcon from 'cozy-ui/transpiled/react/Icons/Openwith' - -import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' - -const BanksLink = ({ banksUrl }) => { - const { t } = useI18n() - - return banksUrl ? ( - - {({ href, onClick, name }) => ( - - - {t('account.success.banksLinkText', { - appName: name - })} - - )} - - ) : ( - - cozy.client.intents.redirect('io.cozy.apps', { slug: 'banks' }, url => { - window.top.location.href = url - }) - } - > - - {t('account.success.banksLinkText')} - - ) -} - -export default BanksLink diff --git a/src/components/Banners/UpdateMessage.jsx b/src/components/Banners/UpdateMessage.jsx index 28836d1fe0..f72691fd4c 100644 --- a/src/components/Banners/UpdateMessage.jsx +++ b/src/components/Banners/UpdateMessage.jsx @@ -1,22 +1,23 @@ -/* global cozy */ - import React, { useState, useCallback } from 'react' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import Button from 'cozy-ui/transpiled/react/deprecated/Button' import Infos from 'cozy-ui/transpiled/react/deprecated/Infos' import Typography from 'cozy-ui/transpiled/react/Typography' import PropTypes from 'prop-types' +import { useClient } from 'cozy-client' +import Intents from 'cozy-interapp' const UpdateMessage = props => { const [isRedirecting, setIsRedirecting] = useState(false) const { t } = useI18n() const { isBlocking, konnector } = props - + const client = useClient() + const intents = new Intents({ client }) const handleRedirectToStore = useCallback(async () => { setIsRedirecting(true) try { - await cozy.client.intents.redirect('io.cozy.apps', { + await intents.redirect('io.cozy.apps', { slug: konnector.slug, step: 'update' }) diff --git a/src/components/IntentRedirect.jsx b/src/components/IntentRedirect.jsx index 1d7029fd4e..a2808014d8 100644 --- a/src/components/IntentRedirect.jsx +++ b/src/components/IntentRedirect.jsx @@ -1,19 +1,35 @@ -/* global cozy */ import React from 'react' import { Navigate, useSearchParams } from 'react-router-dom' -import { connect } from 'react-redux' - -import { getInstalledKonnectors } from 'reducers' - -const IntentRedirect = ({ installedKonnectors }) => { +import { + useClient, + useQuery, + isQueryLoading, + hasQueryBeenLoaded +} from 'cozy-client' +import Intents from 'cozy-interapp' +import { konnectorsConn } from 'queries' +const IntentRedirect = () => { + const client = useClient() + const intents = new Intents({ client }) const [searchParams] = useSearchParams() const queryConnector = searchParams.get('konnector') const queryAccount = searchParams.get('account') + const konnectorsDataResult = useQuery(konnectorsConn.query, konnectorsConn) + + if ( + isQueryLoading(konnectorsDataResult) && + !hasQueryBeenLoaded(konnectorsDataResult) + ) + return null if (!queryConnector) return - if (!installedKonnectors.find(konnector => konnector.slug === queryConnector)) - return cozy.client.intents.redirect('io.cozy.apps', { + if ( + !konnectorsDataResult.data.find( + konnector => konnector.slug === queryConnector + ) + ) + return intents.redirect('io.cozy.apps', { slug: queryConnector }) @@ -24,8 +40,4 @@ const IntentRedirect = ({ installedKonnectors }) => { return } -const mapStateToProps = state => ({ - installedKonnectors: getInstalledKonnectors(state) -}) - -export default connect(mapStateToProps)(IntentRedirect) +export default IntentRedirect diff --git a/src/components/Konnector.jsx b/src/components/Konnector.jsx index c8ebfe0886..db98aee01e 100644 --- a/src/components/Konnector.jsx +++ b/src/components/Konnector.jsx @@ -51,7 +51,7 @@ export const StatelessKonnector = ({ konnector, triggers, slug }) => { } const StatefulKonnector = connect((state, { slug }) => ({ - konnector: getKonnector(state.oldcozy, slug), + konnector: getKonnector(state.cozy, slug), triggers: getTriggersByKonnector(state, slug) }))(StatelessKonnector) diff --git a/src/components/KonnectorErrors.spec.jsx b/src/components/KonnectorErrors.spec.jsx deleted file mode 100644 index 37da816a79..0000000000 --- a/src/components/KonnectorErrors.spec.jsx +++ /dev/null @@ -1,263 +0,0 @@ -import React from 'react' -import MockDate from 'mockdate' -import { KonnectorErrors } from './KonnectorErrors' -import AppLike from 'test/AppLike' -import { render, fireEvent } from '@testing-library/react' - -jest.mock('cozy-ui/transpiled/react/AppIcon', () => () => null) - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => jest.fn() -})) - -describe('KonnectorErrors', () => { - const MOCKED_DATE = '2020-01-08T09:49:23.589Z' - beforeAll(() => { - MockDate.set(MOCKED_DATE) - }) - - afterAll(() => { - jest.restoreAllMocks() - MockDate.reset() - }) - - const mockClient = { - save: jest.fn() - } - - const DEFAULT_INSTALLED_KONNECTORS = [ - { slug: 'test', name: 'Test Konnector' } - ] - - const setup = ({ - triggersInError = [], - accountsWithErrors = [], - installedKonnectors = DEFAULT_INSTALLED_KONNECTORS - } = {}) => { - const root = render( - - - - ) - return { root } - } - - it('should render divider when there are no errors', () => { - const { root } = setup() - expect(root.container).toMatchSnapshot() - }) - - it('should render divider when there are errors but no installed konnector', () => { - const triggersInError = [ - { - _id: '2', - worker: 'konnector', - current_state: { - last_error: 'MUTED_ERROR', - last_success: '2019-10-01T00:48:01.404911778Z' - }, - message: { - konnector: 'test', - account: '456' - } - } - ] - const { root } = setup({ triggersInError }) - - expect(root.container).toMatchSnapshot() - }) - - it('should render divider when all errors are muted', () => { - const triggersInError = [ - { - _id: '2', - worker: 'konnector', - current_state: { - last_error: 'MUTED_ERROR', - last_success: '2019-10-01T00:48:01.404911778Z' - }, - message: { - konnector: 'test', - account: '456' - } - } - ] - const accountsWithErrors = [ - { - _id: '456', - mutedErrors: [ - { - type: 'MUTED_ERROR', - mutedAt: '2019-12-01T00:48:01.404911778Z' - } - ] - } - ] - const installedKonnectors = [{ slug: 'test', name: 'Test Konnector' }] - - const { root } = setup({ - triggersInError, - accountsWithErrors, - installedKonnectors - }) - - expect(root.container).toMatchSnapshot() - }) - - it('should render active errors', async () => { - const triggersInError = [ - { - _id: '1', - worker: 'konnector', - current_state: { - last_error: 'LOGIN_FAILED' - }, - message: { - konnector: 'test', - account: '123' - } - }, - { - _id: '2', - worker: 'konnector', - current_state: { - last_error: 'LOGIN_FAILED.NEEDS_SECRET', // this one is muted - last_success: '2019-10-01T00:48:01.404911778Z' - }, - message: { - konnector: 'test', - account: '456' - } - }, - { - _id: '3', - worker: 'konnector', - current_state: { - last_error: 'USER_ACTION_NEEDED' - }, - message: { - konnector: 'test', - account: '123' - } - }, - { - _id: '4', - worker: 'konnector', - current_state: { - last_error: 'VENDOR_DOWN' // This type of error is not displayed - }, - message: { - konnector: 'test', - account: '123' - } - } - ] - const accountsWithErrors = [ - { _id: '123', mutedErrors: [] }, - { - _id: '456', - mutedErrors: [ - { - type: 'LOGIN_FAILED.NEEDS_SECRET', - mutedAt: '2019-12-01T00:48:01.404911778Z' - } - ] - } - ] - const installedKonnectors = [{ slug: 'test', name: 'Test Konnector' }] - const { root } = setup({ - triggersInError, - accountsWithErrors, - installedKonnectors - }) - - expect( - root.getByText('(1/2) Incorrect or expired credentials') - ).toBeTruthy() - - const dismissButton = root.getByLabelText('Mute error') - await fireEvent.click(dismissButton) - - expect(mockClient.save).toHaveBeenCalledWith({ - _id: '123', - mutedErrors: [{ mutedAt: MOCKED_DATE, type: 'LOGIN_FAILED' }] - }) - }) - - it('should hide errors when the konnector or account is missing', () => { - const triggersInError = [ - { - _id: '1', - worker: 'konnector', - current_state: { - last_error: 'LOGIN_FAILED' - }, - message: { - konnector: 'uninstalled', - account: '123' - } - }, - { - _id: '2', - worker: 'konnector', - current_state: { - last_error: 'LOGIN_FAILED' - }, - message: { - konnector: 'uninstalled', - account: 'no-account' - } - } - ] - const accountsWithErrors = [{ _id: '123', mutedErrors: [] }] - const installedKonnectors = [] - const { root } = setup({ - triggersInError, - accountsWithErrors, - installedKonnectors - }) - expect(root.container).toMatchSnapshot() - }) - - it('should not show slide indicator with only one slide', () => { - const triggersInError = [ - { - _id: '1', - worker: 'konnector', - current_state: { - last_error: 'LOGIN_FAILED' - }, - message: { - konnector: 'test', - account: '123' - } - } - ] - const accountsWithErrors = [ - { _id: '123', mutedErrors: [] }, - { - _id: '456', - mutedErrors: [ - { - type: 'LOGIN_FAILED.NEEDS_SECRET', - mutedAt: '2019-12-01T00:48:01.404911778Z' - } - ] - } - ] - const installedKonnectors = [{ slug: 'test', name: 'Test Konnector' }] - const { root } = setup({ - triggersInError, - accountsWithErrors, - installedKonnectors - }) - - // 1/1 is not displayed - expect(root.getByText('Incorrect or expired credentials')) - }) -}) diff --git a/src/components/KonnectorInstall.jsx b/src/components/KonnectorInstall.jsx index 68e05428c8..fcfb990c56 100644 --- a/src/components/KonnectorInstall.jsx +++ b/src/components/KonnectorInstall.jsx @@ -91,7 +91,7 @@ export class KonnectorInstall extends Component { } const mapStateToProps = (state, ownProps) => ({ - konnector: getKonnector(state.oldcozy, ownProps.connector.slug) + konnector: getKonnector(state.cozy, ownProps.connector.slug) }) export default translate()(connect(mapStateToProps)(KonnectorInstall)) diff --git a/src/components/KonnectorSuccess.spec.jsx b/src/components/KonnectorSuccess.spec.jsx deleted file mode 100644 index eca978fbd4..0000000000 --- a/src/components/KonnectorSuccess.spec.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' - -import AppLike from 'test/AppLike' -import KonnectorSuccess from './KonnectorSuccess' - -// @TODO: some pretty ugly mocks here because the app uses different react-redux versions -jest.mock('react-redux/lib/utils/Subscription', () => ({ - createSubscription: () => ({ - trySubscribe: () => jest.fn(), - tryUnsubscribe: () => jest.fn(), - notifyNestedSubs: () => jest.fn() - }) -})) - -jest.mock( - 'cozy-client/node_modules/react-redux/lib/utils/Subscription', - () => ({ - __esModule: true, - default: () => ({ - trySubscribe: () => jest.fn(), - tryUnsubscribe: () => jest.fn() - }) - }) -) - -describe('KonnectorSuccess', () => { - let trigger, connector - const fakeStore = { - banksUrl: 'https://example-banks.mycozy.cloud', - getState: () => ({}), - subscribe: () => ({}), - dispatch: () => ({}) - } - const setup = () => { - render( - - {}} - connector={connector} - trigger={trigger} - /> - - ) - } - - beforeEach(() => { - connector = {} - trigger = { message: {} } - }) - - it('should not show drive if trigger has no folder_to_save', () => { - setup() - expect( - screen.queryByText('Open the folder in Cozy Drive') - ).not.toBeInTheDocument() - }) - - it('should show drive if trigger has a folder_to_save', async () => { - trigger.message.folder_to_save = 'deadbeef' - setup() - expect( - await screen.findByText('Open the folder in Cozy Drive') - ).toBeInTheDocument() - }) - - it('should show banks if connector has datatypes with bankAccounts', async () => { - connector.data_types = ['bankAccounts'] - setup() - expect( - screen.queryByText('Open the folder in Cozy Drive') - ).not.toBeInTheDocument() - expect( - await screen.findByText('See my accounts in Cozy Banks') - ).toBeInTheDocument() - }) -}) diff --git a/src/components/KonnectorTile.jsx b/src/components/KonnectorTile.jsx index cfc60a452b..1f4780c27b 100644 --- a/src/components/KonnectorTile.jsx +++ b/src/components/KonnectorTile.jsx @@ -1,25 +1,27 @@ +// @ts-check import PropTypes from 'prop-types' import React from 'react' import { NavLink } from 'react-router-dom' -import { connect } from 'react-redux' - +import { useSelector } from 'react-redux' import SquareAppIcon from 'cozy-ui/transpiled/react/SquareAppIcon' import flag from 'cozy-flags' import { getErrorLocaleBound, KonnectorJobError } from 'cozy-harvest-lib' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' -import { getKonnectorTriggersCount } from 'reducers' -import { - getFirstError, - getFirstUserError, - getLastSyncDate -} from 'ducks/connections' - +/** + * + * @param {object} param + * @param {string|null|true|Error} param.error Error message + * @param {string} param.lang Lang (fr/en/es/...) + * @param {import('cozy-client/types/types').IOCozyKonnector} param.konnector + * @returns + */ const getKonnectorError = ({ error, lang, konnector }) => { - if (!error || !error.message) { + if (!error) { return null } - const konnError = new KonnectorJobError(error.message) + + const konnError = new KonnectorJobError(error) return getErrorLocaleBound(konnError, konnector, lang, 'title') } @@ -37,7 +39,16 @@ const statusMap = { [STATUS.ERROR]: 'error', [STATUS.LOADING]: 'loading' } - +/** + * + * @param {object} props + * @param {boolean} props.isInMaintenance Is in maintenance? + * @param {number} props.accountsCount Number of Accounts + * @param {boolean} props.error isInError + * @param {string?} props.userError user error + * @param {boolean} props.loading Loading status + * @returns {number} The status + */ export const getKonnectorStatus = ({ isInMaintenance, error, @@ -52,34 +63,120 @@ export const getKonnectorStatus = ({ else return STATUS.OK } -export const KonnectorTile = props => { +/** + * @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 + * @returns {null|true|string} + */ +function getErrorForTriggers(triggers, jobs) { + const triggersInError = triggers.filter( + t => t.current_state?.status === 'errored' + ) + if (triggersInError?.length > 0) { + const job = Object.values(jobs).find( + job => job.trigger_id === triggersInError[0]._id + ) + // we can have triggers without job? + if (!job) { + return true + } + return job.error + } + 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 + * @returns + */ +const getFirstUserError = triggers => { + const triggersInError = Object.values(triggers).filter( + t => t.current_state?.status === 'errored' + ) + const firstTriggerHavingUserError = Object.values(triggersInError).find( + trigger => { + const e = new KonnectorJobError(trigger.current_state?.last_error) + const isUserError = e.isUserError() + return isUserError + } + ) + return firstTriggerHavingUserError?.current_state + ? firstTriggerHavingUserError.current_state.last_error + : null +} +/** + * + * @param {object} props + * @param {boolean} props.isInMaintenance Is in maintenance + * @param {boolean} props.loading isLoading ? + * @param {import('cozy-client/types/types').IOCozyKonnector} props.konnector + * @returns + */ +export const KonnectorTile = ({ konnector, isInMaintenance, loading }) => { + const allTriggers = + // @ts-ignore + useSelector(state => state.cozy.documents['io.cozy.triggers']) || {} + const triggers = getTriggersBySlug(allTriggers, konnector.slug) + const userError = getFirstUserError(triggers) + // @ts-ignore + const jobs = useSelector(state => state.cozy.documents['io.cozy.jobs']) || {} + const accounts = + // @ts-ignore + useSelector(state => state.cozy.documents['io.cozy.accounts']) || {} + const accountsForKonnector = getAccountsFromTrigger(accounts, triggers) + const error = getErrorForTriggers(triggers, jobs) + const hasAtLeastOneError = error !== null + const { lang } = useI18n() - const { - accountsCount, - error, - isInMaintenance, - userError, - konnector, - loading - } = props const hideKonnectorErrors = flag('home.konnectors.hide-errors') // flag used for some demo instances where we want to ignore all konnector errors const status = hideKonnectorErrors ? STATUS.OK : getKonnectorStatus({ - accountsCount, - error, + accountsCount: accountsForKonnector.length, + error: hasAtLeastOneError, isInMaintenance, - konnector, loading, userError }) - + const errorToDisplay = !userError && userError !== null ? userError : error return ( { } KonnectorTile.propTypes = { - accountsCount: PropTypes.number, - error: PropTypes.object, isInMaintenance: PropTypes.bool, konnector: PropTypes.object, - userError: PropTypes.object -} - -const mapStateToProps = (state, props) => { - const { konnector } = props - - return { - accountsCount: getKonnectorTriggersCount(state, konnector), - // /!\ error can also be a userError. - error: getFirstError(state.connections, konnector.slug), - lastSyncDate: getLastSyncDate(state.connections, konnector.slug), - userError: getFirstUserError(state.connections, konnector.slug) - } + loading: PropTypes.bool } -export default connect(mapStateToProps)(KonnectorTile) +export default /* connect(mapStateToProps)( */ KonnectorTile // ) diff --git a/src/components/KonnectorTile.spec.jsx b/src/components/KonnectorTile.spec.jsx new file mode 100644 index 0000000000..3eb6fcec52 --- /dev/null +++ b/src/components/KonnectorTile.spec.jsx @@ -0,0 +1,208 @@ +'use strict' + +/* eslint-env jest */ + +import React from 'react' +import { render } from '@testing-library/react' +import MuiCozyTheme from 'cozy-ui/transpiled/react/MuiCozyTheme' +import { createMockClient } from 'cozy-client/dist/mock' + +import { + KonnectorTile, + getKonnectorStatus, + STATUS +} from 'components/KonnectorTile' +import AppLike from 'test/AppLike' + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + NavLink: ({ children }) => children +})) + +const mockKonnector = { + name: 'Mock', + slug: 'mock', + available_version: null +} + +const getMockProps = ({ + error, + userError, + konnector = mockKonnector, + isInMaintenance = false +} = {}) => ({ + error, + isInMaintenance, + userError, + konnector +}) + +const TRIGGERS_FIXTURE = [ + { + type: '@cron', + id: '55530b40ac03836006d532b4bc1695ed', + attributes: { + _id: '55530b40ac03836006d532b4bc1695ed', + _rev: '1-810ca92d7f595728f37c442026c736c4', + domain: 'q.cozy.tools:8080', + prefix: 'cozy971032aab50344a685d8862a25234d2c', + type: '@cron', + worker: 'konnector', + arguments: '0 6 11 * * 4', + debounce: '', + options: null, + message: { + account: '55530b40ac03836006d532b4bc1667ae', + konnector: 'alan', + folder_to_save: '55530b40ac03836006d532b4bc1693b3' + }, + current_state: { + trigger_id: '55530b40ac03836006d532b4bc1695ed', + status: 'errored', + last_success: '2022-07-28T16:13:49.21131+02:00', + last_successful_job_id: 'd55d008319889c826304af3f2e00f1a3', + last_execution: '2023-09-21T21:18:08.10586+02:00', + last_executed_job_id: '9ad37b194024adbd7c87fb4e770314ac', + last_failure: '2023-09-21T21:18:08.10586+02:00', + last_failed_job_id: '9ad37b194024adbd7c87fb4e770314ac', + last_error: 'VENDOR_DOWN', + last_manual_execution: '2022-04-07T12:42:53.043043+02:00', + last_manual_job_id: '55530b40ac03836006d532b4bc194546' + }, + cozyMetadata: { + doctypeVersion: '1', + metadataVersion: 1, + createdAt: '2022-04-07T12:33:26.676062+02:00', + createdByApp: 'home', + updatedAt: '2022-04-07T12:33:26.676062+02:00' + } + }, + meta: {}, + links: { + self: '/jobs/triggers/55530b40ac03836006d532b4bc1695ed' + } + } +] + +const JOBS_FIXTURE = [ + { + id: '07d833e78b4db536569d45b94b011f79', + _id: '07d833e78b4db536569d45b94b011f79', + _type: 'io.cozy.jobs', + _rev: '3-313a673f402f469336b523e5d8f86ba6', + domain: 'q.cozy.localhost:8080', + prefix: 'cozy062792dddb72cc4438450983d3ccd55a', + worker: 'konnector', + trigger_id: '55530b40ac03836006d532b4bc1695ed', + message: { + account: '55530b40ac03836006d532b4bc1667ae', + konnector: 'alan', + folder_to_save: '55530b40ac03836006d532b4bc1693b3' + }, + event: null, + state: 'errored', + queued_at: '2023-04-28T17:58:29.082924+02:00', + started_at: '2023-04-28T17:58:29.71011+02:00', + finished_at: '2023-04-28T17:58:31.847548+02:00', + error: "Cannot read properties of null (reading 'secret')" + } +] + +const ACCOUNTS_FIXTURE = [ + { + id: '55530b40ac03836006d532b4bc1667ae', + _id: '55530b40ac03836006d532b4bc1667ae', + _type: 'io.cozy.accounts', + _rev: '56-e8524998a52a44cb812c3a9101d80e81', + account_type: 'alan', + auth: { + credentials_encrypted: 'redacted', + login: 'alice@cozy.localhost' + }, + cozyMetadata: { + createdAt: '2022-04-07T10:29:33.621Z', + metadataVersion: 1, + updatedAt: '2022-04-07T10:42:52.828Z', + updatedByApps: [ + { + date: '2022-04-07T10:42:52.828Z' + } + ] + }, + defaultFolderPath: '/Administrative/Alan/alice@cozy.localhost', + identifier: 'login', + state: 'LOGIN_SUCCESS' + } +] + +const setup = mockProps => { + const client = createMockClient({ + queries: { + 'io.cozy.triggers': { + lastUpdate: new Date(), + data: TRIGGERS_FIXTURE, + doctype: 'io.cozy.triggers', + hasMore: false + }, + 'io.cozy.jobs': { + lastUpdate: new Date(), + data: JOBS_FIXTURE, + doctype: 'io.cozy.jobs', + hasMore: false + }, + 'io.cozy.accounts': { + lastUpdate: new Date(), + data: ACCOUNTS_FIXTURE, + doctype: 'io.cozy.accounts', + hasMore: false + } + } + }) + return render( + + + + + + ) +} + +describe('KonnectorTile component', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}) + }) + + afterEach(() => { + // eslint-disable-next-line no-console + console.error.mockRestore() + }) + + it('should render correctly ', () => { + const mockProps = getMockProps() + const root = setup(mockProps) + expect(root.getByText(mockKonnector.name)).toBeTruthy() + }) + + describe('Util methods', () => { + it('should display correct status if in maintenance', () => { + const status = getKonnectorStatus({ + konnector: mockKonnector, + isInMaintenance: true + }) + expect(status).toEqual(STATUS.MAINTENANCE) + }) + + it('should display correct error status if user error but not in maintenance', () => { + const status = getKonnectorStatus({ + error: null, + userError: new Error('Expected test user error') + }) + expect(status).toEqual(STATUS.ERROR) + }) + + it('should display correct error status if other error but not in maintenance', () => { + const status = getKonnectorStatus({ error: new Error('LOGIN_FAILED') }) + expect(status).toEqual(STATUS.ERROR) + }) + }) +}) diff --git a/src/components/Services.jsx b/src/components/Services.jsx index 54620e2414..8b98dc1dbb 100644 --- a/src/components/Services.jsx +++ b/src/components/Services.jsx @@ -1,37 +1,44 @@ import React, { useMemo } from 'react' -import PropTypes from 'prop-types' import sortBy from 'lodash/sortBy' -import { connect } from 'react-redux' import { useAppsInMaintenance, useQuery } from 'cozy-client' -import { queryConnect } from 'cozy-client' +import { useSelector } from 'react-redux' + import keyBy from 'lodash/keyBy' import has from 'lodash/has' -import flow from 'lodash/flow' -import KonnectorErrors from 'components/KonnectorErrors' +import { useI18n } from 'cozy-ui/transpiled/react/I18n' +import Divider from 'cozy-ui/transpiled/react/Divider' + import AddServiceTile from 'components/AddServiceTile' import KonnectorTile from 'components/KonnectorTile' import CandidateCategoryTile from 'components/CandidateCategoryTile' import CandidateServiceTile from 'components/CandidateServiceTile' import FallbackCandidateServiceTile from 'components/FallbackCandidateServiceTile' import EmptyServicesListTip from 'components/EmptyServicesListTip' -import { getInstalledKonnectors } from 'reducers/index' import candidatesConfig from 'config/candidates' import { suggestedKonnectorsConn } from 'queries' -import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import { fetchRunningKonnectors, getRunningKonnectors } from 'lib/konnectors_typed' -export const Services = ({ installedKonnectors, suggestedKonnectorsQuery }) => { +import { getInstalledKonnectors } from '../selectors/konnectors' +export const Services = () => { const { t } = useI18n() const appsAndKonnectorsInMaintenance = useAppsInMaintenance() const appsAndKonnectorsInMaintenanceBySlug = keyBy( appsAndKonnectorsInMaintenance, 'slug' ) + const konnectors = useSelector(getInstalledKonnectors) || [] + const installedKonnectors = sortBy(konnectors, konnector => + konnector.name.toLowerCase() + ) + const suggestedKonnectorsQuery = useQuery( + suggestedKonnectorsConn.query, + suggestedKonnectorsConn + ) const candidatesSlugBlacklist = appsAndKonnectorsInMaintenance .map(({ slug }) => slug) @@ -73,13 +80,12 @@ export const Services = ({ installedKonnectors, suggestedKonnectorsQuery }) => { return (
- +
{installedKonnectors.map(konnector => ( { ) } -Services.propTypes = { - installedKonnectors: PropTypes.arrayOf( - PropTypes.shape({ slug: PropTypes.string }) - ).isRequired, - suggestedKonnectorsQuery: PropTypes.shape({ - data: PropTypes.array - }).isRequired -} - -const mapStateToProps = state => { - return { - installedKonnectors: sortBy(getInstalledKonnectors(state), konnector => - konnector.name.toLowerCase() - ) - } -} - -export default flow( - connect(mapStateToProps), - queryConnect({ suggestedKonnectorsQuery: suggestedKonnectorsConn }) -)(Services) +export default Services diff --git a/src/components/Services.spec.jsx b/src/components/Services.spec.jsx index cc47472d08..fffc7b1094 100644 --- a/src/components/Services.spec.jsx +++ b/src/components/Services.spec.jsx @@ -18,7 +18,6 @@ jest.mock('cozy-client', () => ({ jest.mock('components/KonnectorTile', () => ({ konnector }) => (
{konnector.slug}
)) -jest.mock('components/KonnectorErrors', () => () => null) jest.mock('hooks/useRegistryInformation', () => (client, slug) => slug) jest.mock('cozy-ui/transpiled/react/utils/color', () => ({ @@ -35,11 +34,26 @@ describe('Services component', () => { const client = createMockClient({ clientOptions: { uri: 'http://cozy.tools:8080' + }, + queries: { + installedKonnectors: { + lastUpdate: new Date(), + data: installedKonnectors, + doctype: 'io.cozy.konnectors', + hasMore: false + }, + 'app-suggestions': { + lastUpdate: new Date(), + data: suggestedKonnectorsQuery ? suggestedKonnectorsQuery.data : [], + doctype: 'io.cozy.apps.suggestions', + hasMore: false + } } }) + useAppsInMaintenance.mockReturnValue(appsAndKonnectorsInMaintenance) const root = render( - + { it('should display a list of services', () => { const installedKonnectors = [ - { slug: 'test1' }, - { slug: 'test2' }, - { slug: 'test3' } + { slug: 'test1', _id: '1', name: 'Test1' }, + { slug: 'test2', _id: '2', name: 'Test 2' }, + { slug: 'test3', _id: 3, name: 'Test 3' } ] const { root } = setup({ installedKonnectors }) root.getByText('test1') @@ -81,9 +95,9 @@ describe('Services component', () => { installedKonnectors: [], suggestedKonnectorsQuery: { data: [ - { slug: 'suggestion-1' }, - { slug: 'suggestion-2' }, - { slug: 'test-1' } + { slug: 'suggestion-1', _id: 1 }, + { slug: 'suggestion-2', _id: 2 }, + { slug: 'test-1', _id: 3 } ] } }) @@ -96,17 +110,17 @@ describe('Services component', () => { it('should display suggestions after services have been installed', () => { const installedKonnectors = [ - { slug: 'test-1' }, - { slug: 'test-2' }, - { slug: 'test-3' } + { slug: 'test-1', _id: '1', name: 'Test1' }, + { slug: 'test-2', _id: '2', name: 'Test 2' }, + { slug: 'test-3', _id: '3', name: 'Test 3' } ] const { root } = setup({ installedKonnectors, suggestedKonnectorsQuery: { data: [ - { slug: 'suggestion-1' }, - { slug: 'suggestion-2' }, - { slug: 'test-1' } + { slug: 'suggestion-1', _id: 1 }, + { slug: 'suggestion-2', _id: 2 }, + { slug: 'test-1', _id: 3 } ] } }) diff --git a/src/components/StoreRedirection.jsx b/src/components/StoreRedirection.jsx index 8bb90e0905..6e4923f959 100644 --- a/src/components/StoreRedirection.jsx +++ b/src/components/StoreRedirection.jsx @@ -1,22 +1,20 @@ -/* global cozy */ -import React, { Component } from 'react' +import React from 'react' +import Intents from 'cozy-interapp' import Spinner from 'cozy-ui/transpiled/react/Spinner' +import { useClient } from 'cozy-client' -export class StoreRedirection extends Component { - constructor(props) { - super(props) - const category = props.match && props.match.params.category - const options = { type: 'konnector' } - if (category && category !== 'all') { - options.category = props.match.params.category - } - cozy.client.intents.redirect('io.cozy.apps', options) +const StoreRedirection = props => { + const client = useClient() + const intents = new Intents({ client }) + const category = props.match && props.match.params.category + const options = { type: 'konnector' } + if (category && category !== 'all') { + options.category = props.match.params.category } + intents.redirect('io.cozy.apps', options) - render() { - return - } + return } export default StoreRedirection diff --git a/src/components/TriggerFolderLink.jsx b/src/components/TriggerFolderLink.jsx deleted file mode 100644 index d8155af65c..0000000000 --- a/src/components/TriggerFolderLink.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { PureComponent } from 'react' -import classNames from 'classnames' - -import { Q, useQuery } from 'cozy-client' -import Icon from 'cozy-ui/transpiled/react/Icon' - -import styles from 'styles/triggerFolderLink.styl' - -import OpenwithIcon from 'cozy-ui/transpiled/react/Icons/Openwith' - -/** - * Renders a link only if href prop is provided - */ -class MaybeLink extends PureComponent { - render() { - const { className, href } = this.props - return href ? ( - - {this.props.children} - - ) : ( - {this.props.children} - ) - } -} - -export const TriggerFolderLink = ({ folderId, label }) => { - const driveQuery = useQuery(Q('io.cozy.apps').getById('io.cozy.apps/drive'), { - as: 'io.cozy.apps/drive' - }) - return ( - 0 && - `${driveQuery.data[0].links.related}#/files/${folderId}` - } - > - - {label} - - ) -} - -export default TriggerFolderLink diff --git a/src/components/__snapshots__/KonnectorErrors.spec.jsx.snap b/src/components/__snapshots__/KonnectorErrors.spec.jsx.snap deleted file mode 100644 index f7efc15899..0000000000 --- a/src/components/__snapshots__/KonnectorErrors.spec.jsx.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KonnectorErrors should hide errors when the konnector or account is missing 1`] = ` -
-
- My connected services -
-
-`; - -exports[`KonnectorErrors should render divider when all errors are muted 1`] = ` -
-
- My connected services -
-
-`; - -exports[`KonnectorErrors should render divider when there are errors but no installed konnector 1`] = ` -
-
- My connected services -
-
-`; - -exports[`KonnectorErrors should render divider when there are no errors 1`] = ` -
-
- My connected services -
-
-`; diff --git a/src/components/appEntryPoint.jsx b/src/components/appEntryPoint.jsx index c9ecd6e92e..23deff5ae8 100644 --- a/src/components/appEntryPoint.jsx +++ b/src/components/appEntryPoint.jsx @@ -1,19 +1,28 @@ -import { cozyConnect } from 'lib/redux-cozy-client' - -import { fetchAccounts } from 'ducks/accounts' -import { fetchKonnectors } from 'ducks/konnectors' -import { fetchKonnectorJobs } from 'ducks/jobs' -import { fetchTriggers } from 'ducks/triggers' +import CozyClient, { Q, queryConnect } from 'cozy-client' import '../flags' -const mapDocumentsToProps = () => ({ - accounts: fetchAccounts(), - jobs: fetchKonnectorJobs(), - konnectors: fetchKonnectors(), - triggers: fetchTriggers() -}) +const ACCOUNT_DOCTYPE = 'io.cozy.accounts' +const KONNECTOR_DOCTYPE = 'io.cozy.konnectors' +const TRIGGER_DOCTYPE = 'io.cozy.triggers' -const appEntryPoint = (WrappedComponent, selectData) => - cozyConnect(mapDocumentsToProps)(WrappedComponent, selectData) +const OLDER_THAN_THIRTY_SECONDS = CozyClient.fetchPolicies.olderThan(30 * 1000) + +const appEntryPoint = queryConnect({ + accounts: { + query: () => Q(ACCOUNT_DOCTYPE), + as: 'io.cozy.accounts', + fetchPolicy: OLDER_THAN_THIRTY_SECONDS + }, + konnectors: { + query: () => Q(KONNECTOR_DOCTYPE), + as: 'io.cozy.konnectors', + fetchPolicy: OLDER_THAN_THIRTY_SECONDS + }, + triggers: { + query: () => Q(TRIGGER_DOCTYPE).where({ worker: ['client', 'konnector'] }), + as: 'io.cozy.triggers/by_worker_client_konnector', + fetchPolicy: OLDER_THAN_THIRTY_SECONDS + } +}) export default appEntryPoint diff --git a/src/components/intents/CreateAccountIntent.jsx b/src/components/intents/CreateAccountIntent.jsx deleted file mode 100644 index 6731e5dd44..0000000000 --- a/src/components/intents/CreateAccountIntent.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -import CreateAccountService from 'components/services/CreateAccountService' -import KonnectorHeaderIcon from 'components/KonnectorHeaderIcon' - -class CreateAccountIntent extends Component { - constructor(props, context) { - super(props, context) - this.store = context.store - this.state = { isSuccess: false } - this.store.fetchUrls() - } - - handleConnectionSuccess = () => { - this.setState({ isSuccess: true }) - } - - render() { - const { konnector, onCancel, onTerminate } = this.props - const { isSuccess } = this.state - return ( -
- {!isSuccess && } - {konnector && ( - onCancel()} - onSuccess={onTerminate} - handleConnectionSuccess={this.handleConnectionSuccess} - /> - )} -
- ) - } -} - -CreateAccountIntent.contextTypes = { - store: PropTypes.object -} - -export default CreateAccountIntent diff --git a/src/containers/App.jsx b/src/containers/App.jsx index 29bfe8f706..a963f0f1a4 100644 --- a/src/containers/App.jsx +++ b/src/containers/App.jsx @@ -46,7 +46,7 @@ const App = ({ accounts, konnectors, triggers }) => { const [status, setStatus] = useState(IDLE) const [contentWrapper, setContentWrapper] = useState(undefined) const [isFetching, setIsFetching] = useState( - [accounts, konnectors, triggers].some(collection => + [accounts, konnectors].some(collection => ['pending', 'loading'].includes(collection.fetchStatus) ) ) @@ -69,7 +69,11 @@ const App = ({ accounts, konnectors, triggers }) => { ) ) }, [accounts, konnectors, triggers]) - + /* useEffect(() => { + client.query(Q('io.cozy.triggers')) + client.query(Q('io.cozy.jobs')) + client.query(Q('io.cozy.accounts')) + }, []) */ useEffect(() => { // if we already have the query, let's refresh in "background" // aka without loading state diff --git a/src/containers/App.spec.jsx b/src/containers/App.spec.jsx index 164bfe8f3d..41140f7d7b 100644 --- a/src/containers/App.spec.jsx +++ b/src/containers/App.spec.jsx @@ -4,26 +4,6 @@ import { render } from '@testing-library/react' import App from '../components/AnimatedWrapper' import AppLike from 'test/AppLike' -jest.mock('lib/redux-cozy-client/connect.jsx') - -jest.mock('lib/redux-cozy-client', () => ({ - cozyConnect: () => App => { - return () => - App({ - accounts: [{ fetchStatus: 'failed' }], - konnectors: [{ fetchStatus: 'failed' }], - triggers: [{ fetchStatus: 'failed' }], - client: { - query: () => {}, - getInstanceOptions: () => ({ - cozyDefaultWallpaper: 'cozyDefaultWallpaper' - }), - getQueryFromState: jest.fn - } - }) - } -})) - // eslint-disable-next-line react/display-name jest.mock('components/HeroHeader', () => () =>
) diff --git a/src/containers/IntentService.jsx b/src/containers/IntentService.jsx index a9862feaaa..4a8d2037a4 100644 --- a/src/containers/IntentService.jsx +++ b/src/containers/IntentService.jsx @@ -1,85 +1,61 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' - -import { translate } from 'cozy-ui/transpiled/react/providers/I18n' -import CreateAccountIntent from 'components/intents/CreateAccountIntent' -import { getKonnector, receiveInstalledKonnector } from 'ducks/konnectors' - -class IntentService extends Component { - constructor(props) { - super(props) - this.state = { error: null } - } - - handleInstallationSuccess(konnector) { - this.props.receiveKonnector(konnector) - } - - async componentDidMount() { - const { data, konnector, receiveKonnector, service } = this.props - if (service && !konnector) { - const installedKonnector = await service.compose( - 'INSTALL', - 'io.cozy.apps', - data - ) - - // if installedKonnector is null, it means the installation have been - // cancelled - if (!installedKonnector) { - return service.cancel() +import { useEffect } from 'react' + +import { useClient } from 'cozy-client' +import { fetchKonnectorBySlug } from 'queries' +import { useNavigate } from 'react-router-dom' + +const IntentService = ({ data, service }) => { + const client = useClient() + const navigate = useNavigate() + + useEffect(() => { + const fetchData = async () => { + let konnectorReq + try { + konnectorReq = await client.query( + fetchKonnectorBySlug(data.slug).query, + fetchKonnectorBySlug + ) + } catch (e) { + // why an error is throwed? + // eslint-disable-next-line + console.log('e', e) + } finally { + if (service && (!konnectorReq || konnectorReq.data.length === 0)) { + const installedKonnector = await service.compose( + 'INSTALL', + 'io.cozy.apps', + data + ) + // setKonnectorData(installedKonnector) + navigate(`/${service.getData().slug}/new`) + // if installedKonnector is null, it means the installation have been + // cancelled + if (!installedKonnector) { + service.cancel() + } + } else { + if (service) { + const intent = service.getIntent() + if (service.hideCross) { + service.hideCross() + } + if ( + intent.attributes.action === 'VIEW' && + intent.attributes.type === 'io.cozy.accounts' + ) { + navigate(`/${service.getData().slug}/accounts/${data.accountId}`) + } else { + navigate(`/${service.getData().slug}/new`) + } + } + } } - - receiveKonnector(installedKonnector) } - } + fetchData() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - onError(error) { - this.setState({ - error: error - }) - } - - render() { - const { appData, konnector, onCancel, service, t } = this.props - const { error } = this.state - - return ( -
- {error && ( -
-

{t(error.message)}

- {error.reason && ( -

{t('intent.service.error.cause', { error: error.reason })}

- )} -
- )} - {!error && konnector && ( - - )} -
- ) - } -} - -const mapDispatchToProps = dispatch => ({ - receiveKonnector: konnector => dispatch(receiveInstalledKonnector(konnector)) -}) - -const mapStateToProps = (state, ownProps) => { - const { data } = ownProps - const { slug } = data - return { - konnector: slug && getKonnector(state.oldcozy, slug) - } + return null } - -export default connect( - mapStateToProps, - mapDispatchToProps -)(translate()(IntentService)) +export default IntentService diff --git a/src/containers/ReloadFocus.jsx b/src/containers/ReloadFocus.jsx index 1ccbf1e090..6f56adea4f 100644 --- a/src/containers/ReloadFocus.jsx +++ b/src/containers/ReloadFocus.jsx @@ -1,23 +1,19 @@ import React from 'react' import PropTypes from 'prop-types' -import { fetchAccounts } from 'ducks/accounts' -import { fetchKonnectors } from 'ducks/konnectors' -import { fetchTriggers } from 'ducks/triggers' import { withClient, Q } from 'cozy-client' class RealoadFocus extends React.Component { static contextTypes = { store: PropTypes.object } componentDidMount() { - const dispatch = this.context.store.dispatch const client = this.props.client window.addEventListener('focus', () => { - dispatch(fetchAccounts()) - dispatch(fetchKonnectors()) - dispatch(fetchTriggers()) - client.query(Q('io.cozy.jobs')) - client.query(Q('io.cozy.apps')) + client.query(Q('io.cozy.jobs')), + client.query( + Q('io.cozy.triggers').where({ worker: ['client', 'konnector'] }) + ), + client.query(Q('io.cozy.apps')) }) } diff --git a/src/cozy-ui.d.ts b/src/cozy-ui.d.ts index a9033a862e..87aec21f6e 100644 --- a/src/cozy-ui.d.ts +++ b/src/cozy-ui.d.ts @@ -28,7 +28,7 @@ declare module 'cozy-ui/transpiled/react/providers/CozyTheme' { } declare module 'cozy-ui/transpiled/react/providers/I18n' { - export const useI18n: () => { t: (key: string) => string } + export const useI18n: () => { t: (key: string) => string; lang: string } } declare module 'cozy-ui/transpiled/react/Buttons' { diff --git a/src/ducks/accounts/index.js b/src/ducks/accounts/index.js deleted file mode 100644 index 24ccfe7a07..0000000000 --- a/src/ducks/accounts/index.js +++ /dev/null @@ -1,28 +0,0 @@ -import { fetchCollection } from 'lib/redux-cozy-client' - -export const DOCTYPE = 'io.cozy.accounts' -const accountCollectionKey = 'accounts' - -export const fetchAccounts = () => - fetchCollection(accountCollectionKey, DOCTYPE) - -// selectors -export const getAccount = (state, id) => { - if (!state.documents || !state.documents[DOCTYPE]) { - return null - } - - return state.documents[DOCTYPE][id] -} - -export const getIds = state => - // state.collection is bugged, it does not update correctly id list on - // RECEIVE_DATA - // (state.collections && - // state.collections[accountCollectionKey] && - // state.collections[accountCollectionKey].ids) || - // [] - (state.documents && - state.documents[DOCTYPE] && - Object.keys(state.documents[DOCTYPE])) || - [] diff --git a/src/ducks/connections/index.js b/src/ducks/connections/index.js deleted file mode 100644 index 9e1c1d68ce..0000000000 --- a/src/ducks/connections/index.js +++ /dev/null @@ -1,445 +0,0 @@ -import { combineReducers } from 'redux' -import moment from 'moment' -import omit from 'lodash/omit' -import get from 'lodash/get' -import { buildKonnectorError, isKonnectorUserError } from 'lib/konnectors' - -// constant -const ACCOUNT_DOCTYPE = 'io.cozy.accounts' -const TRIGGERS_DOCTYPE = 'io.cozy.triggers' -const JOBS_DOCTYPE = 'io.cozy.jobs' - -export const ENQUEUE_CONNECTION = 'ENQUEUE_CONNECTION' -export const LAUNCH_TRIGGER = 'LAUNCH_TRIGGER' -export const PURGE_QUEUE = 'PURGE_QUEUE' -export const RECEIVE_DATA = 'RECEIVE_DATA' -export const RECEIVE_NEW_DOCUMENT = 'RECEIVE_NEW_DOCUMENT' -export const RECEIVE_DELETED_DOCUMENT = 'RECEIVE_DELETED_DOCUMENT' -export const UPDATE_CONNECTION_RUNNING_STATUS = - 'UPDATE_CONNECTION_RUNNING_STATUS' -export const UPDATE_CONNECTION_ERROR = 'UPDATE_CONNECTION_ERROR' -export const START_CONNECTION_CREATION = 'START_CONNECTION_CREATION' -export const END_CONNECTION_CREATION = 'END_CONNECTION_CREATION' - -// Helpers -const getTriggerKonnectorSlug = trigger => - (trigger && trigger.message && trigger.message.konnector) || null - -export const isKonnectorTrigger = doc => - doc._type === TRIGGERS_DOCTYPE && !!doc.message && !!doc.message.konnector - -export const isKonnectorJob = doc => - doc._type === JOBS_DOCTYPE && - (doc.worker === 'konnector' || doc.worker === 'client') - -// reducers -const reducer = (state = {}, action) => { - switch (action.type) { - case ENQUEUE_CONNECTION: - case UPDATE_CONNECTION_ERROR: - case UPDATE_CONNECTION_RUNNING_STATUS: - case LAUNCH_TRIGGER: - // Ignore the action if trigger does not have an id - // This is possible that an enqueue connection is dispatched - // with a trigger without _id, this is because for banking - // konnectors, the LOGIN_SUCCESS action is dispatched before - // the trigger has been created, thus a fake trigger is created - if (!action.trigger || !action.trigger._id) { - return state - } - if (!action.trigger.message || !action.trigger.message.konnector) { - return state - } - return { - ...state, - [getTriggerKonnectorSlug(action.trigger)]: konnectorReducer( - state[getTriggerKonnectorSlug(action.trigger)], - action - ) - } - - case RECEIVE_DATA: - case RECEIVE_NEW_DOCUMENT: - if ( - !action.response || - !action.response.data || - !action.response.data.length - ) { - return state - } - - return action.response.data.reduce((newState, doc) => { - const isTrigger = isKonnectorTrigger(doc) - const isJob = isKonnectorJob(doc) - // Ignore non triggers or non jobs - if (!isTrigger && !isJob) return newState - const konnectorSlug = doc.message.konnector - const triggerId = (isTrigger && doc._id) || (isJob && doc.trigger_id) - if (!triggerId) return newState - - const account = isTrigger && !!doc.message && doc.message.account - - const currentStatus = - (isTrigger && doc.current_state && doc.current_state.status) || - (isJob && doc.state) - - const error = - (isTrigger && - !!doc.current_state && - doc.current_state.status !== 'done' && - !!doc.current_state.last_error && - buildKonnectorError(doc.current_state.last_error)) || - (isJob && !!doc.error && buildKonnectorError(doc.error)) || - null - - const lastSyncDate = - (isTrigger && - !!doc.current_state && - doc.current_state.last_execution) || - (isJob && doc.queued_at) - const existingTriggers = get( - newState, - [konnectorSlug, 'triggers', 'data'], - [] - ) - let rawTriggers = existingTriggers - - if (isTrigger) { - rawTriggers = existingTriggers.filter(({ _id }) => _id !== doc._id) - rawTriggers.push(doc) - } - return { - ...newState, - [konnectorSlug]: { - triggers: { - ...get(newState, [konnectorSlug, 'triggers'], []), - data: rawTriggers, - [triggerId]: { - ...get(newState, [konnectorSlug, 'triggers', triggerId], {}), - account: - account || - get(newState, [ - konnectorSlug, - 'triggers', - triggerId, - 'account' - ]), - error, - hasError: !!error || currentStatus === 'errored', - isRunning: ['queued', 'running'].includes(currentStatus), - isConnected: !error && currentStatus === 'done', - lastSyncDate: lastSyncDate - } - } - } - } - }, state) - - case PURGE_QUEUE: - case RECEIVE_DELETED_DOCUMENT: - return Object.keys(state).reduce((konnectors, slug) => { - return { - ...konnectors, - [slug]: konnectorReducer(state[slug], action) - } - }, state) - - default: - return state - } -} - -const creation = (state = null, action) => { - switch (action.type) { - case RECEIVE_DATA: - case RECEIVE_NEW_DOCUMENT: { - if (!state) return null - if ( - !action.response || - !action.response.data || - action.response.data.length !== 1 - ) { - return state - } - - const doc = action.response.data[0] - const isAccount = doc._type === ACCOUNT_DOCTYPE - - if (isAccount) - return { - ...state, - account: doc._id - } - - const isTrigger = isKonnectorTrigger(doc) - if (isTrigger) - return { - ...state, - trigger: doc._id - } - - return state - } - case START_CONNECTION_CREATION: - // Store all data related to the creation of a new connection in then - // property `creation` - // While a new connection is being configured, new trigger and account - // are store here - return {} - case END_CONNECTION_CREATION: - return null - default: - return state - } -} - -export default combineReducers({ - creation, - konnectors: reducer -}) - -// sub(?) reducers -const konnectorReducer = (state = {}, action) => { - switch (action.type) { - case ENQUEUE_CONNECTION: - case LAUNCH_TRIGGER: - case RECEIVE_DATA: - case RECEIVE_NEW_DOCUMENT: - case RECEIVE_DELETED_DOCUMENT: - case PURGE_QUEUE: - // We assume that document being a trigger has already been validated. - return { - ...state, - triggers: triggersReducer(state.triggers, action) - } - default: - return state - } -} - -const triggersReducer = (state = {}, action) => { - switch (action.type) { - case ENQUEUE_CONNECTION: - return { - ...state, - [action.trigger._id]: { - ...state[action.trigger._id], - isEnqueued: true - } - } - case LAUNCH_TRIGGER: - return { - ...state, - [action.trigger._id]: { - ...state[action.trigger._id], - account: action.trigger.message.account, - isRunning: true - } - } - case PURGE_QUEUE: - return state - ? Object.keys(state).reduce((newState, triggerId) => { - return { - ...newState, - [triggerId]: { - ...newState[triggerId], - isEnqueued: false - } - } - }, state) - : state - case RECEIVE_DELETED_DOCUMENT: { - const { data } = action.response - const { _id, _type } = data[0] - if (_type === TRIGGERS_DOCTYPE) { - return omit(state, _id) - } else return state - } - default: - return state - } -} - -// action creators sync -export const enqueueConnection = trigger => ({ - type: ENQUEUE_CONNECTION, - trigger -}) - -export const purgeQueue = () => ({ - type: PURGE_QUEUE -}) - -export const updateConnectionError = (konnector, account, error) => ({ - type: UPDATE_CONNECTION_ERROR, - konnector, - account, - error -}) - -export const startConnectionCreation = konnector => ({ - type: START_CONNECTION_CREATION, - konnector -}) - -export const endConnectionCreation = () => ({ - type: END_CONNECTION_CREATION -}) - -// selectors -export const getConnectionsByKonnector = ( - state, - konnectorSlug, - validAccounts = [], - validKonnectors = [] -) => { - const konnectorIsValid = - !validKonnectors.length || validKonnectors.includes(konnectorSlug) - const konnectorHasConnections = - state.konnectors[konnectorSlug] && - Object.keys(state.konnectors[konnectorSlug].triggers).length - if (!konnectorIsValid || !konnectorHasConnections) return [] - - return Object.values(state.konnectors[konnectorSlug].triggers).filter( - trigger => validAccounts.includes(trigger.account) - ) -} - -export const getFirstError = (state, konnectorSlug) => { - const firstTriggerHavingError = - !!state.konnectors && - !!state.konnectors[konnectorSlug] && - !!state.konnectors[konnectorSlug].triggers && - Object.values(state.konnectors[konnectorSlug].triggers).find( - trigger => !!trigger.error - ) - return firstTriggerHavingError ? firstTriggerHavingError.error : null -} - -export const getFirstUserError = (state, konnectorSlug) => { - const firstTriggerHavingUserError = - !!state.konnectors && - !!state.konnectors[konnectorSlug] && - !!state.konnectors[konnectorSlug].triggers && - Object.values(state.konnectors[konnectorSlug].triggers).find(trigger => - isKonnectorUserError(trigger.error) - ) - return firstTriggerHavingUserError ? firstTriggerHavingUserError.error : null -} - -/** - * Converts the input date string to a valid ISO 8601 format if needed. - * - * This function checks if the input date string is in the extended ISO 8601 format - * and trims the fractional seconds part to 6 digits if it has more than 6 digits. - * This is done to avoid deprecation warnings from Moment.js regarding - * non-standard date formats, as Moment.js expects a maximum of 6 digits for - * fractional seconds in the ISO 8601 format. - * - * @param {string} dateStr - The input date string - * @returns {string|null} - The converted date string in a valid ISO 8601 format, or null if input is falsy - */ -const convertDateFormat = dateStr => { - if (!dateStr) return null - const iso8601Pattern = - /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,6}(?:\+|-)\d{2}:\d{2}/ - if (iso8601Pattern.test(dateStr)) { - return dateStr.slice(0, 23) + dateStr.slice(23) - } - return dateStr -} - -export const getLastSyncDate = (state, konnectorSlug) => { - const lastExecutions = - !!state.konnectors && - !!state.konnectors[konnectorSlug] && - !!state.konnectors[konnectorSlug].triggers && - Object.values(state.konnectors[konnectorSlug].triggers) - .map(trigger => trigger.lastSyncDate) - .sort((dateA, dateB) => { - const momentA = moment.utc(convertDateFormat(dateA)) - const momentB = moment.utc(convertDateFormat(dateB)) - return momentA.isAfter(momentB) ? -1 : momentA.isBefore(momentB) ? 1 : 0 - }) - return lastExecutions.length && lastExecutions[0] -} - -// Map the trigger status to a status compatible with queue -const getTriggerQueueStatus = trigger => { - if (trigger.isRunning) return 'ongoing' - if (trigger.hasError) return 'error' - if (trigger.isConnected) return 'done' - return 'pending' -} - -export const getQueue = (state, konnectors) => - // state is state.connections - state.konnectors - ? Object.keys(state.konnectors).reduce( - (queuedConnections, konnectorSlug) => { - const triggers = state.konnectors[konnectorSlug].triggers - if (!triggers) return queuedConnections - const konnector = konnectors[konnectorSlug] - return queuedConnections.concat( - Object.keys(triggers).reduce((queuedTriggers, triggerId) => { - if (triggers[triggerId].isEnqueued) { - const label = konnector.name - const status = getTriggerQueueStatus(triggers[triggerId]) - return queuedTriggers.concat({ - konnector, - label, - status, - triggerId - }) - } - - return queuedTriggers - }, []) - ) - }, - [] - ) - : [] - -export const getConnectionError = (state, trigger) => - getTriggerState(state, trigger).error - -export const getCreatedAccount = state => - !!state.creation && state.creation.account - -export const getTriggerIdByKonnectorAndAccount = ( - state, - konnector, - account, - validAccounts = [] -) => - !!konnector && - !!account && - validAccounts.includes(account._id) && - !!state.konnectors[konnector.slug] && - Object.keys(state.konnectors[konnector.slug].triggers).find( - triggerId => - state.konnectors[konnector.slug].triggers[triggerId].account === - account._id - ) - -// get trigger from state, in state.konnectors[konnectorSlug].triggers[triggerId] -const getTriggerState = (state, trigger) => { - const konnectorSlug = getTriggerKonnectorSlug(trigger) - if (!konnectorSlug || !state.konnectors || !state.konnectors[konnectorSlug]) - return false - const triggers = state.konnectors[konnectorSlug].triggers - if (!triggers) return false - return (!!triggers && !!triggers[trigger._id] && triggers[trigger._id]) || {} -} - -export const isCreatingConnection = state => !!state.creation - -export const isConnectionConnected = (state, trigger) => - getTriggerState(state, trigger).isConnected - -export const isConnectionEnqueued = (state, trigger) => - getTriggerState(state, trigger).isEnqueued - -export const isConnectionRunning = (state, trigger) => - getTriggerState(state, trigger).isRunning diff --git a/src/ducks/connections/test/__snapshots__/connections.spec.js.snap b/src/ducks/connections/test/__snapshots__/connections.spec.js.snap deleted file mode 100644 index 10bea51dd8..0000000000 --- a/src/ducks/connections/test/__snapshots__/connections.spec.js.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Connections Duck Action creators enqueueConnection marks trigger as queued 1`] = ` -Object { - "creation": null, - "konnectors": Object { - "testprovider": Object { - "triggers": Object { - "trigger-1": Object { - "isEnqueued": true, - }, - }, - }, - }, -} -`; - -exports[`Connections Duck Action creators enqueueConnection should ignore when trigger has no _id 1`] = ` -Object { - "creation": null, - "konnectors": Object { - "testprovider": Object {}, - }, -} -`; - -exports[`Connections Duck Action creators purgeQueue marks all accounts as not queued 1`] = ` -Object { - "anotherprovider": Object { - "768ccdaa9d7b11e7869aae88b1c12d46": Object { - "isEnqueued": false, - }, - }, - "testprovider": Object { - "17375ac5a59e4d6585fc7d1e1c75ec74": Object { - "isEnqueued": false, - }, - "63c670ea9d7b11e7b5888c88b1c12d46": Object { - "isEnqueued": false, - }, - }, -} -`; - -exports[`Connections Duck Selectors getConnectionsByKonnector returns expected connections 1`] = ` -Array [ - Object { - "account": "81a548fca81455ec2c2644dd55008b52", - "error": "LOGIN_FAILED", - "hasError": true, - "isConnected": false, - "isRunning": false, - }, -] -`; - -exports[`Connections Duck Selectors getQueue returns one queued connection per queued account 1`] = `Array []`; diff --git a/src/ducks/connections/test/connections.spec.js b/src/ducks/connections/test/connections.spec.js deleted file mode 100644 index 60dc7c1d29..0000000000 --- a/src/ducks/connections/test/connections.spec.js +++ /dev/null @@ -1,392 +0,0 @@ -/* eslint-env jest */ -import get from 'lodash/get' - -import connections, { - enqueueConnection, - getConnectionsByKonnector, - getQueue, - purgeQueue, - RECEIVE_DATA -} from '../' - -describe('Connections Duck', () => { - describe('Reducer', () => { - describe('Receiving data', () => { - it('should ignore actions without data', () => { - const initialState = { creation: null, konnectors: {} } - - expect( - connections(initialState, { - type: RECEIVE_DATA - }) - ).toEqual(initialState) - expect( - connections(initialState, { - type: RECEIVE_DATA, - response: { - data: { yo: 1 } - } - }) - ).toEqual(initialState) - expect( - connections(initialState, { - type: RECEIVE_DATA, - response: { - data: [] - } - }) - ).toEqual(initialState) - }) - - it('should add a new trigger', () => { - const initialState = { creation: null, konnectors: {} } - const data = [ - { - _id: 'trigger-id', - _type: 'io.cozy.triggers', - message: { - konnector: 'my-kon', - account: 'account 1' - }, - current_state: { - status: 'done', - last_error: null, - last_execution: '2019-01-01' - } - } - ] - const newState = connections(initialState, { - type: RECEIVE_DATA, - response: { - data - } - }) - - expect(Object.keys(newState.konnectors).length).toEqual(1) - const triggerState = get( - newState, - 'konnectors.my-kon.triggers.trigger-id' - ) - expect(triggerState).toBeDefined() - expect(triggerState.account).toBe('account 1') - expect(triggerState.hasError).toBe(false) - expect(triggerState.isRunning).toBe(false) - expect(triggerState.lastSyncDate).toBe('2019-01-01') - }) - - it('should add a new job', () => { - const initialState = { creation: null, konnectors: {} } - const data = [ - { - _id: 'job-id', - _type: 'io.cozy.jobs', - worker: 'konnector', - trigger_id: 'job-trigger-id', - state: 'done', - error: null, - queued_at: '2019-01-01', - message: { - konnector: 'my-kon' - } - } - ] - const newState = connections(initialState, { - type: RECEIVE_DATA, - response: { - data - } - }) - - expect(Object.keys(newState.konnectors).length).toEqual(1) - const triggerState = get( - newState, - 'konnectors.my-kon.triggers.job-trigger-id' - ) - expect(triggerState).toBeDefined() - expect(triggerState.account).toBe(undefined) - expect(triggerState.hasError).toBe(false) - expect(triggerState.isRunning).toBe(false) - expect(triggerState.lastSyncDate).toBe('2019-01-01') - }) - }) - - describe('full trigger informations', () => { - it('should add a new trigger', () => { - const initialState = { creation: null, konnectors: {} } - const data = [ - { - _id: 'trigger-id', - _type: 'io.cozy.triggers', - message: { - konnector: 'my-kon', - account: 'account 1' - }, - current_state: { - status: 'done', - last_error: null, - last_execution: '2019-01-01' - } - } - ] - const newState = connections(initialState, { - type: RECEIVE_DATA, - response: { - data - } - }) - - expect(get(newState, 'konnectors.my-kon.triggers.data')).toEqual(data) - }) - - it('should keep the latest version of a trigger', () => { - const initialState = { - creation: null, - konnectors: { - 'my-kon': { - triggers: { - data: [ - { - _id: 'trigger-id', - _type: 'io.cozy.triggers', - message: { - konnector: 'my-kon', - account: 'account 1' - }, - current_state: { - status: 'done', - last_error: null, - last_execution: '2019-01-01' - } - } - ] - } - } - } - } - - const newConnectorData = { - _id: 'trigger-id', - _type: 'io.cozy.triggers', - message: { - konnector: 'my-kon', - account: 'account 1' - }, - current_state: { - status: 'done', - last_error: null, - last_execution: '2019-02-01' - } - } - - const newState = connections(initialState, { - type: RECEIVE_DATA, - response: { - data: [newConnectorData] - } - }) - - const triggerState = get(newState, 'konnectors.my-kon.triggers.data') - expect(triggerState.length).toEqual(1) - expect(triggerState[0]).toEqual(newConnectorData) - }) - - it('should not add jobs to the list', () => { - const initialState = { - creation: null, - konnectors: { - 'my-kon': { - triggers: { - data: [ - { - _id: 'trigger-id', - _type: 'io.cozy.triggers', - message: { - konnector: 'my-kon', - account: 'account 1' - }, - current_state: { - status: 'done', - last_error: null, - last_execution: '2019-01-01' - } - } - ] - } - } - } - } - - const newConnectorData = { - _id: 'job-id', - _type: 'io.cozy.jobs', - worker: 'konnector', - trigger_id: 'trigger-id', - state: 'done', - error: null, - queued_at: '2019-02-01', - message: { - konnector: 'my-kon' - } - } - - const newState = connections(initialState, { - type: RECEIVE_DATA, - response: { - data: [newConnectorData] - } - }) - - const triggerState = get(newState, 'konnectors.my-kon.triggers.data') - expect(triggerState.length).toEqual(1) - expect(triggerState[0].current_state).toBeDefined() - }) - }) - }) - - describe('Action creators', () => { - describe('enqueueConnection', () => { - it('marks trigger as queued', () => { - const state = { - konnectors: { - testprovider: {} - } - } - const konnector = { slug: 'testprovider' } - const account = { _id: '17375ac5a59e4d6585fc7d1e1c75ec74' } - const trigger = { - _id: 'trigger-1', - message: { - account: account._id, - konnector: konnector.slug - } - } - - const result = connections(state, enqueueConnection(trigger)) - - expect(result).toMatchSnapshot() - }) - - it('should ignore when trigger has no _id', () => { - const state = { - konnectors: { - testprovider: {} - } - } - const konnector = { slug: 'testprovider' } - const account = { _id: '17375ac5a59e4d6585fc7d1e1c75ec74' } - - // This is possible that an enqueue connection is dispatched - // with a trigger without _id, this is because for banking - // konnectors, the LOGIN_SUCCESS action is dispatched before - // the trigger has been created, thus a fake trigger is - // created. - const trigger = { - message: { - account: account._id, - konnector: konnector.slug - } - } - - const result = connections(state, enqueueConnection(trigger)) - - expect(result).toMatchSnapshot() - }) - }) - - describe('purgeQueue', () => { - it.skip('marks all accounts as not queued', () => { - const state = { - testprovider: { - '17375ac5a59e4d6585fc7d1e1c75ec74': {}, - '63c670ea9d7b11e7b5888c88b1c12d46': { - isEnqueued: true - } - }, - anotherprovider: { - '768ccdaa9d7b11e7869aae88b1c12d46': { - isEnqueued: true - } - } - } - - const result = connections(state, purgeQueue()) - - expect(result).toMatchSnapshot() - }) - }) - }) - - describe('Selectors', () => { - describe('getConnectionsByKonnector', () => { - it('returns expected connections', () => { - const state = { - konnectors: { - provider: { - triggers: { - '81a548fca81455ec2c2644dd55009990': { - account: '81a548fca81455ec2c2644dd55008b52', - error: 'LOGIN_FAILED', - hasError: true, - isConnected: false, - isRunning: false - }, - '63c670ea9d7b11e7b5888c88b1c12d46': { - account: '17375ac5a59e4d6585fc7d1e1c75ec74', - error: null, - hasError: false, - isConnected: true, - isRunning: true - } - } - } - } - } - - const validKonnectors = ['provider'] - const validAccounts = ['81a548fca81455ec2c2644dd55008b52'] - - expect( - getConnectionsByKonnector( - state, - 'provider', - validAccounts, - validKonnectors - ) - ).toMatchSnapshot() - }) - }) - - describe('getQueue', () => { - it('returns one queued connection per queued account', () => { - const state = { - data: { - testprovider: { - '17375ac5a59e4d6585fc7d1e1c75ec74': {}, - '63c670ea9d7b11e7b5888c88b1c12d46': { - isRunning: true, - isEnqueued: true - }, - '768ccdaa9d7b11e7869aae88b1c12d46': { - isEnqueued: true, - error: { - message: 'test error' - } - } - } - } - } - - const konnectors = { - testprovider: { - name: 'Test Provider', - slug: 'testprovider' - } - } - - const result = getQueue(state, konnectors) - - expect(result).toMatchSnapshot() - }) - }) - }) -}) diff --git a/src/ducks/jobs/index.js b/src/ducks/jobs/index.js deleted file mode 100644 index 01bd71e49c..0000000000 --- a/src/ducks/jobs/index.js +++ /dev/null @@ -1,8 +0,0 @@ -// CRUD - -export const fetchKonnectorJobs = () => {} - -// selectors - -export const isJobRunning = (state, job) => - !!job && ['queued', 'running'].includes(job.state) diff --git a/src/ducks/jobs/test/__snapshots__/jobs.spec.js.snap b/src/ducks/jobs/test/__snapshots__/jobs.spec.js.snap deleted file mode 100644 index 8489d6ce42..0000000000 --- a/src/ducks/jobs/test/__snapshots__/jobs.spec.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Jobs Duck isJobRunning returns true for error job 1`] = `false`; - -exports[`Jobs Duck isJobRunning returns true for queued job 1`] = `true`; - -exports[`Jobs Duck isJobRunning returns true for running job 1`] = `true`; diff --git a/src/ducks/jobs/test/jobs.spec.js b/src/ducks/jobs/test/jobs.spec.js deleted file mode 100644 index f8acf63fd4..0000000000 --- a/src/ducks/jobs/test/jobs.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-env jest */ - -import { isJobRunning } from '../' - -describe('Jobs Duck', () => { - describe('isJobRunning', () => { - ;['queued', 'running'].forEach(state => { - it(`returns true for ${state} job`, () => { - expect(isJobRunning({}, { state: state })).toMatchSnapshot() - }) - }) - ;[('done', 'error')].forEach(state => { - it(`returns true for ${state} job`, () => { - expect(isJobRunning({}, { state: state })).toMatchSnapshot() - }) - }) - }) -}) diff --git a/src/ducks/konnectors/index.js b/src/ducks/konnectors/index.js index d8bbf35482..9f5b3bc187 100644 --- a/src/ducks/konnectors/index.js +++ b/src/ducks/konnectors/index.js @@ -1,28 +1,4 @@ -import keyBy from 'lodash/keyBy' -import { fetchKonnectors as cozyClientFetchKonnectors } from 'lib/redux-cozy-client' - export const DOCTYPE = 'io.cozy.konnectors' -const konnectorsCollectionKey = 'konnectors' - -export const fetchKonnectors = () => - cozyClientFetchKonnectors(konnectorsCollectionKey) - -// Action creators -export const receiveInstalledKonnector = konnector => { - const normalized = { - ...konnector, - ...konnector.attributes, - id: `${DOCTYPE}/${konnector.slug}`, - _type: DOCTYPE - } - - return { - doctype: DOCTYPE, - type: 'RECEIVE_NEW_DOCUMENT', - response: { data: [normalized] }, - updateCollections: ['konnectors'] - } -} const getKonnectorsFromState = state => { return !!state && !!state.documents && state.documents[DOCTYPE] @@ -34,16 +10,6 @@ export const getKonnector = (state, slug) => { return konnectors && konnectors[`${DOCTYPE}/${slug}`] } -export const getInstalledKonnectors = state => { - const konnectors = getKonnectorsFromState(state) - return konnectors ? Object.values(konnectors) : [] -} - -export const getIndexedKonnectors = state => { - const konnectors = getKonnectorsFromState(state) - return konnectors ? keyBy(Object.values(konnectors), konn => konn.slug) : {} -} - export const getSlugs = state => { const konnectors = getKonnectorsFromState(state) return konnectors diff --git a/src/ducks/triggers/index.js b/src/ducks/triggers/index.js deleted file mode 100644 index 26f7b53ca0..0000000000 --- a/src/ducks/triggers/index.js +++ /dev/null @@ -1,40 +0,0 @@ -import { fetchTriggers as cozyClientFetchTriggers } from 'lib/redux-cozy-client' - -export const DOCTYPE = 'io.cozy.triggers' - -const triggersCollectionKey = 'triggers' - -// CRUD action creators - -export const fetchTriggers = () => - cozyClientFetchTriggers(triggersCollectionKey, ['client', 'konnector']) - -// selectors -export const getKonnectorTriggers = ( - state, - konnector, - existingAccountIds = [] -) => { - return ( - (!!state.documents[DOCTYPE] && - Object.values(state.documents[DOCTYPE]).filter(trigger => { - return ( - isKonnectorTrigger(trigger) && - trigger.message && - trigger.message.konnector === konnector.slug && - trigger.message.account && - existingAccountIds.includes(trigger.message.account) - ) - })) || - [] - ) -} - -const isKonnectorTrigger = trigger => { - return trigger.worker === 'konnector' || trigger.worker === 'client' -} - -export const getTrigger = (state, id) => - !!state.documents && - !!state.documents[DOCTYPE] && - state.documents[DOCTYPE][id] diff --git a/src/global.d.ts b/src/global.d.ts index 61c495889c..e636611dc1 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,5 +1,4 @@ -declare module 'cozy-harvest-lib' { - export const handleOAuthResponse: () => boolean -} +declare module 'cozy-harvest-lib' declare module 'cozy-ui/transpiled/react/providers/Breakpoints' declare module 'cozy-ui/transpiled/react/Spinner' +declare module 'cozy-ui/transpiled/react/SquareAppIcon' diff --git a/src/lib/HomeStore.js b/src/lib/HomeStore.js index 52849dc242..3fb927bbf8 100644 --- a/src/lib/HomeStore.js +++ b/src/lib/HomeStore.js @@ -1,190 +1,19 @@ -/* global cozy */ -import * as triggers from 'lib/triggers' -import { isKonnectorJob } from 'ducks/connections' - -import { - RECEIVE_CREATED_KONNECTOR, - RECEIVE_DELETED_KONNECTOR, - RECEIVE_UPDATED_KONNECTOR -} from 'lib/redux-cozy-client/reducer' - +import Intents from 'cozy-interapp' export const ACCOUNTS_DOCTYPE = 'io.cozy.accounts' export const JOBS_DOCTYPE = 'io.cozy.jobs' export const TRIGGERS_DOCTYPE = 'io.cozy.triggers' export const KONS_DOCTYPE = 'io.cozy.konnectors' -const normalize = (dbObject, doctype) => { - return { - ...dbObject, - ...dbObject.attributes, - id: dbObject._id, - _type: doctype || dbObject._type - } -} - export default class HomeStore { constructor(context, client, options = {}) { this.client = client this.listener = null this.options = options - + this.intents = new Intents({ client }) this.categories = require('../config/categories') - - this.updateUnfinishedJob = this.updateUnfinishedJob.bind(this) - this.onAccountCreated = this.onAccountCreated.bind(this) - this.onAccountUpdated = this.onAccountUpdated.bind(this) - this.onAccountDeleted = this.onAccountDeleted.bind(this) - this.onTriggerCreated = this.onTriggerCreated.bind(this) - this.onTriggerUpdated = this.onTriggerUpdated.bind(this) - this.onTriggerDeleted = this.onTriggerDeleted.bind(this) - this.onKonnectorCreated = this.onKonnectorCreated.bind(this) - this.onKonnectorUpdated = this.onKonnectorUpdated.bind(this) - this.onKonnectorDeleted = this.onKonnectorDeleted.bind(this) - - this.initializeRealtime() - } - - initializeRealtime() { - const realtime = this.client.plugins.realtime - - realtime.subscribe('created', JOBS_DOCTYPE, this.updateUnfinishedJob) - realtime.subscribe('updated', JOBS_DOCTYPE, this.updateUnfinishedJob) - - realtime.subscribe('created', ACCOUNTS_DOCTYPE, this.onAccountCreated) - realtime.subscribe('updated', ACCOUNTS_DOCTYPE, this.onAccountUpdated) - realtime.subscribe('deleted', ACCOUNTS_DOCTYPE, this.onAccountDeleted) - - realtime.subscribe('created', TRIGGERS_DOCTYPE, this.onTriggerCreated) - realtime.subscribe('updated', TRIGGERS_DOCTYPE, this.onTriggerUpdated) - realtime.subscribe('deleted', TRIGGERS_DOCTYPE, this.onTriggerDeleted) - - realtime.subscribe('created', KONS_DOCTYPE, this.onKonnectorCreated) - realtime.subscribe('updated', KONS_DOCTYPE, this.onKonnectorUpdated) - realtime.subscribe('deleted', KONS_DOCTYPE, this.onKonnectorDeleted) - } - - async onAccountCreated(account) { - this.dispatch({ - type: 'RECEIVE_NEW_DOCUMENT', - response: { data: [normalize(account, ACCOUNTS_DOCTYPE)] }, - updateCollections: ['accounts'] - }) - } - - async onAccountUpdated(account) { - this.dispatch({ - type: 'RECEIVE_UPDATED_DOCUMENT', - response: { data: [normalize(account, ACCOUNTS_DOCTYPE)] }, - updateCollections: ['accounts'] - }) - } - - async onAccountDeleted(account) { - this.dispatch({ - type: 'RECEIVE_DELETED_DOCUMENT', - response: { data: [normalize(account, ACCOUNTS_DOCTYPE)] }, - updateCollections: ['accounts'] - }) - } - - async onTriggerCreated(trigger) { - this.dispatch({ - type: 'RECEIVE_NEW_DOCUMENT', - response: { data: [normalize(trigger, TRIGGERS_DOCTYPE)] }, - updateCollections: ['triggers'] - }) - } - - async onTriggerUpdated(trigger) { - this.dispatch({ - type: 'RECEIVE_UPDATED_DOCUMENT', - response: { data: [normalize(trigger, TRIGGERS_DOCTYPE)] }, - updateCollections: ['triggers'] - }) - } - - async onTriggerDeleted(trigger) { - this.dispatch({ - type: 'RECEIVE_DELETED_DOCUMENT', - response: { data: [normalize(trigger, TRIGGERS_DOCTYPE)] }, - updateCollections: ['triggers'] - }) - } - - async onKonnectorCreated(konnector) { - this.dispatch({ - type: RECEIVE_CREATED_KONNECTOR, - response: { data: [normalize(konnector, KONS_DOCTYPE)] }, - updateCollections: ['konnectors'] - }) - } - - async onKonnectorUpdated(konnector) { - this.dispatch({ - type: RECEIVE_UPDATED_KONNECTOR, - response: { data: [normalize(konnector, KONS_DOCTYPE)] }, - updateCollections: ['konnectors'] - }) - } - - async onKonnectorDeleted(konnector) { - this.dispatch({ - type: RECEIVE_DELETED_KONNECTOR, - response: { data: [normalize(konnector, KONS_DOCTYPE)] }, - updateCollections: ['konnectors'] - }) - } - - async updateUnfinishedJob(job) { - const normalizedJob = normalize(job, JOBS_DOCTYPE) - // TODO Filter by worker on the WebSocket when it will be available in the - // stack - const isDeletedAccountHookJob = !!normalizedJob.account_deleted - const isKonnectorJobWithoutTrigger = !normalizedJob.trigger_id - if ( - !isKonnectorJob || - isDeletedAccountHookJob || - isKonnectorJobWithoutTrigger - ) { - return - } - - this.dispatch({ - type: 'RECEIVE_NEW_DOCUMENT', - response: { data: [normalizedJob] }, - updateCollections: ['jobs'] - }) - const trigger = await triggers.fetch(cozy.client, normalizedJob.trigger_id) - this.onTriggerUpdated(trigger) } createIntentService(intent, window) { - return cozy.client.intents.createService(intent, window) - } - - // Get the drive and banks application url using the list of application - fetchUrls() { - return ( - cozy.client - .fetchJSON('GET', '/apps/') - // eslint-disable-next-line promise/always-return - .then(body => { - body.forEach(item => { - if (!item.attributes || !item.attributes.slug || !item.links) return - switch (item.attributes.slug) { - case 'banks': - this.banksUrl = `${item.links.related}` - break - default: - break - } - }) - }) - .catch(err => { - // eslint-disable-next-line no-console - console.warn(err.message) - return false - }) - ) + return this.intents.createService(intent, window) } } diff --git a/src/lib/HomeStore.spec.js b/src/lib/HomeStore.spec.js deleted file mode 100644 index a54bb001c0..0000000000 --- a/src/lib/HomeStore.spec.js +++ /dev/null @@ -1,211 +0,0 @@ -import CozyClient from 'cozy-client' -import HomeStore, { - ACCOUNTS_DOCTYPE, - JOBS_DOCTYPE, - KONS_DOCTYPE, - TRIGGERS_DOCTYPE -} from './HomeStore' -import triggers from 'lib/triggers' -import { dummyKonnector } from 'lib/redux-cozy-client/reducer.spec' -import { - RECEIVE_CREATED_KONNECTOR, - RECEIVE_DELETED_KONNECTOR, - RECEIVE_UPDATED_KONNECTOR -} from 'lib/redux-cozy-client/reducer' - -const mockSubscribe = jest.fn() - -jest.mock('lib/triggers', () => ({ - fetch: jest.fn() -})) - -global.cozy = { - client: {} -} - -describe('HomeStore', () => { - const setup = () => { - const context = {} - const client = new CozyClient({ - uri: 'http://cozy.tools:8080' - }) - client.plugins.realtime = { - subscribe: mockSubscribe - } - HomeStore.prototype.fetchUrls = jest.fn() - const store = new HomeStore(context, client) - store.dispatch = jest.fn() - store.onTriggerUpdated = jest.fn() - return { client, store } - } - - afterEach(() => { - triggers.fetch.mockReset() - }) - - it('should react to created or updated job', async () => { - const { store } = setup() - const unfinishedJob = { - worker: 'konnector', - account_deleted: false, - trigger_id: '1337' - } - - const foundTrigger = {} - triggers.fetch.mockResolvedValueOnce(foundTrigger) - await store.updateUnfinishedJob(unfinishedJob) - expect(triggers.fetch).toHaveBeenCalledWith(global.cozy.client, '1337') - expect(store.onTriggerUpdated).toHaveBeenCalledWith(foundTrigger) - }) - - it('should not react to job without trigger', async () => { - const { store } = setup() - const unfinishedJob = { - worker: 'konnector', - account_deleted: false - } - await store.updateUnfinishedJob(unfinishedJob) - expect(triggers.fetch).not.toHaveBeenCalled() - }) - - it('should not react to job for deleted account', async () => { - const { store } = setup() - const unfinishedJob = { - worker: 'konnector', - account_deleted: true, - trigger_id: '1337' - } - await store.updateUnfinishedJob(unfinishedJob) - expect(triggers.fetch).not.toHaveBeenCalled() - }) - - describe('initializeRealtime', () => { - it('should subscribe to realtime events - on construct', () => { - // Given - mockSubscribe.mockReset() - - // When - setup() - - // Then - expect(mockSubscribe).toHaveBeenCalledTimes(11) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 1, - 'created', - JOBS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 2, - 'updated', - JOBS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 3, - 'created', - ACCOUNTS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 4, - 'updated', - ACCOUNTS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 5, - 'deleted', - ACCOUNTS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 6, - 'created', - TRIGGERS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 7, - 'updated', - TRIGGERS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 8, - 'deleted', - TRIGGERS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 9, - 'created', - KONS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 10, - 'updated', - KONS_DOCTYPE, - expect.any(Function) - ) - expect(mockSubscribe).toHaveBeenNthCalledWith( - 11, - 'deleted', - KONS_DOCTYPE, - expect.any(Function) - ) - }) - }) - - describe('onKonnectorCreated', () => { - it('should dispatch action RECEIVE_CREATED_KONNECTOR', () => { - // Given - const { store } = setup() - - // When - store.onKonnectorCreated(dummyKonnector()) - - // Then - expect(store.dispatch).toHaveBeenCalledWith({ - type: RECEIVE_CREATED_KONNECTOR, - response: { data: [dummyKonnector()] }, - updateCollections: ['konnectors'] - }) - }) - }) - - describe('onKonnectorUpdated', () => { - it('should dispatch action RECEIVE_CREATED_KONNECTOR', () => { - // Given - const { store } = setup() - - // When - store.onKonnectorUpdated(dummyKonnector()) - - // Then - expect(store.dispatch).toHaveBeenCalledWith({ - type: RECEIVE_UPDATED_KONNECTOR, - response: { data: [dummyKonnector()] }, - updateCollections: ['konnectors'] - }) - }) - }) - - describe('onKonnectorDeleted', () => { - it('should dispatch action RECEIVE_CREATED_KONNECTOR', () => { - // Given - const { store } = setup() - - // When - store.onKonnectorDeleted(dummyKonnector()) - - // Then - expect(store.dispatch).toHaveBeenCalledWith({ - type: RECEIVE_DELETED_KONNECTOR, - response: { data: [dummyKonnector()] }, - updateCollections: ['konnectors'] - }) - }) - }) -}) diff --git a/src/lib/konnectors.js b/src/lib/konnectors.js index 8f51209e2b..77cd3e8ad8 100644 --- a/src/lib/konnectors.js +++ b/src/lib/konnectors.js @@ -1,4 +1,3 @@ -/* konnector lib ready to be added to cozy-client-js */ export const ERROR_TYPES = { CHALLENGE_ASKED: 'CHALLENGE_ASKED', LOGIN_FAILED: 'LOGIN_FAILED', diff --git a/src/lib/redux-cozy-client/CozyClient.js b/src/lib/redux-cozy-client/CozyClient.js deleted file mode 100644 index 54eb85dbd5..0000000000 --- a/src/lib/redux-cozy-client/CozyClient.js +++ /dev/null @@ -1,54 +0,0 @@ -/* global cozy */ - -import CozyStackAdapter from './adapters/CozyStackAdapter' - -export default class CozyClient { - constructor(config) { - const { cozyClient, ...options } = config - this.options = options - this.indexes = {} - this.specialDirectories = {} - cozy.client.init(config) - this.stackAdapter = new CozyStackAdapter(cozyClient.stackClient) - } - - async fetchCollection(name, doctype, options = {}, skip = 0) { - if (options.selector) { - const index = await this.getCollectionIndex(name, doctype, options) - return this.stackAdapter.queryDocuments(doctype, index, { - ...options, - skip - }) - } - return this.stackAdapter.fetchDocuments(doctype) - } - - fetchTriggers(name, worker) { - return this.stackAdapter.fetchTriggers(worker) - } - - fetchKonnectors() { - return this.stackAdapter.fetchKonnectors() - } - - async getCollectionIndex(name, doctype, options) { - if (!this.indexes[name]) { - this.indexes[name] = await this.stackAdapter.createIndex( - doctype, - this.getIndexFields(options) - ) - } - return this.indexes[name] - } - - getIndexFields(options) { - const { selector, sort } = options - if (sort) { - // We filter possible duplicated fields - return [...Object.keys(selector), ...Object.keys(sort)].filter( - (f, i, arr) => arr.indexOf(f) === i - ) - } - return Object.keys(selector) - } -} diff --git a/src/lib/redux-cozy-client/CozyProvider.jsx b/src/lib/redux-cozy-client/CozyProvider.jsx deleted file mode 100644 index c24d6d6357..0000000000 --- a/src/lib/redux-cozy-client/CozyProvider.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Component } from 'react' -import PropTypes from 'prop-types' - -export default class CozyProvider extends Component { - static propTypes = { - domain: PropTypes.string, - secure: PropTypes.bool, - store: PropTypes.shape({ - subscribe: PropTypes.func.isRequired, - dispatch: PropTypes.func.isRequired, - getState: PropTypes.func.isRequired - }), - client: PropTypes.object.isRequired, - children: PropTypes.element.isRequired - } - - static childContextTypes = { - domain: PropTypes.string, - secure: PropTypes.bool, - store: PropTypes.object, - client: PropTypes.object.isRequired - } - - static contextTypes = { - store: PropTypes.object - } - - getChildContext() { - return { - domain: this.props.domain, - secure: this.props.secure, - store: this.props.store || this.context.store, - client: this.props.client - } - } - - render() { - return this.props.children || null - } -} diff --git a/src/lib/redux-cozy-client/__tests__/store.spec.js b/src/lib/redux-cozy-client/__tests__/store.spec.js deleted file mode 100644 index 38afee4dac..0000000000 --- a/src/lib/redux-cozy-client/__tests__/store.spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import { combineReducers } from 'redux' -import { reducer as cozyReducer, fetchCollection, getCollection } from '..' - -const reducer = combineReducers({ cozy: cozyReducer }) -const dispatchInitialAction = (compositeAction, initialState = undefined) => { - const { types, ...rest } = compositeAction - return reducer(initialState, { type: types[0], ...rest }) -} - -const dispatchSuccessfulAction = ( - compositeAction, - response, - initialState = undefined -) => { - const { types, ...rest } = compositeAction - const actions = [ - { type: types[0], ...rest }, - { type: types[1], ...rest, response } - ] - return actions.reduce((state, action) => reducer(state, action), initialState) -} - -describe('Redux store tests', () => { - describe('Given the store is empty', () => { - it('should return a default state', () => { - expect(getCollection(reducer(undefined, {}), 'rockets')).toEqual({ - type: null, - options: {}, - fetchStatus: 'pending', - lastFetch: null, - hasMore: false, - count: 0, - ids: [], - data: null - }) - }) - - describe('When a collection is fetched', () => { - it('should have a `loading` status', () => { - const state = dispatchInitialAction( - fetchCollection('rockets', 'io.cozy.rockets') - ) - const collection = getCollection(state, 'rockets') - expect(collection.fetchStatus).toBe('loading') - }) - }) - }) - - const fakeFetchResponse = { - data: [ - { - id: '33dda00f0eec15bc3b3c59a615001ac7', - _type: 'io.cozy.rockets', - name: 'Falcon 9' - }, - { - id: '33dda00f0eec15bc3b3c59a615001ac8', - _type: 'io.cozy.rockets', - name: 'Falcon Heavy' - }, - { - id: '33dda00f0eec15bc3b3c59a615001ac9', - _type: 'io.cozy.rockets', - name: 'BFR' - } - ] - } - - describe('Given a collection has been successfully fetched', () => { - let state, collection - - beforeEach(() => { - state = dispatchSuccessfulAction( - fetchCollection('rockets', 'io.cozy.rockets'), - fakeFetchResponse - ) - collection = getCollection(state, 'rockets') - }) - - it('should have a `loaded` status', () => { - expect(collection.fetchStatus).toBe('loaded') - }) - - it('should have a `data` property with the list of documents', () => { - expect(collection.data).toEqual(fakeFetchResponse.data) - }) - }) -}) diff --git a/src/lib/redux-cozy-client/adapters/CozyStackAdapter.js b/src/lib/redux-cozy-client/adapters/CozyStackAdapter.js deleted file mode 100644 index 549d4e8b49..0000000000 --- a/src/lib/redux-cozy-client/adapters/CozyStackAdapter.js +++ /dev/null @@ -1,112 +0,0 @@ -/* global cozy */ -const FETCH_LIMIT = 50 - -export default class CozyStackAdapter { - constructor(stackClient) { - this.stackClient = stackClient - } - - async fetchApps(skip = 0) { - const { data, meta } = await this.stackClient.fetchJSON( - 'GET', - '/apps/', - null, - { - processJSONAPI: false - } - ) - - return { - data: data || [], - meta: meta, - skip, - next: !!meta && meta.count > skip + FETCH_LIMIT - } - } - - async fetchDocuments(doctype) { - // WARN: cozy-client-js lacks a cozy.data.findAll method that uses this route - try { - // WARN: if no document of this doctype exist, this route will return a 404, - // so we need to try/catch and return an empty response object in case of a 404 - const resp = await this.stackClient.fetchJSON( - 'GET', - `/data/${doctype}/_all_docs?include_docs=true` - ) - // WARN: the JSON response from the stack is not homogenous with other routes (offset? rows? total_rows?) - // see https://github.com/cozy/cozy-stack/blob/master/docs/data-system.md#list-all-the-documents - // WARN: looks like this route returns something looking like a couchDB design doc, we need to filter it: - // eslint-disable-next-line no-prototype-builtins - const rows = resp.rows.filter(row => !row.doc.hasOwnProperty('views')) - // we normalize the data (note that we add _type so that cozy.client.data.listReferencedFiles works...) - const docs = rows.map(row => - Object.assign({}, row.doc, { id: row.id, _type: doctype }) - ) - // we forge a correct JSONAPI response: - return { - data: docs, - meta: { count: resp.total_rows }, - skip: resp.offset, - next: false - } - } catch (error) { - if (error.message.match(/not_found/)) { - return { data: [], meta: { count: 0 }, skip: 0, next: false } - } - throw error - } - } - - createIndex(doctype, fields) { - return cozy.client.data.defineIndex(doctype, fields) - } - - async fetchKonnectors(skip = 0) { - const { data, meta } = await this.stackClient.fetchJSON( - 'GET', - `/konnectors/`, - null, - { - processJSONAPI: false - } - ) - - return { - data: data - ? data.map(konnector => ({ - ...konnector, - ...konnector.attributes, - id: konnector.id, - _type: 'io.cozy.konnectors' - })) - : [], - meta: meta, - skip, - next: !!meta && meta.count > skip + FETCH_LIMIT - } - } - - async fetchTriggers(worker, skip = 0) { - const { data, meta } = await this.stackClient.fetchJSON( - 'GET', - `/jobs/triggers?Worker=${worker}`, - null, - { - processJSONAPI: false - } - ) - - return { - data: data - ? data.map(trigger => ({ - ...trigger, - ...trigger.attributes, - _type: 'io.cozy.triggers' - })) - : [], - meta: meta, - skip, - next: !!meta && meta.count > skip + FETCH_LIMIT - } - } -} diff --git a/src/lib/redux-cozy-client/connect.jsx b/src/lib/redux-cozy-client/connect.jsx deleted file mode 100644 index 59dd6d929e..0000000000 --- a/src/lib/redux-cozy-client/connect.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { Component } from 'react' -import { connect as reduxConnect } from 'react-redux' -import PropTypes from 'prop-types' - -import { applySelectorForAction, enhancePropsForActions } from '.' -import { mapValues, filterValues } from './utils' - -const connect = mapDocumentsToProps => WrappedComponent => { - class Wrapper extends Component { - componentDidMount() { - const { fetchActions } = this.props - const dispatch = this.context.store.dispatch - for (const propName in fetchActions) { - dispatch(fetchActions[propName]) - } - } - - static contextTypes = { - store: PropTypes.object - } - - render() { - const { store } = this.context - const { fetchActions } = this.props - const props = { - ...this.props, - ...enhancePropsForActions(this.props, fetchActions, store.dispatch) - } - return - } - } - - const makeMapStateToProps = (initialState, initialOwnProps) => { - const initialProps = mapDocumentsToProps(initialOwnProps) - - const isAction = action => action && action.types && action.promise - const fetchActions = filterValues(initialProps, prop => isAction(prop)) - const otherProps = filterValues(initialProps, prop => !isAction(prop)) - - const mapStateToProps = state => ({ - ...mapValues(fetchActions, action => - isAction(action) ? applySelectorForAction(state, action) : action - ), - fetchActions, - ...otherProps - }) - return mapStateToProps - } - - return reduxConnect(makeMapStateToProps)(Wrapper) -} - -export default connect diff --git a/src/lib/redux-cozy-client/index.js b/src/lib/redux-cozy-client/index.js deleted file mode 100644 index fc0b4c0ee6..0000000000 --- a/src/lib/redux-cozy-client/index.js +++ /dev/null @@ -1,13 +0,0 @@ -export { default as CozyProvider } from './CozyProvider' -export { default as CozyClient } from './CozyClient' -export { default as cozyConnect } from './connect' -export { default as cozyMiddleware } from './middleware' -export { - applySelectorForAction, - enhancePropsForActions, - default as reducer, - fetchCollection, - fetchKonnectors, - fetchTriggers, - getCollection -} from './reducer' diff --git a/src/lib/redux-cozy-client/middleware.js b/src/lib/redux-cozy-client/middleware.js deleted file mode 100644 index 673d53f024..0000000000 --- a/src/lib/redux-cozy-client/middleware.js +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable promise/no-callback-in-promise */ -const cozyMiddleware = - client => - ({ dispatch }) => { - return next => action => { - const { promise, type, types, ...rest } = action - if (!promise) { - return next(action) - } - - if (!type && !types) { - return promise(client).then(action => dispatch(action)) - } - - if (type) { - return promise(client).then(response => { - next({ ...rest, response, type }) - return response - }) - } - - const [REQUEST, SUCCESS, FAILURE] = types - next({ ...rest, type: REQUEST }) - - return promise(client) - .then( - response => { - next({ ...rest, response, type: SUCCESS }) - return response - }, - error => { - console.log(error) // eslint-disable-line no-console - next({ ...rest, error, type: FAILURE }) - } - ) - .catch(error => { - console.error('MIDDLEWARE ERROR:', error) // eslint-disable-line no-console - next({ ...rest, error, type: FAILURE }) - }) - } - } - -export default cozyMiddleware diff --git a/src/lib/redux-cozy-client/reducer.js b/src/lib/redux-cozy-client/reducer.js deleted file mode 100644 index 4869496eab..0000000000 --- a/src/lib/redux-cozy-client/reducer.js +++ /dev/null @@ -1,400 +0,0 @@ -import { combineReducers } from 'redux' -import { mapValues, removeObjectProperty } from 'lib/redux-cozy-client/utils' -import uniq from 'lodash/uniq' -import without from 'lodash/without' - -const FETCH_COLLECTION = 'FETCH_COLLECTION' -const RECEIVE_DATA = 'RECEIVE_DATA' -const RECEIVE_ERROR = 'RECEIVE_ERROR' - -export const RECEIVE_CREATED_KONNECTOR = 'RECEIVE_CREATED_KONNECTOR' -export const RECEIVE_UPDATED_KONNECTOR = 'RECEIVE_UPDATED_KONNECTOR' -export const RECEIVE_DELETED_KONNECTOR = 'RECEIVE_DELETED_KONNECTOR' - -const RECEIVE_APP = 'RECEIVE_APP' -const RECEIVE_NEW_DOCUMENT = 'RECEIVE_NEW_DOCUMENT' -const RECEIVE_UPDATED_DOCUMENT = 'RECEIVE_UPDATED_DOCUMENT' -const RECEIVE_DELETED_DOCUMENT = 'RECEIVE_DELETED_DOCUMENT' -const FETCH_REFERENCED_FILES = 'FETCH_REFERENCED_FILES' -const ADD_REFERENCED_FILES = 'ADD_REFERENCED_FILES' -const REMOVE_REFERENCED_FILES = 'REMOVE_REFERENCED_FILES' - -const documents = (state = {}, action) => { - switch (action.type) { - case RECEIVE_CREATED_KONNECTOR: - case RECEIVE_UPDATED_KONNECTOR: { - const { data } = action.response - if (data.length === 0) return state - const dataDoctype = getArrayDoctype(data) - return { - ...state, - [dataDoctype]: { - ...state[dataDoctype], - ...objectifyDocumentsArray(data) - } - } - } - case RECEIVE_DELETED_KONNECTOR: { - const deleted = action.response.data[0] - return { - ...state, - [deleted._type]: removeObjectProperty(state[deleted._type], deleted.id) - } - } - case RECEIVE_DATA: { - const { data } = action.response - if (data.length === 0) return state - const dataDoctype = getArrayDoctype(data) - // This is a temporary fix since old cozyclient is reading the - // documents from this slice even if its queries don't contain - // all the fields. So this hack is here to remove data when - // receiving data... - if ( - state[dataDoctype] && - data.length < Object.values(state[dataDoctype]).length - ) { - return { - ...state, - [dataDoctype]: { - ...objectifyDocumentsArray(data) - } - } - } else { - return { - ...state, - [dataDoctype]: { - ...state[dataDoctype], - ...objectifyDocumentsArray(data) - } - } - } - } - case RECEIVE_NEW_DOCUMENT: - case RECEIVE_UPDATED_DOCUMENT: { - const doc = action.response.data[0] - return { - ...state, - [doc._type]: { - ...state[doc._type], - [doc.id]: doc - } - } - } - case RECEIVE_DELETED_DOCUMENT: { - const deleted = action.response.data[0] - return { - ...state, - [deleted._type]: removeObjectProperty(state[deleted._type], deleted.id) - } - } - case ADD_REFERENCED_FILES: - return { - ...state, - 'io.cozy.files': { - ...state['io.cozy.files'], - ...updateFilesReferences( - state['io.cozy.files'], - action.ids, - action.document - ) - } - } - case REMOVE_REFERENCED_FILES: - return { - ...state, - 'io.cozy.files': { - ...state['io.cozy.files'], - ...removeFilesReferences( - state['io.cozy.files'], - action.ids, - action.document - ) - } - } - default: - return state - } -} - -const objectifyDocumentsArray = documents => - documents.reduce((obj, doc) => ({ ...obj, [doc.id]: doc }), {}) - -const updateFileReference = ( - { relationships: { referenced_by, ...relationships }, ...file }, - doc -) => ({ - ...file, - relationships: { - ...relationships, - [referenced_by.data]: - referenced_by.data === null - ? [{ id: doc.id, type: doc.type }] - : [...referenced_by.data, { id: doc.id, type: doc.type }] - } -}) - -const updateFilesReferences = (files, newlyReferencedIds, doc) => - newlyReferencedIds.reduce( - (updated, id) => ({ - ...updated, - [id]: updateFileReference(files[id], doc) - }), - {} - ) - -const removeFileReferences = ( - { relationships: { referenced_by, ...relationships }, ...file }, - doc -) => ({ - ...file, - relationships: { - ...relationships, - [referenced_by.data]: referenced_by.data.filter( - rel => rel.type !== doc.type && rel.id !== doc.id - ) - } -}) - -const removeFilesReferences = (files, removedIds, doc) => - removedIds.reduce( - (updated, id) => ({ - ...updated, - [id]: removeFileReferences(files[id], doc) - }), - {} - ) - -const getDoctype = ({ _type: doctype }) => { - // TODO: don't know why the stack returns 'file' here.. - if (doctype === 'file') { - return 'io.cozy.files' - } - return doctype -} - -const getArrayDoctype = documents => getDoctype(documents[0]) - -// collection reducers -const collectionInitialState = { - type: null, - options: {}, - fetchStatus: 'pending', - lastFetch: null, - hasMore: false, - count: 0, - ids: [] -} - -const collection = (state = collectionInitialState, action) => { - switch (action.type) { - case FETCH_COLLECTION: - case FETCH_REFERENCED_FILES: - return { - ...state, - type: action.doctype || 'io.cozy.files', - options: action.options, - fetchStatus: - state.fetchStatus === 'loaded' - ? 'loaded' - : action.skip > 0 - ? 'loadingMore' - : 'loading' - } - case RECEIVE_CREATED_KONNECTOR: - case RECEIVE_UPDATED_KONNECTOR: { - const updatedIds = uniq([ - ...state.ids, - ...action.response.data.map(({ _id }) => _id) - ]) - return { - ...state, - count: updatedIds.length, - lastFetch: Date.now(), - ids: updatedIds - } - } - case RECEIVE_DELETED_KONNECTOR: { - const updatedIds = without( - state.ids, - ...action.response.data.map(({ _id }) => _id) - ) - return { - ...state, - count: updatedIds.length, - lastFetch: Date.now(), - ids: updatedIds - } - } - case RECEIVE_APP: - case RECEIVE_DATA: { - const response = action.response - return { - ...state, - fetchStatus: 'loaded', - lastFetch: Date.now(), - hasMore: response.next !== undefined ? response.next : state.hasMore, - count: - response.meta && response.meta.count - ? response.meta.count - : response.data.length, - ids: !action.skip - ? response.data.map(doc => doc.id) - : [...state.ids, ...response.data.map(doc => doc.id)] - } - } - case ADD_REFERENCED_FILES: - return { - ...state, - type: 'io.cozy.files', - count: state.count + action.ids.length, - ids: [...state.ids, ...action.ids] - } - case REMOVE_REFERENCED_FILES: - return { - ...state, - count: state.count - action.ids.length, - ids: state.ids.filter(id => action.ids.indexOf(id) === -1) - } - case RECEIVE_ERROR: - return { - ...state, - fetchStatus: 'failed' - } - case RECEIVE_NEW_DOCUMENT: - return { - ...state, - ids: [...state.ids, action.response.data[0].id] - } - case RECEIVE_DELETED_DOCUMENT: - return { - ...state, - ids: state.ids.filter(id => id !== action.response.data[0].id) - } - default: - return state - } -} - -const collections = (state = {}, action) => { - const applyUpdate = (collections, updateAction) => - updateAction.updateCollections.reduce( - (updated, name) => ({ - ...updated, - [name]: collection(collections[name], action) - }), - {} - ) - - switch (action.type) { - case FETCH_COLLECTION: - case FETCH_REFERENCED_FILES: - case ADD_REFERENCED_FILES: - case REMOVE_REFERENCED_FILES: - case RECEIVE_APP: - case RECEIVE_DATA: - case RECEIVE_ERROR: - if (!action.collection) { - return state - } - return { - ...state, - [action.collection]: collection(state[action.collection], action) - } - case RECEIVE_CREATED_KONNECTOR: - case RECEIVE_DELETED_KONNECTOR: - case RECEIVE_UPDATED_KONNECTOR: - case RECEIVE_NEW_DOCUMENT: - case RECEIVE_DELETED_DOCUMENT: - if (!action.updateCollections) { - return state - } - return { - ...state, - ...applyUpdate(state, action) - } - default: - return state - } -} - -// selectors -const mapDocumentsToIds = (documents, doctype, ids) => - ids.map(id => documents[doctype][id]) - -export const getCollection = (state, name) => { - const collection = - state.oldcozy?.collections?.[name] || state.cozy?.collections?.[name] - - if (!collection) return { ...collectionInitialState, data: null } - - return { - ...collection, - data: mapDocumentsToIds( - state.oldcozy?.documents || state.cozy?.documents, - collection.type, - collection.ids - ) - } -} - -export const makeFetchMoreAction = ({ collection, doctype, options }, skip) => - fetchCollection(collection, doctype, options, skip) - -export const applySelectorForAction = (state, action) => { - switch (action.types[0]) { - case FETCH_COLLECTION: - return getCollection(state, action.collection) - default: - return null - } -} - -export const enhancePropsForActions = (props, fetchActions, dispatch) => - mapValues(fetchActions, (action, propName) => { - const dataObject = props[propName] - switch (action.types[0]) { - case FETCH_COLLECTION: - case FETCH_REFERENCED_FILES: - return { - ...dataObject, - fetchMore: dataObject.hasMore - ? () => - dispatch(makeFetchMoreAction(action, dataObject.data.length)) - : null - } - default: - return dataObject - } - }) - -export default combineReducers({ - collections, - documents -}) - -export const fetchCollection = (name, doctype, options = {}, skip = 0) => ({ - types: [FETCH_COLLECTION, RECEIVE_DATA, RECEIVE_ERROR], - collection: name, - doctype, - options, - skip, - promise: client => client.fetchCollection(name, doctype, options, skip) -}) - -export const fetchTriggers = (name, worker, options = {}, skip = 0) => ({ - types: [FETCH_COLLECTION, RECEIVE_DATA, RECEIVE_ERROR], - collection: name, - doctype: 'io.cozy.triggers', - options, - skip, - promise: client => client.fetchTriggers(name, worker, options, skip) -}) - -export const fetchKonnectors = (name, options = {}, skip = 0) => ({ - types: [FETCH_COLLECTION, RECEIVE_DATA, RECEIVE_ERROR], - collection: name, - doctype: 'io.cozy.konnectors', - options, - skip, - promise: client => client.fetchKonnectors(name, options, skip) -}) diff --git a/src/lib/redux-cozy-client/reducer.spec.js b/src/lib/redux-cozy-client/reducer.spec.js deleted file mode 100644 index 9ab9bc3f87..0000000000 --- a/src/lib/redux-cozy-client/reducer.spec.js +++ /dev/null @@ -1,234 +0,0 @@ -import { combineReducers } from 'redux' -import { reducer as cozyReducer } from './index' -import { ACCOUNTS_DOCTYPE, KONS_DOCTYPE, TRIGGERS_DOCTYPE } from '../HomeStore' -import { - RECEIVE_CREATED_KONNECTOR, - RECEIVE_DELETED_KONNECTOR, - RECEIVE_UPDATED_KONNECTOR -} from './reducer' - -export const dummyKonnector = konnector => ({ - _type: KONS_DOCTYPE, - type: 'konnector', - id: `${KONS_DOCTYPE}/ameli`, - _id: `${KONS_DOCTYPE}/ameli`, - name: 'Ameli', - ...konnector -}) - -describe('reducer', () => { - let reducer - let action - const now = 1651052463319 - const alanKonnector = dummyKonnector({ - name: 'Alan', - _id: `${KONS_DOCTYPE}/alan`, - id: `${KONS_DOCTYPE}/alan` - }) - const initialDocuments = { - [ACCOUNTS_DOCTYPE]: {}, - [TRIGGERS_DOCTYPE]: {}, - [KONS_DOCTYPE]: { - [dummyKonnector().id]: dummyKonnector(), - [alanKonnector.id]: alanKonnector - } - } - const constructInitialState = state => ({ - cozy: { documents: initialDocuments, ...state } - }) - - beforeEach(() => { - jest.spyOn(Date, 'now').mockReturnValue(now) - reducer = combineReducers({ cozy: cozyReducer }) - }) - - describe('RECEIVE_DELETED_KONNECTOR', () => { - beforeEach(() => { - action = { - type: RECEIVE_DELETED_KONNECTOR, - response: { data: [alanKonnector] }, - updateCollections: ['konnectors'] - } - }) - - it('should remove one konnector in the documents', () => { - // Given - const documents = { - [ACCOUNTS_DOCTYPE]: {}, - [TRIGGERS_DOCTYPE]: {}, - [KONS_DOCTYPE]: { - [dummyKonnector().id]: dummyKonnector(), - [alanKonnector.id]: alanKonnector - } - } - const initialState = constructInitialState({ documents }) - - // When - const state = reducer(initialState, action) - - // Then - expect(state.cozy.documents[KONS_DOCTYPE]).toEqual({ - [dummyKonnector().id]: dummyKonnector() - }) - }) - - it('should remove one konnector in the collections', () => { - // Given - const collections = { - ['accounts']: {}, - ['triggers']: {}, - ['konnectors']: { - type: 'io.cozy.konnectors', - options: {}, - fetchStatus: 'loaded', - lastFetch: 1651052463319, - hasMore: false, - count: 2, - ids: ['io.cozy.konnectors/alan', 'io.cozy.konnectors/ameli'] - } - } - const initialState = constructInitialState({ collections }) - - // When - const state = reducer(initialState, action) - - // Then - expect(state.cozy.collections['konnectors']).toEqual({ - type: 'io.cozy.konnectors', - options: {}, - fetchStatus: 'loaded', - lastFetch: now, - hasMore: false, - count: 1, - ids: ['io.cozy.konnectors/ameli'] - }) - }) - }) - - describe('RECEIVE_CREATED_KONNECTOR', () => { - beforeEach(() => { - action = { - type: RECEIVE_CREATED_KONNECTOR, - response: { data: [alanKonnector] }, - updateCollections: ['konnectors'] - } - }) - - it('should add one konnector in the documents', () => { - // Given - const documents = { - [ACCOUNTS_DOCTYPE]: {}, - [TRIGGERS_DOCTYPE]: {}, - [KONS_DOCTYPE]: { - [dummyKonnector().id]: dummyKonnector() - } - } - const initialState = constructInitialState({ documents }) - - // When - const state = reducer(initialState, action) - - // Then - expect(state.cozy.documents[KONS_DOCTYPE]).toEqual({ - [dummyKonnector().id]: dummyKonnector(), - [alanKonnector.id]: alanKonnector - }) - }) - - it('should add one konnector in the collections', () => { - // Given - const collections = { - ['accounts']: {}, - ['triggers']: {}, - ['konnectors']: { - type: 'io.cozy.konnectors', - options: {}, - fetchStatus: 'loaded', - lastFetch: 1651052463319, - hasMore: false, - count: 1, - ids: ['io.cozy.konnectors/ameli'] - } - } - const initialState = constructInitialState({ collections }) - - // When - const state = reducer(initialState, action) - - // Then - expect(state.cozy.collections['konnectors']).toEqual({ - type: 'io.cozy.konnectors', - options: {}, - fetchStatus: 'loaded', - lastFetch: now, - hasMore: false, - count: 2, - ids: ['io.cozy.konnectors/ameli', 'io.cozy.konnectors/alan'] - }) - }) - }) - - describe('RECEIVE_UPDATED_KONNECTOR', () => { - beforeEach(() => { - action = { - type: RECEIVE_UPDATED_KONNECTOR, - response: { data: [dummyKonnector({ editor: 'Yzoc' })] }, - updateCollections: ['konnectors'] - } - }) - - it('should update one konnector in the documents', () => { - // Given - const documents = { - [ACCOUNTS_DOCTYPE]: {}, - [TRIGGERS_DOCTYPE]: {}, - [KONS_DOCTYPE]: { - [dummyKonnector().id]: dummyKonnector(), - [alanKonnector.id]: alanKonnector - } - } - const initialState = constructInitialState({ documents }) - - // When - const state = reducer(initialState, action) - - // Then - expect(state.cozy.documents[KONS_DOCTYPE]).toEqual({ - [dummyKonnector().id]: dummyKonnector({ editor: 'Yzoc' }), - [alanKonnector.id]: alanKonnector - }) - }) - - it('should update one konnector in the collections', () => { - // Given - const collections = { - ['accounts']: {}, - ['triggers']: {}, - ['konnectors']: { - type: 'io.cozy.konnectors', - options: {}, - fetchStatus: 'loaded', - lastFetch: 1651052463319, - hasMore: false, - count: 1, - ids: ['io.cozy.konnectors/ameli', 'io.cozy.konnectors/alan'] - } - } - const initialState = constructInitialState({ collections }) - - // When - const state = reducer(initialState, action) - - // Then - expect(state.cozy.collections['konnectors']).toEqual({ - type: 'io.cozy.konnectors', - options: {}, - fetchStatus: 'loaded', - lastFetch: now, - hasMore: false, - count: 2, - ids: ['io.cozy.konnectors/ameli', 'io.cozy.konnectors/alan'] - }) - }) - }) -}) diff --git a/src/lib/redux-cozy-client/utils.js b/src/lib/redux-cozy-client/utils.js deleted file mode 100644 index 6e5746e123..0000000000 --- a/src/lib/redux-cozy-client/utils.js +++ /dev/null @@ -1,26 +0,0 @@ -export const mapValues = (object, transform) => { - let result = {} - for (const key in object) { - result[key] = transform(object[key], key) - } - return result -} - -export const filterValues = (object, filter) => { - let result = {} - for (const key in object) { - if (filter(object[key], key)) { - result[key] = object[key] - } - } - return result -} - -export const removeObjectProperty = (obj, prop) => { - return Object.keys(obj).reduce((result, key) => { - if (key !== prop) { - result[key] = obj[key] - } - return result - }, {}) -} diff --git a/src/lib/triggers.js b/src/lib/triggers.js deleted file mode 100644 index a95bedf02c..0000000000 --- a/src/lib/triggers.js +++ /dev/null @@ -1,3 +0,0 @@ -export async function fetch(cozy, triggerId) { - return cozy.fetchJSON('GET', `/jobs/triggers/${triggerId}`) -} diff --git a/src/queries.js b/src/queries.js index 7711389fcd..8fffa672ed 100644 --- a/src/queries.js +++ b/src/queries.js @@ -8,6 +8,12 @@ export const appsConn = { fetchPolicy: defaultFetchPolicy } +export const konnectorsConn = { + query: Q('io.cozy.konnectors'), + as: 'io.cozy.konnectors', + fetchPolicy: defaultFetchPolicy +} + export const instanceSettingsConn = { query: Q('io.cozy.settings').getById('io.cozy.settings.instance'), as: 'io.cozy.settings/instance_standalone', @@ -40,7 +46,13 @@ export const mkHomeMagicFolderConn = t => { fetchPolicy: defaultFetchPolicy } } - +export const fetchKonnectorBySlug = slug => { + return { + query: Q('io.cozy.konnectors').getById(`io.cozy.konnectors/${slug}`), + as: `io.cozy.konnectors/${slug}`, + fetchPolicy: defaultFetchPolicy + } +} export const mkHomeShorcutsConn = folderId => { return { query: Q('io.cozy.files') diff --git a/src/reducers/index.js b/src/reducers/index.js index 853767c551..f592739066 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,60 +1,21 @@ import { combineReducers } from 'redux' import get from 'lodash/get' -import { reducer } from 'lib/redux-cozy-client' -import * as fromAccounts from 'ducks/accounts' -import * as fromKonnectors from 'ducks/konnectors' -import * as fromTriggers from 'ducks/triggers' -import connections, * as fromConnections from 'ducks/connections' - +const isKonnectorTrigger = doc => + doc._type === 'io.cozy.triggers' && !!doc.message && !!doc.message.konnector export default cozyClient => combineReducers({ - connections, - oldcozy: reducer, cozy: cozyClient.reducer() }) // selectors -export const getInstalledKonnectors = state => - fromKonnectors.getInstalledKonnectors(state.oldcozy) - -export const getConnectionsByKonnector = (state, konnectorSlug) => - fromConnections.getConnectionsByKonnector( - state.connections, - konnectorSlug, - fromAccounts.getIds(state.oldcozy), - fromKonnectors.getSlugs(state.oldcozy) - ) - -export const getCreatedConnectionAccount = state => - fromAccounts.getAccount( - state.oldcozy, - fromConnections.getCreatedAccount(state.connections) - ) - -export const getKonnectorTriggersCount = (state, konnector) => - fromTriggers.getKonnectorTriggers( - state.oldcozy, - konnector, - fromAccounts.getIds(state.oldcozy) - ).length - -export const getTriggerByKonnectorAndAccount = (state, konnector, account) => { - const triggerId = fromConnections.getTriggerIdByKonnectorAndAccount( - state.connections, - konnector, - account, - fromAccounts.getIds(state.oldcozy) - ) - return fromTriggers.getTrigger(state.oldcozy, triggerId) -} export const getTriggersByKonnector = (state, konnectorSlug) => { - const triggersInState = state.oldcozy.documents['io.cozy.triggers'] || {} + const triggersInState = state.cozy.documents['io.cozy.triggers'] || {} const triggers = Object.keys(triggersInState).reduce((acc, key) => { - const document = state.oldcozy.documents['io.cozy.triggers'][key] + const document = state.cozy.documents['io.cozy.triggers'][key] if ( - fromConnections.isKonnectorTrigger(document) && + isKonnectorTrigger(document) && get(document, 'message.konnector') === konnectorSlug ) { acc.push(document) diff --git a/src/selectors/konnectors.jsx b/src/selectors/konnectors.jsx new file mode 100644 index 0000000000..9db541c066 --- /dev/null +++ b/src/selectors/konnectors.jsx @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect' + +export const getInstalledKonnectors = createSelector( + state => state.cozy.documents['io.cozy.konnectors'], + konnectors => konnectors +) + +export const getKonnectorBySlug = createSelector( + state => state.cozy.documents['io.cozy.konnectors'], + (_, slug) => slug, + (konnectors, slug) => konnectors.filter(konnector => konnector.slug === slug) +) diff --git a/src/store/configureStore.js b/src/store/configureStore.js index fb827985f4..917840069f 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -1,7 +1,5 @@ import { compose, createStore, applyMiddleware } from 'redux' -import { cozyMiddleware } from 'lib/redux-cozy-client' import { createLogger } from 'redux-logger' -import konnectorsI18nMiddleware from 'lib/middlewares/konnectorsI18n' import thunkMiddleware from 'redux-thunk' import { persistStore, persistReducer } from 'redux-persist' import storage from 'redux-persist/lib/storage' // defaults to localStorage for web @@ -17,12 +15,7 @@ const persistConfig = { storage } -const configureWithPersistor = ( - legacyClient, - cozyClient, - context, - options = {} -) => { +const configureWithPersistor = (cozyClient, context, options = {}) => { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose @@ -35,12 +28,9 @@ const configureWithPersistor = ( composeEnhancers( applyMiddleware.apply( this, - [ - cozyMiddleware(legacyClient), - konnectorsI18nMiddleware(options.lang), - thunkMiddleware, - flag('redux-logger') ? createLogger() : null - ].filter(Boolean) + [thunkMiddleware, flag('redux-logger') ? createLogger() : null].filter( + Boolean + ) ) ) ) @@ -54,7 +44,7 @@ const configureWithPersistor = ( } } -const configureDefault = (legacyClient, cozyClient, context, options) => { +const configureDefault = (cozyClient, context, options) => { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose @@ -63,12 +53,9 @@ const configureDefault = (legacyClient, cozyClient, context, options) => { composeEnhancers( applyMiddleware.apply( this, - [ - cozyMiddleware(legacyClient), - konnectorsI18nMiddleware(options.lang), - thunkMiddleware, - flag('redux-logger') ? createLogger() : null - ].filter(Boolean) + [thunkMiddleware, flag('redux-logger') ? createLogger() : null].filter( + Boolean + ) ) ) ) diff --git a/src/styles/accountConnection.styl b/src/styles/accountConnection.styl deleted file mode 100644 index 8428d8c01f..0000000000 --- a/src/styles/accountConnection.styl +++ /dev/null @@ -1,29 +0,0 @@ -@require 'tools/mixins.styl' - -:local // @stylint ignore - h3 - font-size rem(24) - line-height 1.5em - margin .667em 0 - - h4 - font-size rem(20) - line-height 1.5em - margin 1em 0 - - p - font-size rem(16) - line-height rem(24) - - .col-account-connection - display flex - flex-direction column - justify-content center - width 100% - - @media (max-width: rem(400)) - h4 - font-size rem(18) - - p - font-size rem(14) diff --git a/src/styles/accountLoginForm.styl b/src/styles/accountLoginForm.styl deleted file mode 100644 index 803cf13b2e..0000000000 --- a/src/styles/accountLoginForm.styl +++ /dev/null @@ -1,50 +0,0 @@ -@require 'tools/mixins.styl' -@require 'settings/palette.styl' - -:local // @stylint ignore - .coz-form-controls, .coz-form-controls-success - margin rem(32) 0 rem(8) - display flex - flex-wrap wrap - .coz-btn - // Used to position progress bar - flex 1 0 auto - width 100% - - &:first-of-type - margin-left 0 - - &:last-of-type - margin-right 0 - - .coz-form-controls-success - margin-top rem(16) - - .col-btn--regular - margin 0 - font-size rem(16) - height rem(48) - width 100% - - .col-account-form-success-buttons - margin auto - - button - width 100% - - .account-form-login - .col-account-form-advanced-button - font-family Lato, sans-serif - cursor pointer - margin 1rem 0 0 - font-size .8em - border 0 - color var(--dodgerBlue) - background-color var(--white) - text-transform uppercase - font-weight 700 - padding .125rem // Magic number to align button with form fields - - .account-form-fieldset - padding 0 - border 0 diff --git a/src/styles/index.styl b/src/styles/index.styl index 92e59c54db..4c7275eb2d 100644 --- a/src/styles/index.styl +++ b/src/styles/index.styl @@ -4,11 +4,6 @@ @require 'components/*' :global - #svg-sprite-content - position absolute - width 0 - height 0 - visibility hidden @require './app.styl' @require './hero-header.styl' diff --git a/src/styles/intents.styl b/src/styles/intents.styl index dea939ce35..df7d6a13ce 100644 --- a/src/styles/intents.styl +++ b/src/styles/intents.styl @@ -3,11 +3,6 @@ @require 'settings/icons.styl' :global // @stylint ignore - #svg-sprite-content - position absolute - width 0 - height 0 - visibility hidden @require './lists.styl' @require './dialog.styl' diff --git a/src/styles/konnectorHeaderIcon.styl b/src/styles/konnectorHeaderIcon.styl deleted file mode 100644 index e37d223cce..0000000000 --- a/src/styles/konnectorHeaderIcon.styl +++ /dev/null @@ -1,17 +0,0 @@ -:local // @stylint ignore - .col-konnector-header-icon-wrapper - .col-konnector-header-icon-wrapper--center - height 3.5rem - display flex - - .col-konnector-header-icon - height 2rem - - .col-konnector-header-icon--center - height 3.5rem - - img - width auto - - .col-konnector-header-icon-wrapper--center - justify-content center diff --git a/src/styles/konnectorInstall.styl b/src/styles/konnectorInstall.styl deleted file mode 100644 index 84756209ca..0000000000 --- a/src/styles/konnectorInstall.styl +++ /dev/null @@ -1,36 +0,0 @@ -@require 'tools/mixins.styl' -@require 'settings/breakpoints.styl' -@require 'settings/palette.styl' - -:local // @stylint ignore - .col-account-connection-content - padding 0 2rem - - +tiny-screen() - padding 0 - - .col-account-connection-fetching - padding 0 0 2em - position relative - > div - position relative // override default Spinner absolute position - transform translateX(-50%) translateY(0) - - .col-account-connection-security - display flex - align-items top - margin-bottom .5rem - margin-top 1.5rem - - .col-account-connection-security svg - max-height rem(24) - max-width rem(24) - float left - margin-right rem(8) - - .col-account-connection-editor - text-align center - color var(--coolGrey) - margin-top 0 - margin-bottom 0 - font-size .9em diff --git a/src/styles/konnectorMaintenance.styl b/src/styles/konnectorMaintenance.styl deleted file mode 100644 index c185277978..0000000000 --- a/src/styles/konnectorMaintenance.styl +++ /dev/null @@ -1,17 +0,0 @@ -@require 'settings/palette.styl' - -:local // @stylint ignore - .maintenance-intro - text-align center - color var(--pomegranate) - margin 1rem auto - - .maintenance-service - font-size 1.125rem - - .maintenance-icon - width 6.25rem - height 6.25rem - margin 0 auto - background embedurl('../assets/icons/icon-maintenance.svg') 0 no-repeat - diff --git a/src/styles/konnectorSuccess.styl b/src/styles/konnectorSuccess.styl deleted file mode 100644 index 1019110b25..0000000000 --- a/src/styles/konnectorSuccess.styl +++ /dev/null @@ -1,23 +0,0 @@ -@require './accountLoginForm' - -.col-account-success - text-align center - -.col-account-success-links - display flex - align-items center - flex-direction column - -.col-account-success-link - position relative - margin-top 1rem - display flex - align-items center - color var(--dodgerBlue) - text-decoration none - font-weight bold - cursor pointer - -.col-account-success-illu - height rem(112) - width rem(120) diff --git a/src/styles/triggerFolderLink.styl b/src/styles/triggerFolderLink.styl deleted file mode 100644 index c0b21785ae..0000000000 --- a/src/styles/triggerFolderLink.styl +++ /dev/null @@ -1,13 +0,0 @@ -@require 'settings/palette.styl' - -:local // @stylint ignore - .col-trigger-folder - margin-top 2rem - position relative - - &-link - color var(--dodgerBlue) - text-decoration none - font-weight bold - display flex - align-items center diff --git a/src/targets/browser/index.ejs b/src/targets/browser/index.ejs index 1271961676..3b65d8acbf 100644 --- a/src/targets/browser/index.ejs +++ b/src/targets/browser/index.ejs @@ -20,9 +20,6 @@ <% }); %> {{.ThemeCSS}} - <% if (__STACK_ASSETS__) { %> - {{.CozyClientJS}} - <% } %> diff --git a/src/targets/intents/HarvestRoute.jsx b/src/targets/intents/HarvestRoute.jsx new file mode 100644 index 0000000000..91860b5203 --- /dev/null +++ b/src/targets/intents/HarvestRoute.jsx @@ -0,0 +1,35 @@ +import React, { useEffect, useState } from 'react' +import { Routes } from 'cozy-harvest-lib' +import { useParams } from 'react-router-dom' +import CozyTheme from 'cozy-ui/transpiled/react/providers/CozyTheme' +import { useClient } from 'cozy-client' +import Intents from 'cozy-interapp' +import datacardOptions from 'cozy-harvest-lib/dist/datacards/datacardOptions' + +export const HarvestRoutes = ({ intentData, intentId }) => { + const { konnectorSlug } = useParams() + const client = useClient() + + const intents = new Intents({ client }) + const [service, setService] = useState(null) + useEffect(() => { + // eslint-disable-next-line + intents.createService().then(service => { + setService(service) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + + (service ? service.cancel() : undefined)} + datacardOptions={datacardOptions} + intentData={intentData} + intentId={intentId} + /> + + ) +} diff --git a/src/targets/intents/index.ejs b/src/targets/intents/index.ejs index 9b03671b05..6c5e3781d9 100644 --- a/src/targets/intents/index.ejs +++ b/src/targets/intents/index.ejs @@ -11,23 +11,15 @@ <% }); %> {{.ThemeCSS}} -<% if (__STACK_ASSETS__) { %> -{{.CozyClientJS}} -<% } %>
+ data-cozy="{{.CozyData}}" data-cozy-token="{{.Token}}" data-cozy-domain="{{.Domain}}" + data-cozy-locale="{{.Locale}}" data-cozy-app-editor="{{.AppEditor}}" data-cozy-app-name="{{.AppName}}" + data-cozy-app-name-prefix="{{.AppNamePrefix}}" data-cozy-app-slug="{{.AppSlug}}" data-cozy-tracking="{{.Tracking}}" + data-cozy-icon-path="{{.IconPath}}" data-cozy-subdomain-type="{{.SubDomain}}" + data-cozy-default-wallpaper="{{.DefaultWallpaper}}" data-cozy-flags="{{.Flags}}"> <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %> <% }); %> diff --git a/src/targets/intents/index.jsx b/src/targets/intents/index.jsx index da52a00da4..a136c09a7c 100644 --- a/src/targets/intents/index.jsx +++ b/src/targets/intents/index.jsx @@ -1,7 +1,8 @@ import 'cozy-ui/transpiled/react/stylesheet.css' import 'cozy-ui/dist/cozy-ui.utils.min.css' import 'styles/intents.styl' - +import { Route, Routes } from 'react-router-dom' +import { HarvestRoutes } from './HarvestRoute' import React from 'react' import { createRoot } from 'react-dom/client' import { HashRouter } from 'react-router-dom' @@ -11,12 +12,24 @@ import AppWrapper, { AppContext } from 'components/AppWrapper' document.addEventListener('DOMContentLoaded', () => { const container = document.querySelector('[role=application]') const root = createRoot(container) - + // + const params = new URL(document.location).searchParams + const intentId = params.get('intent') root.render( - {({ data }) => } + {({ data }) => ( + + } /> + + } + /> + + )} diff --git a/test/components/KonnectorInstall.spec.jsx b/test/components/KonnectorInstall.spec.jsx deleted file mode 100644 index 818108dcdc..0000000000 --- a/test/components/KonnectorInstall.spec.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' - -import { KonnectorInstall } from 'components/KonnectorInstall' - -jest.mock('cozy-harvest-lib', () => { - const FakeIntentTriggerManager = props => ( -
- - Vault is {props.vaultClosable ? 'closable' : 'sealed'} - -
- ) - - return { - IntentTriggerManager: FakeIntentTriggerManager - } -}) - -describe('KonnectorInstall', () => { - it('should show a non-closable vault', () => { - render( - key} - /> - ) - - expect(screen.getByTestId('vault-status')).toHaveTextContent( - 'Vault is sealed' - ) - }) -}) diff --git a/test/components/KonnectorTile.spec.jsx b/test/components/KonnectorTile.spec.jsx deleted file mode 100644 index 6ef7c66446..0000000000 --- a/test/components/KonnectorTile.spec.jsx +++ /dev/null @@ -1,95 +0,0 @@ -'use strict' - -/* eslint-env jest */ - -import React from 'react' -import { render } from '@testing-library/react' -import MuiCozyTheme from 'cozy-ui/transpiled/react/MuiCozyTheme' - -import { - KonnectorTile, - getKonnectorStatus, - STATUS -} from 'components/KonnectorTile' -import AppLike from '../AppLike' - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - NavLink: ({ children }) => children -})) - -const mockKonnector = { - name: 'Mock', - slug: 'mock', - available_version: null -} - -const getMockProps = ({ - error, - userError, - konnector = mockKonnector, - isInMaintenance = false -} = {}) => ({ - accountsCount: 2, - error, - isInMaintenance, - userError, - konnector, - route: `/${konnector.slug}` -}) - -const setup = mockProps => { - return render( - - - - - - ) -} - -describe('KonnectorTile component', () => { - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}) - }) - - afterEach(() => { - // eslint-disable-next-line no-console - console.error.mockRestore() - }) - - it('should render correctly if success', () => { - const mockProps = getMockProps() - const root = setup(mockProps) - expect(root.getByText('Mock')).toBeTruthy() - }) - - it('should display correct status if in maintenance', () => { - const status = getKonnectorStatus({ - konnector: mockKonnector, - isInMaintenance: true - }) - expect(status).toEqual(STATUS.MAINTENANCE) - }) - - it('should display correct error status if user error but not in maintenance', () => { - const status = getKonnectorStatus({ - error: null, - userError: new Error('Expected test user error') - }) - expect(status).toEqual(STATUS.ERROR) - }) - - it('should display correct error status if other error but not in maintenance', () => { - const status = getKonnectorStatus({ error: new Error('LOGIN_FAILED') }) - expect(status).toEqual(STATUS.ERROR) - }) - - it('should display correct error status if no accounts and no errors', () => { - const mockProps = getMockProps() - mockProps.accountsCount = 0 - - const root = setup(mockProps) - expect(root.getByText('Mock')).toBeTruthy() - }) -}) diff --git a/test/lib/konnectors.spec.js b/test/lib/konnectors.spec.js index 2eb93381e6..1a86dd6a22 100644 --- a/test/lib/konnectors.spec.js +++ b/test/lib/konnectors.spec.js @@ -1,8 +1,5 @@ /* eslint-env jest */ import * as konnectors from 'lib/konnectors' - -// TODO: mutualize this code with jobs -// just to tests calling, results are tested in cozy-client-js const cozyMock = { fetchJSON: jest.fn((method, endpoint, data) => Promise.resolve(data)) } diff --git a/test/lib/redux-cozy-client/adapters/CozyStackAdapter.spec.js b/test/lib/redux-cozy-client/adapters/CozyStackAdapter.spec.js deleted file mode 100644 index a53beb1bd4..0000000000 --- a/test/lib/redux-cozy-client/adapters/CozyStackAdapter.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-env jest */ -import CozyStackAdapter from 'lib/redux-cozy-client/adapters/CozyStackAdapter' - -describe('CozyStack Adapter', () => { - describe('fetchKonnectors', () => { - it('should ignore manifest id', async () => { - const stackClient = { - fetchJSON: async () => { - return { - data: [ - { - slug: 'test', - id: 'io.cozy.konnectors/test', - attributes: { - id: 'manifest id' - } - } - ] - } - } - } - const adapter = new CozyStackAdapter(stackClient) - const konnectors = await adapter.fetchKonnectors() - expect(konnectors).toEqual({ - data: [ - { - _type: 'io.cozy.konnectors', - attributes: { - id: 'manifest id' - }, - id: 'io.cozy.konnectors/test', - slug: 'test' - } - ], - meta: undefined, - next: false, - skip: 0 - }) - }) - }) -}) diff --git a/yarn.lock b/yarn.lock index 79ea93c5d6..e393f7c693 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4339,11 +4339,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -argsarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" - integrity sha1-bnIHtOzbObCviDA/pa4ivajfYcs= - aria-query@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" @@ -5093,7 +5088,7 @@ btoa@^1.2.1: resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== -buffer-from@1.1.0, buffer-from@^1.0.0: +buffer-from@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04" integrity sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ== @@ -5561,11 +5556,6 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clone-buffer@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" - integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= - clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -5852,11 +5842,6 @@ core-js@^2.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== -core-js@^3.6.5: - version "3.21.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.0.tgz#f479dbfc3dffb035a0827602dd056839a774aa71" - integrity sha512-YUdI3fFu4TF/2WykQ2xzSiTQdldLB4KVuL9WeAy5XONZYt5Cun/fpQvctoKbCgvPhmzADeesTk/j2Rdx77AcKQ== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -5907,16 +5892,6 @@ cozy-bi-auth@0.0.25: lodash "^4.17.20" node-jose "^1.1.4" -cozy-client-js@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/cozy-client-js/-/cozy-client-js-0.20.0.tgz#a507ef9ccbeb340aacd58ca1f1d0cdc9d000e853" - integrity sha512-ppguq9hkmtGpS2y+3pE4Pw0CcNOB25Lb82/q0I5r2k+pxCgrbI+6HB85TWQH8OEt/qJVoCCCa9dWE5WSZBUDYw== - dependencies: - core-js "^3.6.5" - cross-fetch "^3.0.6" - pouchdb-browser "7.0.0" - pouchdb-find "7.0.0" - cozy-client@^45.1.0: version "45.1.0" resolved "https://registry.yarnpkg.com/cozy-client/-/cozy-client-45.1.0.tgz#5755bc4d766c467da98a043a6e16657bfffb41b2" @@ -6036,6 +6011,11 @@ cozy-interapp@^0.5.4: resolved "https://registry.yarnpkg.com/cozy-interapp/-/cozy-interapp-0.5.7.tgz#75cafe1732ad660e2caf1ccf412f302594705f39" integrity sha512-laIL/ATYV9oZnmqS+LMoO9qzk8XjJ1v2/YrA1Po2rI8ia/MDgjYO07424x2RuvHhNOBPGjD+JmqwQ0rNDlLW+Q== +cozy-interapp@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/cozy-interapp/-/cozy-interapp-0.8.1.tgz#960fb98a2e93192980449b0b75521f47b4de7d22" + integrity sha512-yYCOMVKRjGzvu/TuTj/9jir+Zr3OvXGWJUu9J9dk1jMcBZqXq4D9uIdnqxMJlpovUUJnehzIdgCfbL3JKaZc0Q== + cozy-keys-lib@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cozy-keys-lib/-/cozy-keys-lib-6.0.0.tgz#1f664ecacea4260043aaa0339e34fa7189745e8d" @@ -6257,7 +6237,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-fetch@^3.0.4, cross-fetch@^3.0.6: +cross-fetch@^3.0.4: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== @@ -7233,11 +7213,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es6-denodeify@^0.1.1: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-denodeify/-/es6-denodeify-0.1.5.tgz#31d4d5fe9c5503e125460439310e16a2a3f39c1f" - integrity sha1-MdTV/pxVA+ElRgQ5MQ4WoqPznB8= - es6-object-assign@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" @@ -8055,14 +8030,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fetch-cookie@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.7.0.tgz#a6fc137ad8363aa89125864c6451b86ecb7de802" - integrity sha512-Mm5pGlT3agW6t71xVM7vMZPIvI7T4FaTuFW4jari6dVzYHFDb3WZZsGpN22r/o3XMdkM0E7sPd1EGeyVbH2Tgg== - dependencies: - es6-denodeify "^0.1.1" - tough-cookie "^2.3.1" - figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -9191,11 +9158,6 @@ image-size@^0.5.1: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= -immediate@3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= - import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -12056,7 +12018,7 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-fetch@2.6.7, node-fetch@^2.0.0, node-fetch@^2.6.1: +node-fetch@2.6.7, node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -13181,117 +13143,6 @@ posthtml@^0.9.2: posthtml-parser "^0.2.0" posthtml-render "^1.0.5" -pouchdb-abstract-mapreduce@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-abstract-mapreduce/-/pouchdb-abstract-mapreduce-7.0.0.tgz#946d79073c9795ca03c9b5c318a64372422e8740" - integrity sha512-C1sb9AIJYTFOUPtuPaAYBCfd09DK82LmeYEtM4h1Z+wG76zj9U1NEg8T+CwxcpOF7eX3ZN5EmSfa3k/ZlyMUgQ== - dependencies: - pouchdb-binary-utils "7.0.0" - pouchdb-collate "7.0.0" - pouchdb-collections "7.0.0" - pouchdb-errors "7.0.0" - pouchdb-fetch "7.0.0" - pouchdb-mapreduce-utils "7.0.0" - pouchdb-md5 "7.0.0" - pouchdb-utils "7.0.0" - -pouchdb-binary-utils@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-binary-utils/-/pouchdb-binary-utils-7.0.0.tgz#cb71a288b09572a231f6bab1b4aed201c4d219a7" - integrity sha512-yUktdOPIPvOVouCjJN3uop+bCcpdPwePrLm9eUAZNgEYnUFu0njdx7Q0WRsZ7UJ6l75HinL5ZHk4bnvEt86FLw== - dependencies: - buffer-from "1.1.0" - -pouchdb-browser@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-browser/-/pouchdb-browser-7.0.0.tgz#d493fd738f21f7c91f17bbb7ee7ad97c2072e546" - integrity sha512-a0AikmIM8BJ4ROWnfRtmQWW3JlFdHvfcQQWxY2V1CesLJYqPkTzwSUDD3QsiU+edpTSK8fIrEq257W15qZzXYQ== - dependencies: - argsarray "0.0.1" - immediate "3.0.6" - inherits "2.0.3" - spark-md5 "3.0.0" - uuid "3.2.1" - vuvuzela "1.0.3" - -pouchdb-collate@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-collate/-/pouchdb-collate-7.0.0.tgz#4f9a0a03c236f1677a971c400b79b7baf6379a4d" - integrity sha512-0O67rnNGVD9OUbDx+6DLPcE3zz7w6gieNCvrbvaI5ibIXuLpyMyLjD6OdRe/19LbstEfZaOp+SYUhQs+TP8Plg== - -pouchdb-collections@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-7.0.0.tgz#fd1f632337dc6301b0ff8649732ca79204e41780" - integrity sha512-DaoUr/vU24Q3gM6ghj0va9j/oBanPwkbhkvnqSyC3Dm5dgf5pculNxueLF9PKMo3ycApoWzHMh6N2N8KJbDU2Q== - -pouchdb-errors@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-errors/-/pouchdb-errors-7.0.0.tgz#4e2a5a8b82af20cbe5f9970ca90b7ec74563caa0" - integrity sha512-dTusY8nnTw4HIztCrNl7AoGgwvS1bVf/3/97hDaGc4ytn72V9/4dK8kTqlimi3UpaurohYRnqac0SGXYP8vgXA== - dependencies: - inherits "2.0.3" - -pouchdb-fetch@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-fetch/-/pouchdb-fetch-7.0.0.tgz#6b56cc49863837f8d5e38b0956ace03f1fcaf8e0" - integrity sha512-9XGEogHQcYZCJp2PvLE7oDgGzIsBy4Vh28EhDS26iJFwtDVpHYm7fIzJ//SDGcUNjnlR9WKTegFLg9p7jYIQWQ== - dependencies: - fetch-cookie "0.7.0" - node-fetch "^2.0.0" - -pouchdb-find@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-find/-/pouchdb-find-7.0.0.tgz#f390c3f7a9455e700eb178eb89b235aaf7dc3beb" - integrity sha512-nqAdnbmmxcIrWF//k5LKDGXaDZScgvhqVoyGjXhiUan35ASI0KYn1R8Z0nGsl0PD/DRK1kveQjbC9+50QgdTRg== - dependencies: - pouchdb-abstract-mapreduce "7.0.0" - pouchdb-collate "7.0.0" - pouchdb-errors "7.0.0" - pouchdb-fetch "7.0.0" - pouchdb-md5 "7.0.0" - pouchdb-selector-core "7.0.0" - pouchdb-utils "7.0.0" - -pouchdb-mapreduce-utils@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-mapreduce-utils/-/pouchdb-mapreduce-utils-7.0.0.tgz#ff342e9d515d4c9cf0b19ad85c78f10f2568493b" - integrity sha512-kj74SpirbQAC7BSlBpPO42RBbUw8XmxbkLCnHyL7CVktyEn24VHbCoirutUI2mRPii7MAVHtleGKXRijR5QIpw== - dependencies: - argsarray "0.0.1" - inherits "2.0.3" - pouchdb-collections "7.0.0" - pouchdb-utils "7.0.0" - -pouchdb-md5@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-md5/-/pouchdb-md5-7.0.0.tgz#935dc6bb507a5f3978fb653ca5790331bae67c96" - integrity sha512-yaSJKhLA3QlgloKUQeb2hLdT3KmUmPfoYdryfwHZuPTpXIRKTnMQTR9qCIRUszc0ruBpDe53DRslCgNUhAyTNQ== - dependencies: - pouchdb-binary-utils "7.0.0" - spark-md5 "3.0.0" - -pouchdb-selector-core@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-selector-core/-/pouchdb-selector-core-7.0.0.tgz#824bd0980bd9778b3ddef869306a6cdb928df703" - integrity sha512-8Lpa8S7TCRGUEy3aEMd+Zy85IU4KwCVNf3TT+HJ8XAKICtmgArPrQGimIXFOHoyjRSpCXtByzEriP8CBCUjp7g== - dependencies: - pouchdb-collate "7.0.0" - pouchdb-utils "7.0.0" - -pouchdb-utils@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-7.0.0.tgz#48bfced6665b8f5a2b2d2317e2aa57635ed1e88e" - integrity sha512-1bnoX1KdZYHv9wicDIFdO0PLiVIMzNDUBUZ/yOJZ+6LW6niQCB8aCv09ZztmKfSQcU5nnN3fe656tScBgP6dOQ== - dependencies: - argsarray "0.0.1" - clone-buffer "1.0.0" - immediate "3.0.6" - inherits "2.0.3" - pouchdb-collections "7.0.0" - pouchdb-errors "7.0.0" - pouchdb-md5 "7.0.0" - uuid "3.2.1" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -13473,7 +13324,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.24, psl@^1.1.28: +psl@^1.1.24: version "1.7.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== @@ -14583,6 +14434,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.1.8: + version "4.1.8" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -15229,11 +15085,6 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -spark-md5@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.0.tgz#3722227c54e2faf24b1dc6d933cc144e6f71bfef" - integrity sha1-NyIifFTi+vJLHcbZM8wUTm9xv+8= - spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" @@ -16086,14 +15937,6 @@ toposort@^2.0.2: resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= -tough-cookie@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - tough-cookie@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" @@ -16651,11 +16494,6 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" - integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA== - uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -16775,11 +16613,6 @@ vue-eslint-parser@^8.0.1: lodash "^4.17.21" semver "^7.3.5" -vuvuzela@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/vuvuzela/-/vuvuzela-1.0.3.tgz#3be145e58271c73ca55279dd851f12a682114b0b" - integrity sha1-O+FF5YJxxzylUnndhR8SpoIRSws= - w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"