diff --git a/.vscode/settings.json b/.vscode/settings.json index a0240964a..bb8e0fdb0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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]": { diff --git a/front/src/App.tsx b/front/src/App.tsx index afa91e55d..9819a65ec 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -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 = () => { } /> } /> + + } + /> + , ]; + +export const suppliersAvailableColumnDefinitions: ColumnDefinition[] = + [ + { + 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, + { + 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, + ]; diff --git a/front/src/modules/metadata/components/FetchMetadataEffect.tsx b/front/src/modules/metadata/components/FetchMetadataEffect.tsx index 698c82168..d4ff6b642 100644 --- a/front/src/modules/metadata/components/FetchMetadataEffect.tsx +++ b/front/src/modules/metadata/components/FetchMetadataEffect.tsx @@ -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, diff --git a/front/src/modules/metadata/components/ObjectDataTableEffect.tsx b/front/src/modules/metadata/components/ObjectDataTableEffect.tsx new file mode 100644 index 000000000..3badecf63 --- /dev/null +++ b/front/src/modules/metadata/components/ObjectDataTableEffect.tsx @@ -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 <>; +}; diff --git a/front/src/modules/metadata/components/ObjectTable.tsx b/front/src/modules/metadata/components/ObjectTable.tsx new file mode 100644 index 000000000..04bd98a34 --- /dev/null +++ b/front/src/modules/metadata/components/ObjectTable.tsx @@ -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 ( + { + // + }, + }} + > + + + { + // + }} + /> + + + ); +}; diff --git a/front/src/modules/metadata/components/useSetDataTableData.ts b/front/src/modules/metadata/components/useSetDataTableData.ts new file mode 100644 index 000000000..17025876b --- /dev/null +++ b/front/src/modules/metadata/components/useSetDataTableData.ts @@ -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 }) => + (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], + ); +}; diff --git a/front/src/modules/metadata/hooks/useCreateOneCustomObject.ts b/front/src/modules/metadata/hooks/useCreateOneCustomObject.ts new file mode 100644 index 000000000..8d6517734 --- /dev/null +++ b/front/src/modules/metadata/hooks/useCreateOneCustomObject.ts @@ -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 +}; diff --git a/front/src/modules/metadata/hooks/useFindManyCustomObjects.ts b/front/src/modules/metadata/hooks/useFindManyCustomObjects.ts new file mode 100644 index 000000000..497a434d9 --- /dev/null +++ b/front/src/modules/metadata/hooks/useFindManyCustomObjects.ts @@ -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, + }; +}; diff --git a/front/src/modules/metadata/utils/generateFindManyCustomObjectsQuery.ts b/front/src/modules/metadata/utils/generateFindManyCustomObjectsQuery.ts new file mode 100644 index 000000000..b1308f127 --- /dev/null +++ b/front/src/modules/metadata/utils/generateFindManyCustomObjectsQuery.ts @@ -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 + } + } + } + `; +}; diff --git a/front/src/modules/types/AppPath.ts b/front/src/modules/types/AppPath.ts index f881040ec..51e7af629 100644 --- a/front/src/modules/types/AppPath.ts +++ b/front/src/modules/types/AppPath.ts @@ -17,6 +17,8 @@ export enum AppPath { PersonShowPage = '/person/:personId', TasksPage = '/tasks', OpportunitiesPage = '/opportunities', + ObjectTablePage = '/:objectName', + SettingsCatchAll = `/settings/*`, // Impersonate diff --git a/front/src/modules/ui/data-table/components/DataTableBody.tsx b/front/src/modules/ui/data-table/components/DataTableBody.tsx index 62af6f4ee..2467762cb 100644 --- a/front/src/modules/ui/data-table/components/DataTableBody.tsx +++ b/front/src/modules/ui/data-table/components/DataTableBody.tsx @@ -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); diff --git a/front/src/modules/ui/data-table/components/DataTableCell.tsx b/front/src/modules/ui/data-table/components/DataTableCell.tsx index 7025e7480..b7086f70e 100644 --- a/front/src/modules/ui/data-table/components/DataTableCell.tsx +++ b/front/src/modules/ui/data-table/components/DataTableCell.tsx @@ -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; } diff --git a/front/src/modules/ui/data-table/components/DataTableRow.tsx b/front/src/modules/ui/data-table/components/DataTableRow.tsx index 0515d6bc0..d6f8eef9d 100644 --- a/front/src/modules/ui/data-table/components/DataTableRow.tsx +++ b/front/src/modules/ui/data-table/components/DataTableRow.tsx @@ -27,7 +27,8 @@ export const DataTableRow = forwardRef( TableRecoilScopeContext, ); const { currentRowSelected } = useCurrentRowSelected(); - + // eslint-disable-next-line no-console + console.log({ visibleTableColumns }); return ( { const { fieldDefinition } = useContext(FieldContext); + // eslint-disable-next-line no-console + console.log({ fieldDefinition }); + const { closeTableCell } = useTableCell(); const { moveLeft, moveRight, moveDown } = useMoveSoftFocus(); diff --git a/front/src/pages/companies/ObjectsTable.tsx b/front/src/pages/companies/ObjectsTable.tsx new file mode 100644 index 000000000..75f909e39 --- /dev/null +++ b/front/src/pages/companies/ObjectsTable.tsx @@ -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 ( + + + + + + + + + + + + + + + + ); +};