From 1dd835199c785470903445f1a4acf4ad70100b39 Mon Sep 17 00:00:00 2001 From: Kolea PLESCO Date: Tue, 21 Nov 2023 18:31:07 +0200 Subject: [PATCH] Updates --- .../backend/core/entrypoints/api/main.py | 4 +- .../fields_registry/entrypoints/api/routes.py | 29 +- .../fields_registry/models/field_registry.py | 9 +- .../backend/fields_registry/services/api.py | 1 + .../frontend/src/api/fields-registry/index.js | 17 ++ .../frontend/src/layouts/app/config.js | 19 ++ .../frontend/src/locales/tokens.js | 1 + .../frontend/src/locales/translations/en.js | 1 + .../pages/app/fields-registry/[id]/edit.js | 107 ++++++++ .../pages/app/fields-registry/[id]/view.js | 160 +++++++++++ .../src/pages/app/fields-registry/create.js | 61 +++++ .../src/pages/app/fields-registry/index.js | 182 +++++++++++++ mapping_workbench/frontend/src/paths.js | 11 + .../app/fields-registry/basic-details.js | 102 +++++++ .../sections/app/fields-registry/edit-form.js | 125 +++++++++ .../app/fields-registry/list-search.js | 255 ++++++++++++++++++ .../app/fields-registry/list-table.js | 251 +++++++++++++++++ .../app/ontology-namespace/edit-form.js | 4 +- 18 files changed, 1319 insertions(+), 20 deletions(-) create mode 100644 mapping_workbench/frontend/src/api/fields-registry/index.js create mode 100644 mapping_workbench/frontend/src/pages/app/fields-registry/[id]/edit.js create mode 100644 mapping_workbench/frontend/src/pages/app/fields-registry/[id]/view.js create mode 100644 mapping_workbench/frontend/src/pages/app/fields-registry/create.js create mode 100644 mapping_workbench/frontend/src/pages/app/fields-registry/index.js create mode 100644 mapping_workbench/frontend/src/sections/app/fields-registry/basic-details.js create mode 100644 mapping_workbench/frontend/src/sections/app/fields-registry/edit-form.js create mode 100644 mapping_workbench/frontend/src/sections/app/fields-registry/list-search.js create mode 100644 mapping_workbench/frontend/src/sections/app/fields-registry/list-table.js diff --git a/mapping_workbench/backend/core/entrypoints/api/main.py b/mapping_workbench/backend/core/entrypoints/api/main.py index ad0eaa67e..a6e071be0 100755 --- a/mapping_workbench/backend/core/entrypoints/api/main.py +++ b/mapping_workbench/backend/core/entrypoints/api/main.py @@ -2,6 +2,7 @@ from fastapi.middleware.cors import CORSMiddleware from httpx_oauth.clients.google import GoogleOAuth2 +from mapping_workbench.backend.fields_registry.entrypoints.api import routes as fields_registry from mapping_workbench.backend.conceptual_mapping_rule.entrypoints.api import routes as conceptual_mapping_rule_routes from mapping_workbench.backend.config import settings from mapping_workbench.backend.config.entrypoints.api import routes as config_routes @@ -77,7 +78,8 @@ async def on_startup(): generic_triple_map_fragment_routes.router, config_routes.router, ontology_routes.router, - tasks_routes.router + tasks_routes.router, + fields_registry.router ] for secured_router in secured_routers: diff --git a/mapping_workbench/backend/fields_registry/entrypoints/api/routes.py b/mapping_workbench/backend/fields_registry/entrypoints/api/routes.py index 4475b6dc6..48ec9e6a0 100644 --- a/mapping_workbench/backend/fields_registry/entrypoints/api/routes.py +++ b/mapping_workbench/backend/fields_registry/entrypoints/api/routes.py @@ -7,7 +7,7 @@ from mapping_workbench.backend.core.models.api_response import APIEmptyContentWithIdResponse from mapping_workbench.backend.fields_registry.models.field_registry import FieldsRegistryOut, FieldsRegistryCreateIn, \ - FieldsRegistryUpdateIn, FieldsRegistry, APIListFieldsRegistrysPaginatedResponse + FieldsRegistryUpdateIn, FieldsRegistry, APIListFieldsRegistriesPaginatedResponse from mapping_workbench.backend.fields_registry.models.field_registry_diff import FieldsRegistryDiff from mapping_workbench.backend.fields_registry.services.api import ( list_fields_registries, @@ -38,18 +38,24 @@ "", description=f"List {NAME_FOR_MANY}", name=f"{NAME_FOR_MANY}:list", - response_model=APIListFieldsRegistrysPaginatedResponse + response_model=APIListFieldsRegistriesPaginatedResponse ) async def route_list_fields_registries( - project: PydanticObjectId = None + project: PydanticObjectId = None, + page: int = None, + limit: int = None, + q: str = None ): filters: dict = {} if project: filters['project'] = Project.link_from_id(project) - items: List[FieldsRegistryOut] = await list_fields_registries(filters) - return APIListFieldsRegistrysPaginatedResponse( + if q is not None: + filters['q'] = q + + items, total_count = await list_fields_registries(filters, page, limit) + return APIListFieldsRegistriesPaginatedResponse( items=items, - count=len(items) + count=total_count ) @@ -61,10 +67,10 @@ async def route_list_fields_registries( status_code=status.HTTP_201_CREATED ) async def route_create_fields_registry( - fields_registry_data: FieldsRegistryCreateIn, + data: FieldsRegistryCreateIn, user: User = Depends(current_active_user) ): - return await create_fields_registry(fields_registry_data=fields_registry_data, user=user) + return await create_fields_registry(data=data, user=user) @router.patch( @@ -74,12 +80,11 @@ async def route_create_fields_registry( response_model=FieldsRegistryOut ) async def route_update_fields_registry( - id: PydanticObjectId, - fields_registry_data: FieldsRegistryUpdateIn, + data: FieldsRegistryUpdateIn, + fields_registry: FieldsRegistry = Depends(get_fields_registry), user: User = Depends(current_active_user) ): - await update_fields_registry(id=id, fields_registry_data=fields_registry_data, user=user) - return await get_fields_registry_out(id) + return await update_fields_registry(fields_registry=fields_registry, data=data, user=user) @router.get( diff --git a/mapping_workbench/backend/fields_registry/models/field_registry.py b/mapping_workbench/backend/fields_registry/models/field_registry.py index 1df9bc14b..37957008f 100644 --- a/mapping_workbench/backend/fields_registry/models/field_registry.py +++ b/mapping_workbench/backend/fields_registry/models/field_registry.py @@ -2,6 +2,7 @@ from pydantic import BaseModel from typing import Optional, List +from mapping_workbench.backend.core.models.api_response import APIListPaginatedResponse from mapping_workbench.backend.core.models.base_project_resource_entity import BaseProjectResourceEntity @@ -46,26 +47,26 @@ class Settings(BaseProjectResourceEntity.Settings): name = "fields_registry" -class FieldsRegistryCreateIn: +class FieldsRegistryCreateIn(BaseModel): title: str fields: List[StructuralField] = [] nodes: List[StructuralNode] = [] root_node_id: Optional[str] = None -class FieldsRegistryUpdateIn: +class FieldsRegistryUpdateIn(BaseModel): title: str fields: List[StructuralField] = [] nodes: List[StructuralNode] = [] root_node_id: Optional[str] = None -class FieldsRegistryOut: +class FieldsRegistryOut(BaseModel): title: str fields: List[StructuralField] = [] nodes: List[StructuralNode] = [] root_node_id: Optional[str] = None -class APIListFieldsRegistrysPaginatedResponse: +class APIListFieldsRegistriesPaginatedResponse(APIListPaginatedResponse): items: List[FieldsRegistryOut] diff --git a/mapping_workbench/backend/fields_registry/services/api.py b/mapping_workbench/backend/fields_registry/services/api.py index 0069d2116..4ee25ce19 100644 --- a/mapping_workbench/backend/fields_registry/services/api.py +++ b/mapping_workbench/backend/fields_registry/services/api.py @@ -28,6 +28,7 @@ async def list_fields_registries(filters: dict = None, page: int = None, limit: skip=skip, limit=limit ).to_list() + total_count: int = await FieldsRegistry.find(query_filters).count() return items, total_count diff --git a/mapping_workbench/frontend/src/api/fields-registry/index.js b/mapping_workbench/frontend/src/api/fields-registry/index.js new file mode 100644 index 000000000..3d57afc9a --- /dev/null +++ b/mapping_workbench/frontend/src/api/fields-registry/index.js @@ -0,0 +1,17 @@ +import {SectionApi} from "../section"; + +class FieldsRegistryApi extends SectionApi { + get SECTION_TITLE() { + return "Fields Registry"; + } + + get SECTION_ITEM_TITLE() { + return "Fields Registry"; + } + + constructor() { + super("fields_registry"); + } +} + +export const fieldsRegistryApi = new FieldsRegistryApi(); diff --git a/mapping_workbench/frontend/src/layouts/app/config.js b/mapping_workbench/frontend/src/layouts/app/config.js index 0af81f82f..e97b72bdf 100644 --- a/mapping_workbench/frontend/src/layouts/app/config.js +++ b/mapping_workbench/frontend/src/layouts/app/config.js @@ -227,6 +227,25 @@ export const useSections = () => { path: paths.app.specific_triple_map_fragments.index } ] + }, + { + title: t(tokens.nav.fields_registry), + path: paths.app.fields_registry.index, + icon: ( + + + + ), + items: [ + { + title: t(tokens.nav.list), + path: paths.app.fields_registry.index + }, + { + title: t(tokens.nav.create), + path: paths.app.fields_registry.create + } + ] }); } items.resources.push(sections); diff --git a/mapping_workbench/frontend/src/locales/tokens.js b/mapping_workbench/frontend/src/locales/tokens.js index a90814c30..24a51d997 100644 --- a/mapping_workbench/frontend/src/locales/tokens.js +++ b/mapping_workbench/frontend/src/locales/tokens.js @@ -69,6 +69,7 @@ export const tokens = { triple_map_fragments: 'nav.triple_map_fragments', generic_triple_map_fragments: 'nav.generic_triple_map_fragments', specific_triple_map_fragments: 'nav.specific_triple_map_fragments', + fields_registry: 'nav.fields_registry', admin: 'nav.admin', users: 'nav.users', diff --git a/mapping_workbench/frontend/src/locales/translations/en.js b/mapping_workbench/frontend/src/locales/translations/en.js index 32996a2a5..b5ff7cdee 100644 --- a/mapping_workbench/frontend/src/locales/translations/en.js +++ b/mapping_workbench/frontend/src/locales/translations/en.js @@ -69,6 +69,7 @@ export const en = { [tokens.nav.triple_map_fragments]: 'Triple Maps', [tokens.nav.specific_triple_map_fragments]: 'Specific Triple Maps', [tokens.nav.generic_triple_map_fragments]: 'Generic Triple Maps', + [tokens.nav.fields_registry]: 'Fields Registry', [tokens.nav.terms_validator]: 'Terms Validator', [tokens.nav.tasks]: 'Tasks', [tokens.nav.generate_cm_assertions_queries]: 'Generate CM Assertions Queries', diff --git a/mapping_workbench/frontend/src/pages/app/fields-registry/[id]/edit.js b/mapping_workbench/frontend/src/pages/app/fields-registry/[id]/edit.js new file mode 100644 index 000000000..daa2e1f0b --- /dev/null +++ b/mapping_workbench/frontend/src/pages/app/fields-registry/[id]/edit.js @@ -0,0 +1,107 @@ +import ArrowLeftIcon from '@untitled-ui/icons-react/build/esm/ArrowLeft'; +import Chip from '@mui/material/Chip'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import SvgIcon from '@mui/material/SvgIcon'; +import Typography from '@mui/material/Typography'; + +import {fieldsRegistryApi as sectionApi} from 'src/api/fields-registry'; +import {RouterLink} from 'src/components/router-link'; +import {Seo} from 'src/components/seo'; +import {usePageView} from 'src/hooks/use-page-view'; +import {Layout as AppLayout} from 'src/layouts/app'; +import {paths} from 'src/paths'; +import {EditForm} from 'src/sections/app/fields-registry/edit-form'; +import {ForItemEditForm} from "src/contexts/app/section/for-item-form"; +import {useItem} from "src/contexts/app/section/for-item-data-state"; +import {useRouter} from "src/hooks/use-router"; + + +const Page = () => { + const router = useRouter(); + if (!router.isReady) return; + + const {id} = router.query; + + if (!id) { + return; + } + + const formState = useItem(sectionApi, id); + const item = formState.item; + + usePageView(); + + if (!item) { + return; + } + + return ( + <> + + + +
+ + + + + + {sectionApi.SECTION_TITLE} + + +
+ + + + + {item.prefix} + + + + + + + +
+ +
+ + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/mapping_workbench/frontend/src/pages/app/fields-registry/[id]/view.js b/mapping_workbench/frontend/src/pages/app/fields-registry/[id]/view.js new file mode 100644 index 000000000..e4586f6ba --- /dev/null +++ b/mapping_workbench/frontend/src/pages/app/fields-registry/[id]/view.js @@ -0,0 +1,160 @@ +import {useCallback, useState} from 'react'; +import ArrowLeftIcon from '@untitled-ui/icons-react/build/esm/ArrowLeft'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import Container from '@mui/material/Container'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Unstable_Grid2'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import SvgIcon from '@mui/material/SvgIcon'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; + +import {fieldsRegistryApi as sectionApi} from 'src/api/fields-registry'; +import {RouterLink} from 'src/components/router-link'; +import {Seo} from 'src/components/seo'; +import {usePageView} from 'src/hooks/use-page-view'; +import {Layout as AppLayout} from 'src/layouts/app'; +import {paths} from 'src/paths'; +import {BasicDetails} from 'src/sections/app/fields-registry/basic-details'; +import {useRouter} from "src/hooks/use-router"; +import {useItem} from "src/contexts/app/section/for-item-data-state"; + +const tabs = [ + {label: 'Details', value: 'details'} +]; + +const Page = () => { + const router = useRouter(); + if (!router.isReady) return; + + const {id} = router.query; + + if (!id) { + return; + } + + const formState = useItem(sectionApi, id); + const item = formState.item; + + usePageView(); + const [currentTab, setCurrentTab] = useState('details'); + + const handleTabsChange = useCallback((event, value) => { + setCurrentTab(value); + }, []); + + if (!item) { + return; + } + + return ( + <> + + + +
+ + + + + + {sectionApi.SECTION_TITLE} + + +
+ + + + + {item.prefix} + + + + + + + +
+ + {tabs.map((tab) => ( + + ))} + + +
+
+ {currentTab === 'details' && ( +
+ + + + + +
+ )} +
+ + ) +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/mapping_workbench/frontend/src/pages/app/fields-registry/create.js b/mapping_workbench/frontend/src/pages/app/fields-registry/create.js new file mode 100644 index 000000000..4612e6aa3 --- /dev/null +++ b/mapping_workbench/frontend/src/pages/app/fields-registry/create.js @@ -0,0 +1,61 @@ +import ArrowLeftIcon from '@untitled-ui/icons-react/build/esm/ArrowLeft'; +import Box from '@mui/material/Box'; +import Container from '@mui/material/Container'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import SvgIcon from '@mui/material/SvgIcon'; +import Typography from '@mui/material/Typography'; + +import {fieldsRegistryApi as sectionApi} from 'src/api/fields-registry'; +import {RouterLink} from 'src/components/router-link'; +import {Seo} from 'src/components/seo'; +import {usePageView} from 'src/hooks/use-page-view'; +import {Layout as AppLayout} from 'src/layouts/app'; +import {paths} from 'src/paths'; +import {EditForm} from 'src/sections/app/fields-registry/edit-form'; +import {ForItemCreateForm} from "src/contexts/app/section/for-item-form"; + + +const Page = () => { + let item = {}; + + usePageView(); + + return ( + <> + + + +
+ + + + + + {sectionApi.SECTION_TITLE} + + +
+
+ +
+ + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/mapping_workbench/frontend/src/pages/app/fields-registry/index.js b/mapping_workbench/frontend/src/pages/app/fields-registry/index.js new file mode 100644 index 000000000..122d7a45a --- /dev/null +++ b/mapping_workbench/frontend/src/pages/app/fields-registry/index.js @@ -0,0 +1,182 @@ +import {useCallback, useEffect, useState} from 'react'; +import PlusIcon from '@untitled-ui/icons-react/build/esm/Plus'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import SvgIcon from '@mui/material/SvgIcon'; +import Typography from '@mui/material/Typography'; + +import {fieldsRegistryApi as sectionApi} from 'src/api/fields-registry'; +import {BreadcrumbsSeparator} from 'src/components/breadcrumbs-separator'; +import {RouterLink} from 'src/components/router-link'; +import {Seo} from 'src/components/seo'; +import {useMounted} from 'src/hooks/use-mounted'; +import {usePageView} from 'src/hooks/use-page-view'; +import {Layout as AppLayout} from 'src/layouts/app'; +import {paths} from 'src/paths'; +import {ListSearch} from "../../../sections/app/fields-registry/list-search"; +import {ListTable} from "../../../sections/app/fields-registry/list-table"; + +const useItemsSearch = () => { + const [state, setState] = useState({ + filters: { + name: undefined, + category: [], + status: [], + inStock: undefined + }, + page: sectionApi.DEFAULT_PAGE, + rowsPerPage: sectionApi.DEFAULT_ROWS_PER_PAGE + }); + + const handleFiltersChange = useCallback((filters) => { + setState((prevState) => ({ + ...prevState, + filters, + page: 0 + })); + }, []); + + const handlePageChange = useCallback((event, page) => { + setState((prevState) => ({ + ...prevState, + page + })); + }, []); + + const handleRowsPerPageChange = useCallback((event) => { + setState((prevState) => ({ + ...prevState, + rowsPerPage: parseInt(event.target.value, 10) + })); + }, []); + + return { + handleFiltersChange, + handlePageChange, + handleRowsPerPageChange, + state + }; +}; + +const useItemsStore = (searchState) => { + const isMounted = useMounted(); + const [state, setState] = useState({ + items: [], + itemsCount: 0 + }); + + const handleItemsGet = useCallback(async () => { + try { + const response = await sectionApi.getItems(searchState); + if (isMounted()) { + setState({ + items: response.items, + itemsCount: response.count + }); + } + } catch (err) { + console.error(err); + } + }, [searchState, isMounted]); + + useEffect(() => { + handleItemsGet(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [searchState]); + + return { + ...state + }; +}; + +const Page = () => { + const itemsSearch = useItemsSearch(); + const itemsStore = useItemsStore(itemsSearch.state); + + usePageView(); + + return ( + <> + + + + + + {sectionApi.SECTION_TITLE} + + }> + + App + + + {sectionApi.SECTION_TITLE} + + + List + + + + + + + + + + + + + + ) +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/mapping_workbench/frontend/src/paths.js b/mapping_workbench/frontend/src/paths.js index 218cd631a..ecd49acf4 100644 --- a/mapping_workbench/frontend/src/paths.js +++ b/mapping_workbench/frontend/src/paths.js @@ -139,6 +139,12 @@ export const paths = { edit: '/app/ontology-terms/[id]/edit', view: '/app/ontology-terms/[id]/view' }, + fields_registry: { + index: '/app/fields-registry', + create: '/app/fields-registry/create', + edit: '/app/fields-registry/[id]/edit', + view: '/app/fields-registry/[id]/view' + }, tasks: { index: '/app/tasks', terms_validator: '/app/tasks/terms_validator', @@ -244,6 +250,11 @@ export const apiPaths = { me: '/users/me' }, + fields_registry: { + items: '/fields_registry', + item: '/fields_registry/:id' + }, + tasks: { terms_validator: '/tasks/terms_validator', generate_cm_assertions_queries: '/tasks/generate_cm_assertions_queries', diff --git a/mapping_workbench/frontend/src/sections/app/fields-registry/basic-details.js b/mapping_workbench/frontend/src/sections/app/fields-registry/basic-details.js new file mode 100644 index 000000000..c835e4a3b --- /dev/null +++ b/mapping_workbench/frontend/src/sections/app/fields-registry/basic-details.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import {ForListItemAction} from 'src/contexts/app/section/for-list-item-action'; +import {ontologyNamespacesApi as sectionApi} from 'src/api/ontology-namespaces'; + +import {PropertyList} from 'src/components/property-list'; +import {PropertyListItem} from 'src/components/property-list-item'; +import Button from "@mui/material/Button"; +import CardActions from "@mui/material/CardActions"; +import {useCallback} from "react"; +import {paths} from "../../../paths"; +import {useRouter} from "../../../hooks/use-router"; +import Switch from "@mui/material/Switch"; +import Stack from "@mui/material/Stack"; + +export const BasicDetails = (props) => { + const {id, prefix, uri, is_syncable, ...other} = props; + + const router = useRouter(); + const itemctx = new ForListItemAction(id, sectionApi); + + const handleEditAction = useCallback(async () => { + router.push({ + //pathname: paths.app[item.api.section].edit, + pathname: paths.app.ontology_namespaces.edit, query: {id: id} + }); + + }, [router]); + + const handleDeleteAction = useCallback(async () => { + const response = await itemctx.api.deleteItem(id); + + router.push({ + pathname: paths.app.ontology_namespaces.index + }); + //window.location.reload(); + }, [router, itemctx]); + + return ( + <> + + + + + + } + /> + + + + + + + + + + + + ) + ; +}; + +BasicDetails.propTypes = { + id: PropTypes.string.isRequired, prefix: PropTypes.string, uri: PropTypes.string, is_syncable: PropTypes.string +}; diff --git a/mapping_workbench/frontend/src/sections/app/fields-registry/edit-form.js b/mapping_workbench/frontend/src/sections/app/fields-registry/edit-form.js new file mode 100644 index 000000000..7aa9062ed --- /dev/null +++ b/mapping_workbench/frontend/src/sections/app/fields-registry/edit-form.js @@ -0,0 +1,125 @@ +import PropTypes from 'prop-types'; +import toast from 'react-hot-toast'; +import * as Yup from 'yup'; +import {useFormik} from 'formik'; +import Button from '@mui/material/Button'; +import {MenuItem} from '@mui/material'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Grid from '@mui/material/Unstable_Grid2'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; + +import {RouterLink} from 'src/components/router-link'; +import {paths} from 'src/paths'; +import {useRouter} from 'src/hooks/use-router'; +import {FormTextField} from "../../../components/app/form/text-field"; +import {FormTextArea} from "../../../components/app/form/text-area"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; +import MenuList from "@mui/material/MenuList"; + + +export const EditForm = (props) => { + const {itemctx, ...other} = props; + const router = useRouter(); + const sectionApi = itemctx.api; + const item = itemctx.data; + + let initialValues = { + title: item.title || '' + }; + + const formik = useFormik({ + initialValues: initialValues, + validationSchema: Yup.object({ + title: Yup + .string() + .max(255) + .required('Title is required') + }), + onSubmit: async (values, helpers) => { + try { + let response; + if (itemctx.isNew) { + response = await sectionApi.createItem(values); + } else { + values['id'] = item._id; + response = await sectionApi.updateItem(values); + } + helpers.setStatus({success: true}); + helpers.setSubmitting(false); + toast.success(sectionApi.SECTION_ITEM_TITLE + ' ' + (itemctx.isNew ? "created" : "updated")); + if (response) { + if (itemctx.isNew) { + router.push({ + pathname: paths.app[sectionApi.section].edit, + query: {id: response._id} + }); + } else if (itemctx.isStateable) { + itemctx.setState(response); + } + } + } catch (err) { + console.error(err); + toast.error('Something went wrong!'); + helpers.setStatus({success: false}); + helpers.setErrors({submit: err.message}); + helpers.setSubmitting(false); + } + } + }); + + return ( +
+ + + + + + + + + + + + + + + + + +
+ ); +}; + +EditForm.propTypes = { + itemctx: PropTypes.object.isRequired +}; diff --git a/mapping_workbench/frontend/src/sections/app/fields-registry/list-search.js b/mapping_workbench/frontend/src/sections/app/fields-registry/list-search.js new file mode 100644 index 000000000..7119eed5f --- /dev/null +++ b/mapping_workbench/frontend/src/sections/app/fields-registry/list-search.js @@ -0,0 +1,255 @@ +import {useCallback, useMemo, useRef, useState} from 'react'; +import PropTypes from 'prop-types'; +import SearchMdIcon from '@untitled-ui/icons-react/build/esm/SearchMd'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Input from '@mui/material/Input'; +import Stack from '@mui/material/Stack'; +import SvgIcon from '@mui/material/SvgIcon'; +import Typography from '@mui/material/Typography'; + +import {MultiSelect} from 'src/components/multi-select'; +import {useUpdateEffect} from 'src/hooks/use-update-effect'; + + +const statusOptions = [ + { + label: 'Published', + value: 'published' + }, + { + label: 'Draft', + value: 'draft' + } +]; + +export const ListSearch = (props) => { + const {onFiltersChange, ...other} = props; + const queryRef = useRef(null); + const [chips, setChips] = useState([]); + + const handleChipsUpdate = useCallback(() => { + const filters = { + q: undefined, + status: [], + }; + + chips.forEach((chip) => { + switch (chip.field) { + case 'q': + // There will (or should) be only one chips with field "q" + // so we can set up it directly + filters.q = chip.value; + break; + case 'status': + filters.status.push(chip.value); + break; + default: + break; + } + }); + + onFiltersChange?.(filters); + }, [chips, onFiltersChange]); + + useUpdateEffect(() => { + handleChipsUpdate(); + }, [chips, handleChipsUpdate]); + + const handleChipDelete = useCallback((deletedChip) => { + setChips((prevChips) => { + return prevChips.filter((chip) => { + // There can exist multiple chips for the same field. + // Filter them by value. + + return !(deletedChip.field === chip.field && deletedChip.value === chip.value); + }); + }); + }, []); + + const handleQueryChange = useCallback((event) => { + event.preventDefault(); + + const value = queryRef.current?.value || ''; + + setChips((prevChips) => { + const found = prevChips.find((chip) => chip.field === 'q'); + + if (found && value) { + return prevChips.map((chip) => { + if (chip.field === 'q') { + return { + ...chip, + value: queryRef.current?.value || '' + }; + } + + return chip; + }); + } + + if (found && !value) { + return prevChips.filter((chip) => chip.field !== 'q'); + } + + if (!found && value) { + const chip = { + label: 'Q', + field: 'q', + value + }; + + return [...prevChips, chip]; + } + + return prevChips; + }); + + if (queryRef.current) { + queryRef.current.value = ''; + } + }, []); + + const handleStatusChange = useCallback((values) => { + setChips((prevChips) => { + const valuesFound = []; + + // First cleanup the previous chips + const newChips = prevChips.filter((chip) => { + if (chip.field !== 'status') { + return true; + } + + const found = values.includes(chip.value); + + if (found) { + valuesFound.push(chip.value); + } + + return found; + }); + + // Nothing changed + if (values.length === valuesFound.length) { + return newChips; + } + + values.forEach((value) => { + if (!valuesFound.includes(value)) { + const option = statusOptions.find((option) => option.value === value); + + newChips.push({ + label: 'Status', + field: 'status', + value, + displayValue: option.label + }); + } + }); + + return newChips; + }); + }, []); + + + // We memoize this part to prevent re-render issues + const statusValues = useMemo(() => chips + .filter((chip) => chip.field === 'status') + .map((chip) => chip.value), [chips]); + + const showChips = chips.length > 0; + + return ( +
+ + + + + + + + {showChips + ? ( + + {chips.map((chip, index) => ( + + <> + + {chip.label} + + : + {' '} + {chip.displayValue || chip.value} + + + )} + onDelete={() => handleChipDelete(chip)} + variant="outlined" + /> + ))} + + ) + : ( + + + No filters applied + + + )} + + {false && + + } +
+ ); +}; + +ListSearch.propTypes = { + onFiltersChange: PropTypes.func +}; diff --git a/mapping_workbench/frontend/src/sections/app/fields-registry/list-table.js b/mapping_workbench/frontend/src/sections/app/fields-registry/list-table.js new file mode 100644 index 000000000..0626fa21e --- /dev/null +++ b/mapping_workbench/frontend/src/sections/app/fields-registry/list-table.js @@ -0,0 +1,251 @@ +import {Fragment, useCallback, useState} from 'react'; +import PropTypes from 'prop-types'; +import ChevronDownIcon from '@untitled-ui/icons-react/build/esm/ChevronDown'; +import ChevronRightIcon from '@untitled-ui/icons-react/build/esm/ChevronRight'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import SvgIcon from '@mui/material/SvgIcon'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableSortLabel from '@mui/material/TableSortLabel'; +import TableHead from '@mui/material/TableHead'; +import TablePagination from '@mui/material/TablePagination'; +import TableRow from '@mui/material/TableRow'; +import Typography from '@mui/material/Typography'; + +import {Scrollbar} from 'src/components/scrollbar'; +import {ListItemActions} from 'src/components/app/list/list-item-actions'; + +import {ForListItemAction} from 'src/contexts/app/section/for-list-item-action'; +import Tooltip from "@mui/material/Tooltip"; +import Switch from "@mui/material/Switch"; + + +export const ListTable = (props) => { + const { + count = 0, + items = [], + onPageChange = () => { + }, + onRowsPerPageChange, + page = 0, + rowsPerPage = 0, + sectionApi + } = props; + + //console.log("PROJECT PROPS: ", props); + + const [currentItem, setCurrentItem] = useState(null); + + const handleItemToggle = useCallback((itemId) => { + setCurrentItem((prevItemId) => { + if (prevItemId === itemId) { + return null; + } + + return itemId; + }); + }, []); + + // const handleItemClose = useCallback(() => { + // setCurrentItem(null); + // }, []); + + // const handleItemUpdate = useCallback(() => { + // setCurrentItem(null); + // toast.success('Item updated'); + // }, []); + + // const handleItemDelete = useCallback(() => { + + // toast.error('Item cannot be deleted'); + // }, []); + + return ( +
+ + + + + + + {/* + + + Name + + + */} + + + + Prefix + + + + + URI + + + + + Syncable + + + + + Actions + + + + + {items.map((item) => { + const item_id = item._id; + const isCurrent = item_id === currentItem; + const statusColor = item.status === 'published' ? 'success' : 'info'; + + return ( + + + + handleItemToggle(item_id)}> + + {isCurrent ? : } + + + + {/* + + + + {item.name} + + + + */} + + + {item.prefix} + + + + {item.uri} + + + + + + + + + {isCurrent && ( + + + + + + + + )} + + ); + })} + +
+
+ +
+ ); +}; + +ListTable.propTypes = { + count: PropTypes.number, + items: PropTypes.array, + onPageChange: PropTypes.func, + onRowsPerPageChange: PropTypes.func, + page: PropTypes.number, + rowsPerPage: PropTypes.number +}; diff --git a/mapping_workbench/frontend/src/sections/app/ontology-namespace/edit-form.js b/mapping_workbench/frontend/src/sections/app/ontology-namespace/edit-form.js index 3cc6d7359..07cde76de 100644 --- a/mapping_workbench/frontend/src/sections/app/ontology-namespace/edit-form.js +++ b/mapping_workbench/frontend/src/sections/app/ontology-namespace/edit-form.js @@ -9,13 +9,11 @@ import CardContent from '@mui/material/CardContent'; import CardHeader from '@mui/material/CardHeader'; import Grid from '@mui/material/Unstable_Grid2'; import Stack from '@mui/material/Stack'; -import TextField from '@mui/material/TextField'; import {RouterLink} from 'src/components/router-link'; import {paths} from 'src/paths'; import {useRouter} from 'src/hooks/use-router'; import {FormTextField} from "../../../components/app/form/text-field"; -import {FormTextArea} from "../../../components/app/form/text-area"; import FormControlLabel from "@mui/material/FormControlLabel"; import Switch from "@mui/material/Switch"; import MenuList from "@mui/material/MenuList"; @@ -39,7 +37,7 @@ export const EditForm = (props) => { prefix: Yup .string() .max(255) - .required('Title is required'), + .required('Prefix is required'), uri: Yup.string().max(2048), is_syncable: Yup.boolean() }),