diff --git a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/JSONEditor.tsx b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/JSONEditor.tsx index 64531a3e5..d9318e3db 100644 --- a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/JSONEditor.tsx +++ b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/JSONEditor.tsx @@ -3,9 +3,14 @@ import { DocumentActions, EditDocumentActions } from './SchemaDataCardActions'; import { useAppDispatch } from '../../../../../redux/store'; import { Schema } from '../../../models/CmsModels'; import { asyncEditSchemaDocument } from '../../../store/databaseSlice'; -import { enqueueErrorNotification } from '../../../../../hooks/useNotifier'; -import { Box, styled } from '@mui/material'; +import { + enqueueErrorNotification, + enqueueSuccessNotification, +} from '../../../../../hooks/useNotifier'; +import { Box, Card, CardContent, CardProps, IconButton, styled, Tooltip } from '@mui/material'; +import { CopyAllOutlined } from '@mui/icons-material'; import JsonEditorComponent from '../../../../../components/common/JsonEditorComponent'; +import { copyJsonToClipboard } from './SchemaDataUtils'; const StyledDocActions = styled(DocumentActions)(() => ({ display: 'flex', @@ -13,16 +18,17 @@ const StyledDocActions = styled(DocumentActions)(() => ({ flexWrap: 'wrap', })); -interface Props { +interface Props extends CardProps { documents: any; schema: Schema; getSchemaDocuments: () => void; onDelete: () => void; } -const JSONEditor: FC = ({ documents, getSchemaDocuments, schema, onDelete }) => { +const JSONEditor: FC = ({ documents, getSchemaDocuments, schema, onDelete, ...rest }) => { const [edit, setEdit] = useState(false); const [documentState, setDocumentState] = useState(documents); + const [editorNonce, setEditorNonce] = useState(0); const dispatch = useAppDispatch(); const onEdit = () => { @@ -35,10 +41,8 @@ const JSONEditor: FC = ({ documents, getSchemaDocuments, schema, onDelete const handleCancel = () => { setEdit(false); - setDocumentState({}); - setTimeout(() => { - setDocumentState(documents); - }, 1); + setDocumentState(documents); + setEditorNonce((nonce) => nonce + 1); }; const handleSave = () => { @@ -62,36 +66,52 @@ const JSONEditor: FC = ({ documents, getSchemaDocuments, schema, onDelete setDocumentState(changedText.jsObject); }; + const handleCopy = () => { + copyJsonToClipboard(documentState) + .then(() => { + dispatch(enqueueSuccessNotification('JSON copied to clipboard')); + }) + .catch(() => { + dispatch(enqueueErrorNotification('Failed to copy JSON')); + }); + }; + return ( - + - + {!edit && ( + + + + + + )} + - + + + - + ); }; diff --git a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataHeader.tsx b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataHeader.tsx index 75f4aa53c..d253260b4 100644 --- a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataHeader.tsx +++ b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataHeader.tsx @@ -5,23 +5,10 @@ import Button from '@mui/material/Button'; import { Paginator } from '@conduitplatform/ui-components'; import { BoxProps } from '@mui/material/Box/Box'; import InputAdornment from '@mui/material/InputAdornment'; -import { Search, Refresh, AccountTree } from '@mui/icons-material'; +import { Search, Refresh, CopyAllOutlined } from '@mui/icons-material'; import useParseQuery from '../../../hooks/useParseQuery'; -import { styled, Typography } from '@mui/material'; - -const ObjText = '{ }'; - -const ButtonContainer = styled(Box)(({ theme }) => ({ - height: theme.spacing(3), - width: theme.spacing(3), - borderRadius: theme.spacing(0.5), - cursor: 'pointer', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - padding: theme.spacing(2), - marginLeft: theme.spacing(1), -})); +import SchemaDataViewToggle from './SchemaDataViewToggle'; +import { SchemaDataViewMode } from './schemaDataViewMode'; interface Filters { page: number; @@ -32,13 +19,14 @@ interface Filters { interface Props extends BoxProps { onCreateDocument: () => void; onRefresh: () => void; + onCopyPage: () => void; filters: Filters; setFilters: (data: Filters) => void; search: string; setSearch: (value: string) => void; count: number; - objectView: boolean; - setObjectView: (value: boolean) => void; + viewMode: SchemaDataViewMode; + onViewModeChange: (mode: SchemaDataViewMode) => void; disabled?: boolean; } @@ -46,13 +34,14 @@ const SchemaDataHeader: FC = ({ disabled = false, onCreateDocument, onRefresh, + onCopyPage, filters, setFilters, search, setSearch, count, - objectView, - setObjectView, + viewMode, + onViewModeChange, ...rest }) => { const isValidSearch = useParseQuery(search, 0); @@ -133,20 +122,16 @@ const SchemaDataHeader: FC = ({ display: 'flex', alignItems: 'center', pb: 2, + gap: 1, + flexWrap: 'wrap', }}> - setObjectView(false)}> - - - setObjectView(true)}> - - {ObjText} - - + + {viewMode === 'json' && count > 0 && ( + + )} {count > 0 && ( handlePageChange(value)} diff --git a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataList.tsx b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataList.tsx new file mode 100644 index 000000000..4e801db66 --- /dev/null +++ b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataList.tsx @@ -0,0 +1,68 @@ +import React, { FC } from 'react'; +import { Schema } from '../../../models/CmsModels'; +import SchemaDataCard from './SchemaDataCard'; +import JSONEditor from './JSONEditor'; +import { SchemaDataViewMode } from './schemaDataViewMode'; + +const documentCardSx = { + background: 'rgba(0,0,0,0.2)', + margin: 1, + paddingLeft: 1, + position: 'relative' as const, +}; + +interface Props { + viewMode: SchemaDataViewMode; + documents: any[]; + schema: Schema; + getSchemaDocuments: () => void; + onDelete: (index: number) => void; +} + +const SchemaDataList: FC = ({ + viewMode, + documents, + schema, + getSchemaDocuments, + onDelete, +}) => { + switch (viewMode) { + case 'json': + return ( + <> + {documents.map((docs, index) => ( + onDelete(index)} + key={docs?._id ?? index} + sx={documentCardSx} + /> + ))} + + ); + case 'tree': + return ( + <> + {documents.map((docs, index) => ( + onDelete(index)} + getSchemaDocuments={getSchemaDocuments} + key={`card${index}`} + id={docs?._id} + /> + ))} + + ); + default: { + const exhaustiveCheck: never = viewMode; + throw new Error(`Unhandled view mode: ${exhaustiveCheck}`); + } + } +}; + +export default SchemaDataList; diff --git a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataUtils.ts b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataUtils.ts index 4838e0a2e..59113e33c 100644 --- a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataUtils.ts +++ b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataUtils.ts @@ -37,6 +37,14 @@ export const getExpandableFields = (fields: any) => { return expandableFields; }; +export const copyJsonToClipboard = (value: unknown): Promise => { + const json = JSON.stringify(value, null, 2); + if (json === undefined) { + return Promise.reject(new Error('Nothing to copy')); + } + return navigator.clipboard.writeText(json); +}; + export const getFieldsArray = (schemaFields: any) => { const fields: any = []; diff --git a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataViewToggle.tsx b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataViewToggle.tsx new file mode 100644 index 000000000..ec06d7519 --- /dev/null +++ b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/SchemaDataViewToggle.tsx @@ -0,0 +1,45 @@ +import React, { FC } from 'react'; +import { ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material'; +import { AccountTree, DataObject } from '@mui/icons-material'; +import { SchemaDataViewMode } from './schemaDataViewMode'; + +interface Props { + viewMode: SchemaDataViewMode; + onChange: (mode: SchemaDataViewMode) => void; + disabled?: boolean; +} + +const SchemaDataViewToggle: FC = ({ viewMode, onChange, disabled = false }) => { + const handleChange = ( + _event: React.MouseEvent, + value: SchemaDataViewMode | null + ) => { + if (value === null) return; + onChange(value); + }; + + return ( + + + + + + Tree + + + + + + JSON + + + ); +}; + +export default SchemaDataViewToggle; diff --git a/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/schemaDataViewMode.ts b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/schemaDataViewMode.ts new file mode 100644 index 000000000..5c559d147 --- /dev/null +++ b/apps/Conduit-UI/src/features/database/components/schemas/SchemaData/schemaDataViewMode.ts @@ -0,0 +1,27 @@ +export type SchemaDataViewMode = 'tree' | 'json'; + +const STORAGE_KEY = 'conduit.schemaData.viewMode'; + +const isSchemaDataViewMode = (value: unknown): value is SchemaDataViewMode => { + return value === 'tree' || value === 'json'; +}; + +export const readSchemaDataViewMode = (): SchemaDataViewMode => { + if (typeof window === 'undefined') return 'tree'; + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (isSchemaDataViewMode(stored)) return stored; + } catch { + return 'tree'; + } + return 'tree'; +}; + +export const writeSchemaDataViewMode = (mode: SchemaDataViewMode): void => { + if (typeof window === 'undefined') return; + try { + localStorage.setItem(STORAGE_KEY, mode); + } catch { + return; + } +}; diff --git a/apps/Conduit-UI/src/features/database/components/schemas/Schemas.tsx b/apps/Conduit-UI/src/features/database/components/schemas/Schemas.tsx index c7c31d876..9d0973865 100644 --- a/apps/Conduit-UI/src/features/database/components/schemas/Schemas.tsx +++ b/apps/Conduit-UI/src/features/database/components/schemas/Schemas.tsx @@ -9,13 +9,22 @@ import { asyncGetSystemSchemas, } from '../../store/databaseSlice'; import { useAppDispatch, useAppSelector } from '../../../../redux/store'; -import SchemaDataCard from './SchemaData/SchemaDataCard'; import { ConfirmationDialog } from '@conduitplatform/ui-components'; import SchemaDataHeader from './SchemaData/SchemaDataHeader'; import useParseQuery from '../../hooks/useParseQuery'; import DocumentCreateDialog from './SchemaData/DocumentCreateDialog'; import SchemaDataPlaceholder from './SchemaData/SchemaDataPlaceholder'; -import JSONEditor from './SchemaData/JSONEditor'; +import SchemaDataList from './SchemaData/SchemaDataList'; +import { + readSchemaDataViewMode, + SchemaDataViewMode, + writeSchemaDataViewMode, +} from './SchemaData/schemaDataViewMode'; +import { copyJsonToClipboard } from './SchemaData/SchemaDataUtils'; +import { + enqueueErrorNotification, + enqueueSuccessNotification, +} from '../../../../hooks/useNotifier'; import { Grid, InputAdornment, TextField, Typography, Tooltip, Icon, Button } from '@mui/material'; import { Archive, Check, InfoOutlined, Search } from '@mui/icons-material'; import useDebounce from '../../../../hooks/useDebounce'; @@ -66,7 +75,7 @@ const Schemas: FC = () => { limit: 25, }); const [search, setSearch] = useState(''); - const [objectView, setObjectView] = useState(false); + const [viewMode, setViewMode] = useState('tree'); const [schemaSearch, setSchemaSearch] = useState(''); const [actualSchema, setActualSchema] = useState(undefined); const [schemaName, setSchemaName] = useState(''); @@ -76,6 +85,10 @@ const Schemas: FC = () => { const debouncedSearch: string = useParseQuery(search, 500); const debouncedSchemaSearch: string = useDebounce(schemaSearch, 500); + useEffect(() => { + setViewMode(readSchemaDataViewMode()); + }, []); + useEffect(() => { dispatch(asyncGetSchemaOwners()); }, [dispatch]); @@ -177,37 +190,34 @@ const Schemas: FC = () => { setCreateDialog(false); }; + const handleViewModeChange = (mode: SchemaDataViewMode) => { + setViewMode(mode); + writeSchemaDataViewMode(mode); + }; + + const handleCopyPage = () => { + copyJsonToClipboard(documentsState.data) + .then(() => { + dispatch(enqueueSuccessNotification('Documents copied to clipboard')); + }) + .catch(() => { + dispatch(enqueueErrorNotification('Failed to copy documents')); + }); + }; + const renderMainContent = () => { if (!actualSchema) return; if (selectedTab === 1) { - if (objectView) { - return documentsState.data.map((docs: any, index: number) => ( - onDelete(index)} - key={index} - /> - )); - } - return documentsState.data.map((docs: any, index: number) => ( - onDelete(index)} getSchemaDocuments={getSchemaDocuments} - key={`card${index}`} - id={docs?._id} + onDelete={onDelete} /> - )); + ); } }; @@ -250,8 +260,9 @@ const Schemas: FC = () => { search={search} setSearch={setSearch} count={documentsCount} - objectView={objectView} - setObjectView={setObjectView} + viewMode={viewMode} + onViewModeChange={handleViewModeChange} + onCopyPage={handleCopyPage} disabled={!actualSchema} /> );