Skip to content

Commit

Permalink
[251] Make the workbench more reusable
Browse files Browse the repository at this point in the history
Bug: #251
Signed-off-by: Stéphane Bégaudeau <[email protected]>
  • Loading branch information
sbegaudeau committed Sep 29, 2023
1 parent e895257 commit e32c707
Show file tree
Hide file tree
Showing 36 changed files with 1,174 additions and 404 deletions.
53 changes: 17 additions & 36 deletions frontend/svalyn-studio-app/src/viewers/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,39 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { gql, useQuery } from '@apollo/client';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { useSnackbar } from 'notistack';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { IItemViewProvider } from '../workbench/api/providers/ItemProviders.types';
import { useAdapterFactory } from '../workbench/api/providers/useAdapterFactory';
import { GraphViewer } from './GraphViewer';
import { GetChangeResourceData, GetChangeResourceVariables, ViewerProps } from './Viewer.types';
import { ViewerProps, ViewerState } from './Viewer.types';

const getChangeResourceQuery = gql`
query getChangeResourceQuery($changeId: ID!, $path: String!, $name: String!) {
viewer {
change(id: $changeId) {
resource(path: $path, name: $name) {
contentType
content
}
}
}
}
`;
export const Viewer = ({ object }: ViewerProps) => {
const [state, setState] = useState<ViewerState>({ viewContent: null });

export const Viewer = ({ changeId, path, name }: ViewerProps) => {
const { enqueueSnackbar } = useSnackbar();

const variables: GetChangeResourceVariables = {
changeId,
path,
name,
};
const { data, error } = useQuery<GetChangeResourceData, GetChangeResourceVariables>(getChangeResourceQuery, {
variables,
});
const { adapterFactory } = useAdapterFactory();
useEffect(() => {
if (error) {
enqueueSnackbar(error.message, { variant: 'error' });
const itemViewProvider = adapterFactory.adapt<IItemViewProvider>(object, 'IItemViewProvider');
if (itemViewProvider) {
itemViewProvider
.getContent(object)
.then((viewContent) => setState((prevState) => ({ ...prevState, viewContent })));
}
}, [error]);
}, [object]);

let rawViewer: JSX.Element | null = null;
if (data && data.viewer.change && data.viewer.change.resource) {
const { resource } = data.viewer.change;

if (resource.contentType === 'TEXT_PLAIN') {
if (state.viewContent) {
if (state.viewContent.contentType === 'TEXT_PLAIN') {
rawViewer = (
<Box sx={{ px: (theme) => theme.spacing(2), overflow: 'scroll' }}>
<pre>
<Typography variant="tcontent">{resource.content}</Typography>
<Typography variant="tcontent">{state.viewContent.content}</Typography>
</pre>
</Box>
);
} else {
rawViewer = <GraphViewer content={resource.content} />;
rawViewer = <GraphViewer content={state.viewContent.content} />;
}
}
return rawViewer;
Expand Down
32 changes: 6 additions & 26 deletions frontend/svalyn-studio-app/src/viewers/Viewer.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,13 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

export interface ViewerProps {
changeId: string;
path: string;
name: string;
}

export interface GetChangeResourceVariables {
changeId: string;
path: string;
name: string;
}

export interface GetChangeResourceData {
viewer: Viewer;
}

export interface Viewer {
change: Change | null;
}
import { IAdaptable } from '../workbench/api/providers/AdapterFactory.types';
import { ViewContent } from '../workbench/api/providers/ItemProviders.types';

export interface Change {
resource: ChangeResource | null;
export interface ViewerProps {
object: IAdaptable;
}

export interface ChangeResource {
contentType: ContentType;
content: string;
export interface ViewerState {
viewContent: ViewContent | null;
}

type ContentType = 'TEXT_PLAIN' | 'UNKNOWN';
19 changes: 18 additions & 1 deletion frontend/svalyn-studio-app/src/viewers/ViewerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,33 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { useApolloClient } from '@apollo/client';
import DownloadIcon from '@mui/icons-material/Download';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { IAdaptable, IAdapterFactory } from '../workbench/api/providers/AdapterFactory.types';
import { AdapterFactoryProvider } from '../workbench/api/providers/AdapterFactoryProvider';
import { FileData, FileDataItemProvider } from '../workspace/data/FileData';
import { Viewer } from './Viewer';
import { ViewerCardProps } from './ViewerCard.types';

const { VITE_BACKEND_URL } = import.meta.env;

export const ViewerCard = ({ changeId, path, name }: ViewerCardProps) => {
const apolloClient = useApolloClient();

const adapterFactory: IAdapterFactory = {
adapt: function <T>(object: IAdaptable, type: unknown): T | null {
if (object.__typename === 'FileData') {
return new FileDataItemProvider(changeId ?? '', apolloClient) as T;
}
return null;
},
};

const fullpath = path.length > 0 ? `${path}/${name}` : name;
return (
<Paper
Expand Down Expand Up @@ -61,7 +76,9 @@ export const ViewerCard = ({ changeId, path, name }: ViewerCardProps) => {
</div>
</Box>
<Divider />
<Viewer changeId={changeId} path={path} name={name} />
<AdapterFactoryProvider value={{ adapterFactory }}>
<Viewer object={new FileData(path, name, null)} />
</AdapterFactoryProvider>
</Paper>
);
};
112 changes: 62 additions & 50 deletions frontend/svalyn-studio-app/src/workbench/Workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,22 @@ import { useTheme } from '@mui/material/styles';
import { useRef, useState } from 'react';
import { ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { Viewer } from '../viewers/Viewer';
import { WorkbenchProps, WorkbenchState } from './Workbench.types';
import { IAdaptable } from './api/providers/AdapterFactory.types';
import { IItemViewProvider } from './api/providers/ItemProviders.types';
import { useAdapterFactory } from './api/providers/useAdapterFactory';
import { Domains } from './domains/Domains';
import { Explorer } from './explorer/Explorer';
import { TabBar } from './tabs/TabBar';
import { ActivityBar } from './viewcontainer/ActivityBar';
import { ViewDescription } from './viewcontainer/ActivityBar.types';
import { Resource, WorkbenchProps, WorkbenchState } from './Workbench.types';

export const Workbench = ({ changeId }: WorkbenchProps) => {
export const Workbench = ({}: WorkbenchProps) => {
const [state, setState] = useState<WorkbenchState>({
selectedViewId: 'explorer',
viewPanelState: 'EXPANDED',
openResources: [],
currentResource: null,
openObjects: [],
currentObject: null,
});

const panelRef = useRef<ImperativePanelHandle>(null);
Expand All @@ -62,48 +65,55 @@ export const Workbench = ({ changeId }: WorkbenchProps) => {
setState((prevState) => ({ ...prevState, viewPanelState: collapsed ? 'COLLAPSED' : 'EXPANDED' }));
};

const onResourceClick = (resource: Resource) => {
setState((prevState) => {
const resourceAlreadyOpen =
prevState.openResources.filter((openResource) => resource.id === openResource.id).length > 0;
if (resourceAlreadyOpen) {
return { ...prevState, currentResource: resource };
}
return { ...prevState, currentResource: resource, openResources: [...prevState.openResources, resource] };
});
const { adapterFactory } = useAdapterFactory();
const onClick = (object: IAdaptable) => {
const itemViewProvider: IItemViewProvider | null = adapterFactory.adapt<IItemViewProvider>(
object,
'IItemViewProvider'
);
const isViewable = itemViewProvider?.isViewable(object) ?? false;
if (isViewable) {
setState((prevState) => {
const resourceAlreadyOpen = prevState.openObjects.filter((openObject) => object === openObject).length > 0;
if (resourceAlreadyOpen) {
return { ...prevState, currentObject: object };
}
return { ...prevState, currentObject: object, openObjects: [...prevState.openObjects, object] };
});
}
};

const onOpen = (resource: Resource) => {
setState((prevState) => ({ ...prevState, currentResource: resource }));
const onOpen = (object: IAdaptable) => {
setState((prevState) => ({ ...prevState, currentObject: object }));
};

const onClose = (event: React.MouseEvent<SVGSVGElement, MouseEvent>, resource: Resource) => {
const onClose = (event: React.MouseEvent<SVGSVGElement, MouseEvent>, object: IAdaptable) => {
event.stopPropagation();

setState((prevState) => {
let currentResource: Resource | null = prevState.currentResource;
if (currentResource) {
if (prevState.openResources.length === 1 && prevState.openResources[0] === resource) {
let currentObject: IAdaptable | null = prevState.currentObject;
if (currentObject) {
if (prevState.openObjects.length === 1 && prevState.openObjects[0] === object) {
// We will close the only tab, nothing will be selected
currentResource = null;
} else if (currentResource.id === resource.id) {
// We will close the current resource among multiple open resources
const index: number = prevState.openResources.indexOf(resource);
if (index === 0 && prevState.openResources.length > 1) {
currentObject = null;
} else if (currentObject === object) {
// We will close the current object among multiple open objects
const index: number = prevState.openObjects.indexOf(object);
if (index === 0 && prevState.openObjects.length > 1) {
// We will close the first tab, let's select the second one
currentResource = prevState.openResources[1];
} else if (index > 0 && index === prevState.openResources.length - 1) {
currentObject = prevState.openObjects[1];
} else if (index > 0 && index === prevState.openObjects.length - 1) {
// We will close the last tab, let's select the one before
currentResource = prevState.openResources[index - 1];
} else if (index > 0 && index < prevState.openResources.length - 1) {
currentObject = prevState.openObjects[index - 1];
} else if (index > 0 && index < prevState.openObjects.length - 1) {
// We will close a tab in the middle, let's select the previous one
currentResource = prevState.openResources[index - 1];
currentObject = prevState.openObjects[index - 1];
}
}
}

const openResources = prevState.openResources.filter((openResource) => openResource.id !== resource.id);
return { ...prevState, openResources, currentResource };
const openObjects = prevState.openObjects.filter((openObject) => openObject !== object);
return { ...prevState, openObjects, currentObject };
});
};

Expand Down Expand Up @@ -138,7 +148,7 @@ export const Workbench = ({ changeId }: WorkbenchProps) => {
onCollapse={onCollapse}
ref={panelRef}
>
{state.selectedViewId === 'explorer' && <Explorer changeId={changeId} onResourceClick={onResourceClick} />}
{state.selectedViewId === 'explorer' && <Explorer onClick={onClick} />}
{state.selectedViewId !== 'explorer' && <Domains />}
</Panel>
<PanelResizeHandle
Expand All @@ -150,24 +160,26 @@ export const Workbench = ({ changeId }: WorkbenchProps) => {
}}
/>
<Panel style={{ display: 'grid', gridTemplateColumns: '1fr', gridTemplateRows: 'min-content 1fr' }}>
<TabBar
resources={state.openResources}
currentResourceId={state.currentResource?.id || ''}
onOpen={onOpen}
onClose={onClose}
/>
<Box
data-testid="editor-area"
sx={{
overflow: 'scroll',
padding: (theme) => theme.spacing(1),
backgroundColor: (theme) => theme.palette.background.paper,
}}
>
{state.currentResource && (
<Viewer changeId={changeId} path={state.currentResource.path} name={state.currentResource.name} />
)}
</Box>
{state.currentObject ? (
<>
<TabBar
objects={state.openObjects}
currentObject={state.currentObject}
onOpen={onOpen}
onClose={onClose}
/>
<Box
data-testid="editor-area"
sx={{
overflow: 'scroll',
padding: (theme) => theme.spacing(1),
backgroundColor: (theme) => theme.palette.background.paper,
}}
>
<Viewer object={state.currentObject} />
</Box>
</>
) : null}
</Panel>
</PanelGroup>
</Box>
Expand Down
16 changes: 5 additions & 11 deletions frontend/svalyn-studio-app/src/workbench/Workbench.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,15 @@
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

export interface WorkbenchProps {
changeId: string;
}
import { IAdaptable } from './api/providers/AdapterFactory.types';

export interface WorkbenchProps {}

export interface WorkbenchState {
selectedViewId: string | null;
viewPanelState: PanelState;
openResources: Resource[];
currentResource: Resource | null;
openObjects: IAdaptable[];
currentObject: IAdaptable | null;
}

export type PanelState = 'EXPANDED' | 'COLLAPSED';

export interface Resource {
id: string;
path: string;
name: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2023 Stéphane Bégaudeau.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { IAdaptable } from '../providers/AdapterFactory.types';

export interface IEditingContext extends IAdaptable {}
Loading

0 comments on commit e32c707

Please sign in to comment.