Fix/metadata object and settings post merge (#2269)
* WIP * WIP2 * Seed views standard objects * Migrate views to the new data model --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -10,7 +10,6 @@ import { TableContext } from '@/ui/data/data-table/contexts/TableContext';
|
||||
import { useUpsertDataTableItem } from '@/ui/data/data-table/hooks/useUpsertDataTableItem';
|
||||
import { TableOptionsDropdown } from '@/ui/data/data-table/options/components/TableOptionsDropdown';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
import { ViewBarEffect } from '@/views/components/ViewBarEffect';
|
||||
import { useViewFields } from '@/views/hooks/internal/useViewFields';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
import { ViewScope } from '@/views/scopes/ViewScope';
|
||||
@ -78,12 +77,7 @@ export const CompanyTable = () => {
|
||||
`;
|
||||
|
||||
return (
|
||||
<ViewScope
|
||||
viewScopeId={tableViewScopeId}
|
||||
onViewFieldsChange={() => {}}
|
||||
onViewSortsChange={() => {}}
|
||||
onViewFiltersChange={() => {}}
|
||||
>
|
||||
<ViewScope viewScopeId={tableViewScopeId}>
|
||||
<StyledContainer>
|
||||
<TableContext.Provider
|
||||
value={{
|
||||
@ -93,14 +87,11 @@ export const CompanyTable = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ViewBarEffect />
|
||||
|
||||
<ViewBar
|
||||
optionsDropdownButton={<TableOptionsDropdown onImport={onImport} />}
|
||||
optionsDropdownScopeId="table-dropdown-option"
|
||||
/>
|
||||
<CompanyTableEffect />
|
||||
|
||||
<DataTableEffect
|
||||
getRequestResultKey="companies"
|
||||
useGetRequest={useGetCompaniesQuery}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
|
||||
import { currentViewIdScopedState } from '@/views/states/currentViewIdScopedState';
|
||||
|
||||
import { useFindManyObjects } from '../hooks/useFindManyObjects';
|
||||
import { useSetObjectDataTableData } from '../hooks/useSetDataTableData';
|
||||
@ -15,6 +9,7 @@ export type ObjectDataTableEffectProps = Pick<
|
||||
'objectNamePlural'
|
||||
>;
|
||||
|
||||
// TODO: merge in a single effect component
|
||||
export const ObjectDataTableEffect = ({
|
||||
objectNamePlural,
|
||||
}: ObjectDataTableEffectProps) => {
|
||||
@ -32,32 +27,5 @@ export const ObjectDataTableEffect = ({
|
||||
}
|
||||
}, [objects, setDataTableData, loading]);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext);
|
||||
const handleViewSelect = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(viewId: string) => {
|
||||
const currentView = snapshot
|
||||
.getLoadable(
|
||||
currentViewIdScopedState({ scopeId: tableRecoilScopeId }),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (currentView === viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(currentViewIdScopedState({ scopeId: tableRecoilScopeId }), viewId);
|
||||
},
|
||||
[tableRecoilScopeId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const viewId = searchParams.get('view');
|
||||
if (viewId) {
|
||||
handleViewSelect(viewId);
|
||||
}
|
||||
}, [handleViewSelect, searchParams, objectNamePlural]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@ -1,10 +1,29 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { DataTable } from '@/ui/data/data-table/components/DataTable';
|
||||
import { TableContext } from '@/ui/data/data-table/contexts/TableContext';
|
||||
import { TableOptionsDropdown } from '@/ui/data/data-table/options/components/TableOptionsDropdown';
|
||||
import { tableColumnsScopedState } from '@/ui/data/data-table/states/tableColumnsScopedState';
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
import { useViewFields } from '@/views/hooks/internal/useViewFields';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
import { ViewScope } from '@/views/scopes/ViewScope';
|
||||
|
||||
import { useUpdateOneObject } from '../hooks/useUpdateOneObject';
|
||||
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
|
||||
|
||||
import { ObjectDataTableEffect } from './ObjectDataTableEffect';
|
||||
import { ObjectTableEffect } from './ObjectTableEffect';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export type ObjectTableProps = Pick<
|
||||
MetadataObjectIdentifier,
|
||||
@ -16,6 +35,11 @@ export const ObjectTable = ({ objectNamePlural }: ObjectTableProps) => {
|
||||
objectNamePlural,
|
||||
});
|
||||
|
||||
const viewScopeId = objectNamePlural ?? '';
|
||||
|
||||
const { persistViewFields } = useViewFields(viewScopeId);
|
||||
const { setCurrentViewFields } = useView({ viewScopeId: viewScopeId });
|
||||
|
||||
const updateEntity = ({
|
||||
variables,
|
||||
}: {
|
||||
@ -32,17 +56,38 @@ export const ObjectTable = ({ objectNamePlural }: ObjectTableProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const updateTableColumns = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(viewFields: ColumnDefinition<FieldMetadata>[]) => {
|
||||
set(tableColumnsScopedState(viewScopeId), viewFields);
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<TableContext.Provider
|
||||
value={{
|
||||
onColumnsChange: () => {
|
||||
//
|
||||
},
|
||||
<ViewScope
|
||||
viewScopeId={viewScopeId}
|
||||
onViewFieldsChange={(viewFields) => {
|
||||
// updateTableColumns(viewFields);
|
||||
}}
|
||||
>
|
||||
<ObjectDataTableEffect objectNamePlural={objectNamePlural} />
|
||||
|
||||
<DataTable updateEntityMutation={updateEntity} />
|
||||
</TableContext.Provider>
|
||||
<StyledContainer>
|
||||
<TableContext.Provider
|
||||
value={{
|
||||
onColumnsChange: (columns) => {
|
||||
// setCurrentViewFields?.(columns);
|
||||
// persistViewFields(columns);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ViewBar
|
||||
optionsDropdownButton={<TableOptionsDropdown />}
|
||||
optionsDropdownScopeId="table-dropdown-option"
|
||||
/>
|
||||
<ObjectTableEffect />
|
||||
<ObjectDataTableEffect objectNamePlural={objectNamePlural} />
|
||||
<DataTable updateEntityMutation={updateEntity} />
|
||||
</TableContext.Provider>
|
||||
</StyledContainer>
|
||||
</ViewScope>
|
||||
);
|
||||
};
|
||||
|
||||
86
front/src/modules/metadata/components/ObjectTableEffect.tsx
Normal file
86
front/src/modules/metadata/components/ObjectTableEffect.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { availableTableColumnsScopedState } from '@/ui/data/data-table/states/availableTableColumnsScopedState';
|
||||
import { useView } from '@/views/hooks/useView';
|
||||
import { ViewType } from '~/generated/graphql';
|
||||
|
||||
import { useMetadataObjectInContext } from '../hooks/useMetadataObjectInContext';
|
||||
|
||||
export const ObjectTableEffect = () => {
|
||||
console.log('ObjectTableEffect');
|
||||
|
||||
const {
|
||||
setAvailableSorts,
|
||||
setAvailableFilters,
|
||||
setAvailableFields,
|
||||
setViewType,
|
||||
setViewObjectId,
|
||||
} = useView();
|
||||
|
||||
// const [, setTableColumns] = useRecoilScopedState(
|
||||
// tableColumnsScopedState,
|
||||
// TableRecoilScopeContext,
|
||||
// );
|
||||
|
||||
// const [, setTableSorts] = useRecoilScopedState(
|
||||
// tableSortsScopedState,
|
||||
// TableRecoilScopeContext,
|
||||
// );
|
||||
|
||||
// const [, setTableFilters] = useRecoilScopedState(
|
||||
// tableFiltersScopedState,
|
||||
// TableRecoilScopeContext,
|
||||
// );
|
||||
|
||||
const { columnDefinitions, objectNamePlural } = useMetadataObjectInContext();
|
||||
|
||||
const setAvailableTableColumns = useSetRecoilState(
|
||||
availableTableColumnsScopedState(objectNamePlural ?? ''),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setAvailableSorts?.([]); // TODO: extract from metadata fields
|
||||
setAvailableFilters?.([]); // TODO: extract from metadata fields
|
||||
setAvailableFields?.(columnDefinitions);
|
||||
setViewObjectId?.(objectNamePlural);
|
||||
setViewType?.(ViewType.Table);
|
||||
|
||||
setAvailableTableColumns(columnDefinitions);
|
||||
}, [
|
||||
setAvailableFields,
|
||||
setAvailableFilters,
|
||||
setAvailableSorts,
|
||||
setAvailableTableColumns,
|
||||
setViewObjectId,
|
||||
setViewType,
|
||||
columnDefinitions,
|
||||
objectNamePlural,
|
||||
]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (currentViewFields) {
|
||||
// setTableColumns([...currentViewFields].sort((a, b) => a.index - b.index));
|
||||
// }
|
||||
// }, [currentViewFields, setTableColumns]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (currentViewSorts) {
|
||||
// setTableSorts(currentViewSorts);
|
||||
// }
|
||||
// }, [currentViewFields, currentViewSorts, setTableColumns, setTableSorts]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (currentViewFilters) {
|
||||
// setTableFilters(currentViewFilters);
|
||||
// }
|
||||
// }, [
|
||||
// currentViewFields,
|
||||
// currentViewFilters,
|
||||
// setTableColumns,
|
||||
// setTableFilters,
|
||||
// setTableSorts,
|
||||
// ]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -38,6 +38,10 @@ export const useFindManyMetadataObjects = () => {
|
||||
},
|
||||
);
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
// eslint-disable-next-line no-console
|
||||
//console.log('useFindManyMetadataObjects data : ', data);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -15,7 +15,14 @@ export const useFindManyObjects = <
|
||||
ObjectType extends { id: string } & Record<string, any>,
|
||||
>({
|
||||
objectNamePlural,
|
||||
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'>) => {
|
||||
filter,
|
||||
orderBy,
|
||||
onCompleted,
|
||||
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'> & {
|
||||
filter?: any;
|
||||
orderBy?: any;
|
||||
onCompleted?: (data: any) => void;
|
||||
}) => {
|
||||
const { foundMetadataObject, objectNotFoundInMetadata, findManyQuery } =
|
||||
useFindOneMetadataObject({
|
||||
objectNamePlural,
|
||||
@ -27,6 +34,12 @@ export const useFindManyObjects = <
|
||||
findManyQuery,
|
||||
{
|
||||
skip: !foundMetadataObject,
|
||||
variables: {
|
||||
filter: filter ?? {},
|
||||
orderBy: orderBy ?? {},
|
||||
},
|
||||
onCompleted: (data) =>
|
||||
objectNamePlural && onCompleted?.(data[objectNamePlural]),
|
||||
onError: (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import { PaginatedObjectTypeResults } from './PaginatedObjectTypeResults';
|
||||
|
||||
export type PaginatedObjectType<ObjectType extends { id: string }> = {
|
||||
[objectNamePlural: string]: {
|
||||
edges: {
|
||||
node: ObjectType;
|
||||
cursor: string;
|
||||
}[];
|
||||
};
|
||||
[objectNamePlural: string]: PaginatedObjectTypeResults<ObjectType>;
|
||||
};
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export type PaginatedObjectTypeResults<ObjectType extends { id: string }> = {
|
||||
edges: {
|
||||
node: ObjectType;
|
||||
cursor: string;
|
||||
}[];
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
import { MetadataObject } from '../types/MetadataObject';
|
||||
|
||||
import { mapFieldMetadataToGraphQLQuery } from './mapFieldMetadataToGraphQLQuery';
|
||||
@ -12,8 +14,10 @@ export const generateFindManyCustomObjectsQuery = ({
|
||||
_fromCursor?: string;
|
||||
}) => {
|
||||
return gql`
|
||||
query FindMany${metadataObject.namePlural} {
|
||||
${metadataObject.namePlural}{
|
||||
query FindMany${metadataObject.namePlural}($filter: ${capitalize(
|
||||
metadataObject.nameSingular,
|
||||
)}FilterInput, $orderBy: ${capitalize(metadataObject.nameSingular)}OrderBy) {
|
||||
${metadataObject.namePlural}(filter: $filter, orderBy: $orderBy){
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
|
||||
@ -14,6 +14,7 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
|
||||
|
||||
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
|
||||
import { ViewBarDetails } from './ViewBarDetails';
|
||||
import { ViewBarEffect } from './ViewBarEffect';
|
||||
import { ViewsDropdownButton } from './ViewsDropdownButton';
|
||||
|
||||
export type ViewBarProps = {
|
||||
@ -43,6 +44,7 @@ export const ViewBar = ({
|
||||
availableSorts={availableSorts}
|
||||
onSortAdd={upsertViewSort}
|
||||
>
|
||||
<ViewBarEffect />
|
||||
<TopBar
|
||||
className={className}
|
||||
leftComponent={
|
||||
|
||||
@ -2,16 +2,15 @@ import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useFindManyObjects } from '@/metadata/hooks/useFindManyObjects';
|
||||
import { PaginatedObjectTypeResults } from '@/metadata/types/PaginatedObjectTypeResults';
|
||||
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
import { Filter } from '@/ui/data/filter/types/Filter';
|
||||
import { Sort } from '@/ui/data/sort/types/Sort';
|
||||
import {
|
||||
SortOrder,
|
||||
useGetViewFieldsQuery,
|
||||
useGetViewFiltersQuery,
|
||||
useGetViewSortsQuery,
|
||||
useGetViewsQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
@ -25,6 +24,8 @@ import { savedViewFieldsScopedFamilyState } from '../states/savedViewFieldsScope
|
||||
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
|
||||
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
|
||||
import { viewsScopedState } from '../states/viewsScopedState';
|
||||
import { View } from '../types/View';
|
||||
import { ViewField } from '../types/ViewField';
|
||||
|
||||
export const ViewBarEffect = () => {
|
||||
const {
|
||||
@ -45,80 +46,78 @@ export const ViewBarEffect = () => {
|
||||
|
||||
const { viewType, viewObjectId } = useViewInternalStates(viewScopeId);
|
||||
|
||||
useGetViewFieldsQuery({
|
||||
skip: !currentViewId,
|
||||
variables: {
|
||||
orderBy: { index: SortOrder.Asc },
|
||||
where: {
|
||||
viewId: { equals: currentViewId },
|
||||
},
|
||||
},
|
||||
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
|
||||
const availableFields = snapshot
|
||||
.getLoadable(availableFieldsScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
useFindManyObjects({
|
||||
objectNamePlural: 'viewFieldsV2',
|
||||
filter: { viewId: { eq: currentViewId } },
|
||||
onCompleted: useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (data: PaginatedObjectTypeResults<ViewField>) => {
|
||||
const availableFields = snapshot
|
||||
.getLoadable(availableFieldsScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
|
||||
if (!availableFields || !currentViewId) {
|
||||
return;
|
||||
}
|
||||
if (!availableFields || !currentViewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const savedViewFields = snapshot
|
||||
.getLoadable(
|
||||
savedViewFieldsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
const savedViewFields = snapshot
|
||||
.getLoadable(
|
||||
savedViewFieldsScopedFamilyState({
|
||||
scopeId: viewScopeId,
|
||||
familyKey: currentViewId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const queriedViewFields = data.viewFields
|
||||
.map<ColumnDefinition<FieldMetadata> | null>((viewField) => {
|
||||
const columnDefinition = availableFields.find(
|
||||
({ key }) => viewField.key === key,
|
||||
);
|
||||
const queriedViewFields = data.edges
|
||||
.map<ColumnDefinition<FieldMetadata> | null>((viewField) => {
|
||||
const columnDefinition = availableFields.find(
|
||||
({ key }) => viewField.node.fieldId === key,
|
||||
);
|
||||
|
||||
return columnDefinition
|
||||
? {
|
||||
...columnDefinition,
|
||||
key: viewField.key,
|
||||
name: viewField.name,
|
||||
index: viewField.index,
|
||||
size: viewField.size ?? columnDefinition.size,
|
||||
isVisible: viewField.isVisible,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter<ColumnDefinition<FieldMetadata>>(assertNotNull);
|
||||
return columnDefinition
|
||||
? {
|
||||
...columnDefinition,
|
||||
key: viewField.node.fieldId,
|
||||
name: viewField.node.fieldId,
|
||||
index: viewField.node.position,
|
||||
size: viewField.node.size ?? columnDefinition.size,
|
||||
isVisible: viewField.node.isVisible,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter<ColumnDefinition<FieldMetadata>>(assertNotNull);
|
||||
|
||||
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
|
||||
setCurrentViewFields?.(queriedViewFields);
|
||||
setSavedViewFields?.(queriedViewFields);
|
||||
}
|
||||
}),
|
||||
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
|
||||
setCurrentViewFields?.(queriedViewFields);
|
||||
setSavedViewFields?.(queriedViewFields);
|
||||
}
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
useGetViewsQuery({
|
||||
variables: {
|
||||
where: {
|
||||
objectId: { equals: viewObjectId },
|
||||
type: { equals: viewType },
|
||||
},
|
||||
},
|
||||
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
|
||||
const nextViews = data.views.map((view) => ({
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
}));
|
||||
const views = snapshot
|
||||
.getLoadable(viewsScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
useFindManyObjects({
|
||||
objectNamePlural: 'viewsV2',
|
||||
filter: { type: { eq: viewType }, objectId: { eq: viewObjectId } },
|
||||
onCompleted: useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (data: PaginatedObjectTypeResults<View>) => {
|
||||
const nextViews = data.edges.map((view) => ({
|
||||
id: view.node.id,
|
||||
name: view.node.name,
|
||||
objectId: view.node.objectId,
|
||||
}));
|
||||
const views = snapshot
|
||||
.getLoadable(viewsScopedState({ scopeId: viewScopeId }))
|
||||
.getValue();
|
||||
|
||||
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
|
||||
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
|
||||
|
||||
if (!nextViews.length) return;
|
||||
if (!nextViews.length) return;
|
||||
|
||||
if (!currentViewId) return changeView(nextViews[0].id);
|
||||
}),
|
||||
if (!currentViewId) return setCurrentViewId(nextViews[0].id);
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
useGetViewSortsQuery({
|
||||
|
||||
@ -17,29 +17,32 @@ export const useViews = (scopeId: string) => {
|
||||
const [updateViewMutation] = useUpdateViewMutation();
|
||||
const [deleteViewMutation] = useDeleteViewMutation();
|
||||
|
||||
const createView = useRecoilCallback(({ snapshot }) => async (view: View) => {
|
||||
const viewObjectId = await snapshot
|
||||
.getLoadable(viewObjectIdScopeState({ scopeId }))
|
||||
.getValue();
|
||||
const createView = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async (view: Pick<View, 'id' | 'name'>) => {
|
||||
const viewObjectId = await snapshot
|
||||
.getLoadable(viewObjectIdScopeState({ scopeId }))
|
||||
.getValue();
|
||||
|
||||
const viewType = await snapshot
|
||||
.getLoadable(viewTypeScopedState({ scopeId }))
|
||||
.getValue();
|
||||
const viewType = await snapshot
|
||||
.getLoadable(viewTypeScopedState({ scopeId }))
|
||||
.getValue();
|
||||
|
||||
if (!viewObjectId || !viewType) {
|
||||
return;
|
||||
}
|
||||
await createViewMutation({
|
||||
variables: {
|
||||
data: {
|
||||
...view,
|
||||
objectId: viewObjectId,
|
||||
type: viewType,
|
||||
},
|
||||
if (!viewObjectId || !viewType) {
|
||||
return;
|
||||
}
|
||||
await createViewMutation({
|
||||
variables: {
|
||||
data: {
|
||||
...view,
|
||||
objectId: viewObjectId,
|
||||
type: viewType,
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
});
|
||||
);
|
||||
|
||||
const updateView = async (view: View) => {
|
||||
await updateViewMutation({
|
||||
|
||||
@ -1 +1 @@
|
||||
export type View = { id: string; name: string };
|
||||
export type View = { id: string; name: string; objectId: string };
|
||||
|
||||
9
front/src/modules/views/types/ViewField.ts
Normal file
9
front/src/modules/views/types/ViewField.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export type ViewField = {
|
||||
id: string;
|
||||
objectId: string;
|
||||
fieldId: string;
|
||||
viewId: string;
|
||||
position: number;
|
||||
isVisible: boolean;
|
||||
size: number;
|
||||
};
|
||||
@ -24,17 +24,16 @@
|
||||
"prisma:generate-gql-select": "node scripts/generate-model-select-map.js",
|
||||
"prisma:generate-nest-graphql": "npx prisma generate --generator nestgraphql",
|
||||
"prisma:generate": "yarn prisma:generate-client && yarn prisma:generate-gql-select && yarn prisma:generate-nest-graphql",
|
||||
"prisma:reset": "npx prisma migrate reset && yarn prisma:generate",
|
||||
"prisma:seed": "npx prisma db seed",
|
||||
"prisma:migrate": "npx prisma migrate deploy",
|
||||
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
|
||||
"typeorm:migrate": "yarn typeorm migration:run -d ./src/metadata/metadata.datasource.ts",
|
||||
"database:init": "yarn database:setup && yarn database:seed",
|
||||
"database:setup": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate",
|
||||
"database:setup": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate && yarn database:generate",
|
||||
"database:truncate": "npx ts-node ./scripts/truncate-db.ts",
|
||||
"database:migrate": "yarn typeorm:migrate && yarn prisma:migrate",
|
||||
"database:generate": "yarn prisma:generate",
|
||||
"database:seed": "yarn prisma:seed",
|
||||
"database:seed": "yarn prisma:seed && yarn command tenant:sync-metadata -w twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419 && yarn command tenant:migrate -w twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419 && yarn command tenant:data-seed -w twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419",
|
||||
"database:reset": "yarn database:truncate && yarn database:init",
|
||||
"command": "node dist/src/command"
|
||||
},
|
||||
|
||||
@ -12,51 +12,4 @@ export const seedMetadata = async (prisma: PrismaClient) => {
|
||||
'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1', 'workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd', 'postgres', 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
|
||||
) ON CONFLICT DO NOTHING`,
|
||||
);
|
||||
|
||||
await prisma.$queryRawUnsafe(`CREATE TABLE IF NOT EXISTS
|
||||
workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company(
|
||||
"id" TEXT PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
"deletedAt" TIMESTAMP WITH TIME ZONE,
|
||||
"domainName" TEXT NOT NULL,
|
||||
"address" TEXT NOT NULL,
|
||||
"employees" INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
await prisma.$queryRawUnsafe(`INSERT INTO workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company(
|
||||
"id", "name", "domainName", "address", "employees"
|
||||
)
|
||||
VALUES (
|
||||
'89bb825c-171e-4bcc-9cf7-43448d6fb278', 'Airbnb', 'airbnb.com', 'San Francisco', 5000
|
||||
), (
|
||||
'04b2e9f5-0713-40a5-8216-82802401d33e', 'Qonto', 'qonto.com', 'San Francisco', 800
|
||||
), (
|
||||
'118995f3-5d81-46d6-bf83-f7fd33ea6102', 'Facebook', 'facebook.com', 'San Francisco', 8000
|
||||
), (
|
||||
'460b6fb1-ed89-413a-b31a-962986e67bb4', 'Microsoft', 'microsoft.com', 'San Francisco', 800
|
||||
), (
|
||||
'fe256b39-3ec3-4fe3-8997-b76aa0bfa408', 'Linkedin', 'linkedin.com', 'San Francisco', 400
|
||||
) ON CONFLICT DO NOTHING`);
|
||||
|
||||
await prisma.$queryRawUnsafe(`INSERT INTO metadata.object_metadata(
|
||||
id, name_singular, name_plural, label_singular, label_plural, description, icon, target_table_name, is_custom, is_active, workspace_id, data_source_id
|
||||
)
|
||||
VALUES (
|
||||
'ba391617-ee08-432f-9438-2e17df5ac279', 'companyV2', 'companiesV2', 'Company', 'Companies', 'Companies', 'IconBuilding', 'company', false, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', 'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1'
|
||||
) ON CONFLICT DO NOTHING`);
|
||||
|
||||
await prisma.$queryRawUnsafe(`INSERT INTO metadata.field_metadata(
|
||||
id, object_id, type, name, label, target_column_map, description, icon, enums, is_custom, is_active, is_nullable, workspace_id
|
||||
)
|
||||
VALUES (
|
||||
'22f5906d-153f-448c-b254-28adce721dcd', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'name', 'Name', '{"value": "name"}', 'Name', 'IconUser', NULL, false, true, false, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
|
||||
), (
|
||||
'19bfab29-1cbb-4ce2-9117-8540ac45a0f1', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'domainName', 'Domain Name', '{"value": "domainName"}', 'Domain Name', 'IconExternalLink', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
|
||||
), (
|
||||
'70130f27-9497-4b44-a04c-1a0fb9a4829c', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'address', 'Address', '{"value": "address"}', 'Address', 'IconMap', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
|
||||
), (
|
||||
'2a63c30e-8e80-475b-b5d7-9dda17adc537', 'ba391617-ee08-432f-9438-2e17df5ac279', 'number', 'employees', 'Employees', '{"value": "employees"}', 'Employees', 'IconUsers', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
|
||||
) ON CONFLICT DO NOTHING`);
|
||||
};
|
||||
|
||||
47
server/src/metadata/commands/data-seed-tenant.command.ts
Normal file
47
server/src/metadata/commands/data-seed-tenant.command.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { TenantInitialisationService } from '../tenant-initialisation/tenant-initialisation.service';
|
||||
import { DataSourceMetadataService } from '../data-source-metadata/data-source-metadata.service';
|
||||
|
||||
// TODO: implement dry-run
|
||||
interface DataSeedTenantOptions {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'tenant:data-seed',
|
||||
description: 'Seed tenant with initial data',
|
||||
})
|
||||
export class DataSeedTenantCommand extends CommandRunner {
|
||||
constructor(
|
||||
private readonly dataSourceMetadataService: DataSourceMetadataService,
|
||||
private readonly tenantInitialisationService: TenantInitialisationService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: DataSeedTenantOptions,
|
||||
): Promise<void> {
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceMetadataService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
options.workspaceId,
|
||||
);
|
||||
// TODO: run in a dedicated job + run queries in a transaction.
|
||||
await this.tenantInitialisationService.prefillWorkspaceWithStandardObjects(
|
||||
dataSourceMetadata,
|
||||
options.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: workspaceId should be optional and we should run migrations for all workspaces
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id',
|
||||
required: true,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data
|
||||
|
||||
import { SyncTenantMetadataCommand } from './sync-tenant-metadata.command';
|
||||
import { RunTenantMigrationsCommand } from './run-tenant-migrations.command';
|
||||
import { DataSeedTenantCommand } from './data-seed-tenant.command';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -19,6 +20,10 @@ import { RunTenantMigrationsCommand } from './run-tenant-migrations.command';
|
||||
DataSourceMetadataModule,
|
||||
TenantInitialisationModule,
|
||||
],
|
||||
providers: [RunTenantMigrationsCommand, SyncTenantMetadataCommand],
|
||||
providers: [
|
||||
RunTenantMigrationsCommand,
|
||||
SyncTenantMetadataCommand,
|
||||
DataSeedTenantCommand,
|
||||
],
|
||||
})
|
||||
export class MetadataCommandModule {}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service';
|
||||
import { FieldMetadataService } from 'src/metadata/field-metadata/services/field-metadata.service';
|
||||
import { TenantInitialisationService } from 'src/metadata/tenant-initialisation/tenant-initialisation.service';
|
||||
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
|
||||
|
||||
@ -17,7 +16,6 @@ interface RunTenantMigrationsOptions {
|
||||
export class SyncTenantMetadataCommand extends CommandRunner {
|
||||
constructor(
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly dataSourceMetadataService: DataSourceMetadataService,
|
||||
private readonly tenantInitialisationService: TenantInitialisationService,
|
||||
) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"id": "1a8487a0-480c-434e-b4c7-e22408b97047",
|
||||
"nameSingular": "companyV2",
|
||||
"namePlural": "companiesV2",
|
||||
"labelSingular": "Company",
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import companyObject from './companies/companies.metadata.json';
|
||||
import personObject from './people/people.metadata.json';
|
||||
import viewObject from './views/views.metadata.json';
|
||||
import viewFieldObject from './view-fields/view-fields.metadata.json';
|
||||
|
||||
export const standardObjectsMetadata = {
|
||||
companyV2: companyObject,
|
||||
personV2: personObject,
|
||||
viewV2: viewObject,
|
||||
viewFieldV2: viewFieldObject,
|
||||
};
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import companySeeds from './companies/companies.seeds.json';
|
||||
import viewSeeds from './views/views.seeds.json';
|
||||
import viewFieldSeeds from './view-fields/view-fields.seeds.json';
|
||||
|
||||
export const standardObjectsSeeds = {
|
||||
companyV2: companySeeds,
|
||||
viewV2: viewSeeds,
|
||||
viewFieldV2: viewFieldSeeds,
|
||||
};
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
{
|
||||
"nameSingular": "viewFieldV2",
|
||||
"namePlural": "viewFieldsV2",
|
||||
"labelSingular": "View Field",
|
||||
"labelPlural": "View Fields",
|
||||
"targetTableName": "viewField",
|
||||
"description": "(System) View Fields",
|
||||
"icon": "arrows-sort",
|
||||
"fields": [
|
||||
{
|
||||
"type": "text",
|
||||
"name": "objectId",
|
||||
"label": "Object Id",
|
||||
"targetColumnMap": {
|
||||
"value": "objectId"
|
||||
},
|
||||
"description": "View Field target object",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "fieldId",
|
||||
"label": "Field Id",
|
||||
"targetColumnMap": {
|
||||
"value": "fieldId"
|
||||
},
|
||||
"description": "View Field target field",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "viewId",
|
||||
"label": "View Id",
|
||||
"targetColumnMap": {
|
||||
"value": "viewId"
|
||||
},
|
||||
"description": "View Field related view",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "isVisible",
|
||||
"label": "Visible",
|
||||
"targetColumnMap": {
|
||||
"value": "isVisible"
|
||||
},
|
||||
"description": "View Field visibility",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"name": "size",
|
||||
"label": "Size",
|
||||
"targetColumnMap": {
|
||||
"value": "size"
|
||||
},
|
||||
"description": "View Field size",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"name": "position",
|
||||
"label": "Position",
|
||||
"targetColumnMap": {
|
||||
"value": "position"
|
||||
},
|
||||
"description": "View Field position",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
[
|
||||
{
|
||||
"objectId": "1a8487a0-480c-434e-b4c7-e22408b97047",
|
||||
"fieldId": "name",
|
||||
"viewId": "10bec73c-0aea-4cc4-a3b2-8c2186f29b43",
|
||||
"position": 0,
|
||||
"isVisible": true,
|
||||
"size": 180
|
||||
},
|
||||
|
||||
{
|
||||
"objectId": "company",
|
||||
"fieldId": "name",
|
||||
"viewId": "37a8a866-eb17-4e76-9382-03143a2f6a80",
|
||||
"position": 0,
|
||||
"isVisible": true,
|
||||
"size": 180
|
||||
},
|
||||
{
|
||||
"objectId": "company",
|
||||
"fieldId": "domainName",
|
||||
"viewId": "37a8a866-eb17-4e76-9382-03143a2f6a80",
|
||||
"position": 1,
|
||||
"isVisible": true,
|
||||
"size": 100
|
||||
},
|
||||
{
|
||||
"objectId": "company",
|
||||
"fieldId": "accountOwner",
|
||||
"viewId": "37a8a866-eb17-4e76-9382-03143a2f6a80",
|
||||
"position": 2,
|
||||
"isVisible": true,
|
||||
"size": 150
|
||||
},
|
||||
{
|
||||
"objectId": "company",
|
||||
"fieldId": "createdAt",
|
||||
"viewId": "37a8a866-eb17-4e76-9382-03143a2f6a80",
|
||||
"position": 3,
|
||||
"isVisible": true,
|
||||
"size": 150
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,44 @@
|
||||
{
|
||||
"nameSingular": "viewV2",
|
||||
"namePlural": "viewsV2",
|
||||
"labelSingular": "View",
|
||||
"labelPlural": "Views",
|
||||
"targetTableName": "view",
|
||||
"description": "(System) Views",
|
||||
"icon": "layout-collage",
|
||||
"fields": [
|
||||
{
|
||||
"type": "text",
|
||||
"name": "name",
|
||||
"label": "Name",
|
||||
"targetColumnMap": {
|
||||
"value": "name"
|
||||
},
|
||||
"description": "View name",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "objectId",
|
||||
"label": "Object Id",
|
||||
"targetColumnMap": {
|
||||
"value": "objectId"
|
||||
},
|
||||
"description": "View target object",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "type",
|
||||
"label": "Type",
|
||||
"targetColumnMap": {
|
||||
"value": "type"
|
||||
},
|
||||
"description": "View type",
|
||||
"icon": null,
|
||||
"isNullable": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "37a8a866-eb17-4e76-9382-03143a2f6a80",
|
||||
"name": "All companies",
|
||||
"objectId": "company",
|
||||
"type": "Table"
|
||||
},
|
||||
{
|
||||
"id": "6095799e-b48f-4e00-b071-10818083593a",
|
||||
"name": "All companies",
|
||||
"objectId": "person",
|
||||
"type": "Table"
|
||||
},
|
||||
{
|
||||
"id": "e26f66b7-f890-4a5c-b4d2-ec09987b5308",
|
||||
"name": "All Opportunities",
|
||||
"objectId": "company",
|
||||
"type": "Pipeline"
|
||||
},
|
||||
{
|
||||
"id": "10bec73c-0aea-4cc4-a3b2-8c2186f29b43",
|
||||
"name": "All Companies (V2)",
|
||||
"objectId": "1a8487a0-480c-434e-b4c7-e22408b97047",
|
||||
"type": "Table"
|
||||
}
|
||||
]
|
||||
@ -98,7 +98,7 @@ export class TenantInitialisationService {
|
||||
);
|
||||
}
|
||||
|
||||
private async prefillWorkspaceWithStandardObjects(
|
||||
public async prefillWorkspaceWithStandardObjects(
|
||||
dataSourceMetadata: DataSourceMetadata,
|
||||
workspaceId: string,
|
||||
) {
|
||||
@ -117,11 +117,7 @@ export class TenantInitialisationService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fields = standardObjectsMetadata[object.nameSingular].fields;
|
||||
|
||||
const columns = fields.map((field: FieldMetadata) =>
|
||||
Object.values(field.targetColumnMap),
|
||||
);
|
||||
const columns = Object.keys(seedData[0]);
|
||||
|
||||
await workspaceDataSource
|
||||
?.createQueryBuilder()
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
|
||||
export const addViewTable: TenantMigrationTableAction[] = [
|
||||
{
|
||||
name: 'view',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'view',
|
||||
action: 'alter',
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'varchar',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'objectId',
|
||||
type: 'varchar',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
type: 'varchar',
|
||||
action: 'create',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,44 @@
|
||||
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
|
||||
|
||||
export const addViewFieldTable: TenantMigrationTableAction[] = [
|
||||
{
|
||||
name: 'viewField',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'viewField',
|
||||
action: 'alter',
|
||||
columns: [
|
||||
{
|
||||
name: 'objectId',
|
||||
type: 'varchar',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'fieldId',
|
||||
type: 'varchar',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'viewId',
|
||||
type: 'varchar',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'position',
|
||||
type: 'integer',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'isVisible',
|
||||
type: 'boolean',
|
||||
action: 'create',
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
type: 'integer',
|
||||
action: 'create',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,8 +1,12 @@
|
||||
import { addCompanyTable } from './migrations/1697618009-addCompanyTable';
|
||||
import { addPeopleTable } from './migrations/1697618010-addPeopleTable';
|
||||
import { addViewTable } from './migrations/1697618011-addViewTable';
|
||||
import { addViewFieldTable } from './migrations/1697618012-addViewFieldTable';
|
||||
|
||||
// TODO: read the folder and return all migrations
|
||||
export const standardMigrations = {
|
||||
'1697618009-addCompanyTable': addCompanyTable,
|
||||
'1697618010-addPeopleTable': addPeopleTable,
|
||||
'1697618011-addViewTable': addViewTable,
|
||||
'1697618012-addViewFieldTable': addViewFieldTable,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user