Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a59694a
feat(Imports): Add Imports components
KillianCourvoisier Oct 20, 2025
81b774e
feat(imports): Add constants imports routes
KillianCourvoisier Oct 20, 2025
d9c7f82
feat(imports): Add french and english locales
KillianCourvoisier Oct 20, 2025
75e4fc9
feat(imports): add Redux slice for Imports state
KillianCourvoisier Oct 20, 2025
8041083
feat(imports): add Nextcloud listing UI with account check and store …
KillianCourvoisier Oct 22, 2025
8b111e0
feat(imports/nextcloud): recursive import via downstream
KillianCourvoisier Oct 23, 2025
c4b7e0f
WIP - feat(imports/nextcloud): force destination to /Imports/Nextclou…
KillianCourvoisier Oct 23, 2025
34e72d7
feat(nextcloud-import): improve 404 handling and expose failed items …
KillianCourvoisier Nov 18, 2025
718844c
feat(imports/nextcloud): add per-item progress and explicit 404 repor…
KillianCourvoisier Nov 18, 2025
c3aaa7e
feat(imports): Add abort import button
KillianCourvoisier Nov 19, 2025
9a488fb
feat(imports): Add parallel downstream (x3 for now)
KillianCourvoisier Nov 19, 2025
d582088
feat: Add POST permission for account creation from Settings
KillianCourvoisier Nov 20, 2025
cbd9f03
feat(imports): allow Nextcloud account creation directly from Settings
KillianCourvoisier Nov 20, 2025
d67f2c8
feat(imports): persist run state and stop import on navigation
KillianCourvoisier Nov 20, 2025
3525efe
feat(imports): improve UI layout and align components with Settings s…
KillianCourvoisier Nov 20, 2025
761d83f
chore: update yarn.lock after rebase
KillianCourvoisier Nov 24, 2025
bed996b
feat: Add DELETE permission for accounts in manifest
KillianCourvoisier Nov 24, 2025
6eb5d8d
refactor(imports): align nextcloud account service with cozy-client c…
KillianCourvoisier Nov 24, 2025
e725f3e
refactor(imports): align nextcloud provider with cozy-client conventions
KillianCourvoisier Nov 25, 2025
2ba846e
fix(imports): avoid nextcloud UI freeze after user stop
KillianCourvoisier Nov 26, 2025
6a04199
feat(imports): add file-level dedup with per-directory cache for next…
KillianCourvoisier Nov 26, 2025
5fa9e0e
refactor(imports): remove Redux/localStorage for enabled flag and mig…
KillianCourvoisier Nov 26, 2025
b814a0b
refactor(imports): split Run.jsx into atomic UI components + CozyDialog
KillianCourvoisier Nov 27, 2025
67d0b2c
fix(imports): Use pageHeaders in imports, change imports icon and rep…
KillianCourvoisier Nov 27, 2025
83c74a5
feat(imports): Hide features behind 'settings.imports' flag
KillianCourvoisier Nov 27, 2025
cc7d72f
fix(imports): Rename 'listNextcloudAccounts' to 'findNextcloudAccounts'
KillianCourvoisier Dec 1, 2025
5ee96a7
feat(imports): Split provider.js in multiple service for flat import
KillianCourvoisier Dec 1, 2025
284dcc6
refactor(imports/nextcloud): extract core logic into hooks and remove…
KillianCourvoisier Dec 2, 2025
a826817
refactor(imports): Remove Nextcloud account creation from hook
KillianCourvoisier Dec 4, 2025
0e9880b
refactor(imports): Remove useless findNextcloudAccounts function
KillianCourvoisier Dec 4, 2025
f04ea68
feat(imports): Add i18n for Nextcloud account dialog texts
KillianCourvoisier Dec 4, 2025
78b12ce
style(imports): Use Stylus module for Nextcloud account dialog and cl…
KillianCourvoisier Dec 4, 2025
ce2134c
style(imports): Use Stylus modulew when relevant and cleanup inline s…
KillianCourvoisier Dec 4, 2025
4845294
style(imports): Apply i18n for nextcloud fr and eng texts
KillianCourvoisier Dec 4, 2025
5fa2c88
chore: Update cozy-client and cozy-stack-client to use FailOnConflict
KillianCourvoisier Dec 4, 2025
6615933
refactor(imports): replace nextcloud custom dir creation with ensureD…
KillianCourvoisier Dec 8, 2025
8df5dd2
refactor(import): surface cozy errors as-is in hook
KillianCourvoisier Dec 8, 2025
88a60eb
refactor(imports): cleanup nextcloud error handling and fix abort flow
KillianCourvoisier Dec 9, 2025
a2506e0
refactor(imports): remove nextcloud build404Reason and simplify 404 h…
KillianCourvoisier Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.webapp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"accounts": {
"description": "Required to manage accounts associated to konnectors",
"type": "io.cozy.accounts",
"verbs": ["GET"]
"verbs": ["GET", "POST", "DELETE"]
},
"jobs": {
"description": "Required to send emails to support",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"@sentry/react": "7.118.0",
"classnames": "2.3.1",
"cozy-bar": "^27.0.0",
"cozy-client": "^60.14.0",
"cozy-client": "^60.18.0",
"cozy-dataproxy-lib": "^4.11.0",
"cozy-device-helper": "^3.8.0",
"cozy-devtools": "^1.3.0",
Expand All @@ -85,7 +85,7 @@
"cozy-realtime": "^5.6.4",
"cozy-search": "^0.13.0",
"cozy-sharing": "^27.0.0",
"cozy-stack-client": "^60.6.0",
"cozy-stack-client": "^60.18.0",
"cozy-ui": "^134.0.0",
"cozy-ui-plus": "^3.0.0",
"identity-obj-proxy": "^3.0.0",
Expand Down
5 changes: 4 additions & 1 deletion src/components/AppProviders.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from 'cozy-ui/transpiled/react/styles'
import CozyTheme from 'cozy-ui-plus/dist/providers/CozyTheme'

import { ImportsProvider } from '@/components/Imports/ImportsContext'
import { PremiumProvider } from '@/components/Premium/PremiumProvider'
import { FILES_DOCTYPE, CONTACTS_DOCTYPE, APPS_DOCTYPE } from '@/doctypes'

Expand Down Expand Up @@ -57,7 +58,9 @@ const AppProviders = ({ client, store, children }) => {
>
<BarProvider>
<PremiumProvider>
<HashRouter>{children}</HashRouter>
<ImportsProvider>
<HashRouter>{children}</HashRouter>
</ImportsProvider>
</PremiumProvider>
</BarProvider>
</DataProxyProvider>
Expand Down
11 changes: 11 additions & 0 deletions src/components/AppRouter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'

import { DeleteAccount } from '@/components/DeleteAccount'
import ChangeEmail from '@/components/Email/ChangeEmail'
import History from '@/components/Imports/History'
import Imports from '@/components/Imports/Imports'
import Run from '@/components/Imports/Run'
import PermissionsApplication from '@/components/Permissions/AppPermissions/AppPermissions'
import DataPermissions from '@/components/Permissions/DataPermissions/DataPermissions'
import PermissionDetails from '@/components/Permissions/PermissionDetails/PermissionDetails'
Expand Down Expand Up @@ -52,6 +55,14 @@ const AppRouter = () => {
<Route path="/connectedDevices/:deviceId" element={<Devices />} />
<Route path="/sessions" element={<Sessions />} />
<Route path="/storage" element={<Storage />} />

{flag('settings.imports') && (
<>
<Route path={routes.imports} element={<Imports />} />
<Route path={routes.importsRun} element={<Run />} />
<Route path={routes.importsHistory} element={<History />} />
</>
)}
<Route path={routes.lockScreen} element={<LockScreen />} />
<Route path="/permissions/:page" element={<PermissionsTab />} />
<Route
Expand Down
33 changes: 33 additions & 0 deletions src/components/Imports/History.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import { useSelector } from 'react-redux'

import Typography from 'cozy-ui/transpiled/react/Typography'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'

import Page from '@/components/Page'

const History = () => {
const { t } = useI18n()
const enabled = useSelector(state => state.importData?.enabled ?? false)

return (
<Page>
<Typography variant="h3" gutterBottom>
{t('ImportsHistory.title')}
</Typography>
<Typography variant="body1" gutterBottom>
{enabled
? t('ImportsHistory.helper')
: t('ImportsHistory.disabled_helper')}
</Typography>

<div style={{ opacity: 0.7 }}>
<Typography variant="caption">
{t('ImportsHistory.placeholder')}
</Typography>
</div>
</Page>
)
}

export default History
48 changes: 48 additions & 0 deletions src/components/Imports/ImportErrors.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react'

import Stack from 'cozy-ui/transpiled/react/Stack'
import Typography from 'cozy-ui/transpiled/react/Typography'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'

const ImportErrors = ({ error, failedItems }) => {
const { t } = useI18n()

if (!error && (!failedItems || failedItems.length === 0)) {
return null
}

return (
<Stack spacing="xs">
{error && (
<Typography variant="caption" color="error">
{String(error)}
</Typography>
)}

{failedItems && failedItems.length > 0 && (
<ul style={{ margin: 0, paddingLeft: 18 }}>
{failedItems.map((item, idx) => {
const path =
item.path || item.name || t('ImportsRun.errors.unknown_path')
const statusCode =
typeof item.status === 'number'
? item.status
: t('ImportsRun.errors.status_na')
const reason = item.reason || ''
const line = reason
? `${path} - ${statusCode} (${reason})`
: `${path} - ${statusCode}`

return (
<li key={idx} style={{ fontSize: 11 }}>
{line}
</li>
)
})}
</ul>
)}
</Stack>
)
}

export default ImportErrors
67 changes: 67 additions & 0 deletions src/components/Imports/Imports.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useCallback } from 'react'
import { useNavigate } from 'react-router-dom'

import Button from 'cozy-ui/transpiled/react/Button'
import Switch from 'cozy-ui/transpiled/react/Switch'
import Typography from 'cozy-ui/transpiled/react/Typography'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'

import styles from './imports.styl'
import { routes } from '../../constants/routes'

import { useImports } from '@/components/Imports/ImportsContext'
import Page from '@/components/Page'
import { PageHeader } from '@/components/PageHeader'

const Imports = () => {
const { t } = useI18n()
const navigate = useNavigate()
const { enabled, setEnabled } = useImports()

const onSwitchChange = useCallback(
ev => {
const next = !!ev?.target?.checked
setEnabled(next)
},
[setEnabled]
)

return (
<Page>
<PageHeader title={t('ImportsView.title')} />

<Typography variant="subtitle1" gutterBottom>
{t('ImportsView.subtitle')}
</Typography>

<Typography variant="body2" gutterBottom>
{t('ImportsView.helper')}
</Typography>

<div className={styles['ImportsView-switchRow']}>
<Switch checked={enabled} onChange={onSwitchChange} />
<Typography variant="body1">{t('ImportsView.toggle')}</Typography>
</div>

<div className={styles['ImportsView-actionsRow']}>
<Button
variant="primary"
disabled={!enabled}
onClick={() => navigate(routes.importsRun)}
>
{t('ImportsView.action_run')}
</Button>
<Button
variant="secondary"
disabled={!enabled}
onClick={() => navigate(routes.importsHistory)}
>
{t('ImportsView.action_history')}
</Button>
</div>
</Page>
)
}

export { Imports }
export default Imports
46 changes: 46 additions & 0 deletions src/components/Imports/ImportsContent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'

import Box from 'cozy-ui/transpiled/react/Box'
import Button from 'cozy-ui/transpiled/react/Buttons'
import Circle from 'cozy-ui/transpiled/react/Circle'
import Icon from 'cozy-ui/transpiled/react/Icon'
import CloudRainbowIcon from 'cozy-ui/transpiled/react/Icons/CloudRainbow'
import Typography from 'cozy-ui/transpiled/react/Typography'
import { useBreakpoints } from 'cozy-ui/transpiled/react/providers/Breakpoints'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'

const ImportsContent = () => {
const { t } = useI18n()
const { isMobile } = useBreakpoints()

const onStartImport = () => {}

return (
<Box
className="u-flex u-flex-column u-flex-items-center u-mh-auto u-mv-0"
maxWidth={600}
>
<Circle size={100} backgroundColor="var(--defaultBackgroundColor)">
<Icon icon={CloudRainbowIcon} size={48} />
</Circle>

<Typography className="u-mt-1-half u-mb-2" variant="h3" align="center">
{t('ImportsView.subtitle')}
</Typography>

<Typography className="u-mb-2" align="center">
{t('ImportsView.helper')}
</Typography>

<Button
theme="primary"
onClick={onStartImport}
className={isMobile ? 'u-w-100' : ''}
>
{t('ImportsView.cta')}
</Button>
</Box>
)
}

export default ImportsContent
24 changes: 24 additions & 0 deletions src/components/Imports/ImportsContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { createContext, useContext, useState } from 'react'

const ImportsContext = createContext(null)

export const ImportsProvider = ({ children }) => {
const [enabled, setEnabled] = useState(false)

const value = {
enabled,
setEnabled
}

return (
<ImportsContext.Provider value={value}>{children}</ImportsContext.Provider>
)
}

export const useImports = () => {
const ctx = useContext(ImportsContext)
if (!ctx) {
throw new Error('useImports must be used within an ImportsProvider')
}
return ctx
}
64 changes: 64 additions & 0 deletions src/components/Imports/ImportsProgress.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react'

import { LinearProgress } from 'cozy-ui/transpiled/react/Progress'
import Stack from 'cozy-ui/transpiled/react/Stack'
import Typography from 'cozy-ui/transpiled/react/Typography'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'

const ImportsProgress = ({ title, progress, busy, status, summary }) => {
const { t } = useI18n()
const hasProgress = progress.total > 0

return (
<Stack spacing="s">
<Typography variant="h5">{title}</Typography>

{hasProgress && (
<div style={{ maxWidth: 500 }}>
<LinearProgress
variant="determinate"
value={
progress.total === 0
? 0
: Math.min(100, (progress.done / progress.total) * 100)
}
className="u-mv-half u-w-100 u-h-half u-bdrs-6"
/>

<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: 4
}}
>
<Typography variant="caption">
{t('ImportsRun.sections.progress.processed', {
count: progress.done
})}
</Typography>

<Typography variant="caption">
{t('ImportsRun.sections.progress.total', {
count: progress.total
})}
</Typography>
</div>

{busy && progress.current && (
<Typography variant="caption">
{t('ImportsRun.sections.progress.processing', {
name: progress.current
})}
</Typography>
)}
</div>
)}

{status && <Typography variant="caption">{status}</Typography>}
{summary && <Typography variant="caption">{summary}</Typography>}
</Stack>
)
}

export default ImportsProgress
25 changes: 25 additions & 0 deletions src/components/Imports/ImportsProviderSelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react'

import Select from '@/components/Select'

const ImportsProviderSelect = ({
providerOptions,
providerValue,
providerFieldProps,
onChange
}) => {
return (
<Select
name="provider"
options={providerOptions}
fieldProps={providerFieldProps}
value={providerValue}
onChange={sel => {
onChange(sel ? sel.value : '')
}}
isSearchable={false}
/>
)
}

export default ImportsProviderSelect
Loading
Loading