diff --git a/.releaserc.json b/.releaserc.json index 232fc3c9..2f90a021 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -11,8 +11,7 @@ ["@semantic-release/git", { "assets": ["package.json", "CHANGELOG.md"], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - }], - "@semantic-release/github" + }] ], "branches": [ { "name": "master" } diff --git a/README.md b/README.md index 2c02024a..1d6e9d82 100755 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ yarn start --scope @marapp/earth-map Initialize the dependencies and link any cross-dependencies between the modules. ``` -yarn install && yarn bootstrap +yarn bootstrap ``` Bootstrap the packages in the current Lerna repo. Installs all of their dependencies and links any cross-dependencies. diff --git a/package.json b/package.json index 6ce90bbc..383bb96a 100644 --- a/package.json +++ b/package.json @@ -19,24 +19,18 @@ "@natgeosociety/marapp-uikit": "^1.0.11", "assert": "^2.0.0", "axios": "^0.18.0", - "axios-cache-adapter": "^2.3.0", - "basic-auth": "^2.0.1", "classnames": "^2.2.6", "d3-ease": "^1.0.5", "d3-geo": "^1.11.3", "deck.gl": "7.3.6", "delay": "^4.1.0", - "flat": "^5.0.2", "iframe-resizer": "^3.6.6", + "flat": "^5.0.2", "json2csv": "^5.0.3", - "jsona": "^1.4.0", "layer-manager": "^3.0.6", - "localforage": "^1.7.3", - "localforage-memoryStorageDriver": "^0.9.2", "lodash": "^4.17.11", "luma.gl": "7.3.2", "moment": "^2.24.0", - "ngs-app-config": "git+ssh://git@github.com:natgeosociety/ngs-app-config.git#v1.0.16", "node-sass": "^4.11.0", "photo-sphere-viewer": "3.5.1", "prop-types": "^15.6.2", @@ -47,6 +41,7 @@ "react-dom": "^16.8.6", "react-fast-compare": "^2.0.4", "react-iframe-resizer-super": "^0.2.2", + "react-hook-form": "^4.8.2", "react-map-gl": "^5.0.7", "react-markdown": "^4.0.8", "react-modal": "^3.8.1", @@ -75,8 +70,7 @@ "vega-lib": "^4.4.0", "viewport-mercator-project": "6.1.1", "vizzuality-components": "^3.0.1", - "vizzuality-redux-tools": "^4.0.2", - "wri-json-api-serializer": "^1.0.1" + "vizzuality-redux-tools": "^4.0.2" }, "devDependencies": { "@babel/cli": "^7.2.3", diff --git a/packages/earth-admin/README.md b/packages/earth-admin/README.md index bbdfa698..cc985b5e 100644 --- a/packages/earth-admin/README.md +++ b/packages/earth-admin/README.md @@ -47,7 +47,7 @@ $marapp-primary-font: 'Primary font'; $marapp-secondary-font: 'Secondary font'; $marapp-icon-font: 'icon-font'; -$marapp-color-sucess: #hex; +$marapp-color-success: #hex; $marapp-color-error: #hex; $marapp-primary-color: #hex; diff --git a/packages/earth-admin/gatsby-browser.js b/packages/earth-admin/gatsby-browser.js index 33e1fbe3..a8456390 100644 --- a/packages/earth-admin/gatsby-browser.js +++ b/packages/earth-admin/gatsby-browser.js @@ -32,7 +32,7 @@ export const wrapRootElement = ({ element }) => ( onSuccessHook={onSuccessHook} onFailureHook={onFailureHook} useRefreshTokens={true} - cacheLocation={'localstorage'} + cacheLocation={'memory'} > {element} diff --git a/packages/earth-admin/package.json b/packages/earth-admin/package.json index 2c156783..3a0b90d6 100644 --- a/packages/earth-admin/package.json +++ b/packages/earth-admin/package.json @@ -33,12 +33,12 @@ "gatsby-plugin-typescript": "^2.1.15", "jshint": "^2.11.0", "lodash": "^4.17.15", + "query-string": "^6.13.5", "query-string-encode": "^1.0.1", "react": "^16.11.0", "react-codemirror2": "^7.1.0", "react-dom": "^16.11.0", "react-helmet": "^6.1.0", - "react-hook-form": "^4.8.2", "swr": "^0.3.2", "ts-jsonapi": "^2.1.2", "url-join": "^4.0.1" diff --git a/packages/earth-admin/src/auth/auth0.tsx b/packages/earth-admin/src/auth/auth0.tsx index eddd0236..c4ead1f9 100644 --- a/packages/earth-admin/src/auth/auth0.tsx +++ b/packages/earth-admin/src/auth/auth0.tsx @@ -115,7 +115,7 @@ export const Auth0Provider = ({ if (isAuthenticated) { const accessToken = await auth0FromHook.getTokenSilently(); - onSuccessHook({ token: accessToken }); + onSuccessHook({ accessToken, authClient: auth0FromHook }); const idToken = await auth0FromHook.getUser(); @@ -180,7 +180,7 @@ export const Auth0Provider = ({ * grant and the refresh token from the cache. * @param options */ - const getToken = useCallback( + const getAccessToken = useCallback( (options: GetTokenSilentlyOptions = {} as any) => { return client.getTokenSilently && client.getTokenSilently(options); }, @@ -213,7 +213,7 @@ export const Auth0Provider = ({ logout, userData, getUser, - getToken, + getAccessToken, setIsLoading, setupUserOrg: setSelectedGroup, getPermissions, diff --git a/packages/earth-admin/src/auth/hooks.ts b/packages/earth-admin/src/auth/hooks.ts index 30102b38..b970a59d 100644 --- a/packages/earth-admin/src/auth/hooks.ts +++ b/packages/earth-admin/src/auth/hooks.ts @@ -17,8 +17,15 @@ specific language governing permissions and limitations under the License. */ +import { Auth0Client } from '@auth0/auth0-spa-js'; import axios from 'axios'; +import { + reqNoopInterceptor, + resErrorInterceptor, + resSuccessInterceptor, +} from '@marapp/earth-shared'; + import { routeToPage } from '../utils'; /** @@ -34,10 +41,16 @@ export const onRedirectCallback = (targetUrl: string) => { * Configure behaviour in case of successful login. * @param params */ -export const onSuccessHook = (params: any = {}) => { - const token = params.token; - if (token) { - axios.defaults.headers.common.Authorization = `Bearer ${token}`; +export const onSuccessHook = (params: { accessToken?: string; authClient?: Auth0Client } = {}) => { + if (params.accessToken) { + axios.defaults.headers.common.Authorization = `Bearer ${params.accessToken}`; + } + if (params.authClient) { + axios.interceptors.request.use(reqNoopInterceptor()); + axios.interceptors.response.use( + resSuccessInterceptor(), + resErrorInterceptor({ authClient: params.authClient }) + ); } }; diff --git a/packages/earth-admin/src/auth/model.ts b/packages/earth-admin/src/auth/model.ts index 6ad5aa63..84a46486 100644 --- a/packages/earth-admin/src/auth/model.ts +++ b/packages/earth-admin/src/auth/model.ts @@ -37,7 +37,7 @@ export interface Auth0 { logout?(o?: LogoutOptions): void; login?(o?: BaseLoginOptions): void; getUser?(o?: GetUserOptions): void; - getToken?(o?: GetTokenWithPopupOptions): void; + getAccessToken?(o?: GetTokenWithPopupOptions): Promise; setupUserOrg?(o?: string): void; setIsLoading(boolean): void; getPermissions?(type: string[]): boolean; diff --git a/packages/earth-admin/src/components/data-listing/DataListing.tsx b/packages/earth-admin/src/components/data-listing/DataListing.tsx index 6e4e0ee0..4629e40c 100644 --- a/packages/earth-admin/src/components/data-listing/DataListing.tsx +++ b/packages/earth-admin/src/components/data-listing/DataListing.tsx @@ -64,19 +64,6 @@ const DataListing = (props: DataListingProps) => { } = props; const { selectedGroup } = useAuth0(); - const renderItem = (index) => { - const item = data[index]; - return ( -
- {React.createElement(childComponent, { - item, - categoryUrl, - selectedItem, - })} -
- ); - }; - return ( <> {searchValueAction && ( @@ -101,7 +88,18 @@ const DataListing = (props: DataListingProps) => { awaitMore={awaitMore} pageSize={pageSize} itemCount={data.length} - renderItem={renderItem} + renderItem={(index) => { + const item = data[index]; + return ( +
+ {React.createElement(childComponent, { + item, + categoryUrl, + selected: selectedItem === item.id, + })} +
+ ); + }} onIntersection={onIntersection} {...otherProps} /> diff --git a/packages/earth-admin/src/components/data-listing/auth0-list-item/Auth0ListItem.tsx b/packages/earth-admin/src/components/data-listing/auth0-list-item/Auth0ListItem.tsx deleted file mode 100644 index 3214a57f..00000000 --- a/packages/earth-admin/src/components/data-listing/auth0-list-item/Auth0ListItem.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -import React from 'react'; - -import { LinkWithOrg } from '../../link-with-org'; - -interface Auth0ListProps { - categoryUrl: string; - item: { id: string; name: string; email: string; groups: Array<{ name: string }> }; -} - -const Auth0ListItem = (props: Auth0ListProps) => { - const { categoryUrl, item } = props; - - return ( - -

{item.name}

- {!!item.groups && ( - - {item.groups.map((group) => group.name).join(', ')} - - )} -
- ); -}; - -export default Auth0ListItem; diff --git a/packages/earth-admin/src/components/data-listing/default-list-item/DefaultListItem.tsx b/packages/earth-admin/src/components/data-listing/default-list-item/DefaultListItem.tsx index 893419a3..aa26c981 100644 --- a/packages/earth-admin/src/components/data-listing/default-list-item/DefaultListItem.tsx +++ b/packages/earth-admin/src/components/data-listing/default-list-item/DefaultListItem.tsx @@ -25,11 +25,11 @@ import { LinkWithOrg } from '@app/components/link-with-org'; interface DataListProps { categoryUrl: string; item: { name: string; id: string; slug: string }; - selectedItem: string; + selected: boolean; } const DefaultListItem = (props: DataListProps) => { - const { categoryUrl, item, selectedItem } = props; + const { categoryUrl, item, selected } = props; return ( { className={classnames( 'marapp-qa-listitem ng-data-link ng-display-block ng-padding-medium-horizontal ng-padding-small-vertical', { - 'ng-data-link-selected': selectedItem === item.id, + 'ng-data-link-selected': selected, } )} > diff --git a/packages/earth-admin/src/components/data-listing/index.tsx b/packages/earth-admin/src/components/data-listing/index.tsx index d69c2fc0..3e5f8bd2 100644 --- a/packages/earth-admin/src/components/data-listing/index.tsx +++ b/packages/earth-admin/src/components/data-listing/index.tsx @@ -18,5 +18,4 @@ */ export { DefaultListItem } from './default-list-item'; -export { Auth0ListItem } from './auth0-list-item'; export { default as DataListing } from './DataListing'; diff --git a/packages/earth-admin/src/components/modals/delete-confirmation/DeleteConfirmation.tsx b/packages/earth-admin/src/components/modals/delete-confirmation/DeleteConfirmation.tsx index e1c388c0..444a015d 100644 --- a/packages/earth-admin/src/components/modals/delete-confirmation/DeleteConfirmation.tsx +++ b/packages/earth-admin/src/components/modals/delete-confirmation/DeleteConfirmation.tsx @@ -24,14 +24,12 @@ import React from 'react'; import { Modal } from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; -import { - deleteDashboards, - deleteLayer, - deleteOrganization, - deletePlace, - deleteUser, - deleteWidgets, -} from '@app/services'; +import DashboardsService from '@app/services/dashboards'; +import LayersService from '@app/services/layers'; +import OrganizationsService from '@app/services/organizations'; +import PlacesService from '@app/services/places'; +import UsersService from '@app/services/users'; +import WidgetsService from '@app/services/widgets'; interface IProps { id: string; @@ -64,27 +62,27 @@ export const DeleteConfirmation = (props: IProps) => { try { switch (navigateRoute) { case 'dashboards': { - await deleteDashboards(id, selectedGroup); + await DashboardsService.deleteDashboards(id, { group: selectedGroup }); break; } case 'layers': { - await deleteLayer(id, selectedGroup); + await LayersService.deleteLayer(id, { group: selectedGroup }); break; } case 'places': { - await deletePlace(id, selectedGroup); + await PlacesService.deletePlace(id, { group: selectedGroup }); break; } case 'widgets': { - await deleteWidgets(id, selectedGroup); + await WidgetsService.deleteWidgets(id, { group: selectedGroup }); break; } case 'users': { - await deleteUser(id, selectedGroup); + await UsersService.deleteUser(id, { group: selectedGroup }); break; } case 'organizations': { - await deleteOrganization(id); + await OrganizationsService.deleteOrganization(id); break; } } diff --git a/packages/earth-admin/src/components/places/metrics/Metrics.tsx b/packages/earth-admin/src/components/places/metrics/Metrics.tsx index bac92912..7360ade5 100644 --- a/packages/earth-admin/src/components/places/metrics/Metrics.tsx +++ b/packages/earth-admin/src/components/places/metrics/Metrics.tsx @@ -22,10 +22,10 @@ import React, { useContext, useState } from 'react'; import { AuthzGuards } from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; -import { calculateForPlace } from '@app/services'; +import { PlaceMetricsProps } from '@app/pages-client/places/model'; +import MetricsService from '@app/services/metrics'; import { Auth0Context } from '@app/utils/contexts'; -import { PlaceMetricsProps } from '../../../pages-client/places/model'; import './styles.scss'; export default function Metrics(props: PlaceMetricsProps) { @@ -47,7 +47,7 @@ export default function Metrics(props: PlaceMetricsProps) { e.stopPropagation(); try { handleServerErrors(false); - await calculateForPlace(placeID, metricId, selectedGroup); + await MetricsService.calculateForPlace(placeID, metricId, { group: selectedGroup }); setLoading(true); } catch (error) { handleServerErrors(error.data.errors); diff --git a/packages/earth-admin/src/components/search-input/SearchInput.tsx b/packages/earth-admin/src/components/search-input/SearchInput.tsx index da49c5b6..80026cc8 100644 --- a/packages/earth-admin/src/components/search-input/SearchInput.tsx +++ b/packages/earth-admin/src/components/search-input/SearchInput.tsx @@ -18,11 +18,14 @@ */ import { remove } from 'lodash'; +import { merge } from 'lodash/fp'; import React, { useContext, useEffect, useState } from 'react'; import { LinkWithOrg } from '@app/components/link-with-org'; -import { getAllLayers, getAllWidgets, getAvailableGroups } from '@app/services'; -import { encodeQueryToURL, normalizeGroupName } from '@app/utils'; +import LayersService from '@app/services/layers'; +import UsersService from '@app/services/users'; +import WidgetsService from '@app/services/widgets'; +import { normalizeGroupName } from '@app/utils'; import { Auth0Context } from '@app/utils/contexts'; interface SearchInputProps { @@ -76,19 +79,14 @@ export default function SearchInput(props: SearchInputProps) { clearTimeout(timer); const initAvailableOptions = async () => { - const encodedOptionsQuery = encodeQueryToURL(baseUrl, { - ...OPTIONS_QUERY, - ...{ - search: searchValue, - page: { size: resultsLimit }, - }, - }); - const res: any = + const query = merge(OPTIONS_QUERY, { search: searchValue, page: { size: resultsLimit } }); + + const res = optionType === 'layers' - ? await getAllLayers(encodedOptionsQuery) + ? await LayersService.getAllLayers(query) : optionType === 'userGroups' - ? processGroupNames(await getAvailableGroups(selectedGroup)) - : await getAllWidgets(encodedOptionsQuery); + ? processGroupNames(await UsersService.getAvailableGroups({ group: selectedGroup })) + : await WidgetsService.getAllWidgets(query); setAvailableOptions(res.data); }; diff --git a/packages/earth-admin/src/components/users/user-details/UserDetails.tsx b/packages/earth-admin/src/components/users/user-details/UserDetails.tsx index 3f36c8c5..e10d8010 100644 --- a/packages/earth-admin/src/components/users/user-details/UserDetails.tsx +++ b/packages/earth-admin/src/components/users/user-details/UserDetails.tsx @@ -23,9 +23,9 @@ import React, { useState } from 'react'; import { AuthzGuards, ErrorMessages } from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; -import { normalizeGroupName } from '@app/utils'; import { LinkWithOrg } from '@app/components/link-with-org'; import { DeleteConfirmation } from '@app/components/modals/delete-confirmation'; +import { normalizeGroupName } from '@app/utils'; import { UserProps } from '../model'; diff --git a/packages/earth-admin/src/components/users/user-edit/UserEdit.tsx b/packages/earth-admin/src/components/users/user-edit/UserEdit.tsx index 9e475334..4408d376 100644 --- a/packages/earth-admin/src/components/users/user-edit/UserEdit.tsx +++ b/packages/earth-admin/src/components/users/user-edit/UserEdit.tsx @@ -26,7 +26,7 @@ import { ErrorMessages } from '@marapp/earth-shared'; import { LinkWithOrg } from '@app/components/link-with-org'; import { SearchInput } from '@app/components/search-input'; -import { handleUserForm } from '@app/services/users'; +import UsersService from '@app/services/users'; import { Auth0Context } from '@app/utils/contexts'; import { UserEditProps } from '../model'; @@ -49,7 +49,9 @@ export default function UserEdit(props: UserEditProps) { const formData = getValues(); try { - await handleUserForm(false, { groups: selectedGroups }, id || formData.email, selectedGroup); + await UsersService.handleUserForm(false, { groups: selectedGroups }, id || formData.email, { + group: selectedGroup, + }); onDataChange(); await navigate(`/${selectedGroup}/users`); } catch (error) { diff --git a/packages/earth-admin/src/layouts/Content.tsx b/packages/earth-admin/src/layouts/Content.tsx index 075c9ad2..a215eb70 100644 --- a/packages/earth-admin/src/layouts/Content.tsx +++ b/packages/earth-admin/src/layouts/Content.tsx @@ -24,10 +24,11 @@ import { favicon, Spinner, UserMenu } from '@marapp/earth-shared'; import { Card } from '@app/components/card'; import { LinkWithOrg } from '@app/components/link-with-org'; +import { MAP_PATH } from '@app/config'; import { Auth0Context } from '@app/utils/contexts'; import '../styles/app.scss'; -import { APP_LOGO, APP_NAME } from '../theme'; +import { APP_NAME } from '../theme'; interface ILayoutProps { children?: any; @@ -67,6 +68,7 @@ export default function ContentLayout(props: ILayoutProps) {
Profile} onLogin={login} onLogout={logout} onSignUp={() => login({ initialScreen: 'signUp' })} diff --git a/packages/earth-admin/src/pages-client/dashboards/details.tsx b/packages/earth-admin/src/pages-client/dashboards/details.tsx index d61ff67f..aeefe883 100644 --- a/packages/earth-admin/src/pages-client/dashboards/details.tsx +++ b/packages/earth-admin/src/pages-client/dashboards/details.tsx @@ -14,26 +14,35 @@ */ import { noop } from 'lodash'; +import { merge } from 'lodash/fp'; import React, { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import renderHTML from 'react-render-html'; import useSWR from 'swr'; -import { AsyncSelect, AuthzGuards, ErrorMessages, InlineEditCard } from '@marapp/earth-shared'; +import { + alphaNumericDashesRule, + AsyncSelect, + AuthzGuards, + ErrorMessages, + InlineEditCard, + Input, + setupErrors, +} from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; import { DetailList } from '@app/components/detail-list'; import { HtmlEditor } from '@app/components/html-editor'; -import { Input } from '@app/components/input'; import { LinkWithOrg } from '@app/components/link-with-org'; import { DeleteConfirmation } from '@app/components/modals/delete-confirmation'; import { Toggle } from '@app/components/toggle'; import { ContentLayout } from '@app/layouts'; -import { getAllWidgets, getDashboard, handleDashboardForm } from '@app/services'; +import { generateCacheKey } from '@app/services'; +import DashboardsService from '@app/services/dashboards'; +import WidgetsService from '@app/services/widgets'; import { CUSTOM_STYLES, SELECT_THEME } from '@app/theme'; -import { encodeQueryToURL, flattenArrayForSelect, formatDate } from '@app/utils'; -import { alphaNumericDashesRule, setupErrors } from '@app/utils/validations'; +import { flattenArrayForSelect, formatDate } from '@app/utils'; const DASHBOARD_DETAIL_QUERY = { include: 'layers,widgets', @@ -42,6 +51,7 @@ const DASHBOARD_DETAIL_QUERY = { }; interface IProps { + page: string; path: string; onDataChange?: () => {}; } @@ -51,13 +61,11 @@ export function DashboardDetail(props: IProps) { const { getPermissions, selectedGroup } = useAuth0(); const writePermissions = getPermissions(AuthzGuards.writeDashboardsGuard); - const encodedQuery = encodeQueryToURL(`dashboards/${page}`, { - ...DASHBOARD_DETAIL_QUERY, - ...{ group: selectedGroup }, - }); + const query = merge(DASHBOARD_DETAIL_QUERY, { group: selectedGroup }); + const cacheKey = generateCacheKey(`dashboards/${page}`, query); - const { data, error, mutate } = useSWR(encodedQuery, (url) => - getDashboard(url).then((res: any) => res.data) + const { data, error, mutate } = useSWR(cacheKey, () => + DashboardsService.getDashboard(page, query).then((res: any) => res.data) ); const [dashboard, setDashboard] = useState({}); @@ -74,7 +82,7 @@ export function DashboardDetail(props: IProps) { createdAt, updatedAt, version, - } = dashboard; + }: any = dashboard; useEffect(() => { data && setDashboard(data); @@ -101,7 +109,7 @@ export function DashboardDetail(props: IProps) { try { setIsLoading && setIsLoading(true); - await handleDashboardForm(false, parsed, id, selectedGroup); + await DashboardsService.handleDashboardForm(false, parsed, id, { group: selectedGroup }); mutate(); setIsEditing && setIsEditing(false); setIsLoading && setIsLoading(false); @@ -276,7 +284,7 @@ export function DashboardDetail(props: IProps) { control={control} getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} - loadFunction={getAllWidgets} + loadFunction={WidgetsService.getAllWidgets} defaultValue={widgets} selectedGroup={selectedGroup} as={AsyncSelect} diff --git a/packages/earth-admin/src/pages-client/dashboards/model.ts b/packages/earth-admin/src/pages-client/dashboards/model.ts index ba0a0d0a..cb686d41 100644 --- a/packages/earth-admin/src/pages-client/dashboards/model.ts +++ b/packages/earth-admin/src/pages-client/dashboards/model.ts @@ -17,7 +17,7 @@ specific language governing permissions and limitations under the License. */ -import { Layer } from '../layers/model'; +import { ILayer } from '../layers/model'; import { Widget } from '../widgets/model'; export interface Dashboard { @@ -26,7 +26,7 @@ export interface Dashboard { name: string; description: string; published: boolean; - layers?: string[] | Layer[]; + layers?: string[] | ILayer[]; widgets?: string[] | Widget[]; } diff --git a/packages/earth-admin/src/pages-client/dashboards/new.tsx b/packages/earth-admin/src/pages-client/dashboards/new.tsx index 1a8eb21d..aef6ea72 100644 --- a/packages/earth-admin/src/pages-client/dashboards/new.tsx +++ b/packages/earth-admin/src/pages-client/dashboards/new.tsx @@ -22,19 +22,24 @@ import { noop } from 'lodash'; import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { AsyncSelect, ErrorMessages, Spinner } from '@marapp/earth-shared'; +import { + alphaNumericDashesRule, + AsyncSelect, + ErrorMessages, + Input, + setupErrors, + Spinner, +} from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; import { HtmlEditor } from '@app/components/html-editor'; -import { Input } from '@app/components/input'; import { LinkWithOrg } from '@app/components/link-with-org'; import { ContentLayout } from '@app/layouts'; -import { addDashboard, getDashboardSlug } from '@app/services/dashboards'; -import { getAllWidgets } from '@app/services/widgets'; +import DashboardsService from '@app/services/dashboards'; +import WidgetsService from '@app/services/widgets'; import { CUSTOM_STYLES, SELECT_THEME } from '@app/theme'; import { flattenArrayForSelect } from '@app/utils'; -import { alphaNumericDashesRule, setupErrors } from '@app/utils/validations'; interface IProps { path?: string; @@ -67,9 +72,9 @@ export function NewDashboard(props: IProps) { try { setIsLoading(true); - const response: any = await addDashboard(parsed, selectedGroup); + const { data } = await DashboardsService.addDashboard(parsed, { group: selectedGroup }); onDataChange(); - await navigate(`/${selectedGroup}/dashboards/${response.data.id}`); + await navigate(`/${selectedGroup}/dashboards/${data.id}`); } catch (error) { setIsLoading(false); setServerErrors(error.data.errors); @@ -79,7 +84,9 @@ export function NewDashboard(props: IProps) { const generateSlug = async (e) => { e.preventDefault(); try { - const { data }: any = await getDashboardSlug(watchName, selectedGroup); + const { data } = await DashboardsService.getDashboardSlug(watchName, { + group: selectedGroup, + }); setValue('slug', data.slug, true); } catch (error) { setServerErrors(error.data.errors); @@ -163,7 +170,7 @@ export function NewDashboard(props: IProps) { control={control} getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} - loadFunction={getAllWidgets} + loadFunction={WidgetsService.getAllWidgets} defaultValue={widgets} selectedGroup={selectedGroup} as={AsyncSelect} diff --git a/packages/earth-admin/src/pages-client/dashboards/routes.tsx b/packages/earth-admin/src/pages-client/dashboards/routes.tsx index 19f3181b..37819ede 100644 --- a/packages/earth-admin/src/pages-client/dashboards/routes.tsx +++ b/packages/earth-admin/src/pages-client/dashboards/routes.tsx @@ -23,8 +23,9 @@ import React, { useState } from 'react'; import { useAuth0 } from '@app/auth/auth0'; import { DataListing, DefaultListItem } from '@app/components/data-listing'; import { SidebarLayout } from '@app/layouts'; -import { getAllDashboards } from '@app/services'; -import { encodeQueryToURL, setPage } from '@app/utils'; +import { RequestQuery } from '@app/services'; +import DashboardsService from '@app/services/dashboards'; +import { setPage } from '@app/utils'; import { useInfiniteList } from '@app/utils/hooks'; import { DashboardDetail } from './details'; @@ -38,7 +39,7 @@ export default function DashboardsPage(props) { const { selectedGroup } = useAuth0(); const [searchValue, setSearchValue] = useState(''); - const getQuery = (cursor) => { + const getQueryFn = (cursor: string): { query: RequestQuery; resourceType: string } => { const query = { search: searchValue, sort: 'name', @@ -46,9 +47,13 @@ export default function DashboardsPage(props) { select: 'name,slug', group: selectedGroup, }; - return encodeQueryToURL('dashboards', query); + return { query, resourceType: 'dashboards' }; }; - const { listProps, mutate } = useInfiniteList(getQuery, getAllDashboards); + const { listProps, mutate } = useInfiniteList(getQueryFn, DashboardsService.getAllDashboards); + + // Matches everything after the resource name in the url. + // In our case that's /resource-id or /new + const selectedItem = props['*']; return ( <> @@ -59,6 +64,7 @@ export default function DashboardsPage(props) { pageTitle="dashboards" searchValueAction={setSearchValue} pageSize={PAGE_SIZE} + selectedItem={selectedItem} searchValue={searchValue} {...listProps} /> diff --git a/packages/earth-admin/src/pages-client/homepage.tsx b/packages/earth-admin/src/pages-client/homepage.tsx index 48b43167..6f5399c8 100644 --- a/packages/earth-admin/src/pages-client/homepage.tsx +++ b/packages/earth-admin/src/pages-client/homepage.tsx @@ -22,22 +22,22 @@ import useSWR from 'swr'; import { Card } from '@app/components/card'; import { ContentLayout, SidebarLayout } from '@app/layouts'; -import { getOrganizationStats } from '@app/services/organizations'; -import { encodeQueryToURL, setPage } from '@app/utils'; +import { generateCacheKey } from '@app/services'; +import OrganizationsService from '@app/services/organizations'; +import { setPage } from '@app/utils'; import './styles.scss'; const PAGE_TYPE = setPage('Home'); const Homepage = (props) => { - // use org from URL - // `const { selectedGroup } = useAuth0()` fires multiple times on change const { org } = props; - const encodedQuery = encodeQueryToURL(`/organizations/stats`, { - group: org, - }); - const { data: organization, error } = useSWR(encodedQuery, (url) => - getOrganizationStats(url).then((res) => res.data) + + const query = { group: org }; + const cacheKey = generateCacheKey(`organizations/stats`, query); + + const { data: organization, error } = useSWR(cacheKey, () => + OrganizationsService.getOrganizationStats(query).then((res: any) => res.data) ); return ( diff --git a/packages/earth-admin/src/pages-client/layers/details.tsx b/packages/earth-admin/src/pages-client/layers/details.tsx index 57fd3d0a..b0cf605a 100644 --- a/packages/earth-admin/src/pages-client/layers/details.tsx +++ b/packages/earth-admin/src/pages-client/layers/details.tsx @@ -17,55 +17,65 @@ import Collapse from '@kunukn/react-collapse'; import classnames from 'classnames'; import { JSHINT } from 'jshint'; import { noop } from 'lodash'; +import { merge } from 'lodash/fp'; import React, { useEffect, useRef, useState } from 'react'; import { Controller, ErrorMessage, useForm } from 'react-hook-form'; import renderHTML from 'react-render-html'; import Select from 'react-select'; import useSWR from 'swr'; -import { AsyncSelect, AuthzGuards, ErrorMessages, InlineEditCard } from '@marapp/earth-shared'; +import { + alphaNumericDashesRule, + AsyncSelect, + AuthzGuards, + ErrorMessages, + InlineEditCard, + Input, + setupErrors, +} from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; import { DetailList } from '@app/components/detail-list'; import { HtmlEditor } from '@app/components/html-editor'; -import { Input } from '@app/components/input'; import { JsonEditor } from '@app/components/json-editor'; import { LinkWithOrg } from '@app/components/link-with-org'; import { DeleteConfirmation } from '@app/components/modals/delete-confirmation'; import { Toggle } from '@app/components/toggle'; import { ContentLayout } from '@app/layouts'; -import { getAllLayers, getLayer, handleLayerForm } from '@app/services/layers'; +import { generateCacheKey } from '@app/services'; +import LayersService from '@app/services/layers'; import { CUSTOM_STYLES, SELECT_THEME } from '@app/theme'; import { copyToClipboard, - encodeQueryToURL, flattenArrayForSelect, flattenObjectForSelect, formatDate, getSelectValues, } from '@app/utils'; -import { alphaNumericDashesRule, setupErrors } from '@app/utils/validations'; -import { LAYER_CATEGORY_OPTIONS, LAYER_PROVIDER_OPTIONS, LAYER_TYPE_OPTIONS } from './model'; +import { ILayer } from './model'; const LAYER_DETAIL_QUERY = { include: 'references', select: 'references.name,references.id' }; export function LayerDetail(props: any) { - const { page, onDataChange = noop } = props; + const { page, onDataChange = noop, dynamicOptions } = props; const { getPermissions, selectedGroup } = useAuth0(); const writePermissions = getPermissions(AuthzGuards.writeLayersGuard); + const { + category: layerCategoryOptions = [], + type: layerTypeOptions = [], + provider: layerProviderOptions = [], + } = dynamicOptions; - const encodedQuery = encodeQueryToURL(`layers/${page}`, { - ...LAYER_DETAIL_QUERY, - ...{ group: selectedGroup }, - }); + const query = merge(LAYER_DETAIL_QUERY, { group: selectedGroup }); + const cacheKey = generateCacheKey(`layers/${page}`, query); - const { data, error, mutate } = useSWR(encodedQuery, (url) => - getLayer(url).then((res: any) => res.data) + const { data, error, mutate } = useSWR(cacheKey, () => + LayersService.getLayer(page, query).then((res: any) => res.data) ); - const [layer, setLayer] = useState({}); + const [layer, setLayer] = useState({}); const [showDeleteModal, setShowDeleteModal] = useState(false); const [jsonError, setJsonError] = useState(false); const [serverErrors, setServerErrors] = useState(); @@ -100,10 +110,10 @@ export function LayerDetail(props: any) { useEffect(() => { layer.config && setLayerConfig(layer.config); - layer.category && setLayerCategory(getSelectValues(LAYER_CATEGORY_OPTIONS, layer.category)); - layer.type && setLayerType(LAYER_TYPE_OPTIONS.find((t) => t.value === layer.type)); + layer.category && setLayerCategory(getSelectValues(layerCategoryOptions, layer.category)); + layer.type && setLayerType(layerTypeOptions.find((t) => t.value === layer.type)); layer.provider && - setLayerProvider(LAYER_PROVIDER_OPTIONS.find((p) => p.value === layer.provider)); + setLayerProvider(layerProviderOptions.find((p) => p.value === layer.provider)); }, [layer]); const { getValues, register, formState, errors, control } = useForm({ @@ -130,7 +140,7 @@ export function LayerDetail(props: any) { try { setIsLoading && setIsLoading(true); - await handleLayerForm(false, parsed, id, selectedGroup); + await LayersService.handleLayerForm(false, parsed, id, { group: selectedGroup }); mutate(); setIsEditing && setIsEditing(false); setIsLoading && setIsLoading(false); @@ -262,7 +272,7 @@ export function LayerDetail(props: any) { control={control} className="marapp-qa-category" name="category" - options={LAYER_CATEGORY_OPTIONS} + options={layerCategoryOptions} defaultValue={layerCategory} isSearchable={true} isMulti={true} @@ -360,7 +370,7 @@ export function LayerDetail(props: any) { control={control} className="marapp-qa-provider" name="provider" - options={LAYER_PROVIDER_OPTIONS} + options={layerProviderOptions} defaultValue={layerProvider} isSearchable={true} placeholder="Select layer provider" @@ -379,7 +389,7 @@ export function LayerDetail(props: any) { control={control} className="marapp-qa-type" name="type" - options={LAYER_TYPE_OPTIONS} + options={layerTypeOptions} defaultValue={layerType} isSearchable={true} placeholder="Select layer type" @@ -474,7 +484,7 @@ export function LayerDetail(props: any) { control={control} getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} - loadFunction={getAllLayers} + loadFunction={LayersService.getAllLayers} defaultValue={references} selectedGroup={selectedGroup} as={AsyncSelect} diff --git a/packages/earth-admin/src/pages-client/layers/model.ts b/packages/earth-admin/src/pages-client/layers/model.ts index 2eff58af..5cf89c0f 100644 --- a/packages/earth-admin/src/pages-client/layers/model.ts +++ b/packages/earth-admin/src/pages-client/layers/model.ts @@ -17,26 +17,11 @@ specific language governing permissions and limitations under the License. */ -export enum LayerType { - raster = 'raster', - vector = 'vector', - geojson = 'geojson', - group = 'group', - video = 'video', -} - -export enum LayerProvider { - cartodb = 'cartodb', - gee = 'gee', - mapbox = 'mapbox', - leaflet = 'leaflet', -} - -export interface Layer { +export interface ILayer { id: string; description: string; primary: boolean; - category: LayerCategory[]; + category: any[]; config: object; published: boolean; createdAt: Date; @@ -44,53 +29,7 @@ export interface Layer { version: number; slug?: string; name?: string; - type?: LayerType; - provider?: LayerProvider; - references?: Layer[]; -} - -export interface LayerProps { - data: Layer; - newLayer?: boolean; + type?: any; + provider?: any; + references?: ILayer[]; } - -export enum LayerCategory { - BIODIVERSITY = 'Biodiversity', - CLIMATE_CARBON = 'Climate & Carbon', - ECOSYSTEM_SERVICES = 'Ecosystem Services', - HUMAN_IMPACT = 'Human Impact', - LAND_COVER = 'Land Cover', - MARINE = 'Marine', - NATURAL_HAZARDS = 'Natural Hazards', - PROTECTED_AREAS = 'Protected Areas', - RESTORATION = 'Restoration', - SOCIO_ECONOMIC = 'Socio-Economic', -} - -export const LAYER_CATEGORY_OPTIONS = [ - { value: 'Biodiversity', label: 'Biodiversity' }, - { value: 'Climate & Carbon', label: 'Climate & Carbon' }, - { value: 'Ecosystem Services', label: 'Ecosystem Services' }, - { value: 'Human Impact', label: 'Human Impact' }, - { value: 'Land Cover', label: 'Land Cover' }, - { value: 'Marine', label: 'Marine' }, - { value: 'Natural Hazards', label: 'Natural Hazards' }, - { value: 'Protected Areas', label: 'Protected Areas' }, - { value: 'Restoration', label: 'Restoration' }, - { value: 'Socio-Economic', label: 'Socio-Economic' }, -]; - -export const LAYER_TYPE_OPTIONS = [ - { value: 'raster', label: 'raster' }, - { value: 'vector', label: 'vector' }, - { value: 'geojson', label: 'geojson' }, - { value: 'group', label: 'group' }, - { value: 'video', label: 'video' }, -]; - -export const LAYER_PROVIDER_OPTIONS = [ - { value: 'cartodb', label: 'cartodb' }, - { value: 'gee', label: 'gee' }, - { value: 'mapbox', label: 'mapbox' }, - { value: 'leaflet', label: 'leaflet' }, -]; diff --git a/packages/earth-admin/src/pages-client/layers/new.tsx b/packages/earth-admin/src/pages-client/layers/new.tsx index 31634fa3..c9adb085 100644 --- a/packages/earth-admin/src/pages-client/layers/new.tsx +++ b/packages/earth-admin/src/pages-client/layers/new.tsx @@ -24,34 +24,46 @@ import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import Select from 'react-select'; -import { AsyncSelect, ErrorMessages, Spinner } from '@marapp/earth-shared'; +import { + alphaNumericDashesRule, + AsyncSelect, + ErrorMessages, + Input, + setupErrors, + Spinner, +} from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; import { HtmlEditor } from '@app/components/html-editor'; -import { Input } from '@app/components/input'; import { JsonEditor } from '@app/components/json-editor'; import { LinkWithOrg } from '@app/components/link-with-org'; import { ContentLayout } from '@app/layouts'; -import { addLayer, getAllLayers, getLayerSlug } from '@app/services/layers'; +import LayersService from '@app/services/layers'; import { CUSTOM_STYLES, SELECT_THEME } from '@app/theme'; import { flattenArrayForSelect, flattenObjectForSelect } from '@app/utils'; -import { alphaNumericDashesRule, setupErrors } from '@app/utils/validations'; - -import { LAYER_CATEGORY_OPTIONS, LAYER_PROVIDER_OPTIONS, LAYER_TYPE_OPTIONS } from './model'; interface IProps { path?: string; onDataChange?: () => {}; + dynamicOptions?: { + category?: any[]; + type?: any[]; + provider?: any[]; + }; } export function NewLayer(props: IProps) { - const { onDataChange = noop } = props; + const { onDataChange = noop, dynamicOptions } = props; const { selectedGroup } = useAuth0(); - const { register, watch, formState, errors, setValue, control, handleSubmit } = useForm({ mode: 'onChange', }); + const { + category: layerCategoryOptions = [], + type: layerTypeOptions = [], + provider: layerProviderOptions = [], + } = dynamicOptions; const { touched, dirty, isValid } = formState; const renderErrorFor = setupErrors(errors, touched); @@ -80,9 +92,9 @@ export function NewLayer(props: IProps) { try { setIsLoading(true); - const response: any = await addLayer(parsed, selectedGroup); + const { data } = await LayersService.addLayer(parsed, { group: selectedGroup }); onDataChange(); - await navigate(`/${selectedGroup}/layers/${response.data.id}`); + await navigate(`/${selectedGroup}/layers/${data.id}`); } catch (error) { setIsLoading(false); setServerErrors(error.data.errors); @@ -92,7 +104,7 @@ export function NewLayer(props: IProps) { const generateSlug = async (e) => { e.preventDefault(); try { - const { data }: any = await getLayerSlug(watchName, selectedGroup); + const { data } = await LayersService.getLayerSlug(watchName, { group: selectedGroup }); setValue('slug', data.slug, true); } catch (error) { setServerErrors(error.data.errors); @@ -172,7 +184,7 @@ export function NewLayer(props: IProps) { control={control} className="marapp-qa-category" name="category" - options={LAYER_CATEGORY_OPTIONS} + options={layerCategoryOptions} isSearchable={true} isMulti={true} placeholder="Select layer category" @@ -209,7 +221,7 @@ export function NewLayer(props: IProps) { control={control} className="marapp-qa-provider" name="provider" - options={LAYER_PROVIDER_OPTIONS} + options={layerProviderOptions} isSearchable={true} placeholder="Select layer provider" styles={CUSTOM_STYLES} @@ -227,7 +239,7 @@ export function NewLayer(props: IProps) { control={control} className="marapp-qa-type" name="type" - options={LAYER_TYPE_OPTIONS} + options={layerTypeOptions} isSearchable={true} placeholder="Select layer type" styles={CUSTOM_STYLES} @@ -261,7 +273,7 @@ export function NewLayer(props: IProps) { control={control} getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} - loadFunction={getAllLayers} + loadFunction={LayersService.getAllLayers} defaultValue={references} selectedGroup={selectedGroup} as={AsyncSelect} diff --git a/packages/earth-admin/src/pages-client/layers/routes.tsx b/packages/earth-admin/src/pages-client/layers/routes.tsx index 96c54756..0d743505 100644 --- a/packages/earth-admin/src/pages-client/layers/routes.tsx +++ b/packages/earth-admin/src/pages-client/layers/routes.tsx @@ -23,8 +23,9 @@ import React, { useState } from 'react'; import { useAuth0 } from '@app/auth/auth0'; import { DataListing, DefaultListItem } from '@app/components/data-listing'; import { SidebarLayout } from '@app/layouts'; -import { getAllLayers } from '@app/services'; -import { encodeQueryToURL, setPage } from '@app/utils'; +import { RequestQuery } from '@app/services'; +import LayersService from '@app/services/layers'; +import { setPage } from '@app/utils'; import { useInfiniteList } from '@app/utils/hooks'; import { LayerDetail } from './details'; @@ -38,7 +39,7 @@ export default function LayersPage(props) { const { selectedGroup } = useAuth0(); const [searchValue, setSearchValue] = useState(''); - const getQuery = (cursor) => { + const getQueryFn = (cursor: string): { query: RequestQuery; resourceType: string } => { const query = { search: searchValue, sort: 'name', @@ -46,9 +47,13 @@ export default function LayersPage(props) { select: 'name,slug', group: selectedGroup, }; - return encodeQueryToURL('layers', query); + return { query, resourceType: 'layers' }; }; - const { listProps, mutate } = useInfiniteList(getQuery, getAllLayers); + const { listProps, filters, mutate } = useInfiniteList(getQueryFn, LayersService.getAllLayers); + + // Matches everything after the resource name in the url. + // In our case that's /resource-id or /new + const selectedItem = props['*']; return ( <> @@ -60,13 +65,14 @@ export default function LayersPage(props) { searchValueAction={setSearchValue} pageSize={PAGE_SIZE} searchValue={searchValue} + selectedItem={selectedItem} {...listProps} /> - - + + ); diff --git a/packages/earth-admin/src/pages-client/organizations/details.tsx b/packages/earth-admin/src/pages-client/organizations/details.tsx index e55f1710..9eeabb2f 100644 --- a/packages/earth-admin/src/pages-client/organizations/details.tsx +++ b/packages/earth-admin/src/pages-client/organizations/details.tsx @@ -22,16 +22,20 @@ import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import useSWR from 'swr'; -import { AuthzGuards, InlineEditCard } from '@marapp/earth-shared'; +import { + AuthzGuards, + InlineEditCard, + Input, + setupErrors, + validEmailRule, +} from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; -import { Input } from '@app/components/input'; import { LinkWithOrg } from '@app/components/link-with-org'; import { DeleteConfirmation } from '@app/components/modals/delete-confirmation'; import { ContentLayout } from '@app/layouts'; -import { getOrganization, updateOrganization } from '@app/services/organizations'; -import { encodeQueryToURL } from '@app/utils'; -import { setupErrors, validEmailRule } from '@app/utils/validations'; +import { generateCacheKey } from '@app/services'; +import OrganizationsService from '@app/services/organizations'; import { OrganizationDetailsProps } from './model'; @@ -41,10 +45,12 @@ export function OrganizationDetails(props: OrganizationDetailsProps) { const [localOrgData, setLocalOrgData] = useState({}); const { getPermissions, selectedGroup } = useAuth0(); const writePermissions = getPermissions(AuthzGuards.accessOrganizationsGuard); - const encodedQuery = encodeQueryToURL(`organizations/${props.page}`, { include: 'owners' }); - const { data, error, mutate } = useSWR(encodedQuery, (url) => - getOrganization(url).then((res: any) => res.data) + const query = { include: 'owners' }; + const cacheKey = generateCacheKey(`organizations/${page}`, query); + + const { data, error, mutate } = useSWR(cacheKey, () => + OrganizationsService.getOrganization(page, query).then((res: any) => res.data) ); useEffect(() => { @@ -75,7 +81,7 @@ export function OrganizationDetails(props: OrganizationDetailsProps) { try { setIsLoading && setIsLoading(true); - await updateOrganization(id, parsed, selectedGroup); + await OrganizationsService.updateOrganization(id, parsed, { group: selectedGroup }); mutate(); setIsLoading && setIsLoading(false); setIsEditing && setIsEditing(false); @@ -86,7 +92,7 @@ export function OrganizationDetails(props: OrganizationDetailsProps) { } } - const { name, owners, slug, id } = localOrgData; + const { name, owners, slug, id }: any = localOrgData; const owner = owners && owners[0]; return ( diff --git a/packages/earth-admin/src/pages-client/organizations/new.tsx b/packages/earth-admin/src/pages-client/organizations/new.tsx index 2cf16e29..3181e64d 100644 --- a/packages/earth-admin/src/pages-client/organizations/new.tsx +++ b/packages/earth-admin/src/pages-client/organizations/new.tsx @@ -22,15 +22,20 @@ import { noop } from 'lodash'; import React, { useContext, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { ErrorMessages, Spinner } from '@marapp/earth-shared'; +import { + ErrorMessages, + Input, + lowerNumericDashesRule, + setupErrors, + Spinner, + validEmailRule, +} from '@marapp/earth-shared'; import { Card } from '@app/components/card'; -import { Input } from '@app/components/input'; import { LinkWithOrg } from '@app/components/link-with-org'; import { ContentLayout } from '@app/layouts'; -import { addOrganization } from '@app/services/organizations'; +import OrganizationsService from '@app/services/organizations'; import { Auth0Context } from '@app/utils/contexts'; -import { setupErrors, upperNumericDashesRule, validEmailRule } from '@app/utils/validations'; interface IProps { path?: string; @@ -51,13 +56,13 @@ export function NewOrganization(props: IProps) { const onSubmit = async (values: any) => { try { setIsLoading(true); - const { data }: any = await addOrganization( + const { data } = await OrganizationsService.addOrganization( { name: values.name, slug: values.slug, owners: [].concat(values.owners), }, - selectedGroup + { group: selectedGroup } ); onDataChange(); await navigate(`/${selectedGroup}/organizations/${data.id}`); @@ -104,7 +109,7 @@ export function NewOrganization(props: IProps) { ref={register({ required: 'Slug name is required', validate: { - upperNumericDashesRule: upperNumericDashesRule(), + lowerNumericDashesRule: lowerNumericDashesRule(), }, })} /> diff --git a/packages/earth-admin/src/pages-client/organizations/routes.tsx b/packages/earth-admin/src/pages-client/organizations/routes.tsx index 95c7c9a0..8b2eaf5e 100644 --- a/packages/earth-admin/src/pages-client/organizations/routes.tsx +++ b/packages/earth-admin/src/pages-client/organizations/routes.tsx @@ -21,10 +21,11 @@ import { Router } from '@reach/router'; import React from 'react'; import { useAuth0 } from '@app/auth/auth0'; -import { Auth0ListItem, DataListing } from '@app/components/data-listing'; +import { DataListing, DefaultListItem } from '@app/components/data-listing'; import { SidebarLayout } from '@app/layouts'; -import { getAllOrganizations } from '@app/services/organizations'; -import { encodeQueryToURL, setPage } from '@app/utils'; +import { RequestQuery } from '@app/services'; +import OrganizationsService from '@app/services/organizations'; +import { setPage } from '@app/utils'; import { useInfiniteListPaged } from '@app/utils/hooks'; import { OrganizationDetails } from './details'; @@ -37,24 +38,33 @@ const PAGE_TYPE = setPage('Organizations'); export default function PlacesPage(props) { const { selectedGroup } = useAuth0(); - const getQuery = (pageIndex) => { + const getQueryFn = (pageIndex: number): { query: RequestQuery; resourceType: string } => { const query = { page: { size: PAGE_SIZE, number: pageIndex }, select: 'name,slug', group: selectedGroup, }; - return encodeQueryToURL('organizations', query); + return { query, resourceType: 'organizations' }; }; - const { listProps, mutate } = useInfiniteListPaged(getQuery, getAllOrganizations); + + const { listProps, mutate } = useInfiniteListPaged( + getQueryFn, + OrganizationsService.getAllOrganizations + ); + + // Matches everything after the resource name in the url. + // In our case that's /resource-id or /new + const selectedItem = props['*']; return ( <> diff --git a/packages/earth-admin/src/pages-client/places/details.tsx b/packages/earth-admin/src/pages-client/places/details.tsx index b732cc88..96d10050 100644 --- a/packages/earth-admin/src/pages-client/places/details.tsx +++ b/packages/earth-admin/src/pages-client/places/details.tsx @@ -18,11 +18,19 @@ */ import { groupBy, map, noop } from 'lodash'; +import { merge } from 'lodash/fp'; import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import useSWR from 'swr'; -import { AuthzGuards, ErrorMessages, InlineEditCard } from '@marapp/earth-shared'; +import { + AuthzGuards, + ErrorMessages, + InlineEditCard, + Input, + noSpecialCharsOrSpaceRule, + setupErrors, +} from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; @@ -30,43 +38,45 @@ import { DetailList } from '@app/components/detail-list'; import { DownloadFile } from '@app/components/download-file'; import { ErrorBoundary } from '@app/components/error-boundary'; import { FakeJsonUpload } from '@app/components/fake-json-upload'; -import { Input } from '@app/components/input'; import { LinkWithOrg } from '@app/components/link-with-org'; import { MapComponent } from '@app/components/map'; import { DeleteConfirmation } from '@app/components/modals/delete-confirmation'; import { Metrics } from '@app/components/places'; import { Toggle } from '@app/components/toggle'; import { ContentLayout } from '@app/layouts'; -import { calculateAllForPlace, getPlace, handlePlaceForm } from '@app/services'; -import { encodeQueryToURL, formatArrayToParentheses, formatDate, km2toHa } from '@app/utils'; +import { generateCacheKey } from '@app/services'; +import MetricService from '@app/services/metrics'; +import PlacesService from '@app/services/places'; +import { formatArrayToParentheses, formatDate, km2toHa } from '@app/utils'; import { MapComponentContext } from '@app/utils/contexts'; -import { noSpecialCharsOrSpaceRule, setupErrors } from '@app/utils/validations'; -import { PLACE_DETAIL_QUERY, PlaceIntersection, PlaceTypeEnum } from './model'; +import { IPlace, PLACE_DETAIL_QUERY, PlaceIntersection } from './model'; interface IProps { path: string; page?: string; onDataChange?: () => {}; + dynamicOptions?: { + type?: any[]; + }; } export function PlaceDetail(props: IProps) { - const { page, onDataChange = noop } = props; + const { page, onDataChange = noop, dynamicOptions } = props; + const { type: placeTypeOptions = [] } = dynamicOptions; const { getPermissions, selectedGroup } = useAuth0(); const writePermissions = getPermissions(AuthzGuards.writePlacesGuard); const metricsPermission = getPermissions(AuthzGuards.accessMetricsGuard); const writeMetricsPermission = getPermissions(AuthzGuards.writeMetricsGuard); - const encodedQuery = encodeQueryToURL(`locations/${page}`, { - ...PLACE_DETAIL_QUERY, - group: selectedGroup, - }); + const query = merge(PLACE_DETAIL_QUERY, { group: selectedGroup }); + const cacheKey = generateCacheKey(`locations/${page}`, query); - const { data, error, mutate, revalidate } = useSWR(encodedQuery, (url) => - getPlace(url).then((response: any) => response.data) + const { data, error, mutate, revalidate } = useSWR(cacheKey, () => + PlacesService.getPlace(page, query).then((response: any) => response.data) ); - const [place, setPlace] = useState({}); + const [place, setPlace] = useState({}); const [mapData, setMapData] = useState({}); const [mappedIntersections, setMappedIntersections] = useState(); const [geojsonValue, setGeojson] = useState(null); @@ -126,7 +136,7 @@ export function PlaceDetail(props: IProps) { try { setIsLoading && setIsLoading(true); - await handlePlaceForm(false, parsed, id, selectedGroup); + await PlacesService.handlePlaceForm(false, parsed, id, { group: selectedGroup }); revalidate(); setIsEditing && setIsEditing(false); setIsLoading && setIsLoading(false); @@ -147,7 +157,7 @@ export function PlaceDetail(props: IProps) { e.stopPropagation(); try { setServerErrors(false); - await calculateAllForPlace(placeID, selectedGroup); + await MetricService.calculateAllForPlace(placeID, { group: selectedGroup }); setMetricsLoading(true); } catch (error) { setServerErrors(error.data.errors); @@ -268,13 +278,9 @@ export function PlaceDetail(props: IProps) { name="type" defaultValue={type} > - {Object.keys(PlaceTypeEnum).map((t, idx) => ( - ))} diff --git a/packages/earth-admin/src/pages-client/places/model.ts b/packages/earth-admin/src/pages-client/places/model.ts index 07ad15c9..bf6b3687 100644 --- a/packages/earth-admin/src/pages-client/places/model.ts +++ b/packages/earth-admin/src/pages-client/places/model.ts @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. */ -export interface Place { +export interface IPlace { id: string; description: string; - type: PlaceTypeEnum; + type: any; slug?: string; name?: string; geojson?: {}; @@ -36,15 +36,6 @@ export interface Place { intersections?: PlaceIntersection[] | string[]; } -export enum PlaceTypeEnum { - CONTINENT = 'Continent', - COUNTRY = 'Country', - JURISDICTION = 'Jurisdiction', - BIOME = 'Biome', - PROTECTED_AREA = 'Protected Area', - SPECIES_AREA = 'Species Area', -} - export interface MetricProps { id: any; slug?: string; @@ -52,7 +43,7 @@ export interface MetricProps { createdAt?: Date; updatedAt?: Date; version?: number; - location?: string | Place; + location?: string | IPlace; } export interface PlaceMetricsProps { @@ -63,7 +54,7 @@ export interface PlaceMetricsProps { } export interface PlaceProps { - data?: Place; + data?: IPlace; newPlace?: boolean; } diff --git a/packages/earth-admin/src/pages-client/places/new.tsx b/packages/earth-admin/src/pages-client/places/new.tsx index cb619dd9..301fef81 100644 --- a/packages/earth-admin/src/pages-client/places/new.tsx +++ b/packages/earth-admin/src/pages-client/places/new.tsx @@ -22,26 +22,26 @@ import { noop } from 'lodash'; import React, { useState } from 'react'; import { useForm } from 'react-hook-form'; -import { ErrorMessages, Spinner } from '@marapp/earth-shared'; +import { ErrorMessages, Input, setupErrors, Spinner } from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; import { FakeJsonUpload } from '@app/components/fake-json-upload'; -import { Input } from '@app/components/input'; import { LinkWithOrg } from '@app/components/link-with-org'; import { ContentLayout } from '@app/layouts'; -import { addPlace, getPlaceSlug } from '@app/services/places'; -import { setupErrors } from '@app/utils/validations'; - -import { PlaceTypeEnum } from './model'; +import PlacesService from '@app/services/places'; interface IProps { path: string; onDataChange?: () => {}; + dynamicOptions?: { + type?: any[]; + }; } export function NewPlace(props: IProps) { - const { onDataChange = noop } = props; + const { onDataChange = noop, dynamicOptions } = props; + const { type: placeTypeOptions = [] } = dynamicOptions; const { getValues, register, watch, formState, errors, setValue } = useForm({ mode: 'onChange', }); @@ -64,9 +64,9 @@ export function NewPlace(props: IProps) { }; try { setIsLoading(true); - const response: any = await addPlace(parsed, selectedGroup); + const { data } = await PlacesService.addPlace(parsed, { group: selectedGroup }); onDataChange(); - await navigate(`/${selectedGroup}/places/${response.data.id}`); + await navigate(`/${selectedGroup}/places/${data.id}`); } catch (error) { // TODO: Remove this when the real "upload file" feature is available. const fallbackError = [ @@ -80,7 +80,7 @@ export function NewPlace(props: IProps) { const generateSlug = async (e) => { e.preventDefault(); try { - const { data }: any = await getPlaceSlug(watchName, selectedGroup); + const { data } = await PlacesService.getPlaceSlug(watchName, { group: selectedGroup }); setValue('slug', data.slug, true); } catch (error) { setServerErrors(error.data.errors); @@ -119,9 +119,9 @@ export function NewPlace(props: IProps) { })} name="type" > - {Object.keys(PlaceTypeEnum).map((t, idx) => ( - ))} diff --git a/packages/earth-admin/src/pages-client/places/routes.tsx b/packages/earth-admin/src/pages-client/places/routes.tsx index 023a251b..d858ae9e 100644 --- a/packages/earth-admin/src/pages-client/places/routes.tsx +++ b/packages/earth-admin/src/pages-client/places/routes.tsx @@ -23,8 +23,9 @@ import React, { useState } from 'react'; import { useAuth0 } from '@app/auth/auth0'; import { DataListing, DefaultListItem } from '@app/components/data-listing'; import { SidebarLayout } from '@app/layouts'; -import { getAllPlaces } from '@app/services/places'; -import { encodeQueryToURL, setPage } from '@app/utils'; +import { RequestQuery } from '@app/services'; +import PlacesService from '@app/services/places'; +import { setPage } from '@app/utils'; import { useInfiniteList } from '@app/utils/hooks'; import { PlaceDetail } from './details'; @@ -38,7 +39,7 @@ export default function PlacesPage(props) { const { selectedGroup } = useAuth0(); const [searchValue, setSearchValue] = useState(''); - const getQuery = (cursor) => { + const getQueryFn = (cursor: string): { query: RequestQuery; resourceType: string } => { const query = { search: searchValue, sort: 'name', @@ -46,9 +47,14 @@ export default function PlacesPage(props) { select: 'name,slug', group: selectedGroup, }; - return encodeQueryToURL('locations', query); + return { query, resourceType: 'locations' }; }; - const { listProps, mutate } = useInfiniteList(getQuery, getAllPlaces); + + const { listProps, filters, mutate } = useInfiniteList(getQueryFn, PlacesService.getAllPlaces); + + // Matches everything after the resource name in the url. + // In our case that's /resource-id or /new + const selectedItem = props['*']; return ( <> @@ -60,14 +66,14 @@ export default function PlacesPage(props) { searchValueAction={setSearchValue} pageSize={PAGE_SIZE} searchValue={searchValue} + selectedItem={selectedItem} {...listProps} - // selectedItem={selectedItem} /> - - + + ); diff --git a/packages/earth-admin/src/pages-client/users/home.tsx b/packages/earth-admin/src/pages-client/users/home.tsx index 55714e8c..3d1c3861 100644 --- a/packages/earth-admin/src/pages-client/users/home.tsx +++ b/packages/earth-admin/src/pages-client/users/home.tsx @@ -24,18 +24,15 @@ import { Controller, useForm } from 'react-hook-form'; import Select from 'react-select'; import Creatable from 'react-select/creatable'; -import { AuthzGuards, ErrorMessages, Spinner } from '@marapp/earth-shared'; +import { AuthzGuards, ErrorMessages, Spinner, validEmail } from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; import { ContentLayout } from '@app/layouts'; -import { getAvailableGroups } from '@app/services'; -import { addUsers, getAllUsers } from '@app/services/users'; +import UsersService from '@app/services/users'; +import { CUSTOM_STYLES, SELECT_THEME } from '@app/theme'; import { encodeQueryToURL, normalizeGroupName } from '@app/utils'; import { useInfiniteListPaged } from '@app/utils/hooks'; -import { validEmail } from '@app/utils/validations'; - -import { CUSTOM_STYLES, SELECT_THEME } from '../../theme'; const PAGE_SIZE = 10; @@ -48,15 +45,18 @@ export function UsersHome(props: any) { const [serverErrors, setServerErrors] = useState([]); const [usersFeedback, setUsersFeedback] = useState([]); - const getQuery = (pageIndex) => { + const getQueryFn = (pageIndex) => { const query = { page: { size: PAGE_SIZE, number: pageIndex }, group: selectedGroup, include: 'groups', }; - return encodeQueryToURL('users', query); + return { query, resourceType: 'users' }; }; - const { listProps: userListProps, mutate } = useInfiniteListPaged(getQuery, getAllUsers); + const { listProps: userListProps, mutate } = useInfiniteListPaged( + getQueryFn, + UsersService.getAllUsers + ); const { watch, setValue, control, getValues } = useForm({ mode: 'onChange', @@ -71,8 +71,8 @@ export function UsersHome(props: any) { } (async () => { - const groupsResponse: any = await getAvailableGroups(selectedGroup); - const groups = groupsResponse.data.map((item) => ({ + const { data } = await UsersService.getAvailableGroups({ group: selectedGroup }); + const groups = data.map((item) => ({ label: normalizeGroupName(item.name), value: item.id, })); @@ -168,15 +168,13 @@ export function UsersHome(props: any) { }; try { - const result: any = await addUsers( - { - emails: users.map((user) => user.value), - groups: [role?.value], - }, - selectedGroup - ); + const data = { + emails: users.map((user) => user.value), + groups: [role?.value], + }; + const response = await UsersService.addUsers(data, { group: selectedGroup }); - processUsersFeedback(result.data, true); + processUsersFeedback(response.data, true); mutate(); } catch (err) { diff --git a/packages/earth-admin/src/pages-client/users/routes.tsx b/packages/earth-admin/src/pages-client/users/routes.tsx index 606b3e56..4a91b145 100644 --- a/packages/earth-admin/src/pages-client/users/routes.tsx +++ b/packages/earth-admin/src/pages-client/users/routes.tsx @@ -18,6 +18,7 @@ */ import { Router } from '@reach/router'; +import { merge } from 'lodash/fp'; import * as React from 'react'; import useSWR from 'swr'; @@ -26,9 +27,10 @@ import { Card } from '@app/components/card'; import { UserDetails, UserEdit } from '@app/components/users'; import { SidebarLayout } from '@app/layouts'; import ContentLayout from '@app/layouts/Content'; -import { getOrganizationStats } from '@app/services/organizations'; -import { getUser } from '@app/services/users'; -import { encodeQueryToURL, setPage } from '@app/utils'; +import { generateCacheKey } from '@app/services'; +import OrganizationsService from '@app/services/organizations'; +import UsersService from '@app/services/users'; +import { setPage } from '@app/utils'; import { UsersHome } from './home'; @@ -39,11 +41,12 @@ const USER_DETAIL_QUERY = { export default function UsersPage(props) { const { org } = props; - const encodedQuery = encodeQueryToURL(`/organizations/stats`, { - group: org, - }); - const { data: organization, error } = useSWR(encodedQuery, (url) => - getOrganizationStats(url).then((res: any) => res.data) + + const query = { group: org }; + const cacheKey = generateCacheKey(`organizations/stats`, query); + + const { data: organization, error } = useSWR(cacheKey, () => + OrganizationsService.getOrganizationStats(query).then((res: any) => res.data) ); return ( @@ -95,14 +98,14 @@ export default function UsersPage(props) { } function DetailsPage(props: any) { + const { page } = props; const { selectedGroup } = useAuth0(); - const encodedQuery = encodeQueryToURL(`users/${props.page}`, { - ...USER_DETAIL_QUERY, - group: selectedGroup, - }); - const { data, error, mutate } = useSWR(encodedQuery, (url) => - getUser(url).then((response: any) => response.data) + const query = merge(USER_DETAIL_QUERY, { group: selectedGroup }); + const cacheKey = generateCacheKey(`users/${page}`, query); + + const { data, error, mutate } = useSWR(cacheKey, () => + UsersService.getUser(page, query).then((response: any) => response.data) ); return ( @@ -118,13 +121,14 @@ function DetailsPage(props: any) { } function EditPage(props: any) { + const { page } = props; const { selectedGroup } = useAuth0(); - const encodedQuery = encodeQueryToURL(`users/${props.page}`, { - ...USER_DETAIL_QUERY, - ...{ group: selectedGroup }, - }); - const { data, error, mutate } = useSWR(!props.newUser && encodedQuery, (url) => - getUser(url).then((response: any) => response.data) + + const query = merge(USER_DETAIL_QUERY, { group: selectedGroup }); + const cacheKey = generateCacheKey(`users/${page}`, query); + + const { data, error, mutate } = useSWR(!props.newUser && cacheKey, () => + UsersService.getUser(page, query).then((response: any) => response.data) ); return ( diff --git a/packages/earth-admin/src/pages-client/widgets/details.tsx b/packages/earth-admin/src/pages-client/widgets/details.tsx index 4171f8e4..6e9dccc5 100644 --- a/packages/earth-admin/src/pages-client/widgets/details.tsx +++ b/packages/earth-admin/src/pages-client/widgets/details.tsx @@ -17,29 +17,37 @@ import Collapse from '@kunukn/react-collapse'; import classnames from 'classnames'; import { JSHINT } from 'jshint'; import { noop } from 'lodash'; +import { merge } from 'lodash/fp'; import React, { useEffect, useRef, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import renderHTML from 'react-render-html'; import Select from 'react-select'; import useSWR from 'swr'; -import { AsyncSelect, AuthzGuards, ErrorMessages, InlineEditCard } from '@marapp/earth-shared'; +import { + alphaNumericDashesRule, + AsyncSelect, + AuthzGuards, + ErrorMessages, + InlineEditCard, + Input, + setupErrors, +} from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; import { DetailList } from '@app/components/detail-list'; import { HtmlEditor } from '@app/components/html-editor'; -import { Input } from '@app/components/input'; import { JsonEditor } from '@app/components/json-editor'; import { LinkWithOrg } from '@app/components/link-with-org'; import { DeleteConfirmation } from '@app/components/modals/delete-confirmation'; import { Toggle } from '@app/components/toggle'; import { ContentLayout } from '@app/layouts'; -import { getAllLayers } from '@app/services/layers'; -import { getWidget, handleWidgetForm } from '@app/services/widgets'; +import { generateCacheKey } from '@app/services'; +import LayersService from '@app/services/layers'; +import WidgetsService from '@app/services/widgets'; import { CUSTOM_STYLES, SELECT_THEME } from '@app/theme'; import { copyToClipboard, encodeQueryToURL, flattenObjectForSelect, formatDate } from '@app/utils'; -import { alphaNumericDashesRule, setupErrors } from '@app/utils/validations'; import { Widget, WidgetProps } from './model'; @@ -50,18 +58,16 @@ const WIDGET_DETAIL_QUERY = { }; export function WidgetsDetail(props: WidgetProps) { - const { page, onDataChange = noop, groupedFilters = {} } = props; - const { metrics = [] } = groupedFilters; + const { page, onDataChange = noop, dynamicOptions = {} } = props; + const { metrics: metricsOptions = [] } = dynamicOptions; const { getPermissions, selectedGroup } = useAuth0(); const writePermissions = getPermissions(AuthzGuards.writeLayersGuard); - const encodedQuery = encodeQueryToURL(`widgets/${page}`, { - ...WIDGET_DETAIL_QUERY, - ...{ group: selectedGroup }, - }); + const query = merge(WIDGET_DETAIL_QUERY, { group: selectedGroup }); + const cacheKey = generateCacheKey(`widgets/${page}`, query); - const { data, error, mutate } = useSWR(encodedQuery, (url) => - getWidget(url).then((res: any) => res.data) + const { data, error, mutate } = useSWR(cacheKey, () => + WidgetsService.getWidget(page, query).then((res: any) => res.data) ); const [widget, setWidget] = useState({}); @@ -116,7 +122,7 @@ export function WidgetsDetail(props: WidgetProps) { try { setIsLoading && setIsLoading(true); - await handleWidgetForm(false, parsed, id, selectedGroup); + await WidgetsService.handleWidgetForm(false, parsed, id, { group: selectedGroup }); await mutate(); onDataChange(); setIsLoading && setIsLoading(false); @@ -307,7 +313,7 @@ export function WidgetsDetail(props: WidgetProps) { control={control} getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} - loadFunction={getAllLayers} + loadFunction={LayersService.getAllLayers} defaultValue={layers} selectedGroup={selectedGroup} as={AsyncSelect} @@ -357,10 +363,7 @@ export function WidgetsDetail(props: WidgetProps) { control={control} className="marapp-qa-metricslug" name="metrics" - options={metrics.map((m) => ({ - value: m.value, - label: m.value, - }))} + options={metricsOptions} defaultValue={{ value: selectedMetrics[0], label: selectedMetrics[0], diff --git a/packages/earth-admin/src/pages-client/widgets/model.ts b/packages/earth-admin/src/pages-client/widgets/model.ts index 571f4bd6..ffec1540 100644 --- a/packages/earth-admin/src/pages-client/widgets/model.ts +++ b/packages/earth-admin/src/pages-client/widgets/model.ts @@ -36,5 +36,7 @@ export interface WidgetProps { newWidget?: boolean; page?: string; onDataChange?: () => {}; - groupedFilters?: any; + dynamicOptions?: { + metrics?: any[]; + }; } diff --git a/packages/earth-admin/src/pages-client/widgets/new.tsx b/packages/earth-admin/src/pages-client/widgets/new.tsx index 243a9ac3..f4c338d6 100644 --- a/packages/earth-admin/src/pages-client/widgets/new.tsx +++ b/packages/earth-admin/src/pages-client/widgets/new.tsx @@ -24,30 +24,37 @@ import React, { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import Select from 'react-select'; -import { AsyncSelect, ErrorMessages, Spinner } from '@marapp/earth-shared'; +import { + alphaNumericDashesRule, + AsyncSelect, + ErrorMessages, + Input, + setupErrors, + Spinner, +} from '@marapp/earth-shared'; import { useAuth0 } from '@app/auth/auth0'; import { Card } from '@app/components/card'; import { HtmlEditor } from '@app/components/html-editor'; -import { Input } from '@app/components/input'; import { JsonEditor } from '@app/components/json-editor'; import { LinkWithOrg } from '@app/components/link-with-org'; import { ContentLayout } from '@app/layouts'; -import { getAllLayers } from '@app/services/layers'; -import { addWidget, getWidgetSlug } from '@app/services/widgets'; +import LayersService from '@app/services/layers'; +import WidgetsService from '@app/services/widgets'; import { CUSTOM_STYLES, SELECT_THEME } from '@app/theme'; import { flattenObjectForSelect } from '@app/utils'; -import { alphaNumericDashesRule, setupErrors } from '@app/utils/validations'; interface IProps { path?: string; onDataChange?: () => {}; - groupedFilters?: any; + dynamicOptions?: { + metrics?: any[]; + }; } export function NewWidget(props: IProps) { - const { onDataChange = noop, groupedFilters = {} } = props; - const { metrics = [] } = groupedFilters; + const { onDataChange = noop, dynamicOptions = {} } = props; + const { metrics: metricsOptions = [] } = dynamicOptions; const { selectedGroup } = useAuth0(); const { register, watch, formState, errors, setValue, control, handleSubmit } = useForm({ mode: 'onChange', @@ -76,9 +83,9 @@ export function NewWidget(props: IProps) { try { setIsLoading(true); - const response: any = await addWidget(parsed, selectedGroup); + const { data } = await WidgetsService.addWidget(parsed, { group: selectedGroup }); onDataChange(); - await navigate(`/${selectedGroup}/widgets/${response.data.id}`); + await navigate(`/${selectedGroup}/widgets/${data.id}`); } catch (error) { setIsLoading(false); setServerErrors(error.data.errors); @@ -88,7 +95,7 @@ export function NewWidget(props: IProps) { const generateSlug = async (e) => { e.preventDefault(); try { - const { data }: any = await getWidgetSlug(watchName, selectedGroup); + const { data } = await WidgetsService.getWidgetSlug(watchName, { group: selectedGroup }); setValue('slug', data.slug, true); } catch (error) { setServerErrors(error.data.errors); @@ -188,7 +195,7 @@ export function NewWidget(props: IProps) { control={control} getOptionLabel={(option) => option.name} getOptionValue={(option) => option.id} - loadFunction={getAllLayers} + loadFunction={LayersService.getAllLayers} selectedGroup={selectedGroup} as={AsyncSelect} isClearable={true} @@ -211,10 +218,7 @@ export function NewWidget(props: IProps) { control={control} className="marapp-qa-metricslug" name="metrics" - options={metrics.map((m) => ({ - value: m.value, - label: m.value, - }))} + options={metricsOptions} placeholder="Select metric slug" styles={CUSTOM_STYLES} error={renderErrorFor('metrics')} diff --git a/packages/earth-admin/src/pages-client/widgets/routes.tsx b/packages/earth-admin/src/pages-client/widgets/routes.tsx index 0e4b733f..d00c2020 100644 --- a/packages/earth-admin/src/pages-client/widgets/routes.tsx +++ b/packages/earth-admin/src/pages-client/widgets/routes.tsx @@ -18,15 +18,14 @@ */ import { Router } from '@reach/router'; -import { groupBy } from 'lodash'; import React, { useState } from 'react'; -import useSWR from 'swr'; import { useAuth0 } from '@app/auth/auth0'; import { DataListing, DefaultListItem } from '@app/components/data-listing'; import { SidebarLayout } from '@app/layouts'; -import { getAllWidgets } from '@app/services'; -import { encodeQueryToURL, setPage } from '@app/utils'; +import { RequestQuery } from '@app/services'; +import WidgetsService from '@app/services/widgets'; +import { setPage } from '@app/utils'; import { useInfiniteList } from '@app/utils/hooks'; import { WidgetsDetail } from './details'; @@ -40,7 +39,7 @@ export default function DashboardsPage(props) { const { selectedGroup } = useAuth0(); const [searchValue, setSearchValue] = useState(''); - const getQuery = (cursor) => { + const getQueryFn = (cursor: string): { query: RequestQuery; resourceType: string } => { const query = { search: searchValue, sort: 'name', @@ -48,26 +47,14 @@ export default function DashboardsPage(props) { select: 'name,slug', group: selectedGroup, }; - return encodeQueryToURL('widgets', query); + return { query, resourceType: 'widgets' }; }; - const { listProps, mutate } = useInfiniteList(getQuery, getAllWidgets); - // Fetch filters from /management/widgets api. - // Can't reuse the above request because it's using search param and - // it might not return filters - // TODO: create a custom hook for reuse on multiple pages - const metricsQuery = { - select: 'name,slug', - page: { size: 1, number: 1 }, - group: selectedGroup, - }; - const { data: groupedFilters } = useSWR( - encodeQueryToURL('widgets', metricsQuery), - async (url) => { - const { filters }: any = await getAllWidgets(url); - return groupBy(filters, 'key'); - } - ); + const { listProps, filters, mutate } = useInfiniteList(getQueryFn, WidgetsService.getAllWidgets); + + // Matches everything after the resource name in the url. + // In our case that's /resource-id or /new + const selectedItem = props['*']; return ( <> @@ -79,13 +66,14 @@ export default function DashboardsPage(props) { searchValueAction={setSearchValue} pageSize={PAGE_SIZE} searchValue={searchValue} + selectedItem={selectedItem} {...listProps} /> - - + + ); diff --git a/packages/earth-admin/src/services/base/APIBase.ts b/packages/earth-admin/src/services/base/APIBase.ts new file mode 100644 index 00000000..a4278ff4 --- /dev/null +++ b/packages/earth-admin/src/services/base/APIBase.ts @@ -0,0 +1,101 @@ +/* + Copyright 2018-2020 National Geographic Society + + Use of this software does not constitute endorsement by National Geographic + Society (NGS). The NGS name and NGS logo may not be used for any purpose without + written permission from NGS. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { merge } from 'lodash/fp'; +import { Deserializer } from 'ts-jsonapi'; + +import { GATSBY_API_URL } from '@app/config'; +import { encodeQueryToURL } from '@app/utils'; + +export interface RequestQuery { + [key: string]: any; +} +export type RequestMethod = 'get' | 'post' | 'put' | 'delete'; +export interface RequestConfig { + query?: RequestQuery; + method?: RequestMethod; + data?: any; +} + +const TIMEOUT = 30000; + +export const DeserializerService = new Deserializer({ + keyForAttribute: (attribute: any) => { + return attribute; + }, +}); + +export const BaseAPIService = { + /** + * Creates an axios request based on path and request options. + * @param path: identifies the specific resource in the host + * @param config + * @param deserializer: JSON:API deserializer + */ + request: ( + path: string, + config: RequestConfig, + deserializer: (response: AxiosResponse) => any = BaseAPIService.deserialize + ) => { + const defaults = { query: {}, method: 'get', data: {} }; + const params = merge(defaults, config); + + const options: AxiosRequestConfig = { + baseURL: GATSBY_API_URL, + url: encodeQueryToURL(path, params.query), + method: params.method, + data: params.data, + timeout: TIMEOUT, + }; + + return new Promise((resolve, reject) => { + axios + .request(options) + .then((response) => resolve(deserializer(response))) + .catch((error) => + reject(error?.response?.data?.data ? deserializer(error?.response) : error?.response) + ); + }); + }, + /** + * JSON:API deserializer + * @param response + */ + deserialize: (response: AxiosResponse): any => { + return DeserializerService.deserialize(response.data); + }, +}; + +/** + * JSON:API meta deserializer + * @param response + */ +export const metaDeserializer = (response: AxiosResponse): any => { + return { data: DeserializerService.deserialize(response?.data), meta: response?.data?.meta }; +}; + +/** + * Generate a cache key based on URL path and query params. + * @param path + * @param query + */ +export const generateCacheKey = (path: string, query: RequestQuery): string => { + return encodeQueryToURL(path, query); +}; diff --git a/packages/earth-admin/src/services/dashboards.tsx b/packages/earth-admin/src/services/dashboards.tsx index d7f08620..ed2c3a37 100644 --- a/packages/earth-admin/src/services/dashboards.tsx +++ b/packages/earth-admin/src/services/dashboards.tsx @@ -17,74 +17,66 @@ specific language governing permissions and limitations under the License. */ -import axios, { AxiosRequestConfig } from 'axios'; +import { merge } from 'lodash/fp'; -import { GATSBY_API_URL } from '@app/config'; +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; -import { deserializeData, encodeQueryToURL } from '../utils'; - -const DashboardAPIService = { - request: (options: AxiosRequestConfig) => { - const instance = axios.create({ - baseURL: GATSBY_API_URL, - timeout: 10000, - // @ts-ignore - transformResponse: axios.defaults.transformResponse.concat((data, headers) => ({ - data: data.data ? deserializeData(data) : data, - pagination: data.meta ? data.meta.pagination : null, - total: data.meta ? data.meta.results : null, - })), - }); - - return new Promise((resolve, reject) => { - instance - .request(options) - .then((res) => resolve(res.data)) - .catch((error) => reject(error.response.data)); - }); - }, +const getAllDashboards = async (query?: string | RequestQuery) => { + return BaseAPIService.request('/dashboards', { query }, metaDeserializer); }; -export const getAllDashboards = async (dashboardQuery: string) => - DashboardAPIService.request({ - url: dashboardQuery, - }); +const addDashboard = async (data: any, query?: RequestQuery) => { + return BaseAPIService.request('/dashboards', { query, method: 'post', data }, metaDeserializer); +}; -export const addDashboard = async (dashboard, group: string) => - DashboardAPIService.request({ - url: encodeQueryToURL('/dashboards', { group }), - method: 'post', - data: dashboard, - }); +const getDashboard = async (dashboardId: string, query?: RequestQuery) => { + return BaseAPIService.request(`/dashboards/${dashboardId}`, { query }, metaDeserializer); +}; -export const getDashboard = async (dashboardQuery: string) => - DashboardAPIService.request({ - url: dashboardQuery, - method: 'get', - }); +const updateDashboard = async (dashboardId: string, data: any, query?: RequestQuery) => { + return BaseAPIService.request( + `/dashboards/${dashboardId}`, + { + method: 'put', + data, + query, + }, + metaDeserializer + ); +}; -export const updateDashboard = async (dashboardId: string, dashboard, group: string) => - DashboardAPIService.request({ - url: encodeQueryToURL(`/dashboards/${dashboardId}`, { group }), - method: 'put', - data: dashboard, - }); +const deleteDashboards = async (dashboardId: string, query?: RequestQuery) => { + return BaseAPIService.request( + `/dashboards/${dashboardId}`, + { query, method: 'delete' }, + metaDeserializer + ); +}; -export const deleteDashboards = async (dashboardId: string, group: string) => - DashboardAPIService.request({ - url: encodeQueryToURL(`/dashboards/${dashboardId}`, { group }), - method: 'delete', - }); +const getDashboardSlug = async (keyword: string, query?: RequestQuery) => { + const params = { keyword, type: 'counter' }; + return BaseAPIService.request( + '/dashboards/slug', + { query: merge(params, query) }, + metaDeserializer + ); +}; -export const handleDashboardForm = async ( +const handleDashboardForm = async ( newDashboard: boolean, - dashboard, + data: any, dashboardId: string, - group: string -) => - newDashboard ? addDashboard(dashboard, group) : updateDashboard(dashboardId, dashboard, group); + query?: RequestQuery +) => { + return newDashboard ? addDashboard(data, query) : updateDashboard(dashboardId, data, query); +}; -export const getDashboardSlug = async (keyword: string, group: string, type: string = 'counter') => - DashboardAPIService.request({ - url: encodeQueryToURL('/dashboards/slug', { keyword, group, type }), - }); +export default { + getAllDashboards, + addDashboard, + getDashboard, + updateDashboard, + deleteDashboards, + getDashboardSlug, + handleDashboardForm, +}; diff --git a/packages/earth-admin/src/services/index.ts b/packages/earth-admin/src/services/index.ts index deb460a1..21d35dee 100644 --- a/packages/earth-admin/src/services/index.ts +++ b/packages/earth-admin/src/services/index.ts @@ -17,6 +17,7 @@ specific language governing permissions and limitations under the License. */ +export * from './base/APIBase'; export * from './places'; export * from './widgets'; export * from './layers'; diff --git a/packages/earth-admin/src/services/layers.tsx b/packages/earth-admin/src/services/layers.tsx index 9db884b2..e1bfe777 100644 --- a/packages/earth-admin/src/services/layers.tsx +++ b/packages/earth-admin/src/services/layers.tsx @@ -17,66 +17,58 @@ specific language governing permissions and limitations under the License. */ -import axios, { AxiosRequestConfig } from 'axios'; +import { merge } from 'lodash/fp'; -import { GATSBY_API_URL } from '@app/config'; -import { deserializeData, encodeQueryToURL } from '@app/utils'; +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; -const LayerAPIService = { - request: (options: AxiosRequestConfig) => { - const instance = axios.create({ - baseURL: GATSBY_API_URL, - timeout: 10000, - // @ts-ignore - transformResponse: axios.defaults.transformResponse.concat((data, headers) => ({ - data: data.data ? deserializeData(data) : data, - pagination: data.meta ? data.meta.pagination : null, - total: data.meta ? data.meta.results : null, - })), - }); - - return new Promise((resolve, reject) => { - instance - .request(options) - .then((res) => resolve(res.data)) - .catch((error) => reject(error.response.data)); - }); - }, +const getAllLayers = async (query?: string | RequestQuery) => { + return BaseAPIService.request('/layers', { query }, metaDeserializer); }; -export const getAllLayers = async (layerQuery: string) => - LayerAPIService.request({ url: layerQuery }); +const addLayer = async (data: any, query?: RequestQuery) => { + return BaseAPIService.request('/layers', { query, method: 'post', data }, metaDeserializer); +}; -export const addLayer = async (layer, group: string) => - LayerAPIService.request({ - url: encodeQueryToURL('/layers', { group }), - method: 'post', - data: layer, - }); +const getLayer = (layerId: string, query?: RequestQuery) => { + return BaseAPIService.request(`/layers/${layerId}`, { query }, metaDeserializer); +}; -export const getLayer = (layerQuery: string) => - LayerAPIService.request({ - url: layerQuery, - method: 'get', - }); +const updateLayer = async (layerId: string, data: any, query?: RequestQuery) => { + return BaseAPIService.request( + `/layers/${layerId}`, + { query, method: 'put', data }, + metaDeserializer + ); +}; -export const updateLayer = async (layerId: string, layer, group: string) => - LayerAPIService.request({ - url: encodeQueryToURL(`/layers/${layerId}`, { group }), - method: 'put', - data: layer, - }); +const deleteLayer = async (layerId: string, query?: RequestQuery) => { + return BaseAPIService.request( + `/layers/${layerId}`, + { query, method: 'delete' }, + metaDeserializer + ); +}; -export const deleteLayer = async (layerId: string, group: string) => - LayerAPIService.request({ - url: encodeQueryToURL(`/layers/${layerId}`, { group }), - method: 'delete', - }); +const getLayerSlug = async (keyword: string, query?: RequestQuery) => { + const params = { keyword, type: 'counter' }; + return BaseAPIService.request('/layers/slug', { query: merge(params, query) }, metaDeserializer); +}; -export const handleLayerForm = async (newLayer: boolean, layer, layerId: string, group: string) => - newLayer ? addLayer(layer, group) : updateLayer(layerId, layer, group); +const handleLayerForm = async ( + newLayer: boolean, + data: any, + layerId: string, + query?: RequestQuery +) => { + return newLayer ? addLayer(data, query) : updateLayer(layerId, data, query); +}; -export const getLayerSlug = async (keyword: string, group: string, type: string = 'counter') => - LayerAPIService.request({ - url: encodeQueryToURL('/layers/slug', { keyword, group, type }), - }); +export default { + getAllLayers, + addLayer, + getLayer, + updateLayer, + deleteLayer, + getLayerSlug, + handleLayerForm, +}; diff --git a/packages/earth-admin/src/services/metrics.tsx b/packages/earth-admin/src/services/metrics.tsx index 519ee5e9..3fdef4a7 100644 --- a/packages/earth-admin/src/services/metrics.tsx +++ b/packages/earth-admin/src/services/metrics.tsx @@ -17,72 +17,57 @@ specific language governing permissions and limitations under the License. */ -import axios, { AxiosRequestConfig } from 'axios'; +import { merge } from 'lodash/fp'; -import { GATSBY_API_URL } from '@app/config'; -import { deserializeData, encodeQueryToURL } from '@app/utils'; +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; interface ResponseSuccess { operationId?: string; } -interface ResponseError { - errors: [ - { - code: number; - title: string; - detail: string; - } - ]; +interface Error { + code: number; + title: string; + detail: string; } -const MetricAPIService = { - request: (options: AxiosRequestConfig) => { - const instance = axios.create({ - baseURL: GATSBY_API_URL, - timeout: 10000, - // @ts-ignore - transformResponse: axios.defaults.transformResponse.concat((data, headers) => ({ - data: data.data ? deserializeData(data) : data, - pagination: data.meta ? data.meta.pagination : null, - })), - }); - - return new Promise((resolve, reject) => { - instance - .request(options) - .then((res) => resolve(res.data)) - .catch((error) => reject(error.response.data)); - }); - }, -}; +interface ResponseError { + errors: Error[]; +} -export const calculateAllForPlace = async ( +const calculateAllForPlace = async ( placeId: string, - selectedGroup: string -): Promise => - MetricAPIService.request({ - url: encodeQueryToURL(`/metrics/${placeId}/action`, { group: selectedGroup }), - method: 'post', - params: { - operationType: 'calculate', + query?: RequestQuery +): Promise => { + const params = { operationType: 'calculate' }; + return BaseAPIService.request( + `/metrics/${placeId}/action`, + { + query: merge(params, query), + method: 'post', }, - }); + metaDeserializer + ); +}; -export const calculateForPlace = async ( - placeID: string, +const calculateForPlace = async ( + placeId: string, metricId: string, - selectedGroup: string -): Promise => - MetricAPIService.request({ - url: encodeQueryToURL(`/metrics/${placeID}/${metricId}/action`, { group: selectedGroup }), - method: 'post', - params: { - operationType: 'calculate', + query?: RequestQuery +): Promise => { + const params = { operationType: 'calculate' }; + return BaseAPIService.request( + `/metrics/${placeId}/${metricId}/action`, + { + query: merge(params, query), + method: 'post', }, - }); + metaDeserializer + ); +}; + +const getAllMetrics = async (query?: RequestQuery) => { + return BaseAPIService.request('/metrics', { query }, metaDeserializer); +}; -export const getAllMetrics = async (query: string) => - MetricAPIService.request({ - url: query, - }); +export default { calculateAllForPlace, calculateForPlace, getAllMetrics }; diff --git a/packages/earth-admin/src/services/organizations.tsx b/packages/earth-admin/src/services/organizations.tsx index 679365fa..ec19ae72 100644 --- a/packages/earth-admin/src/services/organizations.tsx +++ b/packages/earth-admin/src/services/organizations.tsx @@ -17,68 +17,59 @@ specific language governing permissions and limitations under the License. */ -import axios, { AxiosRequestConfig } from 'axios'; +import { merge } from 'lodash/fp'; -import { GATSBY_API_URL } from '@app/config'; +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; -import { deserializeData, encodeQueryToURL } from '../utils'; - -const OrganizationAPIService = { - request: (options: AxiosRequestConfig) => { - const instance = axios.create({ - baseURL: GATSBY_API_URL, - timeout: 100000, - // @ts-ignore - transformResponse: axios.defaults.transformResponse.concat((data, headers) => ({ - data: data.data ? deserializeData(data) : data, - pagination: data.meta ? data.meta.pagination : null, - total: data.meta ? data.meta.results : null, - })), - }); - - return new Promise((resolve, reject) => { - instance - .request(options) - .then((res) => resolve(res.data)) - .catch((error) => { - reject(error.response.data); - }); - }); - }, +const getAllOrganizations = async (query?: string | RequestQuery) => { + return BaseAPIService.request('/organizations', { query }, metaDeserializer); }; -export const getAllOrganizations = async (organizationQuery: string) => - OrganizationAPIService.request({ - url: organizationQuery, - }); +const getOrganization = (orgId: string, query?: RequestQuery) => { + return BaseAPIService.request(`/organizations/${orgId}`, { query }, metaDeserializer); +}; -export const getOrganization = (organizationQuery: string) => - OrganizationAPIService.request({ - url: organizationQuery, - method: 'get', - }); +const getOrganizationStats = async (query?: RequestQuery) => { + return BaseAPIService.request('/organizations/stats', { query }, metaDeserializer); +}; -export const getOrganizationStats = async (query: string) => - OrganizationAPIService.request({ - url: query, - }); +const updateOrganization = async (orgId: string, data: any, query?: RequestQuery) => { + const params = { include: 'owners' }; + return BaseAPIService.request( + `/organizations/${orgId}`, + { + query: merge(params, query), + method: 'put', + data, + }, + metaDeserializer + ); +}; -export const updateOrganization = async (organizationID: string, organization, group: string) => - OrganizationAPIService.request({ - url: encodeQueryToURL(`/organizations/${organizationID}`, { group, include: 'owners' }), - method: 'put', - data: organization, - }); +const addOrganization = async (data: any, query?: RequestQuery) => { + return BaseAPIService.request( + '/organizations', + { query, method: 'post', data }, + metaDeserializer + ); +}; -export const addOrganization = async (request, group: string) => - OrganizationAPIService.request({ - url: encodeQueryToURL('/organizations', { group }), - method: 'post', - data: request, - }); +const deleteOrganization = async (orgId, query?: RequestQuery) => { + return BaseAPIService.request( + `/organizations/${orgId}`, + { + method: 'delete', + query, + }, + metaDeserializer + ); +}; -export const deleteOrganization = async (organizationId) => - OrganizationAPIService.request({ - url: encodeQueryToURL(`/organizations/${organizationId}`), - method: 'delete', - }); +export default { + getAllOrganizations, + getOrganization, + getOrganizationStats, + updateOrganization, + addOrganization, + deleteOrganization, +}; diff --git a/packages/earth-admin/src/services/places.tsx b/packages/earth-admin/src/services/places.tsx index 9aff0d93..f05e7be2 100644 --- a/packages/earth-admin/src/services/places.tsx +++ b/packages/earth-admin/src/services/places.tsx @@ -17,72 +17,83 @@ specific language governing permissions and limitations under the License. */ -import axios, { AxiosRequestConfig } from 'axios'; +import { merge } from 'lodash/fp'; -import { GATSBY_API_URL } from '@app/config'; +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; -import { deserializeData, encodeQueryToURL } from '../utils'; - -const PlacesAPIService = { - request: (options: AxiosRequestConfig) => { - const instance = axios.create({ - baseURL: GATSBY_API_URL, - timeout: 100000, - // @ts-ignore - transformResponse: axios.defaults.transformResponse.concat((data, headers) => ({ - data: data.data ? deserializeData(data) : data, - pagination: data.meta ? data.meta.pagination : null, - total: data.meta ? data.meta.results : null, - })), - }); - - return new Promise((resolve, reject) => { - instance - .request(options) - .then((res) => resolve(res.data)) - .catch((error) => { - reject(error.response?.data); - }); - }); - }, +const getAllPlaces = async (query?: string | RequestQuery) => { + return BaseAPIService.request('/locations', { query }, metaDeserializer); }; -export const getAllPlaces = async (placeQuery: string) => - PlacesAPIService.request({ - url: placeQuery, - }); +const addPlace = async (data: any, query?: RequestQuery) => { + return BaseAPIService.request( + '/locations', + { + query, + method: 'post', + data, + }, + metaDeserializer + ); +}; -export const addPlace = async (request, group: string) => - PlacesAPIService.request({ - url: encodeQueryToURL('/locations', { group }), - method: 'post', - data: request, - }); +const getPlace = (placeId: string, query?: RequestQuery) => { + return BaseAPIService.request( + `/locations/${placeId}`, + { + query, + }, + metaDeserializer + ); +}; -export const getPlace = (placeQuery: string) => - PlacesAPIService.request({ - url: placeQuery, - method: 'get', - }); +const updatePlace = async (placeId: string, data: any, query?: RequestQuery) => { + return BaseAPIService.request( + `/locations/${placeId}`, + { + method: 'put', + data, + query, + }, + metaDeserializer + ); +}; -export const updatePlace = async (placeID: string, place, group: string) => - PlacesAPIService.request({ - url: encodeQueryToURL(`/locations/${placeID}`, { group }), - method: 'put', - data: place, - }); +const deletePlace = async (placeId: string, query?: RequestQuery) => { + return BaseAPIService.request( + `/locations/${placeId}`, + { + method: 'delete', + query, + }, + metaDeserializer + ); +}; -export const deletePlace = async (placeID: string, group: string) => - PlacesAPIService.request({ - url: encodeQueryToURL(`/locations/${placeID}`, { group }), - method: 'delete', - }); +const getPlaceSlug = async (keyword: string, query?: RequestQuery) => { + const params = { keyword, type: 'counter' }; + return BaseAPIService.request( + '/locations/slug', + { query: merge(params, query) }, + metaDeserializer + ); +}; -export const handlePlaceForm = async (newPlace: boolean, place, placeID: string, group: string) => { - newPlace ? await addPlace(place, group) : await updatePlace(placeID, place, group); +const handlePlaceForm = async ( + newPlace: boolean, + data: any, + placeId: string, + query?: RequestQuery +) => { + return newPlace ? addPlace(data, query) : updatePlace(placeId, data, query); }; -export const getPlaceSlug = async (keyword: string, group: string, type: string = 'counter') => - PlacesAPIService.request({ - url: encodeQueryToURL('/locations/slug', { keyword, group, type }), - }); +export default { + getAllPlaces, + addPlace, + getPlace, + updatePlace, + deletePlace, + handlePlaceForm, + getPlaceSlug, +}; diff --git a/packages/earth-admin/src/services/users.tsx b/packages/earth-admin/src/services/users.tsx index db7fe1a8..6b2aed4d 100644 --- a/packages/earth-admin/src/services/users.tsx +++ b/packages/earth-admin/src/services/users.tsx @@ -17,73 +17,82 @@ specific language governing permissions and limitations under the License. */ -import axios, { AxiosRequestConfig } from 'axios'; - -import { GATSBY_API_URL } from '@app/config'; - -import { deserializeData, encodeQueryToURL } from '../utils'; - -const UserAPIService = { - request: (options: AxiosRequestConfig) => { - const instance = axios.create({ - baseURL: GATSBY_API_URL, - timeout: 100000, - // @ts-ignore - transformResponse: axios.defaults.transformResponse.concat((data, headers) => ({ - data: data.data ? deserializeData(data) : data, - pagination: data.meta ? data.meta.pagination : null, - total: data.meta ? data.meta.results : null, - })), - }); - - return new Promise((resolve, reject) => { - instance - .request(options) - .then((res) => resolve(res.data)) - .catch((error) => { - reject(error.response.data); - }); - }); - }, +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; + +const getAllUsers = async (query?: string | RequestQuery) => { + return BaseAPIService.request( + '/users', + { + query, + }, + metaDeserializer + ); }; -export const getAllUsers = async (userQuery: string) => - UserAPIService.request({ - url: userQuery, - }); +const addUsers = async (data: any, query?: RequestQuery) => { + return BaseAPIService.request( + '/users', + { + method: 'put', + data, + query, + }, + metaDeserializer + ); +}; -export const addUsers = async (request, group: string) => - UserAPIService.request({ - url: encodeQueryToURL('/users', { group }), - method: 'put', - data: request, - }); +const getUser = (userId: string, query?: RequestQuery) => { + return BaseAPIService.request( + `/users/${userId}`, + { + query, + }, + metaDeserializer + ); +}; -export const getUser = (userQuery: string) => - UserAPIService.request({ - url: userQuery, - method: 'get', - }); +const getAvailableGroups = async (query?: RequestQuery) => { + return BaseAPIService.request('/users/groups', { query }, metaDeserializer); +}; -export const getAvailableGroups = async (group: string) => - UserAPIService.request({ - url: encodeQueryToURL('/users/groups', { group }), - method: 'get', - }); +const updateUser = async (userId: string, data: any, query?: RequestQuery) => { + return BaseAPIService.request( + `/users/${userId}`, + { + method: 'put', + data, + query, + }, + metaDeserializer + ); +}; -export const updateUser = async (userID: string, user, group: string) => - UserAPIService.request({ - url: encodeQueryToURL(`/users/${userID}`, { group }), - method: 'put', - data: user, - }); +const deleteUser = async (userId: string, query?: RequestQuery) => { + return BaseAPIService.request( + `/users/${userId}`, + { + method: 'delete', + query, + }, + metaDeserializer + ); +}; -export const deleteUser = async (userID: string, group: string) => - UserAPIService.request({ - url: encodeQueryToURL(`/users/${userID}`, { group }), - method: 'delete', - }); +const handleUserForm = async ( + newUser: boolean, + data: any, + userId: string, + query?: RequestQuery +) => { + return newUser ? addUsers(data, query) : updateUser(userId, data, query); +}; -export const handleUserForm = async (newUser: boolean, user, userID: string, group: string) => { - newUser ? await addUsers(user, group) : await updateUser(userID, user, group); +export default { + getAllUsers, + addUsers, + getUser, + getAvailableGroups, + updateUser, + deleteUser, + handleUserForm, }; diff --git a/packages/earth-admin/src/services/widgets.tsx b/packages/earth-admin/src/services/widgets.tsx index b70eb7ac..0ff4bd35 100644 --- a/packages/earth-admin/src/services/widgets.tsx +++ b/packages/earth-admin/src/services/widgets.tsx @@ -17,80 +17,85 @@ specific language governing permissions and limitations under the License. */ -import axios, { AxiosRequestConfig } from 'axios'; - -import { GATSBY_API_URL } from '@app/config'; - -import { deserializeData, encodeQueryToURL } from '../utils'; - -const WidgetAPIService = { - request: (options: AxiosRequestConfig) => { - const instance = axios.create({ - baseURL: GATSBY_API_URL, - timeout: 10000, - // @ts-ignore - transformResponse: axios.defaults.transformResponse.concat((data, headers) => ({ - data: data.data ? deserializeData(data) : data, - pagination: data.meta ? data.meta.pagination : null, - filters: data.meta ? data.meta.filters : null, - total: data.meta ? data.meta.results : null, - })), - }); - - return new Promise((resolve, reject) => { - instance - .request(options) - .then((res) => resolve(res.data)) - .catch((error) => { - if (error.response) { - reject(error.response.data); - } else { - reject(error); - } - }); - }); - }, +import { merge } from 'lodash/fp'; + +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; + +const getAllWidgets = async (query?: string | RequestQuery) => { + return BaseAPIService.request( + '/widgets', + { + query, + }, + metaDeserializer + ); }; -export const getAllWidgets = async (widgetQuery: string) => - WidgetAPIService.request({ - url: widgetQuery, - }); +const addWidget = async (data: any, query?: RequestQuery) => { + return BaseAPIService.request( + '/widgets', + { + query, + method: 'post', + data, + }, + metaDeserializer + ); +}; -export const addWidget = async (widget, group: string) => - WidgetAPIService.request({ - url: encodeQueryToURL('/widgets', { group }), - method: 'post', - data: widget, - }); +const getWidget = async (widgetId: string, query?: RequestQuery) => { + return BaseAPIService.request( + `/widgets/${widgetId}`, + { + query, + }, + metaDeserializer + ); +}; -export const getWidget = async (widgetQuery: string) => - WidgetAPIService.request({ - url: widgetQuery, - method: 'get', - }); +const updateWidget = async (widgetId: string, data: any, query?: RequestQuery) => { + return BaseAPIService.request( + `/widgets/${widgetId}`, + { + query, + method: 'put', + data, + }, + metaDeserializer + ); +}; -export const updateWidget = async (widgetId: string, widget, group: string) => - WidgetAPIService.request({ - url: encodeQueryToURL(`/widgets/${widgetId}`, { group }), - method: 'put', - data: widget, - }); +const deleteWidgets = async (widgetId: string, query?: RequestQuery) => { + return BaseAPIService.request( + `/widgets/${widgetId}`, + { + query, + method: 'delete', + }, + metaDeserializer + ); +}; -export const deleteWidgets = async (widgetID: string, group: string) => - WidgetAPIService.request({ - url: encodeQueryToURL(`/widgets/${widgetID}`, { group }), - method: 'delete', - }); +const getWidgetSlug = async (keyword: string, query?: RequestQuery) => { + const params = { keyword, type: 'counter' }; + return BaseAPIService.request('/widgets/slug', { query: merge(params, query) }, metaDeserializer); +}; -export const handleWidgetForm = async ( +const handleWidgetForm = async ( newWidget: boolean, - widget, + data: any, widgetId: string, - group: string -) => (newWidget ? addWidget(widget, group) : updateWidget(widgetId, widget, group)); + query?: RequestQuery +) => { + return newWidget ? addWidget(data, query) : updateWidget(widgetId, data, query); +}; -export const getWidgetSlug = async (keyword: string, group: string, type: string = 'counter') => - WidgetAPIService.request({ - url: encodeQueryToURL('/widgets/slug', { keyword, group, type }), - }); +export default { + getAllWidgets, + addWidget, + getWidget, + updateWidget, + deleteWidgets, + getWidgetSlug, + handleWidgetForm, +}; diff --git a/packages/earth-admin/src/styles/config.scss b/packages/earth-admin/src/styles/config.scss index bbaf7396..d1c43596 100644 --- a/packages/earth-admin/src/styles/config.scss +++ b/packages/earth-admin/src/styles/config.scss @@ -21,7 +21,7 @@ $marapp-primary-font: 'Roboto'; $marapp-secondary-font: 'Roboto'; $marapp-icon-font: 'icon-font'; -$marapp-color-sucess: #1EBE28!default; +$marapp-color-success: #28A745!default; $marapp-color-error: #FC4349!default; $marapp-primary-color: #0099A1; diff --git a/packages/earth-admin/src/utils/hooks.tsx b/packages/earth-admin/src/utils/hooks.tsx index 3a415813..4b6bed6d 100644 --- a/packages/earth-admin/src/utils/hooks.tsx +++ b/packages/earth-admin/src/utils/hooks.tsx @@ -17,29 +17,40 @@ specific language governing permissions and limitations under the License. */ +import { groupBy } from 'lodash'; import { noop } from 'lodash/fp'; +import qs from 'query-string'; import { useSWRInfinite } from 'swr'; +import { generateCacheKey, RequestQuery } from '@app/services'; + /** * Custom hook that integrates useSWRInfinite with component - * @param getQuery Function responsible for returning the api url - * @param fetcher Function queries the api - * @param options + * @param getQueryFn: Function responsible for returning the request query + * @param fetcher: API service which fetches the data + * @param options: */ export function useInfiniteList( - getQuery: (cursor: number | string) => string, + getQueryFn: (cursor: string) => { query: RequestQuery; resourceType: string }, fetcher: (any) => Promise, options: object = {} ) { - const wrappedQuery = (pageIndex: number, previousPageData: any): string => { - // reached the end - if (previousPageData && !previousPageData.data) { - return null; + const swrKeyLoader = (pageIndex: number, previousPage: any): string => { + if (previousPage && !previousPage.data) { + return null; // reached the end; } - const cursor = pageIndex === 0 ? -1 : previousPageData.pagination.nextCursor; + const cursor = pageIndex === 0 ? -1 : previousPage?.meta?.pagination?.nextCursor; + const { query, resourceType } = getQueryFn(cursor); + + return generateCacheKey(resourceType, query); + }; + const wrappedFetcher = async (pathQuery: string) => { + const [resource, query] = pathQuery.split('?'); + const parsed = qs.parse(query); - return getQuery(cursor); + return fetcher(parsed); }; + const { data: response = [], error, @@ -48,13 +59,18 @@ export function useInfiniteList( setSize, mutate, revalidate, - } = useSWRInfinite(wrappedQuery, fetcher, options); + } = useSWRInfinite(swrKeyLoader, wrappedFetcher, options); const items = mergePages(response); - const [firstPage] = response; - const lastPage = response[response.length - 1]; - const totalResults = firstPage?.total; - const isNoMore = !lastPage?.pagination.nextCursor; + const [firstPage = {}] = response; + const lastPage = response[response.length - 1] || {}; + const totalResults = firstPage?.meta?.results; + const filters = firstPage?.meta?.filters || []; + const filtersWithLabel = filters.map((f) => ({ + ...f, + label: f.value, + })); + const isNoMore = !lastPage?.meta?.pagination?.nextCursor; const awaitMore = !isValidating && !isNoMore; const returnValues = { @@ -69,6 +85,7 @@ export function useInfiniteList( isNoMore, totalResults, }, + filters: groupBy(filtersWithLabel, 'key'), revalidate, mutate, error, @@ -78,14 +95,23 @@ export function useInfiniteList( } export function useInfiniteListPaged( - getQuery: (pageIndex: number) => string, + getQueryFn: (pageIndex: number) => { query: RequestQuery; resourceType: string }, fetcher: (any) => Promise, options: object = {} ) { - const wrappedQuery = (pageIndex: number): string => { + const swrKeyLoader = (pageIndex: number): string => { const offsetPageIndex = pageIndex + 1; - return getQuery(offsetPageIndex); + const { query, resourceType } = getQueryFn(offsetPageIndex); + + return generateCacheKey(resourceType, query); + }; + const wrappedFetcher = async (pathQuery: string) => { + const [resource, query] = pathQuery.split('?'); + const parsed = qs.parse(query); + + return fetcher(parsed); }; + const { data: response = [], error, @@ -94,11 +120,11 @@ export function useInfiniteListPaged( setSize, mutate, revalidate, - } = useSWRInfinite(wrappedQuery, fetcher, options); + } = useSWRInfinite(swrKeyLoader, wrappedFetcher, options); const items = mergePages(response); - const isNoMore = items.data.length >= items.total; - const totalResults = items.total; + const isNoMore = items?.data.length >= items?.meta?.results; + const totalResults = items?.meta?.results; const awaitMore = !isValidating && !isNoMore; const returnValues = { @@ -123,12 +149,15 @@ export function useInfiniteListPaged( interface IMergedResults { data: any[]; - pagination?: { - size: number; - total: number; - nextCursor?: string; + meta: { + filters: Array<{ key: string; value: any; count: number }>; + pagination?: { + size: number; + total: number; + nextCursor?: string; + }; + results?: number; }; - total?: number; } /** diff --git a/packages/earth-admin/src/utils/index.ts b/packages/earth-admin/src/utils/index.ts index ad34c5c2..05482629 100644 --- a/packages/earth-admin/src/utils/index.ts +++ b/packages/earth-admin/src/utils/index.ts @@ -18,22 +18,14 @@ */ import { navigate } from 'gatsby'; +import { capitalize } from 'lodash'; import moment from 'moment'; import queryStringEncode from 'query-string-encode'; import { RefObject } from 'react'; -import { capitalize } from 'lodash'; import { ADMIN_PAGES } from '@app/components/sidebar-select/model'; import { BASE_URL } from '@app/config'; -const JSONAPIDeserializer = require('ts-jsonapi').Deserializer; - -const DeserializerService = new JSONAPIDeserializer({ - keyForAttribute: (attribute: any) => { - return attribute; - }, -}); - /** * Wrapper over navigate that takes into account baseURL. */ @@ -45,11 +37,6 @@ export const routeToPage = (targetPath: string, stripBase: boolean = false) => { navigate(path); }; -/** - * Deserializer - */ -export const deserializeData = (data) => DeserializerService.deserialize(data); - /** * Url encode */ diff --git a/packages/earth-map/.env.sample b/packages/earth-map/.env.sample index eb6fb466..34f71734 100644 --- a/packages/earth-map/.env.sample +++ b/packages/earth-map/.env.sample @@ -16,6 +16,7 @@ REACT_APP_AUTH0_NAMESPACE='https://marapp.org' # required REACT_APP_GTM_TAG='' # optional REACT_APP_ENABLE_PUBLIC_ACCESS='false' # optional +REACT_APP_EXTERNAL_IDP_URL='' # optional NODE_PATH=src SASS_PATH=node_modules:src diff --git a/packages/earth-map/README.md b/packages/earth-map/README.md index 7761e6d6..dc265e06 100755 --- a/packages/earth-map/README.md +++ b/packages/earth-map/README.md @@ -37,6 +37,7 @@ The following environment variables are required by the application. | `REACT_APP_ADMIN_URL` | The URL for admin component (e.g. `/admin/`) | | `REACT_APP_GTM_TAG` | Google Tag Manager ID | | `REACT_APP_ENABLE_PUBLIC_ACCESS` | Enable unauthenticated access | +| `REACT_APP_EXTERNAL_IDP_URL` | External Identity Provider URL | ## Getting started @@ -563,7 +564,7 @@ $marapp-primary-font: 'Primary font'; $marapp-secondary-font: 'Secondary font'; $marapp-icon-font: 'icon-font'; -$marapp-color-sucess: #hex; +$marapp-color-success: #hex; $marapp-color-error: #hex; $marapp-primary-color: #hex; diff --git a/packages/earth-map/package.json b/packages/earth-map/package.json index 054129c5..aae81f34 100644 --- a/packages/earth-map/package.json +++ b/packages/earth-map/package.json @@ -29,6 +29,7 @@ "react-intersection-observer": "^8.26.2", "three": "^0.103.0", "three-css2drender": "^1.0.0", + "ts-jsonapi": "^2.1.3", "url-join": "^4.0.1", "viewport-mercator-project": "^6.1.1", "yn": "^4.0.0" diff --git a/packages/earth-map/src/auth/auth0.tsx b/packages/earth-map/src/auth/auth0.tsx index b711569b..56fdbb19 100644 --- a/packages/earth-map/src/auth/auth0.tsx +++ b/packages/earth-map/src/auth/auth0.tsx @@ -102,7 +102,7 @@ export const Auth0Provider = ({ setIsAuthenticated(isAuthenticated); const accessToken = isAuthenticated ? await auth0FromHook.getTokenSilently() : null; - onSuccessHook({ token: accessToken }); + onSuccessHook({ accessToken, authClient: auth0FromHook }); const idToken = accessToken ? await auth0FromHook.getUser() : {}; @@ -163,7 +163,7 @@ export const Auth0Provider = ({ * grant and the refresh token from the cache. * @param options */ - const getToken = (options = {}) => { + const getAccessToken = (options = {}): Promise => { return client.getTokenSilently(options); }; @@ -187,7 +187,7 @@ export const Auth0Provider = ({ login, logout, getUser, - getToken, + getAccessToken, setupUserOrg: setSelectedGroup, }} > diff --git a/packages/earth-map/src/auth/hooks.tsx b/packages/earth-map/src/auth/hooks.tsx index bfc39eae..8e9f65c0 100644 --- a/packages/earth-map/src/auth/hooks.tsx +++ b/packages/earth-map/src/auth/hooks.tsx @@ -17,9 +17,16 @@ specific language governing permissions and limitations under the License. */ +import { Auth0Client } from '@auth0/auth0-spa-js'; import axios from 'axios'; import { routeToPage } from 'utils'; +import { + reqNoopInterceptor, + resErrorInterceptor, + resSuccessInterceptor, +} from '@marapp/earth-shared'; + /** * Routes the user to the right place after login. * @param params @@ -35,10 +42,16 @@ export const onRedirectCallback = (params: { targetUrl?: string } = {}) => { * Configure behaviour in case of successful login. * @param params */ -export const onSuccessHook = (params: { token?: string } = {}) => { - const { token } = params; - if (token) { - axios.defaults.headers.common.Authorization = `Bearer ${token}`; +export const onSuccessHook = (params: { accessToken?: string; authClient?: Auth0Client } = {}) => { + if (params.accessToken) { + axios.defaults.headers.common.Authorization = `Bearer ${params.accessToken}`; + } + if (params.authClient) { + axios.interceptors.request.use(reqNoopInterceptor()); + axios.interceptors.response.use( + resSuccessInterceptor(), + resErrorInterceptor({ authClient: params.authClient }) + ); } }; diff --git a/packages/earth-map/src/auth/model.tsx b/packages/earth-map/src/auth/model.tsx index 1df4deab..4a737d9c 100644 --- a/packages/earth-map/src/auth/model.tsx +++ b/packages/earth-map/src/auth/model.tsx @@ -40,7 +40,7 @@ export interface Auth0 { logout?(o?: LogoutOptions): void; login?(o?: BaseLoginOptions): void; getUser?(o?: GetUserOptions): void; - getToken?(o?: GetTokenWithPopupOptions): void; + getAccessToken?(o?: GetTokenWithPopupOptions): Promise; setupUserOrg?(org: string): void; } diff --git a/packages/earth-map/src/components/header/component.tsx b/packages/earth-map/src/components/header/component.tsx index ad48b49a..1eba1aa6 100644 --- a/packages/earth-map/src/components/header/component.tsx +++ b/packages/earth-map/src/components/header/component.tsx @@ -25,7 +25,7 @@ import { remove } from 'lodash'; import { EPanels } from 'modules/sidebar/model'; import React, { useContext, useEffect, useState } from 'react'; import Link from 'redux-first-router-link'; -import { fetchStats } from 'services/stats'; +import OrganizationService from 'services/OrganizationService'; import { APP_LOGO } from 'theme'; import { AppContextSwitcher, checkRole } from '@marapp/earth-shared'; @@ -77,7 +77,7 @@ const Header = (props: IProps) => { useEffect(() => { (async () => { try { - const response: any = await fetchStats({ group: groups.join(',') }); + const response = await OrganizationService.fetchStats({ group: groups.join(',') }); setAvailableGroups(response.data); } catch (err) { console.error(err); diff --git a/packages/earth-map/src/components/map/component.tsx b/packages/earth-map/src/components/map/component.tsx index 4718f616..d07feeba 100644 --- a/packages/earth-map/src/components/map/component.tsx +++ b/packages/earth-map/src/components/map/component.tsx @@ -25,6 +25,7 @@ import experienceIMG from 'images/pins/experience-marker.svg'; import debounce from 'lodash/debounce'; import React, { useContext } from 'react'; import isEqual from 'react-fast-compare'; +import Link from 'redux-first-router-link'; import { APP_ABOUT } from 'theme'; import { Map, Spinner, UserMenu } from '@marapp/earth-shared'; @@ -357,6 +358,7 @@ function UserMenuWrapper(props) { Profile} onLogin={login} onLogout={logout} onSignUp={() => login({ initialScreen: 'signUp' })} diff --git a/packages/earth-map/src/components/sidebar/component.tsx b/packages/earth-map/src/components/sidebar/component.tsx index 27ad60bc..168fce01 100644 --- a/packages/earth-map/src/components/sidebar/component.tsx +++ b/packages/earth-map/src/components/sidebar/component.tsx @@ -48,11 +48,6 @@ interface ISidebarPanel { class Sidebar extends React.Component { private sidebarPanel: any; - public componentWillUnmount() { - const { setSidebarOpen } = this.props; - setSidebarOpen(false); - } - public onClose = () => { const { setSidebarOpen } = this.props; setSidebarOpen(false); diff --git a/packages/earth-map/src/config/index.tsx b/packages/earth-map/src/config/index.tsx index a961dec6..c2f753ba 100644 --- a/packages/earth-map/src/config/index.tsx +++ b/packages/earth-map/src/config/index.tsx @@ -33,3 +33,5 @@ export const GTM_TAG = process.env.REACT_APP_GTM_TAG; export const ENABLE_PUBLIC_ACCESS = yn(process.env.REACT_APP_ENABLE_PUBLIC_ACCESS, { default: false, }); + +export const REACT_APP_EXTERNAL_IDP_URL = process.env.REACT_APP_EXTERNAL_IDP_URL || ''; diff --git a/packages/earth-map/src/index.tsx b/packages/earth-map/src/index.tsx index 7efb9a7d..ae730145 100755 --- a/packages/earth-map/src/index.tsx +++ b/packages/earth-map/src/index.tsx @@ -46,7 +46,7 @@ ReactDOM.render( onSuccessHook={onSuccessHook} onFailureHook={onFailureHook} useRefreshTokens={true} - cacheLocation={'localstorage'} + cacheLocation={'memory'} > , diff --git a/packages/earth-map/src/pages/change-email/component.tsx b/packages/earth-map/src/pages/change-email/component.tsx index b1f92327..d29e4f7d 100644 --- a/packages/earth-map/src/pages/change-email/component.tsx +++ b/packages/earth-map/src/pages/change-email/component.tsx @@ -20,7 +20,7 @@ import { useAuth0 } from 'auth/auth0'; import React, { useEffect } from 'react'; import { replace } from 'redux-first-router'; -import { changeEmailConfirmation } from 'services/users'; +import ProfileService from 'services/ProfileService'; import { Spinner } from '@marapp/earth-shared'; @@ -39,8 +39,8 @@ export default function ChangeEmailComponent() { const error_description = params.get('error_description'); if (accessToken) { - const response: any = await changeEmailConfirmation({ accessToken }); - if (response && response?.success) { + const response = await ProfileService.changeEmailConfirmation({ accessToken }); + if (response && response?.data?.success) { alert('Email change successful. Please login using the new credentials.'); // Auth0 sessions are reset when a user’s email or password changes; // force a re-login if email change request successful; @@ -56,7 +56,7 @@ export default function ChangeEmailComponent() { } catch (e) { console.error(e); } finally { - replace('/earth'); + replace('/profile'); } })(); }); diff --git a/packages/earth-map/src/pages/profile/component.tsx b/packages/earth-map/src/pages/profile/component.tsx index d12b7b54..0af5348a 100644 --- a/packages/earth-map/src/pages/profile/component.tsx +++ b/packages/earth-map/src/pages/profile/component.tsx @@ -1,41 +1,201 @@ import { Auth0Context } from 'auth/auth0'; +import { REACT_APP_EXTERNAL_IDP_URL } from 'config'; +import { capitalize, identity, omit, pickBy } from 'lodash'; import React, { useContext, useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; import Link from 'redux-first-router-link'; -import { fetchProfile } from 'services/users'; +import ProfileService from 'services/ProfileService'; import { APP_LOGO } from 'theme'; -import { InlineEditCard, Spinner, UserMenu } from '@marapp/earth-shared'; +import { + InlineEditCard, + Input, + setupErrors, + Spinner, + UserMenu, + validEmailRule, + valueChangedRule, +} from '@marapp/earth-shared'; interface IProps { page: string; } +enum RESET_PASSWORD_STATE { + INITIAL, + SENDING, + SENT, + NOTIFICATION_DISMISS, +} + export function ProfileComponent(props: IProps) { const { page } = props; + + const { getValues, register, formState, errors: formErrors } = useForm({ + mode: 'onChange', + }); + const { userData, logout, login, isAuthenticated } = useContext(Auth0Context); const [isLoading, setIsLoading] = useState(true); const [userName, setUserName] = useState(''); + const [pendingEmail, setPendingEmail] = useState(null); + const [serverErrors, setServerErrors] = useState(); + const [userProfile, setUserProfile] = useState({ + firstName: '', + lastName: '', + name: '', + groups: [], + }); + const [resetPasswordState, setResetPasswordState] = useState(RESET_PASSWORD_STATE.INITIAL); + const [markedOrgsForLeave, setMarkedOrgsForLeave] = useState({}); + const [userRoles, setUserRoles] = useState({}); + const [isDeletingAccountOpen, setIsDeletingAccountOpen] = useState(false); + const [confirmDeleteAccount, setConfirmDeleteAccount] = useState(false); + + const { touched, isValid } = formState; + const renderErrorFor = setupErrors(formErrors, touched); + + const processUserName = ({ firstName, lastName, name }) => { + setUserName(firstName && lastName ? `${firstName} ${lastName}` : name); + }; + + const groupRolesByOrganization = (groups) => { + const result = groups.reduce((acc, c) => { + const groupTokens = c.name.split('-'); - const userRoles = Object.keys(userData.roles); + const groupRole = capitalize(groupTokens.pop()); + const groupName = groupTokens.join('-'); + + acc[groupName] = acc[groupName] || []; + acc[groupName].push(groupRole); + + return acc; + }, {}); + + setUserRoles(result); + }; useEffect(() => { (async () => { - const profile: any = await fetchProfile(); + const response = await ProfileService.fetchProfile({ include: 'groups' }); - setUserName( - profile.firstName && profile.lastName - ? `${profile.firstName} ${profile.lastName}` - : profile.name - ); + setUserProfile(response.data); + processUserName(response.data); + groupRolesByOrganization(response.data?.groups); + + response.data?.pendingEmail && setPendingEmail(response.data.pendingEmail); setIsLoading(false); })(); }, []); + async function onSubmitName(e?, setIsEditing?, setIsLoading?, setServerErrors?) { + e.preventDefault(); + + const formData = getValues(); + try { + setIsLoading && setIsLoading(true); + + const response = await ProfileService.updateProfile(formData); + setUserProfile(response.data); + processUserName(response.data); + + setIsEditing && setIsEditing(false); + setIsLoading && setIsLoading(false); + } catch (error) { + setIsLoading && setIsLoading(false); + setServerErrors && setServerErrors(error.data?.errors); + } + } + + async function sendResetEmail(e) { + e.preventDefault(); + + setResetPasswordState(RESET_PASSWORD_STATE.SENDING); + + await ProfileService.resetPassword(); + + setResetPasswordState(RESET_PASSWORD_STATE.SENT); + } + + function switchMarkOrgForLeave(e, org) { + e.preventDefault(); + + setMarkedOrgsForLeave( + pickBy({ ...markedOrgsForLeave, [org]: !markedOrgsForLeave[org] }, identity) + ); + } + + async function onSubmitOrgLeave(e?, setIsEditing?, setIsLoading?, setServerErrors?) { + e.preventDefault(); + + const orgsToLeave = Object.keys(markedOrgsForLeave); + + setIsLoading && setIsLoading(true); + + try { + await ProfileService.leaveOrganizations(orgsToLeave); + + setUserRoles({ ...omit(userRoles, orgsToLeave) }); + + setIsEditing && setIsEditing(false); + setIsLoading && setIsLoading(false); + } catch (error) { + setIsLoading && setIsLoading(false); + setServerErrors && setServerErrors(error.data?.errors); + } + } + + async function onEmailChange(e?, setIsEditing?, setIsLoading?, setServerErrors?) { + e.preventDefault(); + const formData = getValues(); + + try { + setIsLoading && setIsLoading(true); + const response = await ProfileService.changeEmail(formData); + setPendingEmail(response.data?.pendingEmail); + setIsEditing && setIsEditing(false); + setIsLoading && setIsLoading(false); + } catch (error) { + setIsLoading && setIsLoading(false); + setServerErrors && setServerErrors(error.data?.errors); + } + } + + async function onCancelEmailChange(e) { + e.preventDefault(); + + try { + const response = await ProfileService.cancelEmailChange(); + setUserProfile(response.data); + setPendingEmail(null); + } catch (error) { + setServerErrors && setServerErrors(error.data?.errors); + } + } + + async function deleteAccount(e?, setIsEditing?, setIsLoading?, setServerErrors?) { + e.preventDefault(); + + setIsLoading && setIsLoading(true); + + try { + await ProfileService.deleteAccount(); + + setIsEditing && setIsEditing(false); + setIsLoading && setIsLoading(false); + + await logout(); + } catch (error) { + setIsLoading && setIsLoading(false); + setServerErrors && setServerErrors(error.data?.errors); + } + } + return isLoading ? ( ) : ( -
+
- +
Profile} onLogin={login} onLogout={logout} onSignUp={() => login({ initialScreen: 'signUp' })} @@ -62,30 +223,63 @@ export function ProfileComponent(props: IProps) {
+ {resetPasswordState === RESET_PASSWORD_STATE.SENT && ( +
+
+ + An email has been sent to {userData.email} with a link to reset your password. + + +
+
+ )}
( - // <> - //
- // - //
- //
- // - //
- // - // )}> + {...(!REACT_APP_EXTERNAL_IDP_URL && { + render: ({ setIsEditing, setIsLoading, setServerErrors }) => ( + <> +
+ +
+
+ +
+ + ), + validForm: isValid && formState.dirty, + onSubmit: onSubmitName, + })} >

Name @@ -95,49 +289,72 @@ export function ProfileComponent(props: IProps) {

( - // <> - //
- // - //
- //
- //

- // After saving, we will send an email to your new email address to confirm the change. - //
- // Be sure to check your spam folder if you do not receive the email in a few minutes. - //

- //
- // - // )}> + onSubmit={onEmailChange} + validForm={isValid} + render={({ setIsEditing, setIsLoading, setServerErrors }) => ( + <> +
+ valueChangedRule(value, userData.email), + validEmailRule: validEmailRule(), + }, + })} + /> +
+
+

+ After saving, we will send an email to your new email address to confirm + the change. +
+ Be sure to check your spam folder if you do not receive the email in a few + minutes. +

+
+ + )} > -

- Email -

-

{userData.email}

+ {!pendingEmail && ( + <> +

+ Email +

+ {pendingEmail} +

{userData.email}

+ + )} + {pendingEmail && ( + <> +

+ Current email +

+

{userData.email}

+
+

+ New Email (Pending Confirmation) + +

+

{pendingEmail}

+ + )}
- ( - // <> - //
- // - //
- // - // )}> - > +

Password reset

@@ -147,31 +364,65 @@ export function ProfileComponent(props: IProps) { Be sure to check your spam folder if you do not receive the email in a few minutes.

-
-
- {userRoles.length > 0 && ( + {Object.keys(userRoles).length > 0 && ( +
( - // <> - //
- //

Organizations

- //
- //
Organization name
- //
Role
- // {userRoles.map(org => ( - // <> - //
{org}
- //
{userData.roles[org].join(', ')}
- // - // ))} - //
- //
- // - // )}> + render={({ setIsEditing, setIsLoading, setServerErrors }) => ( + <> +

+ Organizations +

+
+
Organization name
+
Role
+ {Object.keys(userRoles).map((org) => ( + <> +
{org}
+
+ {markedOrgsForLeave[org] ? ( + marked for removal + ) : ( + userRoles[org].join(', ') + )} +
+
+ +
+ + ))} +
+ + )} + validForm={Object.keys(markedOrgsForLeave).length > 0} + onSubmit={onSubmitOrgLeave} + onCancel={() => setMarkedOrgsForLeave({})} >

Organizations @@ -179,17 +430,68 @@ export function ProfileComponent(props: IProps) {
Organization name
Role
- {userRoles.map((org) => ( + {Object.keys(userRoles).map((org) => ( <>
{org}
- {userData.roles[org].join(', ')} + {userRoles[org].join(', ')}
))}
- )} +

+ )} +
+ ( + <> +

+ Account access +

+

+ Deleting your account will remove you from all public & private + workspaces, and +
+ erase all of your settings. +

+

+ +

+ + )} + submitButtonText={'DELETE'} + submitButtonVariant={'danger'} + manualOpen={isDeletingAccountOpen} + onCancel={() => { + setIsDeletingAccountOpen(false); + setConfirmDeleteAccount(false); + }} + onSubmit={deleteAccount} + validForm={confirmDeleteAccount} + > +

+ Account access +

+ +
diff --git a/packages/earth-map/src/sagas/earth/index.ts b/packages/earth-map/src/sagas/earth/index.ts index d913ed34..f4a7d32e 100644 --- a/packages/earth-map/src/sagas/earth/index.ts +++ b/packages/earth-map/src/sagas/earth/index.ts @@ -29,7 +29,7 @@ import { loadDataIndexes } from 'sagas/layers'; import { LOCATION_QUERY } from 'sagas/model'; import { nextPage } from 'sagas/places'; import { getGroup, ignoreRedirectsTo } from 'sagas/saga-utils'; -import { fetchPlaces } from 'services/places'; +import PlacesService from 'services/PlacesService'; const ignoreRedirectsToEarth = ignoreRedirectsTo('EARTH'); @@ -58,7 +58,7 @@ function* loadPlaces() { // PLACES const places: IPlace[] = yield all({ - featured: call(fetchPlaces, { + featured: call(PlacesService.fetchPlaces, { select: 'slug,name,id,organization,type', page: { size: 100 }, filter: 'featured==true', diff --git a/packages/earth-map/src/sagas/layers/index.ts b/packages/earth-map/src/sagas/layers/index.ts index 87a26db5..516fd383 100644 --- a/packages/earth-map/src/sagas/layers/index.ts +++ b/packages/earth-map/src/sagas/layers/index.ts @@ -39,8 +39,8 @@ import { setWidgets, setWidgetsError, setWidgetsLoading } from 'modules/widgets/ import { replace } from 'redux-first-router'; import { call, put, select, takeLatest } from 'redux-saga/effects'; import { flattenLayerConfig, getGroup, getLayers, onlyMatch } from 'sagas/saga-utils'; -import { fetchDataIndexes } from 'services/data-indexes'; -import { fetchLayers } from 'services/layers'; +import DashboardsService from 'services/DashboardsService'; +import LayersService from 'services/LayersService'; import { serializeFilters } from '@marapp/earth-shared'; @@ -71,7 +71,7 @@ function* loadActiveLayers({ payload }) { }), group: group.join(','), }; - const { data: layers } = yield call(fetchLayers, options); + const { data: layers } = yield call(LayersService.fetchLayers, options); const decoratedLayers: ILayer[] = layers.map(flattenLayerConfig); yield put(setListActiveLayers(decoratedLayers)); @@ -114,7 +114,7 @@ export function* nextPage({ payload }) { ...(pageSize && { page: { size: pageSize } }), group: group.toString(), }; - const page = yield call(fetchLayers, options); + const page = yield call(LayersService.fetchLayers, options); const { data: results, meta } = page; yield put( @@ -132,7 +132,7 @@ export function* loadDataIndexes({ payload }) { const group = yield select(getGroup); try { - const indexes: IIndex[] = yield call(fetchDataIndexes, { + const indexes: IIndex[] = yield call(DashboardsService.fetchDashboards, { ...DATA_INDEX_QUERY, ...{ group: group.toString() }, }); diff --git a/packages/earth-map/src/sagas/location/index.ts b/packages/earth-map/src/sagas/location/index.ts index 25dfd503..a87aa8ab 100644 --- a/packages/earth-map/src/sagas/location/index.ts +++ b/packages/earth-map/src/sagas/location/index.ts @@ -36,7 +36,7 @@ import { replace } from 'redux-first-router'; import { call, cancelled, delay, put, select, takeLatest } from 'redux-saga/effects'; import { loadDataIndexes } from 'sagas/layers'; import { ignoreRedirectsTo } from 'sagas/saga-utils'; -import { fetchPlace } from 'services/places'; +import PlacesService from 'services/PlacesService'; let PREV_SLUG = null; const ignoreRedirectsToLocation = ignoreRedirectsTo('LOCATION'); @@ -65,7 +65,7 @@ function* toLocation({ payload, meta }) { yield put(setMetricsLoading(true)); try { - const { data }: { data: IPlace } = yield call(fetchPlace, slug, { + const { data }: { data: IPlace } = yield call(PlacesService.fetchPlaceById, slug, { include: 'metrics', group: organization, }); diff --git a/packages/earth-map/src/sagas/places/index.ts b/packages/earth-map/src/sagas/places/index.ts index cf61470e..3108c3fa 100644 --- a/packages/earth-map/src/sagas/places/index.ts +++ b/packages/earth-map/src/sagas/places/index.ts @@ -28,7 +28,7 @@ import { } from 'modules/places/actions'; import { all, call, put, select, takeLatest } from 'redux-saga/effects'; import { getGroup, getPlaces } from 'sagas/saga-utils'; -import { fetchPlaces } from 'services/places'; +import PlacesService from 'services/PlacesService'; import { serializeFilters } from '@marapp/earth-shared'; @@ -61,7 +61,7 @@ export function* nextPage({ payload }) { const filterQuery = serializeFilters(filters); yield put(setPlacesSearchLoading(true)); - const page = yield call(fetchPlaces, { + const page = yield call(PlacesService.fetchPlaces, { ...LOCATION_QUERY, ...(!!userInput && { search: userInput }), ...(!!filters && { filter: filterQuery }), diff --git a/packages/earth-map/src/services/DashboardsService.tsx b/packages/earth-map/src/services/DashboardsService.tsx new file mode 100644 index 00000000..973b1ac7 --- /dev/null +++ b/packages/earth-map/src/services/DashboardsService.tsx @@ -0,0 +1,30 @@ +/* + Copyright 2018-2020 National Geographic Society + + Use of this software does not constitute endorsement by National Geographic + Society (NGS). The NGS name and NGS logo may not be used for any purpose without + written permission from NGS. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +import { BaseAPIService, RequestQuery } from './base/APIBase'; + +const fetchDashboardById = async (id: string, query?: RequestQuery): Promise => { + return BaseAPIService.request(`/dashboards/${id}`, { query }); +}; + +const fetchDashboards = async (query?: RequestQuery): Promise => { + return BaseAPIService.request('/dashboards', { query }); +}; + +export default { fetchDashboardById, fetchDashboards }; diff --git a/packages/earth-map/src/services/LayersService.tsx b/packages/earth-map/src/services/LayersService.tsx new file mode 100644 index 00000000..05ddceca --- /dev/null +++ b/packages/earth-map/src/services/LayersService.tsx @@ -0,0 +1,30 @@ +/* + Copyright 2018-2020 National Geographic Society + + Use of this software does not constitute endorsement by National Geographic + Society (NGS). The NGS name and NGS logo may not be used for any purpose without + written permission from NGS. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; + +const fetchLayerById = async (id: string, query?: RequestQuery): Promise => { + return BaseAPIService.request(`/layers/${id}`, { query }, metaDeserializer); +}; + +const fetchLayers = async (query?: RequestQuery): Promise => { + return BaseAPIService.request('/layers', { query }, metaDeserializer); +}; + +export default { fetchLayerById, fetchLayers }; diff --git a/packages/earth-admin/src/services/interceptors.tsx b/packages/earth-map/src/services/MetricService.tsx similarity index 76% rename from packages/earth-admin/src/services/interceptors.tsx rename to packages/earth-map/src/services/MetricService.tsx index b54468c2..fde26d93 100644 --- a/packages/earth-admin/src/services/interceptors.tsx +++ b/packages/earth-map/src/services/MetricService.tsx @@ -17,4 +17,10 @@ specific language governing permissions and limitations under the License. */ -export {}; +import { BaseAPIService, RequestQuery } from './base/APIBase'; + +const fetchMetricById = async (id: string, query?: RequestQuery): Promise => { + return BaseAPIService.request(`/metrics/${id}`, { query }); +}; + +export default { fetchMetricById }; diff --git a/packages/earth-admin/src/components/data-listing/auth0-list-item/index.tsx b/packages/earth-map/src/services/OrganizationService.tsx similarity index 74% rename from packages/earth-admin/src/components/data-listing/auth0-list-item/index.tsx rename to packages/earth-map/src/services/OrganizationService.tsx index 9819793a..a551ed1f 100644 --- a/packages/earth-admin/src/components/data-listing/auth0-list-item/index.tsx +++ b/packages/earth-map/src/services/OrganizationService.tsx @@ -16,4 +16,11 @@ CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -export { default as Auth0ListItem } from './Auth0ListItem'; + +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; + +const fetchStats = async (query?: RequestQuery): Promise => { + return BaseAPIService.request(`/organizations/stats`, { query }, metaDeserializer); +}; + +export default { fetchStats }; diff --git a/packages/earth-map/src/services/PlacesService.tsx b/packages/earth-map/src/services/PlacesService.tsx new file mode 100644 index 00000000..f296ed4d --- /dev/null +++ b/packages/earth-map/src/services/PlacesService.tsx @@ -0,0 +1,30 @@ +/* + Copyright 2018-2020 National Geographic Society + + Use of this software does not constitute endorsement by National Geographic + Society (NGS). The NGS name and NGS logo may not be used for any purpose without + written permission from NGS. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; + +const fetchPlaceById = async (id: string, query?: RequestQuery): Promise => { + return BaseAPIService.request(`/locations/${id}`, { query }, metaDeserializer); +}; + +const fetchPlaces = async (query?: RequestQuery): Promise => { + return BaseAPIService.request('/locations', { query }, metaDeserializer); +}; + +export default { fetchPlaceById, fetchPlaces }; diff --git a/packages/earth-map/src/services/ProfileService.tsx b/packages/earth-map/src/services/ProfileService.tsx new file mode 100644 index 00000000..154c37dc --- /dev/null +++ b/packages/earth-map/src/services/ProfileService.tsx @@ -0,0 +1,79 @@ +/* + Copyright 2018-2020 National Geographic Society + + Use of this software does not constitute endorsement by National Geographic + Society (NGS). The NGS name and NGS logo may not be used for any purpose without + written permission from NGS. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +import { BaseAPIService, metaDeserializer, RequestQuery } from './base/APIBase'; + +const fetchProfile = async (query?: RequestQuery): Promise => { + return BaseAPIService.request('/users/profile', { query }, metaDeserializer); +}; + +const updateProfile = async (data: any, query?: RequestQuery): Promise => { + return BaseAPIService.request('/users/profile', { query, method: 'put', data }, metaDeserializer); +}; + +const changeEmail = async (data: any, query?: RequestQuery): Promise => { + return BaseAPIService.request( + '/users/profile/change-email', + { method: 'post', query, data }, + metaDeserializer + ); +}; + +const changeEmailConfirmation = async (query?: RequestQuery): Promise => { + return BaseAPIService.request('/users/profile/change-email', { query }, metaDeserializer); +}; + +const cancelEmailChange = async (query?: RequestQuery): Promise => { + return BaseAPIService.request( + 'users/profile/change-email', + { method: 'delete', query }, + metaDeserializer + ); +}; + +const resetPassword = async (query?: RequestQuery): Promise => { + return BaseAPIService.request( + '/users/profile/change-password', + { method: 'post', query }, + metaDeserializer + ); +}; + +const leaveOrganizations = async (data: any, query?: RequestQuery): Promise => { + return BaseAPIService.request( + '/users/profile/organizations', + { method: 'post', query, data }, + metaDeserializer + ); +}; + +const deleteAccount = async (): Promise => { + return BaseAPIService.request('/users/profile', { method: 'delete' }, metaDeserializer); +}; + +export default { + fetchProfile, + updateProfile, + changeEmail, + changeEmailConfirmation, + cancelEmailChange, + resetPassword, + leaveOrganizations, + deleteAccount, +}; diff --git a/packages/earth-map/src/services/WidgetsService.tsx b/packages/earth-map/src/services/WidgetsService.tsx new file mode 100644 index 00000000..925a824b --- /dev/null +++ b/packages/earth-map/src/services/WidgetsService.tsx @@ -0,0 +1,30 @@ +/* + Copyright 2018-2020 National Geographic Society + + Use of this software does not constitute endorsement by National Geographic + Society (NGS). The NGS name and NGS logo may not be used for any purpose without + written permission from NGS. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +import { BaseAPIService, RequestQuery } from './base/APIBase'; + +const fetchWidgetById = async (id: string, query?: RequestQuery): Promise => { + return BaseAPIService.request(`/widgets/${id}`, { query }); +}; + +const fetchWidgets = async (query?: RequestQuery): Promise => { + return BaseAPIService.request('/widgets', { query }); +}; + +export default { fetchWidgetById, fetchWidgets }; diff --git a/packages/earth-map/src/services/base/APIBase.ts b/packages/earth-map/src/services/base/APIBase.ts new file mode 100644 index 00000000..a19b325f --- /dev/null +++ b/packages/earth-map/src/services/base/APIBase.ts @@ -0,0 +1,91 @@ +/* + Copyright 2018-2020 National Geographic Society + + Use of this software does not constitute endorsement by National Geographic + Society (NGS). The NGS name and NGS logo may not be used for any purpose without + written permission from NGS. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +*/ + +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { API_URL } from 'config'; +import { merge } from 'lodash/fp'; +import { Deserializer } from 'ts-jsonapi'; +import { encodeQueryToURL } from 'utils/query'; + +export interface RequestQuery { + [key: string]: any; +} +export type RequestMethod = 'get' | 'post' | 'put' | 'delete'; +export interface RequestConfig { + query?: RequestQuery; + method?: RequestMethod; + data?: any; +} + +const TIMEOUT = 30000; + +export const DeserializerService = new Deserializer({ + keyForAttribute: (attribute: any) => { + return attribute; + }, +}); + +export const BaseAPIService = { + /** + * Creates an axios request based on path and request options. + * @param path: identifies the specific resource in the host + * @param config + * @param deserializer: JSON:API deserializer + */ + request: ( + path: string, + config: RequestConfig, + deserializer: (response: AxiosResponse) => any = BaseAPIService.deserialize + ) => { + const defaults = { query: {}, method: 'get', data: {} }; + const params = merge(defaults, config); + + const options: AxiosRequestConfig = { + baseURL: API_URL, + url: encodeQueryToURL(path, params.query), + method: params.method, + data: params.data, + timeout: TIMEOUT, + }; + + return new Promise((resolve, reject) => { + axios + .request(options) + .then((response) => resolve(deserializer(response))) + .catch((error) => + reject(error?.response?.data?.data ? deserializer(error?.response) : error?.response) + ); + }); + }, + /** + * JSON:API deserializer + * @param response + */ + deserialize: (response: AxiosResponse): any => { + return DeserializerService.deserialize(response.data); + }, +}; + +/** + * JSON:API meta deserializer + * @param response + */ +export const metaDeserializer = (response: AxiosResponse): any => { + return { data: DeserializerService.deserialize(response?.data), meta: response?.data?.meta }; +}; diff --git a/packages/earth-map/src/services/data-indexes/index.tsx b/packages/earth-map/src/services/data-indexes/index.tsx deleted file mode 100644 index 92b1fc5f..00000000 --- a/packages/earth-map/src/services/data-indexes/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -import { AxiosInstance } from 'axios'; -import { setup } from 'axios-cache-adapter'; -import { API_URL } from 'config'; -import Jsona, { SwitchCaseJsonMapper, SwitchCaseModelMapper } from 'jsona'; -import { encodeQueryToURL } from 'utils/query'; - -/** - * DataIndexes service class. - */ -class DataIndexesService { - private dataFormatter: Jsona; - private api: AxiosInstance; - - /** - * Creates dataFormatter and add it to service. - * @constructor - */ - constructor() { - this.configure(); - this.dataFormatter = new Jsona({ - modelPropertiesMapper: new SwitchCaseModelMapper(), - jsonPropertiesMapper: new SwitchCaseJsonMapper(), - }); - } - - public configure = () => { - this.api = setup({ baseURL: API_URL }); - }; - - /** - * request - * Creates an axios request based on type an options. - * @param {string} path - The path of the request. - */ - public request(path) { - return new Promise((resolve, reject) => { - this.api - .get(path) - .then((response) => { - resolve(this.dataFormatter.deserialize(response.data)); - }) - .catch((err) => { - reject(err); - }); - }); - } -} - -export const service = new DataIndexesService(); - -// ROUTES -export function fetchDataIndex(id, options = {}) { - const dataIndexQuery = encodeQueryToURL(`/dashboards/${id}`, options); - return service.request(dataIndexQuery); -} - -export function fetchDataIndexes(options = {}) { - const dataIndexQuery = encodeQueryToURL('dashboards', options); - return service.request(dataIndexQuery); -} - -export default service; diff --git a/packages/earth-map/src/services/index.ts b/packages/earth-map/src/services/index.ts new file mode 100644 index 00000000..100bc491 --- /dev/null +++ b/packages/earth-map/src/services/index.ts @@ -0,0 +1,7 @@ +export * from './DashboardsService'; +export * from './LayersService'; +export * from './MetricService'; +export * from './OrganizationService'; +export * from './PlacesService'; +export * from './ProfileService'; +export * from './WidgetsService'; diff --git a/packages/earth-map/src/services/layers/index.tsx b/packages/earth-map/src/services/layers/index.tsx deleted file mode 100644 index 09ab4d58..00000000 --- a/packages/earth-map/src/services/layers/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -import { AxiosInstance } from 'axios'; -import { setup } from 'axios-cache-adapter'; -import { API_URL } from 'config'; -import Jsona, { SwitchCaseJsonMapper, SwitchCaseModelMapper } from 'jsona'; -import { encodeQueryToURL } from 'utils/query'; - -/** - * Layers service class. - */ -class LayersService { - private dataFormatter: Jsona; - private api: AxiosInstance; - - constructor() { - this.configure(); - - this.dataFormatter = new Jsona({ - modelPropertiesMapper: new SwitchCaseModelMapper(), - jsonPropertiesMapper: new SwitchCaseJsonMapper(), - }); - } - - public configure = () => { - this.api = setup({ baseURL: API_URL }); - }; - - /** - * request - * Creates an axios request based on type an options. - * @param {string} path - The path of the request. - */ - public request(path) { - return new Promise((resolve, reject) => { - this.api - .get(path) - .then((response) => { - const result = this.dataFormatter.deserialize(response.data); - resolve({ - data: result, - meta: response.data.meta, - }); - }) - .catch((err) => { - reject(err); - }); - }); - } -} - -export const service = new LayersService(); - -export function fetchLayers(options = {}) { - const layerQuery = encodeQueryToURL('/layers', options); - return service.request(layerQuery); -} - -export function fetchLayer(id, options = {}) { - const layerQuery = encodeQueryToURL(`/layers/${id}`, options); - return service.request(layerQuery); -} - -export default service; diff --git a/packages/earth-map/src/services/metrics/index.tsx b/packages/earth-map/src/services/metrics/index.tsx deleted file mode 100644 index 6d5846f8..00000000 --- a/packages/earth-map/src/services/metrics/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -import { AxiosInstance } from 'axios'; -import { setup } from 'axios-cache-adapter'; -import { API_URL } from 'config'; -import Jsona, { SwitchCaseJsonMapper, SwitchCaseModelMapper } from 'jsona'; -import { encodeQueryToURL } from 'utils/query'; - -/** - * Metrics service class. - */ -class MetricsService { - private dataFormatter: Jsona; - private api: AxiosInstance; - - constructor() { - this.configure(); - - this.dataFormatter = new Jsona({ - modelPropertiesMapper: new SwitchCaseModelMapper(), - jsonPropertiesMapper: new SwitchCaseJsonMapper(), - }); - } - - public configure = () => { - this.api = setup({ baseURL: API_URL }); - }; - - /** - * request - * Creates an axios request based on type an options. - * @param {string} path - The path of the request. - */ - public request(path) { - return new Promise((resolve, reject) => { - this.api - .get(path) - .then((response) => { - resolve(this.dataFormatter.deserialize(response.data)); - }) - .catch((err) => { - reject(err); - }); - }); - } -} - -export const service = new MetricsService(); - -export function fetchMetric(id, options = {}) { - const metricQuery = encodeQueryToURL(`/metrics/${id}`, options); - return service.request(metricQuery); -} - -export default service; diff --git a/packages/earth-map/src/services/places/index.tsx b/packages/earth-map/src/services/places/index.tsx deleted file mode 100644 index 28776c03..00000000 --- a/packages/earth-map/src/services/places/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -import { AxiosInstance } from 'axios'; -import { setup } from 'axios-cache-adapter'; -import { API_URL } from 'config'; -import Jsona, { SwitchCaseJsonMapper, SwitchCaseModelMapper } from 'jsona'; -import { encodeQueryToURL } from 'utils/query'; - -/** - * Places service class. - */ -class PlacesService { - private dataFormatter: Jsona; - private api: AxiosInstance; - - /** - * Creates dataFormatter and add it to service. - * @constructor - */ - constructor() { - this.configure(); - - this.dataFormatter = new Jsona({ - modelPropertiesMapper: new SwitchCaseModelMapper(), - jsonPropertiesMapper: new SwitchCaseJsonMapper(), - }); - } - - public configure = () => { - this.api = setup({ baseURL: API_URL }); - }; - - /** - * request - * Creates an axios request based on type an options. - * @param {string} path - The path of the request. - */ - public request(path) { - return new Promise((resolve, reject) => { - this.api - .get(path) - .then((response) => { - const result = this.dataFormatter.deserialize(response.data); - resolve({ - data: result, - meta: response.data.meta, - }); - }) - .catch((err) => { - reject(err); - }); - }); - } -} - -export const service = new PlacesService(); - -// ROUTES -export function fetchPlace(id, options = {}) { - const locationsQuery = encodeQueryToURL(`/locations/${id}`, options); - return service.request(locationsQuery); -} - -export function fetchPlaces(options = {}) { - const locationsQuery = encodeQueryToURL(`/locations`, options); - return service.request(locationsQuery); -} - -export default service; diff --git a/packages/earth-map/src/services/request-access/index.tsx b/packages/earth-map/src/services/request-access/index.tsx deleted file mode 100644 index 7e55b428..00000000 --- a/packages/earth-map/src/services/request-access/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -const GOOGLE_FORM_ACTION = - 'https://docs.google.com/forms/d/e/1FAIpQLScte28qq7XwDBh11KUBlwskRP-Vng9X73AHjWL7k5VtDD-mjQ/formResponse'; - -// form entry names -const GOOGLE_FORM_USER_EMAIL = 'entry.816737307'; -const GOOGLE_FORM_USER_NAME = 'entry.1301721177'; -const GOOGLE_FORM_USER_ORG = 'entry.1570749264'; - -const GOOGLE_FORM_EAE = 'entry.870898483'; -const GOOGLE_FORM_TERMS = 'entry.1348777197'; - -class RequestAccessService { - public sendMessage(data) { - const formData = new FormData(); - - formData.append(GOOGLE_FORM_USER_EMAIL, data.userEmail); - formData.append(GOOGLE_FORM_USER_NAME, data.userName); - formData.append(GOOGLE_FORM_USER_ORG, data.userOrg); - formData.append(GOOGLE_FORM_EAE, data.agreeEAE); - formData.append(GOOGLE_FORM_TERMS, data.agreeTerms); - - // using default fetch API, axios does not suport no-cors mode - return fetch(GOOGLE_FORM_ACTION, { - method: 'POST', - mode: 'no-cors', - cache: 'no-cache', - credentials: 'omit', - headers: { - 'Content-Type': 'application/json', - }, - body: formData, - }).catch((error) => { - console.log('Request failure: ', error); - }); - } -} - -export const service = new RequestAccessService(); - -export default service; diff --git a/packages/earth-map/src/services/stats/index.tsx b/packages/earth-map/src/services/stats/index.tsx deleted file mode 100644 index 2e8ff84d..00000000 --- a/packages/earth-map/src/services/stats/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -import { AxiosInstance } from 'axios'; -import { setup } from 'axios-cache-adapter'; -import { API_URL } from 'config'; -import Jsona, { SwitchCaseJsonMapper, SwitchCaseModelMapper } from 'jsona'; -import { encodeQueryToURL } from 'utils/query'; - -/** - * Stats service class - * It is a singleton for not instanciate Jsona on each request. - */ -class StatsService { - private dataFormatter: Jsona; - private api: AxiosInstance; - - constructor() { - this.configure(); - - this.dataFormatter = new Jsona({ - modelPropertiesMapper: new SwitchCaseModelMapper(), - jsonPropertiesMapper: new SwitchCaseJsonMapper(), - }); - } - - public configure = () => { - this.api = setup({ baseURL: API_URL }); - }; - - /** - * request - * Creates an axios request based on type an options. - * @param {string} path - The path of the request. - */ - - public request(path) { - return new Promise((resolve, reject) => { - this.api - .get(path) - .then((response) => { - const result = this.dataFormatter.deserialize(response.data); - resolve({ - data: result, - meta: response.data.meta, - }); - }) - .catch((err) => { - reject(err); - }); - }); - } -} - -export const service = new StatsService(); - -export function fetchStats(options = {}) { - const layerQuery = encodeQueryToURL('/organizations/stats', options); - return service.request(layerQuery); -} - -export default service; diff --git a/packages/earth-map/src/services/users/index.tsx b/packages/earth-map/src/services/users/index.tsx deleted file mode 100644 index 96eef6b5..00000000 --- a/packages/earth-map/src/services/users/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -import { AxiosInstance } from 'axios'; -import { setup } from 'axios-cache-adapter'; -import { API_URL } from 'config'; -import Jsona, { SwitchCaseJsonMapper, SwitchCaseModelMapper } from 'jsona'; -import { encodeQueryToURL } from 'utils/query'; - -/** - * Users service class. - */ -class UsersService { - private dataFormatter: Jsona; - private api: AxiosInstance; - - constructor() { - this.configure(); - - this.dataFormatter = new Jsona({ - modelPropertiesMapper: new SwitchCaseModelMapper(), - jsonPropertiesMapper: new SwitchCaseJsonMapper(), - }); - } - - public configure = () => { - this.api = setup({ baseURL: API_URL }); - }; - - /** - * request - * Creates an axios request based on type an options. - * @param {string} path - The path of the request. - */ - public request(path) { - return new Promise((resolve, reject) => { - this.api - .get(path) - .then((response) => { - resolve(this.dataFormatter.deserialize(response.data)); - }) - .catch((err) => { - reject(err); - }); - }); - } -} - -export const service = new UsersService(); - -export function changeEmailConfirmation(options = {}) { - const widgetsQuery = encodeQueryToURL(`/users/profile/change-email`, options); - return service.request(widgetsQuery); -} - -export function fetchProfile(options = {}) { - const profileQuery = encodeQueryToURL(`/users/profile`, options); - return service.request(profileQuery); -} - -export default service; diff --git a/packages/earth-map/src/services/widgets/index.tsx b/packages/earth-map/src/services/widgets/index.tsx deleted file mode 100644 index ab7b4033..00000000 --- a/packages/earth-map/src/services/widgets/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2018-2020 National Geographic Society - - Use of this software does not constitute endorsement by National Geographic - Society (NGS). The NGS name and NGS logo may not be used for any purpose without - written permission from NGS. - - Licensed under the Apache License, Version 2.0 (the "License"); you may not use - this file except in compliance with the License. You may obtain a copy of the - License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. -*/ - -import { AxiosInstance } from 'axios'; -import { setup } from 'axios-cache-adapter'; -import { API_URL } from 'config'; -import Jsona, { SwitchCaseJsonMapper, SwitchCaseModelMapper } from 'jsona'; -import { encodeQueryToURL } from 'utils/query'; - -/** - * Widgets service class. - */ -class WidgetsService { - private dataFormatter: Jsona; - private api: AxiosInstance; - - constructor() { - this.configure(); - - this.dataFormatter = new Jsona({ - modelPropertiesMapper: new SwitchCaseModelMapper(), - jsonPropertiesMapper: new SwitchCaseJsonMapper(), - }); - } - - public configure = () => { - this.api = setup({ baseURL: API_URL }); - }; - - /** - * request - * Creates an axios request based on type an options. - * @param {string} path - The path of the request. - */ - public request(path) { - return new Promise((resolve, reject) => { - this.api - .get(path) - .then((response) => { - resolve(this.dataFormatter.deserialize(response.data)); - }) - .catch((err) => { - reject(err); - }); - }); - } -} - -export const service = new WidgetsService(); - -export function fetchWidgets(options = {}) { - const widgetsQuery = encodeQueryToURL(`/widget`, options); - return service.request(widgetsQuery); -} - -export function fetchWidget(id, options = {}) { - const widgetsQuery = encodeQueryToURL(`/widget/${id}`, options); - return service.request(widgetsQuery); -} - -export default service; diff --git a/packages/earth-map/src/styles/_base.scss b/packages/earth-map/src/styles/_base.scss index 8460ca21..17c661d2 100644 --- a/packages/earth-map/src/styles/_base.scss +++ b/packages/earth-map/src/styles/_base.scss @@ -92,3 +92,7 @@ body { .ng-color-primary { color: $marapp-primary-color; } + +.ng-background-success { + background: linear-gradient(0deg, rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.25)), $marapp-color-success; +} \ No newline at end of file diff --git a/packages/earth-map/src/styles/_overwrites.scss b/packages/earth-map/src/styles/_overwrites.scss index bfca7ce5..79e85538 100644 --- a/packages/earth-map/src/styles/_overwrites.scss +++ b/packages/earth-map/src/styles/_overwrites.scss @@ -36,6 +36,28 @@ } } +.ng-button-danger { + background: $marapp-red-1; + border: 1px solid $marapp-red-1; + color: $marapp-gray-0; + &:hover { + background: $marapp-red-1; + border: 1px solid $marapp-red-1; + color: $marapp-gray-0; + } + &:active, &:focus, &.active { + background: $marapp-red-1; + border: 1px solid $marapp-red-1; + color: $marapp-gray-0; + } + &:disabled { + background: $marapp-red-1; + color: $marapp-gray-0; + border: 1px solid $marapp-red-1; + opacity: .65; + } +} + .ng-small-select { min-width: 100px; width: auto; diff --git a/packages/earth-map/src/styles/_variables.scss b/packages/earth-map/src/styles/_variables.scss index 659859a8..646cec06 100644 --- a/packages/earth-map/src/styles/_variables.scss +++ b/packages/earth-map/src/styles/_variables.scss @@ -102,9 +102,11 @@ $theme-primary-muted-background: lighten($theme-primary-background, 25%); $base-heading-font-family: $marapp-primary-font; $base-body-font-family: $marapp-secondary-font; +$form-focus-border--dark: $marapp-primary-color; + $global-link-color: $marapp-gray-0; // Shadows $shadow-small: 0px 2px 4px rgba(0, 0, 0, 0.075); $shadow-medium: 0px 8px 16px rgba(0, 0, 0, 0.15); -$shadow-large: 0px 16px 48px rgba(0, 0, 0, 0.175); \ No newline at end of file +$shadow-large: 0px 16px 48px rgba(0, 0, 0, 0.175); diff --git a/packages/earth-map/src/styles/config.scss b/packages/earth-map/src/styles/config.scss index b5079e4f..08d0483d 100644 --- a/packages/earth-map/src/styles/config.scss +++ b/packages/earth-map/src/styles/config.scss @@ -21,7 +21,7 @@ $marapp-primary-font: 'Roboto'; $marapp-secondary-font: 'Roboto'; $marapp-icon-font: 'icon-font'; -$marapp-color-sucess: #1EBE28!default; +$marapp-color-success: #28A745!default; $marapp-color-error: #FC4349!default; $marapp-primary-color: #0099A1; @@ -41,5 +41,6 @@ $marapp-gray-9: #212529; $marapp-gray-100: #212529; +$marapp-red-1: #CE2746; @import "variables"; diff --git a/packages/earth-map/src/utils/index.tsx b/packages/earth-map/src/utils/index.tsx index 9146c0a7..5b75e5ee 100644 --- a/packages/earth-map/src/utils/index.tsx +++ b/packages/earth-map/src/utils/index.tsx @@ -19,8 +19,15 @@ import { BASE_URL } from 'config'; import React from 'react'; +import { Deserializer } from 'ts-jsonapi'; import urljoin from 'url-join'; +const DeserializerService = new Deserializer({ + keyForAttribute: (attribute: any) => { + return attribute; + }, +}); + /** * Route to target URL in case of success/failure. */ @@ -49,3 +56,8 @@ export const parseHintBold = (text: string = '') => { ) ); }; + +/** + * Deserializer + */ +export const deserializeData = (data) => DeserializerService.deserialize(data); diff --git a/packages/earth-map/src/utils/query.tsx b/packages/earth-map/src/utils/query.tsx index abe6abe7..aa0c90d3 100644 --- a/packages/earth-map/src/utils/query.tsx +++ b/packages/earth-map/src/utils/query.tsx @@ -22,8 +22,7 @@ import queryStringEncode from 'query-string-encode'; /** * Url encode */ - -export const encodeQueryToURL = (baseUrl, query) => +export const encodeQueryToURL = (baseUrl: string, query: { [key: string]: any } = {}): string => [baseUrl, decodeURIComponent(queryStringEncode(query))].join('?'); export default { encodeQueryToURL }; diff --git a/packages/earth-shared/src/components/index.ts b/packages/earth-shared/src/components/index.ts index 0dfb1f97..3538cab2 100644 --- a/packages/earth-shared/src/components/index.ts +++ b/packages/earth-shared/src/components/index.ts @@ -41,3 +41,4 @@ export { ErrorMessages } from './error-messages'; export { default as ErrorTemplate } from './error-template'; export { UserMenu } from './user-menu'; export { AppContextSwitcher } from './app-context-switcher'; +export { Input } from './input'; diff --git a/packages/earth-shared/src/components/inline-edit-card/InlineEditCard.tsx b/packages/earth-shared/src/components/inline-edit-card/InlineEditCard.tsx index c7d7a6bb..cc1d758c 100644 --- a/packages/earth-shared/src/components/inline-edit-card/InlineEditCard.tsx +++ b/packages/earth-shared/src/components/inline-edit-card/InlineEditCard.tsx @@ -18,13 +18,14 @@ */ import classnames from 'classnames'; -import React, { ReactNode, useState } from 'react'; +import React, { ReactNode, useEffect, useState } from 'react'; import { animated, Keyframes } from 'react-spring/renderprops'; import { ErrorMessages } from '@marapp/earth-shared'; import { InlineCardOverlay } from './index'; import './styles.scss'; +import { noop, isNil } from 'lodash'; interface IOptionsBag { isEditing: boolean; @@ -45,9 +46,12 @@ export interface InlineCardProps { setIsLoading: (value: boolean) => void, setServerErrors: (value: boolean) => void ) => void; + onCancel?: () => void; submitButtonText?: string; cancelButtonText?: string; + submitButtonVariant?: string; validForm?: boolean; + manualOpen?: boolean; } const Card: any = Keyframes.Spring({ @@ -60,16 +64,22 @@ export default function InlineEditCard(props: InlineCardProps) { children, render, editButtonText = 'edit', - onSubmit, + onSubmit = noop, + onCancel = noop, submitButtonText = 'Save', cancelButtonText = 'Cancel', + submitButtonVariant, validForm, + manualOpen, } = props; - const [isEditing, setIsEditing] = useState(false); const [isLoading, setIsLoading] = useState(false); const [serverErrors, setServerErrors] = useState(null); + useEffect(() => { + setIsEditing(manualOpen); + }, [manualOpen]); + const state = isEditing ? 'open' : 'close'; const optionsBag: IOptionsBag = { @@ -84,6 +94,8 @@ export default function InlineEditCard(props: InlineCardProps) { const handleCancel = () => { setIsEditing(false); setServerErrors(false); + + onCancel(); }; const renderEditable = () => ( @@ -93,7 +105,10 @@ export default function InlineEditCard(props: InlineCardProps) {