diff --git a/client/assets/delete_icon.svg b/client/assets/delete_icon.svg deleted file mode 100644 index 473dbb128..000000000 --- a/client/assets/delete_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/components/Application/Browser.tsx b/client/components/Application/Browser.tsx index ef888c50c..9a974a1dc 100644 --- a/client/components/Application/Browser.tsx +++ b/client/components/Application/Browser.tsx @@ -1,22 +1,22 @@ +import * as store from '@client/store' import Box from '@mui/material/Box' +import Button from '@mui/material/Button' import { ErrorBoundary } from 'react-error-boundary' +import { Trans, useTranslation } from 'react-i18next' +import createFolderIcon from '../../assets/create_folder_icon.svg' import SpinnerCard from '../Parts/Cards/Spinner' import FileTree from '../Parts/Trees/File' -import Button from '@mui/material/Button' -import * as store from '@client/store' -import createFolderIcon from '../../assets/create_folder_icon.svg' -import { useTranslation, Trans } from 'react-i18next' export default function Browser() { const files = store.useStore((state) => state.files) const loading = store.useStore((state) => state.loading) - return loading ? ( - - ) : files.length ? ( - - ) : ( - + const Browser = loading ? LoadingBrowser : files.length ? DefaultBrowser : EmptyBrowser + + return ( + + + ) } diff --git a/client/components/Application/Dialogs/UploadFile/UploadFile.tsx b/client/components/Application/Dialogs/UploadFile/UploadFile.tsx index a6ff74120..0fbb6330d 100644 --- a/client/components/Application/Dialogs/UploadFile/UploadFile.tsx +++ b/client/components/Application/Dialogs/UploadFile/UploadFile.tsx @@ -2,9 +2,9 @@ import uploadFilesDialogImg from '@client/assets/dialog_upload_files.png' import iconUploadFolderImg from '@client/assets/folder-open-big-plus.png' import iconLinkTextField from '@client/assets/icon_link_textfield.svg' import iconUploadFileImg from '@client/assets/icon_upload_file.png' -import SimpleTabs from '@client/components/Parts/Tabs/SimpleTabs' import SimpleButton from '@client/components/Parts/Buttons/SimpleButton' import Columns from '@client/components/Parts/Grids/Columns' +import SimpleTabs from '@client/components/Parts/Tabs/SimpleTabs' import { LinearProgress } from '@client/components/Progress' import * as appStore from '@client/store' import CloseIcon from '@mui/icons-material/Close' @@ -108,7 +108,7 @@ function LocalFileForm(props: { isFolder?: boolean }) { webkitdirectory={props.isFolder ? '' : undefined} onChange={(ev) => { if (ev.target.files) { - store.ingestFiles({ source: ev.target.files }, t) + store.ingestFiles({ source: ev.target.files }) } }} /> @@ -153,7 +153,7 @@ function RemoteFileForm() { hoverBgColor="OKFNBlue" color="OKFNBlack" disabled={!url} - onClick={() => store.ingestFiles({ source: url }, t)} + onClick={() => store.ingestFiles({ source: url })} /> ) diff --git a/client/components/Application/Dialogs/UploadFile/store.tsx b/client/components/Application/Dialogs/UploadFile/store.tsx index 0fce11ab7..c73b14c97 100644 --- a/client/components/Application/Dialogs/UploadFile/store.tsx +++ b/client/components/Application/Dialogs/UploadFile/store.tsx @@ -24,14 +24,14 @@ export function closeDialog() { } } -export async function ingestFiles(props: { source: FileList | string }, t: any) { +export async function ingestFiles(props: { source: FileList | string }) { const files = props.source instanceof FileList ? await uploadLocalFiles({ source: props.source }) - : await uploadRemoteFile({ source: props.source }, t) + : await uploadRemoteFile({ source: props.source }) if (files) { - await validateAndSelectFiles({ files }, t) + await validateAndSelectFiles({ files }) } } @@ -57,7 +57,7 @@ async function uploadLocalFiles(props: { source: FileList }) { return files } -async function uploadRemoteFile(props: { source: string }, t: any) { +async function uploadRemoteFile(props: { source: string }) { state.progress = { type: 'loading', title: t('loading'), blocking: true } if (!props.source) { @@ -74,9 +74,10 @@ async function uploadRemoteFile(props: { source: string }, t: any) { const result = await client.fileFetch({ folder, url: props.source, deduplicate: true }) if (result instanceof client.Error) { + console.log(result) const message = props.source.includes('docs.google.com/spreadsheets') ? t('error-google-sheets-address-invalid') - : t('error-url-not-table') + : t(result.detail as any) state.progress = { type: 'error', message } return } @@ -85,7 +86,7 @@ async function uploadRemoteFile(props: { source: string }, t: any) { return [result] } -async function validateAndSelectFiles(props: { files: IFile[] }, t: any) { +async function validateAndSelectFiles(props: { files: IFile[] }) { state.progress = { type: 'validating', title: t('checking-errors'), blocking: true } const totalSize = props.files.reduce((acc, file) => acc + file.size, 0) diff --git a/client/components/Application/Sidebar.tsx b/client/components/Application/Sidebar.tsx index 58369f029..c18513819 100644 --- a/client/components/Application/Sidebar.tsx +++ b/client/components/Application/Sidebar.tsx @@ -25,9 +25,13 @@ export default function Sidebar() { borderRight: 'solid 1px #ddd', }} > - - - + + + + + + + ) diff --git a/client/components/Editors/Base/ListItem.tsx b/client/components/Editors/Base/ListItem.tsx index 50db7b52a..f0e941ea7 100644 --- a/client/components/Editors/Base/ListItem.tsx +++ b/client/components/Editors/Base/ListItem.tsx @@ -4,7 +4,7 @@ import Button from '@mui/material/Button' import Typography from '@mui/material/Typography' import { useTheme } from '@mui/material/styles' import { useTranslation } from 'react-i18next' -import deleteIcon from '../../../assets/delete_icon.svg' +import DeleteIcon from '../../Parts/Icons/DeleteIcon' interface EditorListItemProps { kind: string @@ -31,8 +31,11 @@ export default function EditorListItem(props: EditorListItemProps) { component="span" title={`${t('remove')} ${capitalize(props.kind)}`} sx={{ - '& img': { + '& svg': { width: '22px', + '&:hover': { + fill: (theme) => theme.palette.OKFNRed500.main, + }, }, }} onClick={(ev) => { @@ -40,7 +43,7 @@ export default function EditorListItem(props: EditorListItemProps) { props.onRemoveClick?.() }} > - + ) } @@ -70,6 +73,15 @@ export default function EditorListItem(props: EditorListItemProps) { padding: '12px 16px', whiteSpace: 'nowrap', borderColor: '#E6E7EB', + animation: 'highlight 3s ease-in-out', + '@keyframes highlight': { + '0%': { + borderColor: (theme) => theme.palette.OKFNBlue.main, + }, + '100%': { + borderColor: '#E6E7EB', + }, + }, '&:hover': { borderColor: (theme) => theme.palette.OKFNBlue.main, }, diff --git a/client/components/Editors/Resource/Sections/Licenses.tsx b/client/components/Editors/Resource/Sections/Licenses.tsx index aa2cb7734..8e1a34e83 100644 --- a/client/components/Editors/Resource/Sections/Licenses.tsx +++ b/client/components/Editors/Resource/Sections/Licenses.tsx @@ -37,7 +37,11 @@ function LicenseList() { return ( <> - + 0 ? () => setDialogOpen(true) : null} + > {licenseItems.length > 0 ? ( licenseItems.map(({ index, license }) => ( @@ -53,7 +57,9 @@ function LicenseList() { ) : ( setDialogOpen(true)} + onAddClick={() => { + setDialogOpen(true) + }} /> )} diff --git a/client/components/Parts/Buttons/SimpleButton.tsx b/client/components/Parts/Buttons/SimpleButton.tsx index 84cb0fa19..b04b933ba 100644 --- a/client/components/Parts/Buttons/SimpleButton.tsx +++ b/client/components/Parts/Buttons/SimpleButton.tsx @@ -15,7 +15,7 @@ interface SimpleButtonProps extends ButtonProps { } export default function SimpleButton(props: SimpleButtonProps) { - const { label, small, hoverBgColor, ...others } = props + const { label, small, hoverBgColor, sx, ...others } = props const buttonTextColor = props.color === 'OKFNWhite' ? 'gray' : 'white' return ( @@ -24,13 +24,14 @@ export default function SimpleButton(props: SimpleButtonProps) { color={props.color} {...others} sx={{ + ...sx, padding: '14px 24px', borderRadius: '9px', - border: props.label === 'Cancel' ? '1px solid #D3D7D8' : 0, boxShadow: 'none', '&:hover': { backgroundColor: (theme) => hoverBgColor ? theme.palette[hoverBgColor].main : 'unset', + borderColor: 'white' }, }} > diff --git a/client/components/Parts/Cards/NothingToSee.tsx b/client/components/Parts/Cards/NothingToSee.tsx index 3cceb2864..ecece4d7c 100644 --- a/client/components/Parts/Cards/NothingToSee.tsx +++ b/client/components/Parts/Cards/NothingToSee.tsx @@ -14,8 +14,8 @@ export default function NothingToSee(props: any){ textTransform: 'capitalize', marginTop: '10px', '&:hover': { - backgroundColor: (theme) => theme.palette.OKFNBlue.main, - color: 'white' + backgroundColor: (theme) => theme.palette.OKFNBlue.main, + color: 'white' } } }}> {props.buttonText} diff --git a/client/components/Parts/Dialogs/TwoButton.tsx b/client/components/Parts/Dialogs/TwoButton.tsx index 0285dcc84..35fc83402 100644 --- a/client/components/Parts/Dialogs/TwoButton.tsx +++ b/client/components/Parts/Dialogs/TwoButton.tsx @@ -95,7 +95,7 @@ export default function TwoButtonDialog(props: TwoButtonDialogProps) { `1px solid ${theme.palette.OKFNCoolGray400.main}`}} onClick={handleCancel} aria-label={t('cancel')} variant="contained" diff --git a/client/components/Parts/Fields/Input.tsx b/client/components/Parts/Fields/Input.tsx index e42e8c1c9..a66bef1c7 100644 --- a/client/components/Parts/Fields/Input.tsx +++ b/client/components/Parts/Fields/Input.tsx @@ -32,43 +32,44 @@ export default function InputField(props: InputFieldProps) { const onFocus = props.onFocus || noop const onBlur = props.onBlur || noop return ( -
- - {props.label} - - onChange(ev.target.value)} - error={props.error} - required={props.required} - helperText={props.helperText} - onKeyDown={onKeyDown} - placeholder={props.placeholder} - onFocus={onFocus} - onBlur={onBlur} - autoFocus={props.autoFocus} - /> +
+ {props.label} + onChange(ev.target.value)} + error={props.error} + required={props.required} + helperText={props.helperText} + onKeyDown={onKeyDown} + placeholder={props.placeholder} + onFocus={onFocus} + onBlur={onBlur} + autoFocus={props.autoFocus} + />
) } -export const StyledTextField = styled(TextField)(() => ({ - width: '100%', +export const StyledTextField = styled(TextField, { +})(() => ({ + width: '100%', '& label.Mui-focused': { color: '#00D1FF', }, '& .MuiInput-underline:after': { borderBottomColor: '#00D1FF', }, + '& .MuiTextField-root': { + width: '100%' + }, '& .MuiOutlinedInput-root': { '& fieldset': { borderColor: 'gray', diff --git a/client/components/Parts/Fields/Multiline.tsx b/client/components/Parts/Fields/Multiline.tsx index 6109594d2..ceffa1498 100644 --- a/client/components/Parts/Fields/Multiline.tsx +++ b/client/components/Parts/Fields/Multiline.tsx @@ -21,27 +21,23 @@ interface MultilineFieldProps { export default function MultilineField(props: MultilineFieldProps) { const onFocus = props.onFocus || noop return ( -
- - {props.label} - - props.onChange(ev.target.value as any)} - onFocus={onFocus} - autoFocus={props.autoFocus} - required={props.required} - /> +
+ {props.label} + props.onChange(ev.target.value as any)} + onFocus={onFocus} + autoFocus={props.autoFocus} + required={props.required} + />
) } \ No newline at end of file diff --git a/client/components/Parts/Fields/YesNo.tsx b/client/components/Parts/Fields/YesNo.tsx index e43e2a3c5..66a94b5b8 100644 --- a/client/components/Parts/Fields/YesNo.tsx +++ b/client/components/Parts/Fields/YesNo.tsx @@ -14,14 +14,11 @@ interface YesNoFieldProps { export default function YesNoField(props: YesNoFieldProps) { const onFocus = props.onFocus || noop return ( -
- - {props.label} - +
+ {props.label} + + , + 'Delete', + ) + +export default DeleteIcon \ No newline at end of file diff --git a/client/components/Parts/Trees/File.tsx b/client/components/Parts/Trees/File.tsx index 647c5c755..dd4366d13 100644 --- a/client/components/Parts/Trees/File.tsx +++ b/client/components/Parts/Trees/File.tsx @@ -11,8 +11,9 @@ import { alpha, styled, useTheme } from '@mui/material/styles' import { keyframes } from '@mui/system' import { TreeItem, TreeItemProps, TreeView, treeItemClasses } from '@mui/x-tree-view' import * as React from 'react' +import { useTranslation } from 'react-i18next' import closedFolderIcon from '../../../assets/closed_folder_icon.svg' -import deleteIcon from '../../../assets/delete_icon.svg' +import DeleteIcon from '../Icons/DeleteIcon' import openFileLocationIcon from '../../../assets/open_file_location_icon.svg' import openFolderIcon from '../../../assets/open_folder_icon.svg' import renameIcon from '../../../assets/rename_icon.svg' @@ -20,7 +21,6 @@ import * as helpers from '../../../helpers' import * as types from '../../../types' import IconButton from '../../Parts/Buttons/Icon' import ScrollBox from '../Boxes/Scroll' -import { useTranslation } from 'react-i18next' export interface FileTreeProps { files: types.IFile[] @@ -48,7 +48,7 @@ export default function FileTree(props: FileTreeProps) { return ( - + - {} + theme.palette.OKFNRed500.main, }} - primary={`${t('delete-filefolder', { fileOrFolder })}`} secondary={t('context-menu-delete-description')} + primary={`${t('delete-filefolder', { fileOrFolder })}`} + secondary={t('context-menu-delete-description')} /> diff --git a/client/themes.ts b/client/themes.ts index 597f5d117..a06d68372 100644 --- a/client/themes.ts +++ b/client/themes.ts @@ -35,6 +35,12 @@ declare module '@mui/material/Button' { OKFNGray700: true } } +declare module './components/Parts/Icons/DeleteIcon' { + interface SvgIconPropsColorOverrides { + OKFNRed500: true + OKFNGray700: true + } +} export const DEFAULT = createTheme({ typography: { diff --git a/portal/content/docs/documentation/getting-started.md b/portal/content/docs/documentation/getting-started.md index b9b49d1ec..55d05f59e 100644 --- a/portal/content/docs/documentation/getting-started.md +++ b/portal/content/docs/documentation/getting-started.md @@ -45,11 +45,11 @@ Go to the [RELEASES](https://github.com/okfn/opendataeditor/releases) and downlo Go to the [RELEASES](https://github.com/okfn/opendataeditor/releases) and download the most recent **DMG** file. -1. If you encounter security message, click on the question mark and then click the link in the first section. +1. If you encounter a security message, click on the question mark and then click the link in the first section. ![DOWNLOAD SECURITY](./assets/getting-started/gs-macos-download.png) -2. Change settings to allow app to execute. +2. Change settings to allow the app to execute. ![DOWNLOAD SETTINGS](./assets/getting-started/gs-macos-download-step2.png) diff --git a/server/endpoints/file/create.py b/server/endpoints/file/create.py index d7628079a..64a5628a4 100644 --- a/server/endpoints/file/create.py +++ b/server/endpoints/file/create.py @@ -25,6 +25,11 @@ class Result(BaseModel, extra="forbid"): size: int +# TODO: this operation needs to be atomic (has to include file creation and table validation) +# it will allow proper clean-ups in case of errors and better error messages +# (see "/file/fetch" as an example) + + @router.post("/file/create") async def endpoint( request: Request, diff --git a/server/endpoints/file/fetch.py b/server/endpoints/file/fetch.py index de24d9b14..55ac8155e 100644 --- a/server/endpoints/file/fetch.py +++ b/server/endpoints/file/fetch.py @@ -31,6 +31,10 @@ def server_file_read(request: Request, props: Props) -> Result: return action(request.app.get_project(), props) +# TODO: implement proper cleanup in case of failed uploading +# currently, it only cleanups if the file is not a table + + def action(project: Project, props: Props) -> Result: from ... import endpoints @@ -65,11 +69,11 @@ def action(project: Project, props: Props) -> Result: ).record # Ensure tabular - # Currently, we support only fetching tabular files because otherwise - # it's not possible to differentiate between HTML documents (wrong links) and data files if record.type != "table": endpoints.file.delete.action(project, endpoints.file.delete.Props(path=path)) - raise Exception("The file is not tabular") + # TODO: currently, we just use a tranlation key here + # later it might need to be migrated to proper error codes + raise Exception("error-url-not-table") return Result(path=path, size=len(bytes)) diff --git a/tsconfig.json b/tsconfig.json index 297458398..7d08d4448 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "DOM", "dom.iterable" ], + "allowJs": true, "declaration": true, "esModuleInterop": true, "resolveJsonModule": true,