diff --git a/src/common/actions/entity.actions.ts b/src/common/actions/entity.actions.ts index 835b36f58..eddbc07ca 100644 --- a/src/common/actions/entity.actions.ts +++ b/src/common/actions/entity.actions.ts @@ -15,6 +15,7 @@ export interface IAction { class: string; titleTranslationId: string; views: TabViews[]; + disabled?: boolean; dependentField?: DependentField; } @@ -115,6 +116,14 @@ const ACTIONS_CONFIG: IEntityActions[] = [ dependentField: { field: 'layerURLMissing', expectedValue: false }, views: [TabViews.CATALOG, TabViews.SEARCH_RESULTS], }, + { + action: 'delete', + frequent: false, + icon: '', + class: 'mc-icon-Delete', + titleTranslationId: 'action.delete.tooltip', + views: [TabViews.CATALOG, TabViews.SEARCH_RESULTS], + }, ], }, ], diff --git a/src/common/components/error/error-presentor.css b/src/common/components/error/error-presentor.css index 034df865f..5978ef84b 100644 --- a/src/common/components/error/error-presentor.css +++ b/src/common/components/error/error-presentor.css @@ -16,6 +16,7 @@ height: 60px; min-width: 264px; overflow-y: auto; + overflow-x: hidden; } body[dir='rtl'] .errorsList { diff --git a/src/common/components/grid/cell-renderer/actions.cell-renderer.css b/src/common/components/grid/cell-renderer/actions.cell-renderer.css index 85e2f0078..494311860 100644 --- a/src/common/components/grid/cell-renderer/actions.cell-renderer.css +++ b/src/common/components/grid/cell-renderer/actions.cell-renderer.css @@ -106,3 +106,8 @@ body[dir='rtl'] #gridActionsCellRenderer .actionMenuItemTitle { .actionMenuItemContainer:hover { background: var(--mdc-theme-gc-alternative-surface); } + +#gridActionsCellRenderer .disabled { + opacity: 0.5; + pointer-events: none; +} diff --git a/src/common/components/grid/cell-renderer/actions.cell-renderer.tsx b/src/common/components/grid/cell-renderer/actions.cell-renderer.tsx index 048442be7..a6a9ed15f 100644 --- a/src/common/components/grid/cell-renderer/actions.cell-renderer.tsx +++ b/src/common/components/grid/cell-renderer/actions.cell-renderer.tsx @@ -17,7 +17,7 @@ const FIRST = 0; const EMPTY_ACTION_GROUP = 0; interface IActionsRendererParams extends ICellRendererParams { - actions: Record; + actions: IActionGroup[]; actionHandler: (action: Record) => void; } @@ -50,10 +50,7 @@ export const ActionsRenderer: React.FC = (props) => { return filteredActionGroups; }; - const actions = useMemo( - () => filterActionsByDependentFields(props.actions[entity]), - [props.actions[entity]] - ); + const actions = useMemo(() => filterActionsByDependentFields(props.actions), [props.actions]); let frequentActions: IAction[] = []; let allFlatActions: IAction[] = []; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -88,6 +85,7 @@ export const ActionsRenderer: React.FC = (props) => { : `actionIcon actionDismissible` } icon={action.icon} + disabled={action.disabled} key={`freqAct_${action.action}_${idx}`} onClick={(): void => { sendAction(entity, action, props.data); @@ -114,6 +112,11 @@ export const ActionsRenderer: React.FC = (props) => { { + if (action.disabled) { + evt.preventDefault(); + evt.stopPropagation(); + return; + } sendAction(entity, action, props.data); setOpenActionsMenu(false); }} @@ -121,14 +124,19 @@ export const ActionsRenderer: React.FC = (props) => { > - + {action.titleTranslationId} diff --git a/src/common/components/tree/icon-renderers/actions.button-renderer.css b/src/common/components/tree/icon-renderers/actions.button-renderer.css index d019c1808..1cd0f37e3 100644 --- a/src/common/components/tree/icon-renderers/actions.button-renderer.css +++ b/src/common/components/tree/icon-renderers/actions.button-renderer.css @@ -65,3 +65,8 @@ body[dir='rtl'] .actionMenuItemTitle { padding-left: unset; padding-right: 8px; } + +.actionsContainer .disabled { + opacity: 0.5; + pointer-events: none; +} diff --git a/src/common/components/tree/icon-renderers/actions.button-renderer.tsx b/src/common/components/tree/icon-renderers/actions.button-renderer.tsx index 8f2ceb617..26c071384 100644 --- a/src/common/components/tree/icon-renderers/actions.button-renderer.tsx +++ b/src/common/components/tree/icon-renderers/actions.button-renderer.tsx @@ -52,6 +52,7 @@ export const ActionsRenderer: React.FC = ({ : `actionIcon actionDismissible` } icon={action.icon} + disabled={action.disabled} key={`freqAct_${node.id as string}_${idx}`} onClick={(): void => { sendAction(entity, action, node); @@ -72,20 +73,28 @@ export const ActionsRenderer: React.FC = ({ { + if (action.disabled) { + evt.preventDefault(); + evt.stopPropagation(); + return; + } sendAction(entity, action, node); setOpenActionsMenu(false); }} className="actionMenuItem" > - + {action.titleTranslationId} diff --git a/src/common/i18n/en.json b/src/common/i18n/en.json index f31325064..61c4861da 100644 --- a/src/common/i18n/en.json +++ b/src/common/i18n/en.json @@ -179,6 +179,9 @@ "delete.dialog.action": "Delete", "delete.dialog.checkbox": "Confirm deleting", + "delete.approver-name": "Approver name:", + "delete.approval-code": "Approval code:", + "snack.message.success": "SUCCESS", "snack.message.failed": "FAILURE", "snack.dismiss-btn.text": "Dismiss", diff --git a/src/common/i18n/he.json b/src/common/i18n/he.json index cf55a4a4d..618976aeb 100644 --- a/src/common/i18n/he.json +++ b/src/common/i18n/he.json @@ -179,6 +179,9 @@ "delete.dialog.action": "מחיקת", "delete.dialog.checkbox": "אישור מחיקה", + "delete.approver-name": "שם מבצע:", + "delete.approval-code": "קוד מאשר:", + "snack.message.success": "הצלחה", "snack.message.failed": "כשלון", "snack.dismiss-btn.text": "אישור", diff --git a/src/common/i18n/helpers/helpers.ts b/src/common/i18n/helpers/helpers.ts deleted file mode 100644 index 6fb34a986..000000000 --- a/src/common/i18n/helpers/helpers.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const isRtl = (locale: string) => { - return locale === 'he'; -}; diff --git a/src/discrete-layer/components/catalog-tree/catalog-tree.tsx b/src/discrete-layer/components/catalog-tree/catalog-tree.tsx index 64a3afd1d..ddbea9a2e 100644 --- a/src/discrete-layer/components/catalog-tree/catalog-tree.tsx +++ b/src/discrete-layer/components/catalog-tree/catalog-tree.tsx @@ -7,7 +7,6 @@ import { changeNodeAtPath, getNodeAtPath, find, ExtendedNodeData } from 'react-s import { useIntl } from 'react-intl'; import { Box } from '@map-colonies/react-components'; import { useTheme } from '@map-colonies/react-core'; -import { IActionGroup } from '../../../common/actions/entity.actions'; import { TreeComponent, TreeItem } from '../../../common/components/tree'; import { ActionsRenderer } from '../../../common/components/tree/icon-renderers/actions.button-renderer'; import { FootprintRenderer } from '../../../common/components/tree/icon-renderers/footprint.icon-renderer'; @@ -15,7 +14,7 @@ import { LayerImageRenderer } from '../../../common/components/tree/icon-rendere import { ProductTypeRenderer } from '../../../common/components/tree/icon-renderers/product-type.icon-renderer'; import { Error } from '../../../common/components/tree/statuses/error'; import { Loading } from '../../../common/components/tree/statuses/loading'; -import { getTextStyle } from '../../../common/helpers/style'; +import { getTextStyle, isUnpublished } from '../../../common/helpers/style'; import { isValidLayerMetadata } from '../../../common/helpers/layer-url'; import { LinkType } from '../../../common/models/link-type.enum'; import { @@ -30,6 +29,7 @@ import { TabViews } from '../../views/tab-views'; import { LayerMetadataMixedUnion } from '../../models'; import { BestInEditDialog } from '../dialogs/best-in-edit.dialog'; import { getLinkUrlWithToken } from '../helpers/layersUtils'; +import { disableActionByPredicate } from '../helpers/actionsUtils'; import { queue } from '../snackbar/notification-queue'; import './catalog-tree.css'; @@ -310,9 +310,12 @@ export const CatalogTreeComponent: React.FC = observe hoveredNode.parentPath === rowInfo.path.slice(0, -1).toString() && ( !isUnpublished(data) + )} entity={rowInfo.node.__typename} actionHandler={dispatchAction} /> diff --git a/src/discrete-layer/components/helpers/actionsUtils.ts b/src/discrete-layer/components/helpers/actionsUtils.ts new file mode 100644 index 000000000..2429a7f09 --- /dev/null +++ b/src/discrete-layer/components/helpers/actionsUtils.ts @@ -0,0 +1,27 @@ +import { IActionGroup } from '../../../common/actions/entity.actions'; + +export const disableActionByPredicate = ( + entityPermittedActions: Record, + data: Record, + actionToDisable: string, + predicate: (data: Record) => boolean +): IActionGroup[] => { + const actionGroups = (entityPermittedActions[data.__typename] as IActionGroup[]).map((group) => ({ + ...group, + group: group.group.map((action) => ({ ...action })), + })); + + if (predicate(data)) { + const targetGroup = actionGroups.find((group) => + group.group.some((action) => action.action === actionToDisable) + ); + + const targetAction = targetGroup?.group.find((action) => action.action === actionToDisable); + + if (targetAction) { + targetAction.disabled = true; + } + } + + return actionGroups; +}; diff --git a/src/discrete-layer/components/job-manager/grids/job-manager-grid.common.tsx b/src/discrete-layer/components/job-manager/grids/job-manager-grid.common.tsx index 4996fb202..35dc3fa5e 100644 --- a/src/discrete-layer/components/job-manager/grids/job-manager-grid.common.tsx +++ b/src/discrete-layer/components/job-manager/grids/job-manager-grid.common.tsx @@ -25,12 +25,11 @@ import { PriorityRenderer } from '../cell-renderer/priority.cell-renderer'; import PlaceholderCellRenderer from '../cell-renderer/placeholder.cell-renderer'; import { StatusRenderer } from '../cell-renderer/status.cell-renderer'; import { TooltippedCellRenderer } from '../cell-renderer/tool-tipped.cell-renderer'; -import { JOB_ENTITY } from '../job.types'; export interface ICommonJobManagerGridProps { rowData: unknown[]; dispatchAction: (action: Record | undefined) => void; - getJobActions: { [JOB_ENTITY]: IActionGroup[] }; + getJobActions: IActionGroup[]; updateJobCB: (updateParam: Record) => void; rowDataChangeCB?: () => void; gridOptionsOverride?: Partial; @@ -299,10 +298,10 @@ const JobManagerGrid: React.FC = (props) => { headerName: '', width: 0, cellRenderer: 'actionsRenderer', - cellRendererParams: { + cellRendererParams: (params: any) => ({ actions: getJobActions, actionHandler: dispatchAction, - }, + }), }, ], [] diff --git a/src/discrete-layer/components/job-manager/jobs.dialog.tsx b/src/discrete-layer/components/job-manager/jobs.dialog.tsx index 07d636677..5a1b64700 100644 --- a/src/discrete-layer/components/job-manager/jobs.dialog.tsx +++ b/src/discrete-layer/components/job-manager/jobs.dialog.tsx @@ -104,9 +104,7 @@ export const JobsDialog: React.FC = observer((props: JobsDialog return { ...action, group: groupsWithTranslation }; }); - return { - [JOB_ENTITY]: actions, - }; + return actions; }, []); useEffect(() => { diff --git a/src/discrete-layer/components/layer-details/entity.delete-dialog.css b/src/discrete-layer/components/layer-details/3D/entity.3d.delete-dialog.css similarity index 64% rename from src/discrete-layer/components/layer-details/entity.delete-dialog.css rename to src/discrete-layer/components/layer-details/3D/entity.3d.delete-dialog.css index 2504f12cb..65e00aaa6 100644 --- a/src/discrete-layer/components/layer-details/entity.delete-dialog.css +++ b/src/discrete-layer/components/layer-details/3D/entity.3d.delete-dialog.css @@ -1,9 +1,12 @@ #entityDeleteDialog { - --update-layer-header-height: 150px; + --warning-message-height: 60px; + --header-height: 150px; --footer-height: 60px; - --footer-offset: 4px; - --modal-height: calc(100vh - 80px); + --modal-height: 100vh; --modal-width: 888px; + --map-height: calc( + 100% - var(--header-height) - var(--footer-height) - var(--warning-message-height) + ); } #entityDeleteDialog .icon { @@ -14,15 +17,10 @@ #entityDeleteDialog .footer { display: flex; - margin-top: var(--footer-offset); width: 100%; height: var(--footer-height); } -body[dir='rtl'] #entityDeleteDialog .footer { - left: 20px; -} - #entityDeleteDialog .buttons { display: flex; gap: 20px; @@ -45,21 +43,14 @@ body[dir='rtl'] #entityDeleteDialog .closeIcon { } #entityDeleteDialog .headerWarning { - height: 100px; + height: var(--warning-message-height); display: flex; align-items: center; } -@media (min-width: 900px) { - #entityDeleteDialog .mdc-dialog .mdc-dialog__surface { - max-width: var(--modal-width); - height: var(--modal-height); - } - - #entityDeleteDialog .mdc-dialog .mdc-dialog__surface .mdc-dialog .mdc-dialog__surface { - max-width: var(--modal-width); - height: var(--chonky-modal-height); - } +#entityDeleteDialog .mdc-dialog .mdc-dialog__surface { + max-width: var(--modal-width); + height: var(--modal-height); } #deleteLayerDetailsContainer { @@ -70,7 +61,7 @@ body[dir='rtl'] #entityDeleteDialog .closeIcon { align-items: center; justify-content: center; width: 100%; - height: var(--update-layer-header-height); + height: var(--header-height); } #deleteLayerDetails { diff --git a/src/discrete-layer/components/layer-details/entity.delete-dialog.tsx b/src/discrete-layer/components/layer-details/3D/entity.3d.delete-dialog.tsx similarity index 59% rename from src/discrete-layer/components/layer-details/entity.delete-dialog.tsx rename to src/discrete-layer/components/layer-details/3D/entity.3d.delete-dialog.tsx index 5526b68ee..4a4e33b0d 100644 --- a/src/discrete-layer/components/layer-details/entity.delete-dialog.tsx +++ b/src/discrete-layer/components/layer-details/3D/entity.3d.delete-dialog.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { observer } from 'mobx-react'; import { @@ -7,69 +7,44 @@ import { CircularProgress, DialogActions, DialogContent, - Tooltip, } from '@map-colonies/react-core'; -import { Dialog, DialogTitle, Icon, IconButton, Typography } from '@map-colonies/react-core'; +import { Dialog, Icon, Typography } from '@map-colonies/react-core'; import { Box } from '@map-colonies/react-components'; -import { GraphQLError } from '../../../common/components/error/graphql.error-presentor'; -import { emphasizeByHTML } from '../../../common/helpers/formatters'; -import { Mode } from '../../../common/models/mode.enum'; -import { ILayerImage } from '../../models/layerImage'; -import { IDispatchAction } from '../../models/actionDispatcherStore'; -import { UserAction } from '../../models/userStore'; +import { GraphQLError } from '../../../../common/components/error/graphql.error-presentor'; +import { Mode } from '../../../../common/models/mode.enum'; +import { ILayerImage } from '../../../models/layerImage'; +import { UserAction } from '../../../models/userStore'; import { EntityDescriptorModelType, RecordStatus, RecordType, useQuery, useStore, -} from '../../models'; -import { GeoJsonMapValuePresentorComponent } from './field-value-presentors/geojson-map.value-presentor'; -import { LayersDetailsComponent } from './layer-details'; +} from '../../../models'; +import { GeoJsonMapValuePresentorComponent } from '../field-value-presentors/geojson-map.value-presentor'; +import { LayersDetailsComponent } from '../layer-details'; +import { useDeleteLayer, VALID } from '../delete.hook'; -import './entity.delete-dialog.css'; +import './entity.3d.delete-dialog.css'; +import { DialogActionTitle } from '../dialog.helpers'; -interface EntityDeleteDialogProps { +export interface EntityDeleteDialogProps { isOpen: boolean; onSetOpen: (open: boolean) => void; - recordType?: RecordType; layerRecord: ILayerImage; + recordType?: RecordType; } -const VALID = 'ok'; - export const EntityDeleteDialog: React.FC = observer( (props: EntityDeleteDialogProps) => { const { isOpen, onSetOpen, layerRecord } = props; const store = useStore(); - const mutationQuery = useQuery(); const intl = useIntl(); + const mutationQuery = useQuery(); const [allowDeleting, setAllowDeleting] = useState(false); - const [recordType] = useState( - props.recordType ?? (layerRecord?.type as RecordType) - ); - - const dialogTitleParam = recordType; - const dialogTitleParamTranslation = intl.formatMessage({ - id: `record-type.${(dialogTitleParam as string).toLowerCase()}.label`, - }); - - const dialogTitle = intl.formatMessage( - { id: `general.title.delete` }, - { value: dialogTitleParamTranslation } - ); - - const closeDialog = useCallback(() => { - onSetOpen(false); - }, [onSetOpen, store.discreteLayersStore]); - - const dispatchAction = (action: Record): void => { - store.actionDispatcherStore.dispatchAction({ - action: action.action, - data: action.data, - } as IDispatchAction); - }; + const { dialogTitleParamTranslation, closeDialog, dispatchAction, warningMessage } = + useDeleteLayer({ onSetOpen, layerRecord, recordType: props.recordType }); useEffect(() => { if ( @@ -91,7 +66,7 @@ export const EntityDeleteDialog: React.FC = observer( const deleteLayer = (): void => { mutationQuery.setQuery( - store.mutateDeleteLayer({ + store.mutateDelete3DLayer({ data: { id: layerRecord.id, type: layerRecord.type as RecordType, @@ -100,34 +75,20 @@ export const EntityDeleteDialog: React.FC = observer( ); }; - const deleteMessage = useMemo((): string => { - return intl.formatMessage( - { id: 'delete.dialog.message' }, - { action: emphasizeByHTML(`${intl.formatMessage({ id: 'delete.dialog.action' })}`) } - ); - }, []); - return ( -
+ - - {dialogTitle} - { - closeDialog(); - }} - /> - + - - - + @@ -147,11 +108,10 @@ export const EntityDeleteDialog: React.FC = observer( mode={Mode.VIEW} jsonValue={JSON.stringify(props.layerRecord?.footprint)} fitOptions={{ padding: [80, 160, 80, 160] }} - style={{ width: '100%', height: '480px' }} + style={{ width: '100%', height: 'var(--map-height)' }} /> - = observer( -
+ ); } ); diff --git a/src/discrete-layer/components/layer-details/delete.hook.ts b/src/discrete-layer/components/layer-details/delete.hook.ts new file mode 100644 index 000000000..8729e4b4f --- /dev/null +++ b/src/discrete-layer/components/layer-details/delete.hook.ts @@ -0,0 +1,56 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { emphasizeByHTML } from '../../../common/helpers/formatters'; +import { ILayerImage } from '../../models/layerImage'; +import { IDispatchAction } from '../../models/actionDispatcherStore'; +import { RecordType, useStore } from '../../models'; + +export const VALID = 'ok'; + +interface DeleteLayerDialogOptions { + onSetOpen: (open: boolean) => void; + layerRecord: ILayerImage; + recordType?: RecordType; +} + +export const useDeleteLayer = ({ + onSetOpen, + layerRecord, + recordType: recordTypeProp, +}: DeleteLayerDialogOptions) => { + const store = useStore(); + + const intl = useIntl(); + + const [recordType] = useState(recordTypeProp ?? (layerRecord?.type as RecordType)); + + const dialogTitleParamTranslation = intl.formatMessage({ + id: `record-type.${(recordType as string).toLowerCase()}.label`, + }); + + const closeDialog = useCallback(() => { + onSetOpen(false); + }, [onSetOpen, store.discreteLayersStore]); + + const dispatchAction = (action: Record): void => { + store.actionDispatcherStore.dispatchAction({ + action: action.action, + data: action.data, + } as IDispatchAction); + }; + + const warningMessage = useMemo((): string => { + return intl.formatMessage( + { id: 'delete.dialog.message' }, + { action: emphasizeByHTML(`${intl.formatMessage({ id: 'delete.dialog.action' })}`) } + ); + }, []); + + return { + recordType, + dialogTitleParamTranslation, + closeDialog, + dispatchAction, + warningMessage, + }; +}; diff --git a/src/discrete-layer/components/layer-details/dialog.helpers.tsx b/src/discrete-layer/components/layer-details/dialog.helpers.tsx new file mode 100644 index 000000000..f610a1a85 --- /dev/null +++ b/src/discrete-layer/components/layer-details/dialog.helpers.tsx @@ -0,0 +1,30 @@ +import { useIntl } from 'react-intl'; +import { Mode } from '../../../common/models/mode.enum'; +import { DialogTitle, IconButton } from '@map-colonies/react-core'; + +export interface DeleteTitleProps { + domain: string; + action: Mode; + onClose: () => void; +} + +export const DialogActionTitle: React.FC = (props) => { + const intl = useIntl(); + const title = intl.formatMessage( + { id: `general.title.${props.action.toLowerCase()}` }, + { value: props.domain } + ); + + return ( + + {title} + { + props.onClose(); + }} + /> + + ); +}; diff --git a/src/discrete-layer/components/layer-details/raster/entity.raster.delete-dialog.css b/src/discrete-layer/components/layer-details/raster/entity.raster.delete-dialog.css new file mode 100644 index 000000000..bb3ce8c9c --- /dev/null +++ b/src/discrete-layer/components/layer-details/raster/entity.raster.delete-dialog.css @@ -0,0 +1,94 @@ +#rasterDeleteDialog { + --warning-message-height: 60px; + --header-height: 150px; + --fields-height: 36px; + --fields-offset: 8px; + --footer-height: 60px; + --modal-height: 100vh; + --modal-width: 888px; + --map-height: calc( + 100% - var(--header-height) - var(--warning-message-height) - var(--fields-height) - + var(--fields-offset) - var(--footer-height) + ); +} + +#rasterDeleteDialog .icon { + color: var(--mdc-theme-gc-warning-high); + font-size: 28px; + margin: 12px; +} + +#rasterDeleteDialog .footer { + display: flex; + width: 100%; + align-items: center; + height: var(--footer-height); +} + +#rasterDeleteDialog .buttons { + display: flex; + gap: 20px; +} + +#rasterDeleteDialog .closeIcon { + margin-left: auto; +} + +body[dir='rtl'] #rasterDeleteDialog .closeIcon { + margin-left: unset; + margin-right: auto; +} + +#rasterDeleteDialog .deleteWarning { + height: var(--warning-message-height); + display: flex; + align-items: center; +} + +#rasterDeleteDialog .mdc-dialog .mdc-dialog__surface { + max-width: var(--modal-width); + height: var(--modal-height); +} + +#rasterDeleteDialog .deleteLayerDetailsContainer { + position: relative; + display: flex; + background-color: var(--mdc-theme-text-primary-on-light); + padding: 8px; + align-items: center; + justify-content: center; + width: 100%; + height: var(--header-height); +} + +#rasterDeleteDialog .deleteLayerDetails { + margin: 0 0; + width: 100%; + height: 100%; + overflow-y: auto; +} + +#rasterDeleteDialog .errors { + width: 100%; + margin: 0px 12px; +} + +#rasterDeleteDialog .fields { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + height: var(--fields-height); + gap: 16px; + margin-top: var(--fields-offset); +} + +#rasterDeleteDialog .field { + display: flex; + flex-direction: row; + width: 100%; + padding: 4px 4px; +} + +#rasterDeleteDialog .field .mdc-text-field { + flex: 1; +} diff --git a/src/discrete-layer/components/layer-details/raster/entity.raster.delete-dialog.tsx b/src/discrete-layer/components/layer-details/raster/entity.raster.delete-dialog.tsx new file mode 100644 index 000000000..56d9887e6 --- /dev/null +++ b/src/discrete-layer/components/layer-details/raster/entity.raster.delete-dialog.tsx @@ -0,0 +1,201 @@ +import React, { useRef, useEffect } from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { observer } from 'mobx-react'; +import { Formik, FormikProps } from 'formik'; +import { Button, CircularProgress, DialogContent, TextField } from '@map-colonies/react-core'; +import { Dialog, Icon, Typography } from '@map-colonies/react-core'; +import { Box } from '@map-colonies/react-components'; +import { FieldLabelComponent } from '../../../../common/components/form/field-label'; +import { GraphQLError } from '../../../../common/components/error/graphql.error-presentor'; +import { Mode } from '../../../../common/models/mode.enum'; +import { UserAction } from '../../../models/userStore'; +import { EntityDescriptorModelType, RecordType, useQuery, useStore } from '../../../models'; +import { LayersDetailsComponent } from '../layer-details'; +import { EntityDeleteDialogProps } from '../3D/entity.3d.delete-dialog'; +import { useDeleteLayer, VALID } from '../delete.hook'; +import { GeoFeaturesPresentorComponent } from './pp-map'; + +import './entity.raster.delete-dialog.css'; +import { DialogActionTitle } from '../dialog.helpers'; + +export const RasterDeleteDialog: React.FC = observer( + (props: EntityDeleteDialogProps) => { + const { isOpen, onSetOpen, layerRecord } = props; + const store = useStore(); + const intl = useIntl(); + const mutationQuery = useQuery(); + + const { dialogTitleParamTranslation, closeDialog, dispatchAction, warningMessage } = + useDeleteLayer({ onSetOpen, layerRecord, recordType: props.recordType }); + + useEffect(() => { + if ( + !mutationQuery.loading && + (mutationQuery.data as { deleteLayer: string } | undefined)?.deleteLayer === VALID + ) { + onSetOpen(false); + const payload = { + action: UserAction.SYSTEM_CALLBACK_DELETE, + data: { ...layerRecord }, + }; + + dispatchAction(payload); + } + }, [mutationQuery.data]); + + const deleteLayer = (approverName: string, approvalCode: string): void => { + mutationQuery.setQuery( + store.mutateDeleteRasterLayer({ + data: { + id: layerRecord.id, + type: layerRecord.type as RecordType, + approverName, + approvalCode, + }, + }) + ); + }; + + let formikRef = useRef>() as any; + + const [initialDeleteValues] = React.useState({ approvalCode: '', approverName: '' }); + + return ( + + + + + + + + + + + + + + + + { + if (instance) { + formikRef.current = instance; + } + }} + validateOnMount + validate={(values) => { + const errors: any = {}; + + if (!values.approverName?.trim()) { + errors.approverName = true; + } + + if (!values.approvalCode?.trim()) { + errors.approvalCode = true; + } + + return errors; + }} + onSubmit={(values, actions) => { + if (formikRef.current?.isValid) { + deleteLayer( + formikRef.current?.values.approverName, + formikRef.current?.values.approvalCode + ); + } + }} + > + {(props) => ( +
+ + + + + + + + + + + + + + + + + + + +
+ )} +
+
+
+
+ ); + } +); diff --git a/src/discrete-layer/components/layer-details/raster/layer-details-form.raster.css b/src/discrete-layer/components/layer-details/raster/layer-details-form.raster.css index 5023721ba..61561413c 100644 --- a/src/discrete-layer/components/layer-details/raster/layer-details-form.raster.css +++ b/src/discrete-layer/components/layer-details/raster/layer-details-form.raster.css @@ -116,29 +116,6 @@ body[dir='rtl'] #layerDetailsFormRaster .enumOptions .mdc-select__dropdown-icon direction: inherit !important; } -#layerDetailsFormRaster .mdc-checkbox__ripple { - display: none; -} - -#layerDetailsFormRaster .flexCheckItem { - flex: 1; - position: relative; -} - -#layerDetailsFormRaster .mdc-checkbox__background { - width: 12px; - height: 12px; -} - -#layerDetailsFormRaster .showOnMapContainer .mdc-checkbox { - padding-left: 0px; -} - -#layerDetailsFormRaster .showOnMapContainer label { - padding-bottom: 6px; - color: var(--mdc-theme-text-icon-on-dark); -} - #layerDetailsFormRaster .remainingTime { width: auto; position: absolute; diff --git a/src/discrete-layer/components/layer-details/raster/pp-map.css b/src/discrete-layer/components/layer-details/raster/pp-map.css index 8b6bb6dd0..a5586a572 100644 --- a/src/discrete-layer/components/layer-details/raster/pp-map.css +++ b/src/discrete-layer/components/layer-details/raster/pp-map.css @@ -1,4 +1,4 @@ -#entityRasterDialog .geoFeaturesMapContainer .checkbox { +#geoFeaturesMapContainer .checkbox { display: inline-block; position: absolute; bottom: 1px; @@ -9,22 +9,40 @@ padding-right: 12px; } -body[dir='rtl'] #entityRasterDialog .geoFeaturesMapContainer .checkbox { +body[dir='rtl'] #geoFeaturesMapContainer .checkbox { direction: rtl; padding-right: unset; padding-left: 12px; } -.geoFeaturesMapContainer { +#geoFeaturesMapContainer { position: relative; } -.geoFeaturesMapContainer .ol-zoom button, -.geoFeaturesMapContainer .ol-legend button { +#geoFeaturesMapContainer .ol-zoom button, +#geoFeaturesMapContainer .ol-legend button { cursor: pointer; } -.featurePropertiesPopup { +#geoFeaturesMapContainer .mdc-checkbox__ripple { + display: none; +} + +#geoFeaturesMapContainer .showOnMapContainer .mdc-checkbox { + padding-left: 0px; +} + +#geoFeaturesMapContainer .mdc-checkbox__background { + width: 12px; + height: 12px; +} + +#geoFeaturesMapContainer .showOnMapContainer label { + padding-bottom: 6px; + color: var(--mdc-theme-text-icon-on-dark); +} + +#geoFeaturesMapContainer .featurePropertiesPopup { position: absolute; top: 8px; left: 8px; diff --git a/src/discrete-layer/components/layer-details/raster/pp-map.tsx b/src/discrete-layer/components/layer-details/raster/pp-map.tsx index ccaa3471c..98fa3cd7f 100644 --- a/src/discrete-layer/components/layer-details/raster/pp-map.tsx +++ b/src/discrete-layer/components/layer-details/raster/pp-map.tsx @@ -262,7 +262,7 @@ export const GeoFeaturesPresentorComponent: React.FC }; return ( - + {previewBaseMap} @@ -286,10 +286,10 @@ export const GeoFeaturesPresentorComponent: React.FC /> - {mode === Mode.UPDATE && ( + {mode !== Mode.NEW && ( ): void => { diff --git a/src/discrete-layer/components/layers-results/layers-results.tsx b/src/discrete-layer/components/layers-results/layers-results.tsx index 0aa4841a6..0bb89e697 100644 --- a/src/discrete-layer/components/layers-results/layers-results.tsx +++ b/src/discrete-layer/components/layers-results/layers-results.tsx @@ -28,11 +28,12 @@ import { Loading } from '../../../common/components/tree/statuses/loading'; import CONFIG from '../../../common/config'; import { getMax } from '../../../common/helpers/array'; import { dateFormatter } from '../../../common/helpers/formatters'; -import { isPolygonPartsShown } from '../../../common/helpers/style'; +import { isPolygonPartsShown, isUnpublished } from '../../../common/helpers/style'; import { getResponseErrorMesssage, getResponseErrorURL, } from '../../../common/helpers/server-error'; +import { disableActionByPredicate } from '../helpers/actionsUtils'; // import { usePrevious } from '../../../common/hooks/previous.hook'; import { LayerRasterRecordModelType } from '../../models'; import { IDispatchAction } from '../../models/actionDispatcherStore'; @@ -259,10 +260,15 @@ export const LayersResults: React.FC = observer((props) => { headerName: '', width: 0, cellRenderer: 'actionsRenderer', - cellRendererParams: { - actions: entityPermittedActions, + cellRendererParams: (params: any) => ({ + actions: disableActionByPredicate( + entityPermittedActions, + params.data, + 'delete', + (data) => !isUnpublished(data) + ), actionHandler: dispatchAction, - }, + }), }, ]; diff --git a/src/discrete-layer/models/RootStore.base.ts b/src/discrete-layer/models/RootStore.base.ts index 41d074608..a7f4ede6e 100644 --- a/src/discrete-layer/models/RootStore.base.ts +++ b/src/discrete-layer/models/RootStore.base.ts @@ -425,10 +425,16 @@ export type LayerDemRecordInput = { keywords?: string links?: LinkInput[] } -export type RecordDeletePartial = { +export type RecordDelete3D = { id: string type: RecordType } +export type RecordDeleteRaster = { + id: string + type: RecordType + approverName: string + approvalCode: string +} export type JobUpdateData = { parameters?: any status?: string @@ -503,7 +509,8 @@ mutateStartRasterIngestion="mutateStartRasterIngestion", mutateStartRasterUpdateGeopkg="mutateStartRasterUpdateGeopkg", mutateStart3DIngestion="mutateStart3DIngestion", mutateStartDemIngestion="mutateStartDemIngestion", -mutateDeleteLayer="mutateDeleteLayer", +mutateDelete3DLayer="mutateDelete3DLayer", +mutateDeleteRasterLayer="mutateDeleteRasterLayer", mutateUpdateJob="mutateUpdateJob", mutateJobAbort="mutateJobAbort", mutateJobRetry="mutateJobRetry", @@ -716,8 +723,11 @@ export const RootStoreBase = withTypedRefs()(MSTGQLStore mutateStartDemIngestion(variables: { data: IngestionDemData }, optimisticUpdate?: () => void) { return self.mutate<{ startDemIngestion: string }>(`mutation startDemIngestion($data: IngestionDemData!) { startDemIngestion(data: $data) }`, variables, optimisticUpdate) }, - mutateDeleteLayer(variables: { data: RecordDeletePartial }, optimisticUpdate?: () => void) { - return self.mutate<{ deleteLayer: string }>(`mutation deleteLayer($data: RecordDeletePartial!) { deleteLayer(data: $data) }`, variables, optimisticUpdate) + mutateDelete3DLayer(variables: { data: RecordDelete3D }, optimisticUpdate?: () => void) { + return self.mutate<{ delete3DLayer: string }>(`mutation delete3DLayer($data: RecordDelete3D!) { delete3DLayer(data: $data) }`, variables, optimisticUpdate) + }, + mutateDeleteRasterLayer(variables: { data: RecordDeleteRaster }, optimisticUpdate?: () => void) { + return self.mutate<{ deleteRasterLayer: string }>(`mutation deleteRasterLayer($data: RecordDeleteRaster!) { deleteRasterLayer(data: $data) }`, variables, optimisticUpdate) }, mutateUpdateJob(variables: { data: JobUpdateData, id: string }, optimisticUpdate?: () => void) { return self.mutate<{ updateJob: string }>(`mutation updateJob($data: JobUpdateData!, $id: String!) { updateJob(data: $data, id: $id) }`, variables, optimisticUpdate) diff --git a/src/discrete-layer/models/actionDispatcherStore.ts b/src/discrete-layer/models/actionDispatcherStore.ts index 2b06ef369..8b287d771 100644 --- a/src/discrete-layer/models/actionDispatcherStore.ts +++ b/src/discrete-layer/models/actionDispatcherStore.ts @@ -9,7 +9,7 @@ import CONTEXT_ACTIONS_CONFIG, { IContextActionGroup, IContextActions } from '.. export interface IDispatchAction { action: string; - data: Record; + data: Record; }; export type CombinedActionsType = (IEntityActions | IContextActions)[]; @@ -50,11 +50,34 @@ export const actionDispatcherStore = ModelBase const actions = self.actionsConfig?.find(entityActions => entityActions.entity === entity); return actions ?? undefined; }; - - function dispatchAction(action:IDispatchAction | undefined): void { + + function dispatchAction(action: IDispatchAction | undefined): void { self.action = cloneDeep(action); }; + // function disableAction(data: Record) { + // const actionGroups = (entityPermittedActions[treeItem.__typename] as IActionGroup[]).map( + // (group) => ({ + // ...group, + // group: group.group.map((action) => ({ ...action })), + // }) + // ); + + // if (!isUnpublished(treeItem)) { + // const deleteGroup = actionGroups.find((group) => + // group.group.some((action) => action.action === 'delete') + // ); + + // const deleteAction = deleteGroup?.group.find((action) => action.action === 'delete'); + + // if (deleteAction) { + // deleteAction.disabled = true; + // } + // } + + // return actionGroups; + // } + return { getEntityActionGroups, getEntityActionConfiguration, diff --git a/src/discrete-layer/models/userStore.ts b/src/discrete-layer/models/userStore.ts index 96359060e..222e32bca 100644 --- a/src/discrete-layer/models/userStore.ts +++ b/src/discrete-layer/models/userStore.ts @@ -116,7 +116,7 @@ const ROLES: IRole[] = [ [UserAction.ENTITY_ACTION_VECTORBESTRECORD_EDIT]: {enabled: false}, [UserAction.ENTITY_ACTION_QUANTIZEDMESHBESTRECORD_EDIT]: {enabled: true}, [UserAction.ENTITY_ACTION_LAYERRASTERRECORD_UPDATE]: {enabled: true}, - [UserAction.ENTITY_ACTION_LAYERRASTERRECORD_DELETE]: {enabled: false}, + [UserAction.ENTITY_ACTION_LAYERRASTERRECORD_DELETE]: {enabled: true}, [UserAction.ENTITY_ACTION_LAYER3DRECORD_DELETE]: {enabled: true}, [UserAction.ENTITY_ACTION_LAYERDEMRECORD_DELETE]: {enabled: false}, [UserAction.ENTITY_ACTION_LAYERRASTERRECORD_PUBLISH]: {enabled: true}, diff --git a/src/discrete-layer/views/components/action-resolver.component.tsx b/src/discrete-layer/views/components/action-resolver.component.tsx index d0aabf932..c8da3402e 100644 --- a/src/discrete-layer/views/components/action-resolver.component.tsx +++ b/src/discrete-layer/views/components/action-resolver.component.tsx @@ -206,40 +206,38 @@ export const ActionResolver: React.FC = observer((props) => break; case 'LayerRasterRecord.edit': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, LayerRasterRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity(data, LayerRasterRecordModelKeys) as unknown as LayerMetadataMixedUnion ); store.discreteLayersStore.setSelectedLayerOperationMode(Mode.EDIT); handleOpenEntityDialog(RecordType.RECORD_RASTER, true); break; case 'Layer3DRecord.edit': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, Layer3DRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity(data, Layer3DRecordModelKeys) as unknown as LayerMetadataMixedUnion ); store.discreteLayersStore.setSelectedLayerOperationMode(Mode.EDIT); handleOpenEntityDialog(RecordType.RECORD_3D, true); break; case 'LayerDemRecord.edit': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, LayerDemRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity(data, LayerDemRecordModelKeys) as unknown as LayerMetadataMixedUnion ); store.discreteLayersStore.setSelectedLayerOperationMode(Mode.EDIT); handleOpenEntityDialog(RecordType.RECORD_DEM, true); break; case 'VectorBestRecord.edit': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, VectorBestRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity(data, VectorBestRecordModelKeys) as unknown as LayerMetadataMixedUnion ); store.discreteLayersStore.setSelectedLayerOperationMode(Mode.EDIT); handleOpenEntityDialog(RecordType.RECORD_VECTOR, true); break; case 'QuantizedMeshBestRecord.edit': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, QuantizedMeshBestRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity( + data, + QuantizedMeshBestRecordModelKeys + ) as unknown as LayerMetadataMixedUnion ); store.discreteLayersStore.setSelectedLayerOperationMode(Mode.EDIT); handleOpenEntityDialog(RecordType.RECORD_DEM, true); @@ -254,49 +252,51 @@ export const ActionResolver: React.FC = observer((props) => break; case 'LayerRasterRecord.flyTo': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, LayerRasterRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity(data, LayerRasterRecordModelKeys) as unknown as LayerMetadataMixedUnion ); handleFlyTo(); break; case 'Layer3DRecord.flyTo': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, Layer3DRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity(data, Layer3DRecordModelKeys) as unknown as LayerMetadataMixedUnion ); handleFlyTo(); break; case 'LayerDemRecord.flyTo': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, LayerDemRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity(data, LayerDemRecordModelKeys) as unknown as LayerMetadataMixedUnion ); handleFlyTo(); break; case 'VectorBestRecord.flyTo': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, VectorBestRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity(data, VectorBestRecordModelKeys) as unknown as LayerMetadataMixedUnion ); handleFlyTo(); break; case 'QuantizedMeshBestRecord.flyTo': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, QuantizedMeshBestRecordModelKeys) as LayerMetadataMixedUnion + cleanUpEntity( + data, + QuantizedMeshBestRecordModelKeys + ) as unknown as LayerMetadataMixedUnion ); handleFlyTo(); break; case 'LayerRasterRecord.update': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, LayerRasterRecordModelKeys) as LayerMetadataMixedUnion, - // @ts-ignore - true + cleanUpEntity(data, LayerRasterRecordModelKeys) as unknown as LayerMetadataMixedUnion ); store.discreteLayersStore.setSelectedLayerOperationMode(Mode.UPDATE); handleOpenEntityDialog(RecordType.RECORD_RASTER, true); break; + case 'LayerRasterRecord.delete': + store.discreteLayersStore.selectLayer( + cleanUpEntity(data, LayerRasterRecordModelKeys) as unknown as LayerMetadataMixedUnion + ); + store.discreteLayersStore.setSelectedLayerOperationMode(Mode.DELETE); + handleOpenEntityDialog(RecordType.RECORD_RASTER, true); + break; case 'Layer3DRecord.viewer': window.open( `${CONFIG.WEB_TOOLS_URL}/${CONFIG.MODEL_VIEWER_ROUTE}?model_ids=${data.productId}&token=${CONFIG.MODEL_VIEWER_TOKEN_VALUE}` @@ -304,11 +304,7 @@ export const ActionResolver: React.FC = observer((props) => break; case 'Layer3DRecord.delete': store.discreteLayersStore.selectLayer( - // @ts-ignore - cleanUpEntity(data, Layer3DRecordModelKeys) as LayerMetadataMixedUnion, - // @ts-ignore - false, - true + cleanUpEntity(data, Layer3DRecordModelKeys) as unknown as LayerMetadataMixedUnion ); store.discreteLayersStore.setSelectedLayerOperationMode(Mode.DELETE); handleOpenEntityDialog(RecordType.RECORD_3D, true); diff --git a/src/discrete-layer/views/discrete-layer-view.tsx b/src/discrete-layer/views/discrete-layer-view.tsx index 6665ad21f..8a13e6a83 100644 --- a/src/discrete-layer/views/discrete-layer-view.tsx +++ b/src/discrete-layer/views/discrete-layer-view.tsx @@ -53,7 +53,7 @@ import { ExportLayerComponent } from '../components/export-layer/export-layer.co import ExportPolygonsRenderer from '../components/export-layer/export-polygons-renderer.component'; // import { Filters } from '../components/filters/filters'; import { JobsDialog } from '../components/job-manager/jobs.dialog'; -import { EntityDeleteDialog } from '../components/layer-details/entity.delete-dialog'; +import { EntityDeleteDialog } from '../components/layer-details/3D/entity.3d.delete-dialog'; import { EntityDialog } from '../components/layer-details/entity.dialog'; import { EntityRasterDialog } from '../components/layer-details/raster/entity.raster.dialog'; import { LayersResults } from '../components/layers-results/layers-results'; @@ -80,6 +80,7 @@ import { ILayerImage } from '../models/layerImage'; import { useStore } from '../models/RootStore'; import { FilterField } from '../models/RootStore.base'; import { UserAction, UserRole } from '../models/userStore'; +import { RasterDeleteDialog } from '../components/layer-details/raster/entity.raster.delete-dialog'; import { ActionResolver } from './components/action-resolver.component'; import AppTitle from './components/app-title/app-title.component'; import { DetailsPanel } from './components/details-panel.component'; @@ -142,6 +143,7 @@ const DiscreteLayerView: React.FC = observer(() => { const [isDemIngestDialogOpen, setIsDemIngestDialogOpen] = useState(false); const [isEntityDialogOpen, setIsEntityDialogOpen] = useState(false); const [isEntityDeleteDialogOpen, setIsEntityDeleteDialogOpen] = useState(false); + const [isRasterDeleteDialogOpen, setIsRasterDeleteDialogOpen] = useState(false); const [isSystemsJobsDialogOpen, setIsSystemsJobsDialogOpen] = useState(false); const [isSystemCoreInfoDialogOpen, setIsSystemCoreInfoDialogOpen] = useState(false); const [isCreateEntityMenuOpen, setIsCreateEntityMenuOpen] = useState(false); @@ -491,6 +493,7 @@ const DiscreteLayerView: React.FC = observer(() => { [Mode.UPDATE]: setIsRasterDialogOpen, [Mode.EDIT]: setIsEntityDialogOpen, [Mode.VIEW]: setIsEntityDialogOpen, + [Mode.DELETE]: setIsRasterDeleteDialogOpen, }, [RecordType.RECORD_3D]: { [Mode.NEW]: setIs3DIngestDialogOpen, @@ -1459,6 +1462,18 @@ const DiscreteLayerView: React.FC = observer(() => { layerRecord={store.discreteLayersStore.selectedLayer} /> )} + {permissions.isDeleteAllowed && + store.discreteLayersStore.selectedLayer && + isRasterDeleteDialogOpen && ( + { + setIsRasterDeleteDialogOpen(open); + onCloseDialog(); + }} + layerRecord={store.discreteLayersStore.selectedLayer} + /> + )} {isSystemsJobsDialogOpen && (