From 0418ab5923a32850cc78afa3880360c54877121e Mon Sep 17 00:00:00 2001 From: Guergana Tzatchkova Date: Mon, 16 Dec 2024 09:31:30 -0800 Subject: [PATCH] Implement metadata panel new design (#687) --- .../Application/Dialogs/OpenLocation.tsx | 2 +- .../Dialogs/UploadFile/UploadFile.tsx | 7 +- client/components/Editors/Base/Item.tsx | 38 ++++-- client/components/Editors/Base/List.tsx | 24 ++-- client/components/Editors/Base/ListItem.tsx | 44 +++++-- client/components/Editors/Dialect/Layout.tsx | 67 +++------- .../Editors/Dialect/Sections/Dialect.tsx | 3 + .../Editors/Dialect/Sections/Format/Csv.tsx | 3 + .../Editors/Dialect/Sections/Format/Excel.tsx | 3 + .../Editors/Dialect/Sections/Format/Json.tsx | 3 + client/components/Editors/Portal/Layout.tsx | 6 +- client/components/Editors/Resource/Layout.tsx | 116 ++++-------------- .../Resource/Sections/Contributors.tsx | 47 ++++--- .../Editors/Resource/Sections/Integrity.tsx | 3 + .../Editors/Resource/Sections/Licenses.tsx | 32 +++-- .../Editors/Resource/Sections/Resource.tsx | 3 + .../Editors/Resource/Sections/Sources.tsx | 40 ++++-- client/components/Editors/Schema/Layout.tsx | 77 ++++-------- .../Editors/Schema/Sections/Fields.tsx | 6 +- .../Editors/Schema/Sections/ForeignKeys.tsx | 43 +++++-- .../Editors/Schema/Sections/Schema.tsx | 3 + client/components/Parts/Cards/Help.tsx | 2 +- .../components/Parts/Cards/NothingToSee.tsx | 32 +++++ client/components/Parts/Fields/Input.tsx | 10 +- client/components/Parts/Fields/Multiline.tsx | 10 +- .../components/Parts/Fields/Multiselect.tsx | 9 +- client/components/Parts/Fields/Select.tsx | 47 ++++--- client/components/Parts/Fields/YesNo.tsx | 32 +++-- .../Parts/Tabs/{Dialog.tsx => SimpleTabs.tsx} | 48 +++++--- locale/translations/en.json | 8 +- locale/translations/es.json | 3 +- locale/translations/fr.json | 3 +- locale/translations/pt.json | 3 +- 33 files changed, 439 insertions(+), 338 deletions(-) create mode 100644 client/components/Parts/Cards/NothingToSee.tsx rename client/components/Parts/Tabs/{Dialog.tsx => SimpleTabs.tsx} (50%) diff --git a/client/components/Application/Dialogs/OpenLocation.tsx b/client/components/Application/Dialogs/OpenLocation.tsx index ebce7a32..e11c823b 100644 --- a/client/components/Application/Dialogs/OpenLocation.tsx +++ b/client/components/Application/Dialogs/OpenLocation.tsx @@ -55,7 +55,7 @@ export default function OpenLocationDialog() { aria-describedby="dialog-description" > - @@ -80,7 +81,7 @@ export function UploadFileDialog() { - + diff --git a/client/components/Editors/Base/Item.tsx b/client/components/Editors/Base/Item.tsx index 4393478c..5414e343 100644 --- a/client/components/Editors/Base/Item.tsx +++ b/client/components/Editors/Base/Item.tsx @@ -5,7 +5,8 @@ import Link from '@mui/material/Link' import Button from '@mui/material/Button' import Typography from '@mui/material/Typography' import Columns from '../../Parts/Grids/Columns' -import HeadingBox from './Heading/Box' +import { useTranslation } from 'react-i18next' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' export interface EditorItemProps { kind: string @@ -24,23 +25,45 @@ export default function EditorItem(props: React.PropsWithChildren (props.onExtrasClick ? props.onExtrasClick() : undefined)} + sx={{ + border: '1px solid #D3D7D8', + color: '#3F4345', + backgroundColor: '#FAFAFA', + borderRadius: '4px', + padding: '4px 10px', + '&.MuiButton-root': { + textTransform: 'capitalize', + }, + }} > {startCase(props.extrasName)} ) } const BackButton = () => { + const { t } = useTranslation() return ( - ) } return ( - - - + + + + {startCase(props.kind)}s @@ -51,10 +74,9 @@ export default function EditorItem(props: React.PropsWithChildren - - + {props.children} ) diff --git a/client/components/Editors/Base/List.tsx b/client/components/Editors/Base/List.tsx index e286c602..7ae3cb51 100644 --- a/client/components/Editors/Base/List.tsx +++ b/client/components/Editors/Base/List.tsx @@ -5,11 +5,12 @@ import { useTranslation } from 'react-i18next' import Columns from '../../Parts/Grids/Columns' import HeadingBox from './Heading/Box' import EditorListItem from './ListItem' +import startCase from 'lodash/startCase' export interface EditorListProps { kind: string query?: string - onAddClick?: () => void + onAddClick?: (() => void) | null // We accept search as a prop otherwise it loses focus SearchInput?: React.ReactNode } @@ -19,18 +20,27 @@ export default function EditorList(props: React.PropsWithChildren { if (!props.onAddClick) return null - // @ts-ignore - const title = t(`add-${props.kind}`) as any - return ( - ) } // @ts-ignore - const title = t(`${props.kind}s`) as any + const title = t(`${props.kind.replace(' ', '-')}s`) as any // TODO: we can make HeadingBox (or with Tabs/Help) "sticky" with CSS: // https://developer.mozilla.org/en-US/docs/Web/CSS/position diff --git a/client/components/Editors/Base/ListItem.tsx b/client/components/Editors/Base/ListItem.tsx index a1e4837a..50db7b52 100644 --- a/client/components/Editors/Base/ListItem.tsx +++ b/client/components/Editors/Base/ListItem.tsx @@ -4,6 +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' interface EditorListItemProps { kind: string @@ -29,27 +30,30 @@ export default function EditorListItem(props: EditorListItemProps) { color="warning" component="span" title={`${t('remove')} ${capitalize(props.kind)}`} - sx={{ marginLeft: 2, textDecoration: 'underline' }} + sx={{ + '& img': { + width: '22px', + }, + }} onClick={(ev) => { ev.stopPropagation() props.onRemoveClick?.() }} > - {t('remove')} + ) } const EndIcon = () => { - const label = (props.type || 'item').toUpperCase() return ( - {label} ) } + const label = props.type || 'item' return ( ) } diff --git a/client/components/Editors/Dialect/Layout.tsx b/client/components/Editors/Dialect/Layout.tsx index e24094de..580810e6 100644 --- a/client/components/Editors/Dialect/Layout.tsx +++ b/client/components/Editors/Dialect/Layout.tsx @@ -1,69 +1,38 @@ -import * as React from 'react' import capitalize from 'lodash/capitalize' import Box from '@mui/material/Box' -import Columns from '../../Parts/Grids/Columns' -import MenuTree from '../../Parts/Trees/Menu' -import EditorHelp from '../Base/Help' import DialectSection from './Sections/Dialect' import FormatSection from './Sections/Format' import { useStore } from './store' import * as types from '../../../types' import { useTranslation } from 'react-i18next' +import SimpleTabs from '../../Parts/Tabs/SimpleTabs' export default function Layout() { - const externalMenu = useStore((state) => state.externalMenu) - return ( - - {!externalMenu ? : } - - ) -} - -function LayoutWithMenu() { const format = useStore((state) => state.format) - const section = useStore((state) => state.section) const updateHelp = useStore((state) => state.updateHelp) const updateState = useStore((state) => state.updateState) const { t } = useTranslation() + const MENU_ITEMS: types.IMenuItem[] = [ - { section: 'dialect', name: t('dialect') }, + { section: 'dialect', name: t('default') }, { section: 'dialect/format', name: capitalize(format) || t('format') }, ] - return ( - - - { - updateHelp(section) - updateState({ section }) - }} - /> - - - - ) -} -function LayoutWithoutMenu() { - const section = useStore((state) => state.externalMenu?.section || state.section) - const updateHelp = useStore((state) => state.updateHelp) - const helpItem = useStore((state) => state.helpItem) - React.useEffect(() => updateHelp(section), [section]) - if (!section) return null + const MENU_LABELS = MENU_ITEMS.map((item) => item.name) + return ( - - - - - - - + + { + updateHelp(MENU_ITEMS[newValue].section) + updateState({ section: MENU_ITEMS[newValue].section }) + }} + > + + + + ) } diff --git a/client/components/Editors/Dialect/Sections/Dialect.tsx b/client/components/Editors/Dialect/Sections/Dialect.tsx index 45e24d74..45e24f1d 100644 --- a/client/components/Editors/Dialect/Sections/Dialect.tsx +++ b/client/components/Editors/Dialect/Sections/Dialect.tsx @@ -10,12 +10,15 @@ import { useStore } from '../store' import validator from 'validator' import * as settings from '../../../../settings' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' export default function General() { const updateHelp = useStore((state) => state.updateHelp) + const helpItem = useStore((state) => state.helpItem) const { t } = useTranslation() return ( updateHelp('dialect')}> + diff --git a/client/components/Editors/Dialect/Sections/Format/Csv.tsx b/client/components/Editors/Dialect/Sections/Format/Csv.tsx index 816650b0..198ebf0c 100644 --- a/client/components/Editors/Dialect/Sections/Format/Csv.tsx +++ b/client/components/Editors/Dialect/Sections/Format/Csv.tsx @@ -6,11 +6,14 @@ import EditorSection from '../../../Base/Section' import * as settings from '../../../../../settings' import { useStore, selectors, select } from '../../store' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../../Base/Help' export default function General() { const updateHelp = useStore((state) => state.updateHelp) + const helpItem = useStore((state) => state.helpItem) return ( <EditorSection name="Csv" onHeadingClick={() => updateHelp('dialect/format')}> + <EditorHelp helpItem={helpItem} withIcon /> <Columns spacing={3}> <Box> <Delimiter /> diff --git a/client/components/Editors/Dialect/Sections/Format/Excel.tsx b/client/components/Editors/Dialect/Sections/Format/Excel.tsx index 55a1fd51..733e57b5 100644 --- a/client/components/Editors/Dialect/Sections/Format/Excel.tsx +++ b/client/components/Editors/Dialect/Sections/Format/Excel.tsx @@ -8,11 +8,14 @@ import * as settings from '../../../../../settings' import { useStore, selectors, select } from '../../store' // import validator from 'validator' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../../Base/Help' export default function General() { const updateHelp = useStore((state) => state.updateHelp) + const helpItem = useStore((state) => state.helpItem) return ( <EditorSection name="Excel" onHeadingClick={() => updateHelp('dialect/format')}> + <EditorHelp helpItem={helpItem} withIcon /> <Columns spacing={3}> <Box> <Sheet /> diff --git a/client/components/Editors/Dialect/Sections/Format/Json.tsx b/client/components/Editors/Dialect/Sections/Format/Json.tsx index ad61e062..54a2b808 100644 --- a/client/components/Editors/Dialect/Sections/Format/Json.tsx +++ b/client/components/Editors/Dialect/Sections/Format/Json.tsx @@ -6,11 +6,14 @@ import * as settings from '../../../../../settings' import { useStore, selectors, select } from '../../store' import YesNoField from '../../../../Parts/Fields/YesNo' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../../Base/Help' export default function General() { const updateHelp = useStore((state) => state.updateHelp) + const helpItem = useStore((state) => state.helpItem) return ( <EditorSection name="Json" onHeadingClick={() => updateHelp('dialect/format')}> + <EditorHelp helpItem={helpItem} withIcon /> <Columns spacing={3}> <Box> <Keys /> diff --git a/client/components/Editors/Portal/Layout.tsx b/client/components/Editors/Portal/Layout.tsx index dd79d4e3..b4fdeafa 100644 --- a/client/components/Editors/Portal/Layout.tsx +++ b/client/components/Editors/Portal/Layout.tsx @@ -1,6 +1,6 @@ import publishDialogImg from '@client/assets/dialog_publish.png' import Box from '@mui/material/Box' -import DialogTabs from '../../Parts/Tabs/Dialog' +import SimpleTabs from '../../Parts/Tabs/SimpleTabs' import CkanSection from './Sections/Ckan' import GithubSection from './Sections/Github' import ZenodoSection from './Sections/Zenodo' @@ -21,7 +21,7 @@ export default function Layout() { <img src={publishDialogImg} alt="Image Publish Dialog" /> </Box> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> - <DialogTabs labels={tabLabels}> + <SimpleTabs labels={tabLabels} centered> <Box> <CkanSection /> </Box> @@ -31,7 +31,7 @@ export default function Layout() { <Box> <ZenodoSection /> </Box> - </DialogTabs> + </SimpleTabs> </Box> </Box> ) diff --git a/client/components/Editors/Resource/Layout.tsx b/client/components/Editors/Resource/Layout.tsx index b03da50b..27c3caac 100644 --- a/client/components/Editors/Resource/Layout.tsx +++ b/client/components/Editors/Resource/Layout.tsx @@ -1,9 +1,5 @@ import * as React from 'react' -import capitalize from 'lodash/capitalize' import Box from '@mui/material/Box' -import Columns from '../../Parts/Grids/Columns' -import EditorHelp from '../Base/Help' -import MenuTree from '../../Parts/Trees/Menu' import Dialect from '../Dialect' import Schema from '../Schema' import ResourceSection from './Sections/Resource' @@ -14,20 +10,9 @@ import ContributorsSection from './Sections/Contributors' import { useStore } from './store' import * as types from '../../../types' import { useTranslation } from 'react-i18next' +import SimpleTabs from '../../Parts/Tabs/SimpleTabs' export default function Layout() { - const externalMenu = useStore((state) => state.externalMenu) - return ( - <Box sx={{ height: '100%' }}> - {!externalMenu ? <LayoutWithMenu /> : <LayoutWithoutMenu />} - </Box> - ) -} - -// TODO: improve menu implementation (move some state to store / reduce re-renders) -function LayoutWithMenu() { - const section = useStore((state) => state.section) - const type = useStore((state) => state.descriptor.type) const format = useStore((state) => state.descriptor.format) const dialect = useStore((state) => state.descriptor.dialect) const schema = useStore((state) => state.descriptor.schema) @@ -37,19 +22,18 @@ function LayoutWithMenu() { const onFieldSelected = useStore((state) => state.onFieldSelected) const { t } = useTranslation() - const MENU_ITEMS: types.IMenuItem[] = [ - { section: 'resource', name: t('resource') }, + const TOP_TAB_LABELS = [t('resource'), t('dialect'), t('schema')] + + const RESOURCE_MENU_ITEMS: types.IMenuItem[] = [ + { section: 'resource', name: t('default') }, { section: 'resource/integrity', name: t('integrity') }, { section: 'resource/licenses', name: t('licenses') }, { section: 'resource/contributors', name: t('contributors') }, { section: 'resource/sources', name: t('sources') }, - { section: 'dialect', name: t('dialect'), disabled: type !== 'table' }, - { section: 'dialect/format', name: capitalize(format) || t('format') }, - { section: 'schema', name: t('schema'), disabled: type !== 'table' }, - { section: 'schema/fields', name: t('fields') }, - { section: 'schema/foreignKeys', name: t('foreign-keys') }, ] + const MENU_LABELS = RESOURCE_MENU_ITEMS.map((item) => item.name) + // We use memo to avoid nested editors re-rerender const handleDialectChange = React.useMemo(() => { return (dialect: types.IDialect) => updateDescriptor({ dialect }) @@ -58,80 +42,30 @@ function LayoutWithMenu() { return (schema: types.ISchema) => updateDescriptor({ schema }) }, []) - // We use memo to avoid nested editors re-rerender - const externalMenu = React.useMemo(() => { - return { section } - }, []) - return ( - <Columns spacing={3} layout={[2, 8]} columns={10}> - <Box sx={{ padding: 2, borderRight: 'solid 1px #ddd', height: '100%' }}> - <MenuTree - menuItems={MENU_ITEMS} - selected={section} - defaultExpanded={['resource']} - onSelect={(section) => { - updateHelp(section) - updateState({ section }) - externalMenu.section = section + <Box sx={{ height: '100%', padding: 2 }}> + <SimpleTabs labels={TOP_TAB_LABELS}> + <SimpleTabs + labels={MENU_LABELS} + orientation="vertical" + onChange={(newValue: number) => { + updateHelp(RESOURCE_MENU_ITEMS[newValue].section) + updateState({ section: RESOURCE_MENU_ITEMS[newValue].section }) }} - /> - </Box> - <Box> - <Box hidden={!section.startsWith('resource')}> - <LayoutWithoutMenu /> - </Box> - {type === 'table' && ( - <Box> - <Box hidden={!section.startsWith('dialect')}> - <Dialect - format={format} - dialect={dialect} - externalMenu={externalMenu} - onChange={handleDialectChange} - /> - </Box> - <Box hidden={!section.startsWith('schema')}> - <Schema - schema={schema} - externalMenu={externalMenu} - onChange={handleSchemaChange} - onFieldSelected={onFieldSelected} - /> - </Box> - </Box> - )} - </Box> - </Columns> - ) -} - -function LayoutWithoutMenu() { - const section = useStore((state) => state.externalMenu?.section || state.section) - const updateHelp = useStore((state) => state.updateHelp) - const helpItem = useStore((state) => state.helpItem) - React.useEffect(() => updateHelp(section), [section]) - if (!section) return null - return ( - <Columns spacing={3} layout={[5, 3]} columns={8}> - <Box sx={{ flexGrow: 1 }}> - <Box hidden={section !== 'resource'}> + > <ResourceSection /> - </Box> - <Box hidden={section !== 'resource/integrity'}> <IntegritySection /> - </Box> - <Box hidden={section !== 'resource/licenses'}> <LicensesSection /> - </Box> - <Box hidden={section !== 'resource/contributors'}> <ContributorsSection /> - </Box> - <Box hidden={section !== 'resource/sources'}> <SourcesSection /> - </Box> - </Box> - <EditorHelp helpItem={helpItem} /> - </Columns> + </SimpleTabs> + <Dialect format={format} dialect={dialect} onChange={handleDialectChange} /> + <Schema + schema={schema} + onChange={handleSchemaChange} + onFieldSelected={onFieldSelected} + /> + </SimpleTabs> + </Box> ) } diff --git a/client/components/Editors/Resource/Sections/Contributors.tsx b/client/components/Editors/Resource/Sections/Contributors.tsx index fd678ec6..8579c348 100644 --- a/client/components/Editors/Resource/Sections/Contributors.tsx +++ b/client/components/Editors/Resource/Sections/Contributors.tsx @@ -6,6 +6,8 @@ import EditorList from '../../Base/List' import EditorListItem from '../../Base/ListItem' import { useStore, selectors, select } from '../store' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' +import NothingToSee from '@client/components/Parts/Cards/NothingToSee' export default function Contributors() { const index = useStore((state) => state.contributorState.index) @@ -18,20 +20,32 @@ function ContributorList() { const updateContributorState = useStore((state) => state.updateContributorState) const addContributor = useStore((state) => state.addContributor) const removeContributor = useStore((state) => state.removeContributor) + const helpItem = useStore((state) => state.helpItem) + const { t } = useTranslation() + return ( - <EditorList kind="contributor" query={query} onAddClick={() => addContributor()}> - {contributorItems.map(({ index, contributor }) => ( - <EditorListItem - key={index} - kind="contributor" - name={contributor.title} - type="contributor" - onClick={() => { - updateContributorState({ index }) - }} - onRemoveClick={() => removeContributor(index)} - /> - ))} + <EditorList + kind="contributor" + query={query} + onAddClick={contributorItems.length > 0 ? addContributor : null} + > + <EditorHelp helpItem={helpItem} withIcon /> + {contributorItems.length > 0 ? ( + contributorItems.map(({ index, contributor }) => ( + <EditorListItem + key={index} + kind="contributor" + name={contributor.title} + type="contributor" + onClick={() => { + updateContributorState({ index }) + }} + onRemoveClick={() => removeContributor(index)} + /> + )) + ) : ( + <NothingToSee buttonText={t('add-contributor')} onAddClick={addContributor} /> + )} </EditorList> ) } @@ -42,13 +56,18 @@ function ContributorItem() { ) const isExtras = useStore((state) => state.contributorState.isExtras) const updateContributorState = useStore((state) => state.updateContributorState) + const updateHelp = useStore((state) => state.updateHelp) + return ( <EditorItem kind="contributor" name={title} isExtras={isExtras} onExtrasClick={() => updateContributorState({ isExtras: !isExtras })} - onBackClick={() => updateContributorState({ index: undefined, isExtras: false })} + onBackClick={() => { + updateContributorState({ index: undefined, isExtras: false }) + updateHelp('resource/contributors') + }} > <Columns spacing={3}> <Box> diff --git a/client/components/Editors/Resource/Sections/Integrity.tsx b/client/components/Editors/Resource/Sections/Integrity.tsx index d2724bb9..f567bdcd 100644 --- a/client/components/Editors/Resource/Sections/Integrity.tsx +++ b/client/components/Editors/Resource/Sections/Integrity.tsx @@ -4,15 +4,18 @@ import EditorSection from '../../Base/Section' import Columns from '../../../Parts/Grids/Columns' import { useStore } from '../store' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' export default function Integrity() { const updateHelp = useStore((state) => state.updateHelp) + const helpItem = useStore((state) => state.helpItem) const { t } = useTranslation() return ( <EditorSection name={t('integrity')} onHeadingClick={() => updateHelp('resource/integrity')} > + <EditorHelp helpItem={helpItem} withIcon /> <Columns spacing={3}> <Box> <Hash /> diff --git a/client/components/Editors/Resource/Sections/Licenses.tsx b/client/components/Editors/Resource/Sections/Licenses.tsx index 4c32a0f8..aa2cb773 100644 --- a/client/components/Editors/Resource/Sections/Licenses.tsx +++ b/client/components/Editors/Resource/Sections/Licenses.tsx @@ -17,6 +17,8 @@ import Autocomplete from '@mui/material/Autocomplete' import { useStore, selectors, select } from '../store' import validator from 'validator' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' +import NothingToSee from '@client/components/Parts/Cards/NothingToSee' export default function Licenses() { const index = useStore((state) => state.licenseState.index) @@ -30,20 +32,30 @@ function LicenseList() { const licenseItems = useStore(selectors.licenseItems) const updateLicenseState = useStore((state) => state.updateLicenseState) const removeLicense = useStore((state) => state.removeLicense) + const helpItem = useStore((state) => state.helpItem) + const { t } = useTranslation() return ( <> - <EditorList kind="license" query={query} onAddClick={() => setDialogOpen(true)}> - {licenseItems.map(({ index, license }) => ( - <EditorListItem - key={index} - kind="license" - name={license.name} - type="license" - onClick={() => updateLicenseState({ index })} - onRemoveClick={() => removeLicense(index)} + <EditorList kind="license" query={query}> + <EditorHelp helpItem={helpItem} withIcon /> + {licenseItems.length > 0 ? ( + licenseItems.map(({ index, license }) => ( + <EditorListItem + key={index} + kind="license" + name={license.name} + type="license" + onClick={() => updateLicenseState({ index })} + onRemoveClick={() => removeLicense(index)} + /> + )) + ) : ( + <NothingToSee + buttonText={t('add-license')} + onAddClick={() => setDialogOpen(true)} /> - ))} + )} </EditorList> <LicenseDialog open={dialogOpen} onClose={() => setDialogOpen(false)} /> </> diff --git a/client/components/Editors/Resource/Sections/Resource.tsx b/client/components/Editors/Resource/Sections/Resource.tsx index 9a267002..6f483830 100644 --- a/client/components/Editors/Resource/Sections/Resource.tsx +++ b/client/components/Editors/Resource/Sections/Resource.tsx @@ -9,10 +9,12 @@ import { useStore, selectors } from '../store' import * as store from '@client/store' import validator from 'validator' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' export default function Resource() { const updateHelp = useStore((state) => state.updateHelp) const onBackClick = useStore((state) => state.onBackClick) + const helpItem = useStore((state) => state.helpItem) const { t } = useTranslation() return ( @@ -21,6 +23,7 @@ export default function Resource() { onHeadingClick={() => updateHelp('resource')} onBackClick={onBackClick} > + <EditorHelp helpItem={helpItem} withIcon /> <Columns spacing={3}> <Box> <Name /> diff --git a/client/components/Editors/Resource/Sections/Sources.tsx b/client/components/Editors/Resource/Sections/Sources.tsx index fc3e2664..ecf4b3cd 100644 --- a/client/components/Editors/Resource/Sections/Sources.tsx +++ b/client/components/Editors/Resource/Sections/Sources.tsx @@ -8,6 +8,8 @@ import EditorListItem from '../../Base/ListItem' import { useStore, selectors, select } from '../store' import validator from 'validator' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' +import NothingToSee from '@client/components/Parts/Cards/NothingToSee' export default function Sources() { const index = useStore((state) => state.sourceState.index) @@ -20,20 +22,32 @@ function SourceList() { const updateSourceState = useStore((state) => state.updateSourceState) const addSource = useStore((state) => state.addSource) const removeSource = useStore((state) => state.removeSource) + const helpItem = useStore((state) => state.helpItem) + const { t } = useTranslation() + return ( - <EditorList kind="source" query={query} onAddClick={() => addSource()}> - {sourceItems.map(({ index, source }) => ( - <EditorListItem - key={index} - kind="source" - name={source.title} - type="source" - onClick={() => { - updateSourceState({ index }) - }} - onRemoveClick={() => removeSource(index)} - /> - ))} + <EditorList + kind="source" + query={query} + onAddClick={sourceItems.length > 0 ? addSource : null} + > + <EditorHelp helpItem={helpItem} withIcon /> + {sourceItems.length > 0 ? ( + sourceItems.map(({ index, source }) => ( + <EditorListItem + key={index} + kind="source" + name={source.title} + type="source" + onClick={() => { + updateSourceState({ index }) + }} + onRemoveClick={() => removeSource(index)} + /> + )) + ) : ( + <NothingToSee buttonText={t('add-source')} onAddClick={addSource} /> + )} </EditorList> ) } diff --git a/client/components/Editors/Schema/Layout.tsx b/client/components/Editors/Schema/Layout.tsx index 2f668ec4..4902b390 100644 --- a/client/components/Editors/Schema/Layout.tsx +++ b/client/components/Editors/Schema/Layout.tsx @@ -1,70 +1,39 @@ -import * as React from 'react' import Box from '@mui/material/Box' -import Columns from '../../Parts/Grids/Columns' -import MenuTree from '../../Parts/Trees/Menu' -import EditorHelp from '../Base/Help' import SchemaSection from './Sections/Schema' import FieldsSection from './Sections/Fields' import ForeignKeysSection from './Sections/ForeignKeys' import { useStore } from './store' import * as types from '../../../types' +import SimpleTabs from '../../Parts/Tabs/SimpleTabs' +import { useTranslation } from 'react-i18next' export default function Layout() { - const externalMenu = useStore((state) => state.externalMenu) - return ( - <Box sx={{ height: '100%' }}> - {!externalMenu ? <LayoutWithMenu /> : <LayoutWithoutMenu />} - </Box> - ) -} - -function LayoutWithMenu() { - const section = useStore((state) => state.section) const updateHelp = useStore((state) => state.updateHelp) const updateState = useStore((state) => state.updateState) + const { t } = useTranslation() + const MENU_ITEMS: types.IMenuItem[] = [ - { section: 'schema', name: 'Schema' }, - { section: 'schema/fields', name: 'Fields' }, - { section: 'schema/foreignKeys', name: 'Foreign Keys' }, + { section: 'schema', name: t('default') }, + { section: 'schema/fields', name: t('fields') }, + { section: 'schema/foreignKeys', name: t('foreign-keys') }, ] - return ( - <Columns spacing={3} layout={[2, 8]} columns={10}> - <Box sx={{ padding: 2, borderRight: 'solid 1px #ddd', height: '100%' }}> - <MenuTree - menuItems={MENU_ITEMS} - selected={section} - defaultExpanded={['schema']} - onSelect={(section) => { - updateHelp(section) - updateState({ section }) - }} - /> - </Box> - <LayoutWithoutMenu /> - </Columns> - ) -} -function LayoutWithoutMenu() { - const section = useStore((state) => state.externalMenu?.section || state.section) - const updateHelp = useStore((state) => state.updateHelp) - const helpItem = useStore((state) => state.helpItem) - React.useEffect(() => updateHelp(section), [section]) - if (!section) return null + const MENU_LABELS = MENU_ITEMS.map((item) => item.name) + return ( - <Columns spacing={3} layout={[5, 3]} columns={8}> - <Box> - <Box hidden={section !== 'schema'}> - <SchemaSection /> - </Box> - <Box hidden={section !== 'schema/fields'}> - <FieldsSection /> - </Box> - <Box hidden={section !== 'schema/foreignKeys'}> - <ForeignKeysSection /> - </Box> - </Box> - {helpItem ? <EditorHelp helpItem={helpItem} /> : null} - </Columns> + <Box sx={{ height: '100%' }}> + <SimpleTabs + labels={MENU_LABELS} + orientation="vertical" + onChange={(newValue: number) => { + updateHelp(MENU_ITEMS[newValue].section) + updateState({ section: MENU_ITEMS[newValue].section }) + }} + > + <SchemaSection /> + <FieldsSection /> + <ForeignKeysSection /> + </SimpleTabs> + </Box> ) } diff --git a/client/components/Editors/Schema/Sections/Fields.tsx b/client/components/Editors/Schema/Sections/Fields.tsx index 821e0957..5e8a08f9 100644 --- a/client/components/Editors/Schema/Sections/Fields.tsx +++ b/client/components/Editors/Schema/Sections/Fields.tsx @@ -19,6 +19,7 @@ import TimePickerField from '../../../Parts/Fields/TimePicker' import validator from 'validator' import dayjs from 'dayjs' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' export default function Fields() { const index = useStore((state) => state.fieldState.index) @@ -58,15 +59,18 @@ function FieldItem() { const name = useStore(select(selectors.field, (field) => field.name)) const isExtras = useStore((state) => state.fieldState.isExtras) const updateFieldState = useStore((state) => state.updateFieldState) + const helpItem = useStore((state) => state.helpItem) + const { t } = useTranslation() return ( <EditorItem kind="field" name={name} isExtras={isExtras} - extrasName="constraints" + extrasName={t('constraints')} onExtrasClick={() => updateFieldState({ isExtras: !isExtras })} onBackClick={() => updateFieldState({ index: undefined, isExtras: false })} > + <EditorHelp helpItem={helpItem} withIcon /> {isExtras ? <FieldItemExtras /> : <FieldItemMain />} </EditorItem> ) diff --git a/client/components/Editors/Schema/Sections/ForeignKeys.tsx b/client/components/Editors/Schema/Sections/ForeignKeys.tsx index e77976ea..7b2e0a39 100644 --- a/client/components/Editors/Schema/Sections/ForeignKeys.tsx +++ b/client/components/Editors/Schema/Sections/ForeignKeys.tsx @@ -7,6 +7,8 @@ import EditorList from '../../Base/List' import EditorListItem from '../../Base/ListItem' import { useStore, selectors, select } from '../store' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' +import NothingToSee from '@client/components/Parts/Cards/NothingToSee' export default function ForeignKey() { const index = useStore((state) => state.foreignKeyState.index) @@ -19,18 +21,29 @@ function ForeignKeyList() { const updateForeignKeyState = useStore((state) => state.updateForeignKeyState) const addForeignKey = useStore((state) => state.addForeignKey) const removeForeignKey = useStore((state) => state.removeForeignKey) + const helpItem = useStore((state) => state.helpItem) + const { t } = useTranslation() return ( - <EditorList kind="foreign key" query={query} onAddClick={() => addForeignKey()}> - {foreignKeyItems.map(({ index, foreignKey }) => ( - <EditorListItem - key={index} - kind="foreign key" - name={foreignKey.fields.join(',')} - type="fk" - onClick={() => updateForeignKeyState({ index })} - onRemoveClick={() => removeForeignKey(index)} - /> - ))} + <EditorList + kind="foreign key" + query={query} + onAddClick={foreignKeyItems.length > 0 ? addForeignKey : null} + > + <EditorHelp helpItem={helpItem} withIcon /> + {foreignKeyItems.length > 0 ? ( + foreignKeyItems.map(({ index, foreignKey }) => ( + <EditorListItem + key={index} + kind="foreign key" + name={foreignKey.fields.join(',')} + type="fk" + onClick={() => updateForeignKeyState({ index })} + onRemoveClick={() => removeForeignKey(index)} + /> + )) + ) : ( + <NothingToSee buttonText={t('add-foreign-key')} onAddClick={addForeignKey} /> + )} </EditorList> ) } @@ -39,14 +52,20 @@ function ForeignKeyItem() { const fields = useStore(select(selectors.foreignKey, (foreignKey) => foreignKey.fields)) const isExtras = useStore((state) => state.foreignKeyState.isExtras) const updateForeignKeyState = useStore((state) => state.updateForeignKeyState) + const updateHelp = useStore((state) => state.updateHelp) + const helpItem = useStore((state) => state.helpItem) return ( <EditorItem kind="foreignKey" name={fields.join(',')} isExtras={isExtras} onExtrasClick={() => updateForeignKeyState({ isExtras: !isExtras })} - onBackClick={() => updateForeignKeyState({ index: undefined, isExtras: false })} + onBackClick={() => { + updateForeignKeyState({ index: undefined, isExtras: false }) + updateHelp('schema/foreignKeys') + }} > + <EditorHelp helpItem={helpItem} withIcon /> <Columns spacing={3}> <Box> <SourceField /> diff --git a/client/components/Editors/Schema/Sections/Schema.tsx b/client/components/Editors/Schema/Sections/Schema.tsx index 780b21c9..ca7c3b34 100644 --- a/client/components/Editors/Schema/Sections/Schema.tsx +++ b/client/components/Editors/Schema/Sections/Schema.tsx @@ -8,12 +8,15 @@ import Columns from '../../../Parts/Grids/Columns' import { useStore, selectors } from '../store' import validator from 'validator' import { useTranslation } from 'react-i18next' +import EditorHelp from '../../Base/Help' export default function General() { const updateHelp = useStore((state) => state.updateHelp) + const helpItem = useStore((state) => state.helpItem) const { t } = useTranslation() return ( <EditorSection name={t('schema')} onHeadingClick={() => updateHelp('schema')}> + <EditorHelp helpItem={helpItem} withIcon /> <Columns spacing={3}> <Box> <Name /> diff --git a/client/components/Parts/Cards/Help.tsx b/client/components/Parts/Cards/Help.tsx index 63527e7d..546f07f8 100644 --- a/client/components/Parts/Cards/Help.tsx +++ b/client/components/Parts/Cards/Help.tsx @@ -32,7 +32,7 @@ export function HelpCardWithIcon(props: React.PropsWithChildren<HelpCardProps>){ <Card variant="outlined" square={true} - sx={{ height: '100%', border: '1px solid #00D1FF', borderRadius: '8px', marginLeft: '12px', marginRight: '12px', marginBottom: '15px', backgroundColor: '#F5FDFE' }} + sx={{ height: '100%', border: '1px solid #00D1FF', borderRadius: '8px', marginBottom: '15px', backgroundColor: '#F5FDFE' }} > <CardContent sx={{ display: 'flex', padding: '18px !important' diff --git a/client/components/Parts/Cards/NothingToSee.tsx b/client/components/Parts/Cards/NothingToSee.tsx new file mode 100644 index 00000000..3cceb286 --- /dev/null +++ b/client/components/Parts/Cards/NothingToSee.tsx @@ -0,0 +1,32 @@ +import { Typography } from '@mui/material' +import Button from '@mui/material/Button' +import Box from '@mui/material/Box' +import { useTranslation } from 'react-i18next' + +export default function NothingToSee(props: any){ + const { t } = useTranslation() + + const AddButton = () => { + if (!props.onAddClick) return null + + return ( + <Button title={props.buttonText} onClick={() => props.onAddClick?.()} sx={{ backgroundColor: 'black', color: 'white', '&.MuiButton-root': { + textTransform: 'capitalize', + marginTop: '10px', + '&:hover': { + backgroundColor: (theme) => theme.palette.OKFNBlue.main, + color: 'white' + } + } }}> + {props.buttonText} + </Button> + ) + } + + return ( + <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', marginTop: '64px' }}> + <Typography sx={{ fontSize: '28px', fontWeight: 700 }}>{t('nothing-to-see')}</Typography> + <AddButton /> + </Box> + ) +} \ No newline at end of file diff --git a/client/components/Parts/Fields/Input.tsx b/client/components/Parts/Fields/Input.tsx index a45878b1..e42e8c1c 100644 --- a/client/components/Parts/Fields/Input.tsx +++ b/client/components/Parts/Fields/Input.tsx @@ -1,5 +1,6 @@ import noop from 'lodash/noop' import TextField from '@mui/material/TextField' +import InputLabel from '@mui/material/InputLabel' import { styled } from '@mui/material/styles' // TODO: rework all the fields wrappign props (see buttons) @@ -31,14 +32,18 @@ export default function InputField(props: InputFieldProps) { const onFocus = props.onFocus || noop const onBlur = props.onBlur || noop return ( + <div> + <InputLabel + shrink={false}> + {props.label} + </InputLabel> <StyledTextField name={props.name || props.label} type={props.type} margin="normal" - label={props.label} value={props.value} size={props.size || 'small'} - style={{ maxWidth: '350px' }} + style={{ maxWidth: '350px', marginTop: '5px' }} disabled={props.disabled} inputProps={props.inputProps} onChange={(ev) => onChange(ev.target.value)} @@ -51,6 +56,7 @@ export default function InputField(props: InputFieldProps) { onBlur={onBlur} autoFocus={props.autoFocus} /> + </div> ) } diff --git a/client/components/Parts/Fields/Multiline.tsx b/client/components/Parts/Fields/Multiline.tsx index d6ae45ca..6109594d 100644 --- a/client/components/Parts/Fields/Multiline.tsx +++ b/client/components/Parts/Fields/Multiline.tsx @@ -1,5 +1,6 @@ import noop from 'lodash/noop' import { StyledTextField } from './Input' +import InputLabel from '@mui/material/InputLabel' // TODO: shall we open a modal editor for this field? @@ -20,6 +21,11 @@ interface MultilineFieldProps { export default function MultilineField(props: MultilineFieldProps) { const onFocus = props.onFocus || noop return ( + <div> + <InputLabel + shrink={false}> + {props.label} + </InputLabel> <StyledTextField multiline fullWidth @@ -28,8 +34,7 @@ export default function MultilineField(props: MultilineFieldProps) { rows={props.rows} type={props.type} margin="normal" - style={{ maxWidth: '350px' }} - label={props.label} + style={{ maxWidth: '350px', marginTop: '5px' }} value={props.value} size={props.size || 'small'} onChange={(ev) => props.onChange(ev.target.value as any)} @@ -37,5 +42,6 @@ export default function MultilineField(props: MultilineFieldProps) { autoFocus={props.autoFocus} required={props.required} /> + </div> ) } \ No newline at end of file diff --git a/client/components/Parts/Fields/Multiselect.tsx b/client/components/Parts/Fields/Multiselect.tsx index a26ae641..3092ac04 100644 --- a/client/components/Parts/Fields/Multiselect.tsx +++ b/client/components/Parts/Fields/Multiselect.tsx @@ -1,6 +1,7 @@ import noop from 'lodash/noop' import MenuItem from '@mui/material/MenuItem' import { StyledTextField } from './Input' +import InputLabel from '@mui/material/InputLabel' // TODO: rework? merge with SelectField? // TODO: handle different value types properly (string/number/etc) @@ -22,12 +23,17 @@ export default function MultiselectField(props: MultiselectFieldProps) { ) return ( + <div> + <InputLabel + shrink={false}> + {props.label} + </InputLabel> <StyledTextField select fullWidth - label={props.label} margin="normal" value={props.value} + style={{ marginTop: '5px' }} size={props.size || 'small'} disabled={props.options.length < 2} onChange={(ev) => props.onChange((ev.target as any).value)} @@ -40,5 +46,6 @@ export default function MultiselectField(props: MultiselectFieldProps) { </MenuItem> ))} </StyledTextField> + </div> ) } diff --git a/client/components/Parts/Fields/Select.tsx b/client/components/Parts/Fields/Select.tsx index 5bacc78e..d49090a0 100644 --- a/client/components/Parts/Fields/Select.tsx +++ b/client/components/Parts/Fields/Select.tsx @@ -1,6 +1,7 @@ import MenuItem from '@mui/material/MenuItem' import { noop } from 'lodash' import { StyledTextField } from './Input' +import InputLabel from '@mui/material/InputLabel' // TODO: rework? merge with SelectField? // TODO: handle different value types properly (string/number/etc) @@ -27,25 +28,31 @@ export default function SelectField(props: SelectFieldProps) { typeof option === 'string' ? { label: option || 'select', value: option } : option ) return ( - <StyledTextField - select - fullWidth - label={props.label} - margin={props.margin || 'normal'} - value={props.value} - size={props.size || 'small'} - InputProps={props.InputProps} - disabled={props.disabled} - onChange={(ev) => props.onChange && props.onChange((ev.target as any).value)} - color={props.color} - focused={props.focused} - onFocus={onFocus} - > - {options.map((option) => ( - <MenuItem key={option.label} value={option.value}> - {option.label} - </MenuItem> - ))} - </StyledTextField> + <div> + <InputLabel + shrink={false}> + {props.label} + </InputLabel> + <StyledTextField + select + fullWidth + margin={props.margin || 'normal'} + value={props.value} + size={props.size || 'small'} + style={{ marginTop: '5px' }} + InputProps={props.InputProps} + disabled={props.disabled} + onChange={(ev) => props.onChange && props.onChange((ev.target as any).value)} + color={props.color} + focused={props.focused} + onFocus={onFocus} + > + {options.map((option) => ( + <MenuItem key={option.label} value={option.value}> + {option.label} + </MenuItem> + ))} + </StyledTextField> + </div> ) } diff --git a/client/components/Parts/Fields/YesNo.tsx b/client/components/Parts/Fields/YesNo.tsx index 5c251bb7..e43e2a3c 100644 --- a/client/components/Parts/Fields/YesNo.tsx +++ b/client/components/Parts/Fields/YesNo.tsx @@ -1,6 +1,7 @@ import noop from 'lodash/noop' import MenuItem from '@mui/material/MenuItem' import { StyledTextField } from './Input' +import InputLabel from '@mui/material/InputLabel' interface YesNoFieldProps { label: string @@ -13,18 +14,23 @@ interface YesNoFieldProps { export default function YesNoField(props: YesNoFieldProps) { const onFocus = props.onFocus || noop return ( - <StyledTextField - select - style= {{ maxWidth: '350px' }} - margin="normal" - size={props.size || 'small'} - label={props.label} - value={props.value ? 'yes' : 'no'} - onChange={(ev) => props.onChange(ev.target.value === 'yes')} - onFocus={onFocus} - > - <MenuItem value={'yes'}>Yes</MenuItem> - <MenuItem value={'no'}>No</MenuItem> - </StyledTextField> + <div> + <InputLabel + shrink={false}> + {props.label} + </InputLabel> + <StyledTextField + select + style= {{ maxWidth: '350px', marginTop: '5px' }} + margin="normal" + size={props.size || 'small'} + value={props.value ? 'yes' : 'no'} + onChange={(ev) => props.onChange(ev.target.value === 'yes')} + onFocus={onFocus} + > + <MenuItem value={'yes'}>Yes</MenuItem> + <MenuItem value={'no'}>No</MenuItem> + </StyledTextField> + </div> ) } diff --git a/client/components/Parts/Tabs/Dialog.tsx b/client/components/Parts/Tabs/SimpleTabs.tsx similarity index 50% rename from client/components/Parts/Tabs/Dialog.tsx rename to client/components/Parts/Tabs/SimpleTabs.tsx index 79b39142..67735aa0 100644 --- a/client/components/Parts/Tabs/Dialog.tsx +++ b/client/components/Parts/Tabs/SimpleTabs.tsx @@ -2,9 +2,23 @@ import Box from '@mui/material/Box' import Tab from '@mui/material/Tab' import Tabs from '@mui/material/Tabs' import * as React from 'react' +import { useTranslation } from 'react-i18next' -export default function DialogTabs(props: any) { - const [currentTabIndex, setCurrentTabIndex] = React.useState(0) +interface SimpleTabsProps { + selectedIndex?: number + disabled? : boolean + onChange?: (arg0: number) => void + centered?: boolean, + labels: Array<string> + children: React.ReactNode + orientation?: 'horizontal' | 'vertical' +} + +export default function SimpleTabs(props: SimpleTabsProps) { + + const { orientation, selectedIndex, disabled, onChange, centered, labels, children} = props + + const [currentTabIndex, setCurrentTabIndex] = React.useState(selectedIndex || 0) function a11yProps(index: number) { return { @@ -17,27 +31,31 @@ export default function DialogTabs(props: any) { // no unused variable error in the typescript check // @ts-ignore const handleChange = (event: React.SyntheticEvent, newValue: number) => { - if (!props.disabled) { + if (!disabled) { setCurrentTabIndex(newValue) - props.onChange?.() + onChange?.(newValue) } + } + const { t } = useTranslation() return ( - <Box> - <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> + <Box sx={{ display: orientation === 'vertical' ? 'flex' : '' }}> + <Box sx={{ borderBottom: orientation === 'vertical' ? 0 : 1, borderColor: 'divider', width: orientation === 'vertical' ? '250px': 'unset', marginTop: orientation === 'vertical' ? '16px' : 0 }}> <Tabs value={currentTabIndex} onChange={handleChange} - aria-label="File Upload Tabs" - centered + aria-label={t('aria-tabs-component')} + centered={centered} + orientation={orientation} sx={{ '& .MuiTabs-indicator': { - backgroundColor: (theme) => theme.palette.OKFNBlue.main, + backgroundColor: (theme) => orientation === 'vertical'? 'unset' : theme.palette.OKFNBlue.main, }, '& .MuiButtonBase-root.MuiTab-root': { color: (theme) => theme.palette.OKFNGray500.main, transition: 'color 0.2s ease-in-out', + alignItems: orientation === 'vertical' ? 'flex-start' : 'center', '&:hover': { opacity: 0.7, }, @@ -47,7 +65,7 @@ export default function DialogTabs(props: any) { }, }} > - {props.labels.map((label: string, index: number) => ( + {labels.map((label: string, index: number) => ( <Tab key={label} label={label} @@ -57,8 +75,8 @@ export default function DialogTabs(props: any) { ))} </Tabs> </Box> - {React.Children.map(props.children, (child, index) => ( - <CustomTabPanel value={currentTabIndex} index={index}> + {React.Children.map(children, (child, index) => ( + <CustomTabPanel value={currentTabIndex} index={index} orientation={orientation}> <Box>{child}</Box> </CustomTabPanel> ))} @@ -70,8 +88,9 @@ function CustomTabPanel(props: { children?: React.ReactNode index: number value: number + orientation?: 'horizontal' | 'vertical' }) { - const { children, value, index, ...other } = props + const { children, value, orientation, index, ...other } = props return ( <div @@ -80,8 +99,9 @@ function CustomTabPanel(props: { id={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} {...other} + style={{ width: orientation === 'vertical' ? '720px' : 'unset' }} > - {value === index && <Box sx={{ p: 3 }}>{children}</Box>} + {value === index && <Box sx={{ p: orientation === 'vertical' ? 0: 3 }}>{children}</Box>} </div> ) } diff --git a/locale/translations/en.json b/locale/translations/en.json index 371a526a..3a789587 100644 --- a/locale/translations/en.json +++ b/locale/translations/en.json @@ -73,6 +73,7 @@ "add-xsl-or-csv-files": "Add one or more Excel or csv files", "link-external-table": "Link to the external table:", "add": "Add", + "aria-tabs-component": "Tabs Component", "enter-or-paste-url": "Enter or paste URL", "error-url-blank": "The URL is blank", "error-url-not-valid": "The URL is not valid", @@ -145,6 +146,7 @@ "AI-assistant": "AI Assistant", "alt-image-folder-dialog": "Image Folder Dialog", "alt-icon-upload-file": "Icon-Upload File", + "aria-file-upload-tabs": "File Upload Tabs", "resource": "Resource", "integrity": "Integrity", "licenses": "Licenses", @@ -157,8 +159,7 @@ "schema": "Schema", "fields": "Fields", "foreign-keys": "Foreign Keys", - "foreign keys": "Foreign Keys", - "add-foreign key": "Add foreign key", + "add-foreign-key": "Add foreign key", "search": "Search", "name": "Name", "name-not-valid": "Name is not valid.", @@ -228,7 +229,10 @@ "generating-response": "AI assistant is generating the response.", "prompt-required": "Prompt is required", "api-required": "API key is required", + "default": "Default", "AI-assistant-find-your-key": "Click <link1>here</link1> to learn how to find your key. You can also check OpenAI terms and policies <link2>here</link2>.", + "constraints": "Constraints", + "nothing-to-see": "Nothing to see yet", "help-resource": { "path": "resource", "title": "Resource", diff --git a/locale/translations/es.json b/locale/translations/es.json index d7fe33dd..ad6ae5d2 100644 --- a/locale/translations/es.json +++ b/locale/translations/es.json @@ -157,8 +157,7 @@ "schema": "Esquema", "fields": "Campos", "foreign-keys": "Claves foráneas", - "foreign keys": "Claves foráneas", - "add-foreign key": "Agregar clave foránea", + "add-foreign-key": "Agregar clave foránea", "search": "Buscar", "name": "Nombre", "name-not-valid": "El nombre no es válido", diff --git a/locale/translations/fr.json b/locale/translations/fr.json index fc4db1e1..8a711c13 100644 --- a/locale/translations/fr.json +++ b/locale/translations/fr.json @@ -157,8 +157,7 @@ "schema": "Schéma", "fields": "Champs", "foreign-keys": "Clés étrangères", - "foreign keys": "Clés étrangères", - "add-foreign key": "Ajouter une clé étrangère", + "add-foreign-key": "Ajouter une clé étrangère", "search": "Rechercher", "name": "Nom", "name-not-valid": "Le nom n'est pas valide.", diff --git a/locale/translations/pt.json b/locale/translations/pt.json index 1a510491..aab8ef62 100644 --- a/locale/translations/pt.json +++ b/locale/translations/pt.json @@ -157,8 +157,7 @@ "schema": "Esquema", "fields": "Campos", "foreign-keys": "Chaves estrangeiras", - "foreign keys": "Chaves estrangeiras", - "add-foreign key": "Adicionar chave estrangeira", + "add-foreign-key": "Adicionar chave estrangeira", "search": "Pesquisar", "name": "Nome", "name-not-valid": "O nome não é válido.",