Feat/front forge graphql query (#2007)

* wip

* Wip

* Wip

* Finished v1

* Wip

* Fix from PR

* Removed unused fragment masking feature

* Fix from PR

* Removed POC from nav bar

* Fix lint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-10-13 22:27:57 +02:00
committed by GitHub
parent 3ef9132525
commit a35ea5e8f9
16 changed files with 406 additions and 2 deletions

View File

@@ -5,18 +5,21 @@
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.addMissingImports": "always"
}
},
"[javascript]": {
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.addMissingImports": "always"
}
},
"[typescriptreact]": {
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.addMissingImports": "always"
}
},
"[json]": {

View File

@@ -28,6 +28,8 @@ import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMemb
import { Tasks } from '~/pages/tasks/Tasks';
import { getPageTitleFromPath } from '~/utils/title-utils';
import { ObjectTablePage } from './pages/companies/ObjectsTable';
export const App = () => {
const { pathname } = useLocation();
const pageTitle = getPageTitleFromPath(pathname);
@@ -54,6 +56,16 @@ export const App = () => {
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
<Route path={AppPath.OpportunitiesPage} element={<Opportunities />} />
<Route
path={AppPath.ObjectTablePage}
element={
<ObjectTablePage
objectName="supplier"
objectNameSingular="Supplier"
/>
}
/>
<Route
path={AppPath.SettingsCatchAll}
element={

View File

@@ -186,3 +186,39 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<FieldMetadata
infoTooltipContent: 'The company Twitter account.',
} satisfies ColumnDefinition<FieldURLMetadata>,
];
export const suppliersAvailableColumnDefinitions: ColumnDefinition<FieldMetadata>[] =
[
{
key: 'name',
name: 'Name',
Icon: IconBuildingSkyscraper,
size: 180,
index: 0,
type: 'text',
metadata: {
fieldName: 'name',
placeHolder: 'Company Name',
},
isVisible: true,
buttonIcon: IconArrowUpRight,
infoTooltipContent: 'The company name.',
basePathToShowPage: '/companies/',
} satisfies ColumnDefinition<FieldTextMetadata>,
{
key: 'city',
name: 'City',
Icon: IconBuildingSkyscraper,
size: 180,
index: 0,
type: 'text',
metadata: {
fieldName: 'city',
placeHolder: 'Company Name',
},
isVisible: true,
buttonIcon: IconArrowUpRight,
infoTooltipContent: 'The company name.',
basePathToShowPage: '/companies/',
} satisfies ColumnDefinition<FieldTextMetadata>,
];

View File

@@ -21,7 +21,10 @@ export const FetchMetadataEffect = () => {
query: GET_ALL_OBJECTS,
});
if (objects.data.objects.edges.length > 0) {
if (
objects.data.objects.edges.length > 0 &&
metadataObjects.length === 0
) {
const formattedObjects: MetadataObject[] =
objects.data.objects.edges.map((object) => ({
...object.node,

View File

@@ -0,0 +1,68 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
import { currentViewIdScopedState } from '@/ui/view-bar/states/currentViewIdScopedState';
import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState';
import { savedFiltersFamilyState } from '@/ui/view-bar/states/savedFiltersFamilyState';
import { savedSortsFamilyState } from '@/ui/view-bar/states/savedSortsFamilyState';
import { sortsScopedState } from '@/ui/view-bar/states/sortsScopedState';
import { useFindManyCustomObjects } from '../hooks/useFindManyCustomObjects';
import { useSetObjectDataTableData } from './useSetDataTableData';
export const ObjectDataTableEffect = ({
objectName,
objectNameSingular,
}: {
objectNameSingular: string;
objectName: string;
}) => {
const setDataTableData = useSetObjectDataTableData();
const { data } = useFindManyCustomObjects({ objectName });
useEffect(() => {
const entities = data?.['findMany' + objectNameSingular]?.edges ?? [];
setDataTableData(entities);
}, [data, objectNameSingular, setDataTableData]);
const [searchParams] = useSearchParams();
const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext);
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
const currentView = await snapshot.getPromise(
currentViewIdScopedState(tableRecoilScopeId),
);
if (currentView === viewId) {
return;
}
const savedFilters = await snapshot.getPromise(
savedFiltersFamilyState(viewId),
);
const savedSorts = await snapshot.getPromise(
savedSortsFamilyState(viewId),
);
set(filtersScopedState(tableRecoilScopeId), savedFilters);
set(sortsScopedState(tableRecoilScopeId), savedSorts);
set(currentViewIdScopedState(tableRecoilScopeId), viewId);
},
[tableRecoilScopeId],
);
useEffect(() => {
const viewId = searchParams.get('view');
if (viewId) {
handleViewSelect(viewId);
}
}, [handleViewSelect, searchParams]);
return <></>;
};

View File

@@ -0,0 +1,57 @@
import { suppliersAvailableColumnDefinitions } from '@/companies/constants/companiesAvailableColumnDefinitions';
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
import { DataTable } from '@/ui/data-table/components/DataTable';
import { TableContext } from '@/ui/data-table/contexts/TableContext';
import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { ViewBarContext } from '@/ui/view-bar/contexts/ViewBarContext';
import { useTableViews } from '@/views/hooks/useTableViews';
import { ObjectDataTableEffect } from './ObjectDataTableEffect';
export const ObjectTable = ({
objectName,
objectNameSingular,
}: {
objectNameSingular: string;
objectName: string;
}) => {
const { createView, deleteView, submitCurrentView, updateView } =
useTableViews({
objectId: 'company',
columnDefinitions: suppliersAvailableColumnDefinitions,
});
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
return (
<TableContext.Provider
value={{
onColumnsChange: () => {
//
},
}}
>
<ObjectDataTableEffect
objectName={objectName}
objectNameSingular={objectNameSingular}
/>
<ViewBarContext.Provider
value={{
defaultViewName: 'All Suppliers',
onCurrentViewSubmit: submitCurrentView,
onViewCreate: createView,
onViewEdit: updateView,
onViewRemove: deleteView,
onImport: openCompanySpreadsheetImport,
ViewBarRecoilScopeContext: TableRecoilScopeContext,
}}
>
<DataTable
updateEntityMutation={() => {
//
}}
/>
</ViewBarContext.Provider>
</TableContext.Provider>
);
};

View File

@@ -0,0 +1,61 @@
import { useRecoilCallback } from 'recoil';
import { useResetTableRowSelection } from '@/ui/data-table/hooks/useResetTableRowSelection';
import { isFetchingDataTableDataState } from '@/ui/data-table/states/isFetchingDataTableDataState';
import { numberOfTableRowsState } from '@/ui/data-table/states/numberOfTableRowsState';
import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { tableRowIdsState } from '@/ui/data-table/states/tableRowIdsState';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
import { availableFiltersScopedState } from '@/ui/view-bar/states/availableFiltersScopedState';
import { availableSortsScopedState } from '@/ui/view-bar/states/availableSortsScopedState';
import { entityCountInCurrentViewState } from '@/ui/view-bar/states/entityCountInCurrentViewState';
export const useSetObjectDataTableData = () => {
const resetTableRowSelection = useResetTableRowSelection();
const tableContextScopeId = useRecoilScopeId(TableRecoilScopeContext);
return useRecoilCallback(
({ set, snapshot }) =>
<T extends { node: { id: string } }>(newEntityArrayRaw: T[]) => {
const newEntityArray = newEntityArrayRaw.map((entity) => entity.node);
for (const entity of newEntityArray) {
const currentEntity = snapshot
.getLoadable(entityFieldsFamilyState(entity.id))
.valueOrThrow();
if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) {
set(entityFieldsFamilyState(entity.id), entity);
}
}
const entityIds = newEntityArray.map((entity) => entity.id);
// eslint-disable-next-line no-console
console.log({ newEntityArray, entityIds });
set(tableRowIdsState, (currentRowIds) => {
if (JSON.stringify(currentRowIds) !== JSON.stringify(entityIds)) {
return entityIds;
}
return currentRowIds;
});
resetTableRowSelection();
set(numberOfTableRowsState, entityIds.length);
set(entityCountInCurrentViewState, entityIds.length);
set(availableFiltersScopedState(tableContextScopeId), []);
set(availableSortsScopedState(tableContextScopeId), []);
set(isFetchingDataTableDataState, false);
},
[resetTableRowSelection, tableContextScopeId],
);
};

View File

@@ -0,0 +1,8 @@
// TODO: add zod to validate that we have at least id on each object
export const useCreateOneCustomObject = ({
_objectName,
}: {
_objectName: string;
}) => {
// TODO : code
};

View File

@@ -0,0 +1,59 @@
import { gql, useQuery } from '@apollo/client';
import { useRecoilState } from 'recoil';
import { metadataObjectsState } from '../states/metadataObjectsState';
import { generateFindManyCustomObjectsQuery } from '../utils/generateFindManyCustomObjectsQuery';
// TODO: add zod to validate that we have at least id on each object
export const useFindManyCustomObjects = ({
objectName,
}: {
objectName: string;
}) => {
const [metadataObjects] = useRecoilState(metadataObjectsState);
const foundObject = metadataObjects.find(
(object) => object.nameSingular === objectName,
);
// eslint-disable-next-line no-console
console.log({ foundObject });
const generatedQuery = foundObject
? generateFindManyCustomObjectsQuery({
metadataObject: foundObject,
})
: gql`
query EmptyQuery {
empty
}
`;
const {
fetchMore: fetchMoreBase,
data,
loading,
error,
} = useQuery(generatedQuery, {
skip: !foundObject,
});
// eslint-disable-next-line no-console
console.log({ data, loading, error });
const fetchMore = ({ fromCursor }: { fromCursor: string }) => {
fetchMoreBase({
variables: { fromCursor },
});
};
const objectNotFoundInMetadata = metadataObjects.length > 0 && !foundObject;
return {
data,
loading,
error,
fetchMore,
objectNotFoundInMetadata,
};
};

View File

@@ -0,0 +1,27 @@
import { gql } from '@apollo/client';
import { MetadataObject } from '../types/MetadataObject';
export const generateFindManyCustomObjectsQuery = ({
metadataObject,
_fromCursor,
}: {
metadataObject: MetadataObject;
_fromCursor?: string;
}) => {
return gql`
query CustomQuery${metadataObject.nameSingular} {
findMany${metadataObject.nameSingular}{
edges {
node {
id
${metadataObject.fields
.map((field) => field.nameSingular)
.join('\n')}
}
cursor
}
}
}
`;
};

View File

@@ -17,6 +17,8 @@ export enum AppPath {
PersonShowPage = '/person/:personId',
TasksPage = '/tasks',
OpportunitiesPage = '/opportunities',
ObjectTablePage = '/:objectName',
SettingsCatchAll = `/settings/*`,
// Impersonate

View File

@@ -27,6 +27,9 @@ export const DataTableBody = () => {
const tableRowIds = useRecoilValue(tableRowIdsState);
// eslint-disable-next-line no-console
console.log({ tableRowIds });
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
const isFetchingDataTableData = useRecoilValue(isFetchingDataTableDataState);

View File

@@ -37,6 +37,9 @@ export const DataTableCell = ({ cellIndex }: { cellIndex: number }) => {
const updateEntityMutation = useContext(EntityUpdateMutationContext);
// eslint-disable-next-line no-console
console.log({ columnDefinition, currentRowId });
if (!columnDefinition || !currentRowId) {
return null;
}

View File

@@ -27,7 +27,8 @@ export const DataTableRow = forwardRef<HTMLTableRowElement, DataTableRowProps>(
TableRecoilScopeContext,
);
const { currentRowSelected } = useCurrentRowSelected();
// eslint-disable-next-line no-console
console.log({ visibleTableColumns });
return (
<StyledRow
ref={ref}

View File

@@ -18,6 +18,9 @@ export const TableCell = ({
}) => {
const { fieldDefinition } = useContext(FieldContext);
// eslint-disable-next-line no-console
console.log({ fieldDefinition });
const { closeTableCell } = useTableCell();
const { moveLeft, moveRight, moveDown } = useMoveSoftFocus();

View File

@@ -0,0 +1,58 @@
import styled from '@emotion/styled';
import { v4 } from 'uuid';
import { ObjectTable } from '@/metadata/components/ObjectTable';
import { DataTableActionBar } from '@/ui/data-table/action-bar/components/DataTableActionBar';
import { DataTableContextMenu } from '@/ui/data-table/context-menu/components/DataTableContextMenu';
import { TableRecoilScopeContext } from '@/ui/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { IconBuildingSkyscraper } from '@/ui/icon';
import { PageAddButton } from '@/ui/layout/components/PageAddButton';
import { PageBody } from '@/ui/layout/components/PageBody';
import { PageContainer } from '@/ui/layout/components/PageContainer';
import { PageHeader } from '@/ui/layout/components/PageHeader';
import { PageHotkeysEffect } from '@/ui/layout/components/PageHotkeysEffect';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
const StyledTableContainer = styled.div`
display: flex;
width: 100%;
`;
export const ObjectTablePage = ({
objectName,
objectNameSingular,
}: {
objectNameSingular: string;
objectName: string;
}) => {
const handleAddButtonClick = async () => {
const newCompanyId: string = v4();
// eslint-disable-next-line no-console
console.log('newCompanyId', newCompanyId);
};
return (
<PageContainer>
<PageHeader title="Objects" Icon={IconBuildingSkyscraper}>
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
<PageAddButton onClick={handleAddButtonClick} />
</PageHeader>
<PageBody>
<RecoilScope
scopeId="objects"
CustomRecoilScopeContext={TableRecoilScopeContext}
>
<StyledTableContainer>
<ObjectTable
objectName={objectName}
objectNameSingular={objectNameSingular}
/>
</StyledTableContainer>
<DataTableActionBar />
<DataTableContextMenu />
</RecoilScope>
</PageBody>
</PageContainer>
);
};