mirror of
https://github.com/lingble/twenty.git
synced 2025-10-30 20:27:55 +00:00
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:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -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]": {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>,
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <></>;
|
||||
};
|
||||
57
front/src/modules/metadata/components/ObjectTable.tsx
Normal file
57
front/src/modules/metadata/components/ObjectTable.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
61
front/src/modules/metadata/components/useSetDataTableData.ts
Normal file
61
front/src/modules/metadata/components/useSetDataTableData.ts
Normal 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],
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
59
front/src/modules/metadata/hooks/useFindManyCustomObjects.ts
Normal file
59
front/src/modules/metadata/hooks/useFindManyCustomObjects.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
};
|
||||
@@ -17,6 +17,8 @@ export enum AppPath {
|
||||
PersonShowPage = '/person/:personId',
|
||||
TasksPage = '/tasks',
|
||||
OpportunitiesPage = '/opportunities',
|
||||
ObjectTablePage = '/:objectName',
|
||||
|
||||
SettingsCatchAll = `/settings/*`,
|
||||
|
||||
// Impersonate
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
58
front/src/pages/companies/ObjectsTable.tsx
Normal file
58
front/src/pages/companies/ObjectsTable.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user