Field level permission checks on widgets (#14774)
This commit is contained in:
+160
@@ -0,0 +1,160 @@
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { checkFieldPermissions } from '../checkFieldPermissions';
|
||||
|
||||
describe('checkFieldPermissions', () => {
|
||||
const mockObjectPermissions: ObjectPermissions = {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
};
|
||||
|
||||
it('should return true when no fields are restricted', () => {
|
||||
const fieldIds = ['field-1', 'field-2', 'field-3'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when field is not in restrictedFields map', () => {
|
||||
const fieldIds = ['field-1', 'field-2'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-3': { canRead: false },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when any field has canRead set to false', () => {
|
||||
const fieldIds = ['field-1', 'field-2', 'field-3'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-2': { canRead: false },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when multiple fields are restricted', () => {
|
||||
const fieldIds = ['field-1', 'field-2', 'field-3'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canRead: false },
|
||||
'field-2': { canRead: false },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when field has canRead set to true', () => {
|
||||
const fieldIds = ['field-1', 'field-2'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canRead: true },
|
||||
'field-2': { canRead: true },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when field has canRead set to null', () => {
|
||||
const fieldIds = ['field-1'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canRead: null },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when field has canRead set to undefined', () => {
|
||||
const fieldIds = ['field-1'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canRead: undefined },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for empty field list', () => {
|
||||
const fieldIds: string[] = [];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canRead: false },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle mixed permissions correctly', () => {
|
||||
const fieldIds = ['field-1', 'field-2', 'field-3', 'field-4'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canRead: true },
|
||||
'field-2': { canRead: null },
|
||||
'field-3': { canRead: undefined },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail fast and return false on first restricted field', () => {
|
||||
const fieldIds = ['field-1', 'field-2', 'field-3'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canRead: false },
|
||||
'field-2': { canRead: false },
|
||||
'field-3': { canRead: false },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(false);
|
||||
});
|
||||
|
||||
it('should only check canRead, not canUpdate', () => {
|
||||
const fieldIds = ['field-1'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canRead: true, canUpdate: false },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle field with only canUpdate restriction', () => {
|
||||
const fieldIds = ['field-1'];
|
||||
const permissions = {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-1': { canUpdate: false },
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkFieldPermissions(fieldIds, permissions)).toBe(true);
|
||||
});
|
||||
});
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
type PageLayoutWidget,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
AggregateOperations,
|
||||
GraphOrderBy,
|
||||
GraphType,
|
||||
} from '~/generated/graphql';
|
||||
import { extractFieldMetadataIdsFromWidget } from '../extractFieldMetadataIdsFromWidget';
|
||||
|
||||
const createMockWidget = (
|
||||
overrides: Partial<PageLayoutWidget>,
|
||||
): PageLayoutWidget => ({
|
||||
id: 'widget-1',
|
||||
type: WidgetType.GRAPH,
|
||||
title: 'Test',
|
||||
configuration: null,
|
||||
objectMetadataId: null,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 1, columnSpan: 1 },
|
||||
pageLayoutTabId: 'tab-1',
|
||||
createdAt: '2024-01-01',
|
||||
updatedAt: '2024-01-01',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('extractFieldMetadataIdsFromWidget', () => {
|
||||
it('should return empty array for IFRAME widget', () => {
|
||||
const widget = createMockWidget({
|
||||
type: WidgetType.IFRAME,
|
||||
configuration: {
|
||||
__typename: 'IframeConfiguration' as const,
|
||||
url: 'https://example.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(extractFieldMetadataIdsFromWidget(widget)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for widget without configuration', () => {
|
||||
const widget = createMockWidget({
|
||||
configuration: null,
|
||||
});
|
||||
|
||||
expect(extractFieldMetadataIdsFromWidget(widget)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should extract field IDs from BarChartConfiguration', () => {
|
||||
const widget = createMockWidget({
|
||||
configuration: {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
graphType: GraphType.BAR,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
const result = extractFieldMetadataIdsFromWidget(widget);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContain('field-1');
|
||||
expect(result).toContain('field-2');
|
||||
});
|
||||
|
||||
it('should extract field IDs from BarChartConfiguration with optional Y grouping', () => {
|
||||
const widget = createMockWidget({
|
||||
configuration: {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
graphType: GraphType.BAR,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
groupByFieldMetadataIdY: 'field-3',
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
const result = extractFieldMetadataIdsFromWidget(widget);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toContain('field-1');
|
||||
expect(result).toContain('field-2');
|
||||
expect(result).toContain('field-3');
|
||||
});
|
||||
|
||||
it('should extract field IDs from LineChartConfiguration', () => {
|
||||
const widget = createMockWidget({
|
||||
configuration: {
|
||||
__typename: 'LineChartConfiguration' as const,
|
||||
graphType: GraphType.LINE,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
orderByX: GraphOrderBy.FIELD_DESC,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
const result = extractFieldMetadataIdsFromWidget(widget);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContain('field-1');
|
||||
expect(result).toContain('field-2');
|
||||
});
|
||||
|
||||
it('should extract field IDs from PieChartConfiguration', () => {
|
||||
const widget = createMockWidget({
|
||||
configuration: {
|
||||
__typename: 'PieChartConfiguration' as const,
|
||||
graphType: GraphType.PIE,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataId: 'field-2',
|
||||
orderBy: GraphOrderBy.FIELD_ASC,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
const result = extractFieldMetadataIdsFromWidget(widget);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContain('field-1');
|
||||
expect(result).toContain('field-2');
|
||||
});
|
||||
|
||||
it('should extract field IDs from NumberChartConfiguration', () => {
|
||||
const widget = createMockWidget({
|
||||
configuration: {
|
||||
__typename: 'NumberChartConfiguration' as const,
|
||||
graphType: GraphType.NUMBER,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.AVG,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
const result = extractFieldMetadataIdsFromWidget(widget);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContain('field-1');
|
||||
});
|
||||
|
||||
it('should extract field IDs from GaugeChartConfiguration', () => {
|
||||
const widget = createMockWidget({
|
||||
configuration: {
|
||||
__typename: 'GaugeChartConfiguration' as const,
|
||||
graphType: GraphType.GAUGE,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
aggregateFieldMetadataIdTotal: 'field-2',
|
||||
aggregateOperationTotal: AggregateOperations.COUNT,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
const result = extractFieldMetadataIdsFromWidget(widget);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toContain('field-1');
|
||||
expect(result).toContain('field-2');
|
||||
});
|
||||
|
||||
it('should not include undefined optional fields', () => {
|
||||
const widget = createMockWidget({
|
||||
configuration: {
|
||||
__typename: 'BarChartConfiguration' as const,
|
||||
graphType: GraphType.BAR,
|
||||
aggregateFieldMetadataId: 'field-1',
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: 'field-2',
|
||||
groupByFieldMetadataIdY: undefined,
|
||||
orderByX: GraphOrderBy.FIELD_ASC,
|
||||
},
|
||||
objectMetadataId: 'object-1',
|
||||
});
|
||||
|
||||
const result = extractFieldMetadataIdsFromWidget(widget);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).not.toContain(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const checkFieldPermissions = (
|
||||
fieldMetadataIds: string[],
|
||||
objectPermissions: ObjectPermissions,
|
||||
): boolean => {
|
||||
const { restrictedFields } = objectPermissions;
|
||||
|
||||
const hasInaccessibleField = fieldMetadataIds.some((fieldId) => {
|
||||
const fieldPermission = restrictedFields[fieldId];
|
||||
|
||||
if (!isDefined(fieldPermission)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fieldPermission.canRead === false;
|
||||
});
|
||||
|
||||
return !hasInaccessibleField;
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type PageLayoutWidget,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const extractFieldMetadataIdsFromWidget = (
|
||||
widget: PageLayoutWidget,
|
||||
): string[] => {
|
||||
if (widget.type !== WidgetType.GRAPH || !widget.configuration) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const config = widget.configuration;
|
||||
|
||||
switch (config.__typename) {
|
||||
case 'BarChartConfiguration':
|
||||
case 'LineChartConfiguration':
|
||||
return [
|
||||
config.aggregateFieldMetadataId,
|
||||
config.groupByFieldMetadataIdX,
|
||||
...(isDefined(config.groupByFieldMetadataIdY)
|
||||
? [config.groupByFieldMetadataIdY]
|
||||
: []),
|
||||
];
|
||||
|
||||
case 'PieChartConfiguration':
|
||||
return [config.aggregateFieldMetadataId, config.groupByFieldMetadataId];
|
||||
|
||||
case 'NumberChartConfiguration':
|
||||
return [config.aggregateFieldMetadataId];
|
||||
|
||||
case 'GaugeChartConfiguration':
|
||||
return [
|
||||
config.aggregateFieldMetadataId,
|
||||
config.aggregateFieldMetadataIdTotal,
|
||||
];
|
||||
|
||||
case 'IframeConfiguration':
|
||||
return [];
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { type WidgetAccessDenialInfo } from '@/page-layout/widgets/types/WidgetAccessDenialInfo';
|
||||
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AppTooltip } from 'twenty-ui/display';
|
||||
|
||||
type PageLayoutWidgetForbiddenDisplayProps = {
|
||||
widgetId: string;
|
||||
restriction: WidgetAccessDenialInfo;
|
||||
};
|
||||
|
||||
export const PageLayoutWidgetForbiddenDisplay = ({
|
||||
widgetId,
|
||||
restriction,
|
||||
}: PageLayoutWidgetForbiddenDisplayProps) => {
|
||||
const tooltipId = `widget-forbidden-tooltip-${widgetId}`;
|
||||
|
||||
const getTooltipContent = () => {
|
||||
if (restriction.type === 'object' && isDefined(restriction.objectName)) {
|
||||
const objectName = restriction.objectName;
|
||||
return t`You do not have permission to access the ${objectName} object`;
|
||||
}
|
||||
|
||||
if (
|
||||
restriction.type === 'field' &&
|
||||
isDefined(restriction.fieldNames) &&
|
||||
restriction.fieldNames.length > 0
|
||||
) {
|
||||
const fieldsList = restriction.fieldNames.join(', ');
|
||||
const fieldWord =
|
||||
restriction.fieldNames.length === 1 ? 'field' : 'fields';
|
||||
return t`You do not have permission to access the ${fieldsList} ${fieldWord}`;
|
||||
}
|
||||
|
||||
return t`You do not have permission to view this widget`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id={tooltipId}>
|
||||
<ForbiddenFieldDisplay />
|
||||
</div>
|
||||
<AppTooltip
|
||||
anchorSelect={`#${tooltipId}`}
|
||||
content={getTooltipContent()}
|
||||
place="top"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+10
-7
@@ -1,14 +1,14 @@
|
||||
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
|
||||
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
|
||||
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { PageLayoutWidgetForbiddenDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetForbiddenDisplay';
|
||||
import { WidgetContainer } from '@/page-layout/widgets/components/WidgetContainer';
|
||||
import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer';
|
||||
import { WidgetHeader } from '@/page-layout/widgets/components/WidgetHeader';
|
||||
import { useWidgetObjectPermissions } from '@/page-layout/widgets/hooks/useWidgetObjectPermissions';
|
||||
import { type PageLayoutWidget } from '~/generated/graphql';
|
||||
import { useWidgetPermissions } from '@/page-layout/widgets/hooks/useWidgetPermissions';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
type WidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
@@ -25,14 +25,14 @@ const StyledContent = styled.div`
|
||||
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
|
||||
const { handleEditWidget } = useEditPageLayoutWidget();
|
||||
const { haveAccessToWidgetsObject } = useWidgetObjectPermissions(widget);
|
||||
const { hasAccess, restriction } = useWidgetPermissions(widget);
|
||||
|
||||
const isPageLayoutInEditMode = useRecoilComponentValue(
|
||||
isPageLayoutInEditModeComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<WidgetContainer isRestricted={!haveAccessToWidgetsObject}>
|
||||
<WidgetContainer isRestricted={!hasAccess}>
|
||||
<WidgetHeader
|
||||
isInEditMode={isPageLayoutInEditMode}
|
||||
title={widget.title}
|
||||
@@ -42,8 +42,11 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
onRemove={() => deletePageLayoutWidget(widget.id)}
|
||||
/>
|
||||
<StyledContent>
|
||||
{!haveAccessToWidgetsObject ? (
|
||||
<ForbiddenFieldDisplay />
|
||||
{!hasAccess ? (
|
||||
<PageLayoutWidgetForbiddenDisplay
|
||||
widgetId={widget.id}
|
||||
restriction={restriction}
|
||||
/>
|
||||
) : (
|
||||
<WidgetContentRenderer widget={widget} />
|
||||
)}
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type PageLayoutWidget } from '~/generated-metadata/graphql';
|
||||
|
||||
type UseWidgetObjectPermissionsReturn = {
|
||||
haveAccessToWidgetsObject: boolean;
|
||||
};
|
||||
|
||||
export const useWidgetObjectPermissions = (
|
||||
widget: PageLayoutWidget,
|
||||
): UseWidgetObjectPermissionsReturn => {
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
if (!isDefined(widget.objectMetadataId)) {
|
||||
return {
|
||||
haveAccessToWidgetsObject: true,
|
||||
};
|
||||
}
|
||||
|
||||
const objectPermissions = getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
widget.objectMetadataId,
|
||||
);
|
||||
|
||||
return {
|
||||
haveAccessToWidgetsObject: objectPermissions.canReadObjectRecords,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { checkFieldPermissions } from '@/page-layout/utils/checkFieldPermissions';
|
||||
import { extractFieldMetadataIdsFromWidget } from '@/page-layout/utils/extractFieldMetadataIdsFromWidget';
|
||||
import { type WidgetAccessDenialInfo } from '@/page-layout/widgets/types/WidgetAccessDenialInfo';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type PageLayoutWidget } from '~/generated-metadata/graphql';
|
||||
|
||||
export type UseWidgetPermissionsReturn = {
|
||||
hasAccess: boolean;
|
||||
restriction: WidgetAccessDenialInfo;
|
||||
};
|
||||
|
||||
export const useWidgetPermissions = (
|
||||
widget: PageLayoutWidget,
|
||||
): UseWidgetPermissionsReturn => {
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
if (!isDefined(widget.objectMetadataId)) {
|
||||
return {
|
||||
hasAccess: true,
|
||||
restriction: {
|
||||
type: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const objectMetadata = objectMetadataItems.find(
|
||||
(item) => item.id === widget.objectMetadataId,
|
||||
);
|
||||
|
||||
const objectPermissions = getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
widget.objectMetadataId,
|
||||
);
|
||||
|
||||
const hasObjectAccess = objectPermissions.canReadObjectRecords;
|
||||
|
||||
if (!hasObjectAccess) {
|
||||
return {
|
||||
hasAccess: false,
|
||||
restriction: {
|
||||
type: 'object',
|
||||
objectName: objectMetadata?.labelSingular,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const fieldMetadataIds = extractFieldMetadataIdsFromWidget(widget);
|
||||
const allFieldsAccessible = checkFieldPermissions(
|
||||
fieldMetadataIds,
|
||||
objectPermissions,
|
||||
);
|
||||
|
||||
if (!allFieldsAccessible) {
|
||||
const restrictedFieldNames = fieldMetadataIds
|
||||
.filter((fieldId) => {
|
||||
const fieldPermission = objectPermissions.restrictedFields[fieldId];
|
||||
return isDefined(fieldPermission) && fieldPermission.canRead === false;
|
||||
})
|
||||
.map((fieldId) => {
|
||||
const field = objectMetadata?.fields?.find((f) => f.id === fieldId);
|
||||
return field?.label || field?.name || 'Unknown';
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
hasAccess: false,
|
||||
restriction: {
|
||||
type: 'field',
|
||||
objectName: objectMetadata?.labelSingular,
|
||||
fieldNames: restrictedFieldNames,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasAccess: true,
|
||||
restriction: {
|
||||
type: null,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type WidgetAccessDenialInfo = {
|
||||
type: 'object' | 'field' | null;
|
||||
objectName?: string;
|
||||
fieldNames?: string[];
|
||||
};
|
||||
+2
@@ -12,6 +12,8 @@ export const PAGE_LAYOUT_WIDGET_SEEDS = {
|
||||
CUSTOMER_ANNUAL_RECURRING_REVENUE: 'CUSTOMER_ANNUAL_RECURRING_REVENUE_WIDGET',
|
||||
CUSTOMER_REVENUE_DISTRIBUTION: 'CUSTOMER_REVENUE_DISTRIBUTION_WIDGET',
|
||||
CUSTOMER_AVERAGE_ARR: 'CUSTOMER_AVERAGE_ARR_WIDGET',
|
||||
CUSTOMER_LINKEDIN_COUNT: 'CUSTOMER_LINKEDIN_COUNT_WIDGET',
|
||||
CUSTOMER_LINKEDIN_DISTRIBUTION: 'CUSTOMER_LINKEDIN_DISTRIBUTION_WIDGET',
|
||||
|
||||
TEAM_SIZE: 'TEAM_SIZE_WIDGET',
|
||||
TEAM_GEOGRAPHIC_DISTRIBUTION: 'TEAM_GEOGRAPHIC_DISTRIBUTION_WIDGET',
|
||||
|
||||
+221
-112
@@ -1,15 +1,11 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { WidgetType } from 'src/engine/core-modules/page-layout/enums/widget-type.enum';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PAGE_LAYOUT_TAB_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-tab-seeds.constant';
|
||||
import { PAGE_LAYOUT_WIDGET_SEEDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/page-layout-widget-seeds.constant';
|
||||
import { generateSeedId } from 'src/engine/workspace-manager/dev-seeder/core/utils/generate-seed-id.util';
|
||||
import {
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS,
|
||||
COMPANY_STANDARD_FIELD_IDS,
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS,
|
||||
PERSON_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
|
||||
type PageLayoutWidgetDataSeed = {
|
||||
@@ -27,6 +23,13 @@ type PageLayoutWidgetDataSeed = {
|
||||
objectMetadataId: string | null;
|
||||
};
|
||||
|
||||
const getFieldId = (
|
||||
object: ObjectMetadataEntity | undefined,
|
||||
fieldName: string,
|
||||
): string | undefined => {
|
||||
return object?.fields?.find((field) => field.name === fieldName)?.id;
|
||||
};
|
||||
|
||||
export const getPageLayoutWidgetDataSeeds = (
|
||||
workspaceId: string,
|
||||
objectMetadataItems: ObjectMetadataEntity[],
|
||||
@@ -47,6 +50,31 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
(obj) => obj.nameSingular === 'rocket',
|
||||
);
|
||||
|
||||
const opportunityAmountFieldId = getFieldId(opportunityObject, 'amount');
|
||||
const opportunityCloseDateFieldId = getFieldId(
|
||||
opportunityObject,
|
||||
'closeDate',
|
||||
);
|
||||
const opportunityStageFieldId = getFieldId(opportunityObject, 'stage');
|
||||
|
||||
const companyIdFieldId = getFieldId(companyObject, 'id');
|
||||
const companyCreatedAtFieldId = getFieldId(companyObject, 'createdAt');
|
||||
const companyEmployeesFieldId = getFieldId(companyObject, 'employees');
|
||||
const companyArrFieldId = getFieldId(companyObject, 'annualRecurringRevenue');
|
||||
const companyNameFieldId = getFieldId(companyObject, 'name');
|
||||
const companyLinkedinLinkFieldId = getFieldId(companyObject, 'linkedinLink');
|
||||
|
||||
const personIdFieldId = getFieldId(personObject, 'id');
|
||||
const personCityFieldId = getFieldId(personObject, 'city');
|
||||
const personJobTitleFieldId = getFieldId(personObject, 'jobTitle');
|
||||
|
||||
const opportunityIdFieldId = getFieldId(opportunityObject, 'id');
|
||||
|
||||
const taskIdFieldId = getFieldId(taskObject, 'id');
|
||||
|
||||
const rocketIdFieldId = getFieldId(rocketObject, 'id');
|
||||
const rocketCreatedAtFieldId = getFieldId(rocketObject, 'createdAt');
|
||||
|
||||
return [
|
||||
// Sales Overview Tab Widgets
|
||||
{
|
||||
@@ -61,11 +89,13 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Total Pipeline Value',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 },
|
||||
configuration: {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.amount,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
},
|
||||
configuration: isDefined(opportunityAmountFieldId)
|
||||
? {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: opportunityAmountFieldId,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: opportunityObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -77,16 +107,16 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_TAB_SEEDS.SALES_OVERVIEW,
|
||||
),
|
||||
title: 'Average Deal Size',
|
||||
title: 'Rocket Count (Object Permission Test)',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 3, rowSpan: 4, columnSpan: 4 },
|
||||
configuration: {
|
||||
graphType: 'GAUGE',
|
||||
aggregateFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.amount,
|
||||
aggregateOperation: AggregateOperations.AVG,
|
||||
aggregateFieldMetadataIdTotal: OPPORTUNITY_STANDARD_FIELD_IDS.amount,
|
||||
aggregateOperationTotal: AggregateOperations.MAX,
|
||||
},
|
||||
configuration: isDefined(rocketIdFieldId)
|
||||
? {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: rocketIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: rocketObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -101,13 +131,17 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Revenue Forecast',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 7, rowSpan: 8, columnSpan: 5 },
|
||||
configuration: {
|
||||
graphType: 'LINE',
|
||||
aggregateFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.amount,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
groupByFieldMetadataIdX: OPPORTUNITY_STANDARD_FIELD_IDS.closeDate,
|
||||
orderByX: 'FIELD_ASC',
|
||||
},
|
||||
configuration:
|
||||
isDefined(opportunityAmountFieldId) &&
|
||||
isDefined(opportunityCloseDateFieldId)
|
||||
? {
|
||||
graphType: 'LINE',
|
||||
aggregateFieldMetadataId: opportunityAmountFieldId,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
groupByFieldMetadataIdX: opportunityCloseDateFieldId,
|
||||
orderByX: 'FIELD_ASC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: opportunityObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -122,13 +156,17 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Deals by Stage',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 4, column: 0, rowSpan: 4, columnSpan: 6 },
|
||||
configuration: {
|
||||
graphType: 'BAR',
|
||||
aggregateFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.amount,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
groupByFieldMetadataIdX: OPPORTUNITY_STANDARD_FIELD_IDS.stage,
|
||||
orderByX: 'FIELD_DESC',
|
||||
},
|
||||
configuration:
|
||||
isDefined(opportunityAmountFieldId) &&
|
||||
isDefined(opportunityStageFieldId)
|
||||
? {
|
||||
graphType: 'BAR',
|
||||
aggregateFieldMetadataId: opportunityAmountFieldId,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
groupByFieldMetadataIdX: opportunityStageFieldId,
|
||||
orderByX: 'FIELD_DESC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: opportunityObject?.id ?? null,
|
||||
},
|
||||
|
||||
@@ -142,16 +180,19 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_TAB_SEEDS.SALES_DETAILS,
|
||||
),
|
||||
title: 'Deal Distribution',
|
||||
title: 'Rockets by Created Date (Object Permission Test)',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 5, columnSpan: 5 },
|
||||
configuration: {
|
||||
graphType: 'PIE',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataId: OPPORTUNITY_STANDARD_FIELD_IDS.stage,
|
||||
orderBy: 'VALUE_DESC',
|
||||
},
|
||||
configuration:
|
||||
isDefined(rocketIdFieldId) && isDefined(rocketCreatedAtFieldId)
|
||||
? {
|
||||
graphType: 'BAR',
|
||||
aggregateFieldMetadataId: rocketIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: rocketCreatedAtFieldId,
|
||||
orderByX: 'FIELD_ASC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: rocketObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -166,11 +207,13 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Opportunity Count',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 5, rowSpan: 5, columnSpan: 7 },
|
||||
configuration: {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
},
|
||||
configuration: isDefined(opportunityIdFieldId)
|
||||
? {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: opportunityIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: opportunityObject?.id ?? null,
|
||||
},
|
||||
|
||||
@@ -187,11 +230,13 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Total Customers',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 2, columnSpan: 3 },
|
||||
configuration: {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
},
|
||||
configuration: isDefined(companyIdFieldId)
|
||||
? {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: companyIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: companyObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -206,13 +251,16 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'New Customers Over Time',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 3, rowSpan: 6, columnSpan: 5 },
|
||||
configuration: {
|
||||
graphType: 'LINE',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
|
||||
orderByX: 'FIELD_ASC',
|
||||
},
|
||||
configuration:
|
||||
isDefined(companyIdFieldId) && isDefined(companyCreatedAtFieldId)
|
||||
? {
|
||||
graphType: 'LINE',
|
||||
aggregateFieldMetadataId: companyIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: companyCreatedAtFieldId,
|
||||
orderByX: 'FIELD_ASC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: companyObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -227,13 +275,16 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Companies by Size',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 8, rowSpan: 6, columnSpan: 4 },
|
||||
configuration: {
|
||||
graphType: 'BAR',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: COMPANY_STANDARD_FIELD_IDS.employees,
|
||||
orderByX: 'FIELD_ASC',
|
||||
},
|
||||
configuration:
|
||||
isDefined(companyIdFieldId) && isDefined(companyEmployeesFieldId)
|
||||
? {
|
||||
graphType: 'BAR',
|
||||
aggregateFieldMetadataId: companyIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: companyEmployeesFieldId,
|
||||
orderByX: 'FIELD_ASC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: companyObject?.id ?? null,
|
||||
},
|
||||
|
||||
@@ -250,12 +301,13 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Annual Recurring Revenue',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 4 },
|
||||
configuration: {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId:
|
||||
COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
},
|
||||
configuration: isDefined(companyArrFieldId)
|
||||
? {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: companyArrFieldId,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: companyObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -270,14 +322,16 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Revenue Distribution',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 4, rowSpan: 2, columnSpan: 3 },
|
||||
configuration: {
|
||||
graphType: 'PIE',
|
||||
aggregateFieldMetadataId:
|
||||
COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
groupByFieldMetadataId: COMPANY_STANDARD_FIELD_IDS.name,
|
||||
orderBy: 'VALUE_DESC',
|
||||
},
|
||||
configuration:
|
||||
isDefined(companyArrFieldId) && isDefined(companyNameFieldId)
|
||||
? {
|
||||
graphType: 'PIE',
|
||||
aggregateFieldMetadataId: companyArrFieldId,
|
||||
aggregateOperation: AggregateOperations.SUM,
|
||||
groupByFieldMetadataId: companyNameFieldId,
|
||||
orderBy: 'VALUE_DESC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: companyObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -292,15 +346,60 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Average ARR',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 7, rowSpan: 6, columnSpan: 5 },
|
||||
configuration: {
|
||||
graphType: 'GAUGE',
|
||||
aggregateFieldMetadataId:
|
||||
COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue,
|
||||
aggregateOperation: AggregateOperations.AVG,
|
||||
aggregateFieldMetadataIdTotal:
|
||||
COMPANY_STANDARD_FIELD_IDS.annualRecurringRevenue,
|
||||
aggregateOperationTotal: AggregateOperations.MAX,
|
||||
},
|
||||
configuration: isDefined(companyArrFieldId)
|
||||
? {
|
||||
graphType: 'GAUGE',
|
||||
aggregateFieldMetadataId: companyArrFieldId,
|
||||
aggregateOperation: AggregateOperations.AVG,
|
||||
aggregateFieldMetadataIdTotal: companyArrFieldId,
|
||||
aggregateOperationTotal: AggregateOperations.MAX,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: companyObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_LINKEDIN_COUNT,
|
||||
),
|
||||
pageLayoutTabId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW,
|
||||
),
|
||||
title: 'LinkedIn Profiles Count (Field Permission Test)',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 2, column: 0, rowSpan: 4, columnSpan: 3 },
|
||||
configuration: isDefined(companyLinkedinLinkFieldId)
|
||||
? {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: companyLinkedinLinkFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: companyObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
id: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_WIDGET_SEEDS.CUSTOMER_LINKEDIN_DISTRIBUTION,
|
||||
),
|
||||
pageLayoutTabId: generateSeedId(
|
||||
workspaceId,
|
||||
PAGE_LAYOUT_TAB_SEEDS.CUSTOMER_OVERVIEW,
|
||||
),
|
||||
title: 'Companies by LinkedIn (Field Permission Test)',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 6, column: 0, rowSpan: 4, columnSpan: 6 },
|
||||
configuration:
|
||||
isDefined(companyIdFieldId) && isDefined(companyLinkedinLinkFieldId)
|
||||
? {
|
||||
graphType: 'PIE',
|
||||
aggregateFieldMetadataId: companyIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataId: companyLinkedinLinkFieldId,
|
||||
orderBy: 'VALUE_DESC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: companyObject?.id ?? null,
|
||||
},
|
||||
|
||||
@@ -314,11 +413,13 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Team Size',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 5, columnSpan: 6 },
|
||||
configuration: {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
},
|
||||
configuration: isDefined(personIdFieldId)
|
||||
? {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: personIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: personObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -333,13 +434,16 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Geographic Distribution',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 6, rowSpan: 5, columnSpan: 6 },
|
||||
configuration: {
|
||||
graphType: 'BAR',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: PERSON_STANDARD_FIELD_IDS.city,
|
||||
orderByX: 'FIELD_DESC',
|
||||
},
|
||||
configuration:
|
||||
isDefined(personIdFieldId) && isDefined(personCityFieldId)
|
||||
? {
|
||||
graphType: 'BAR',
|
||||
aggregateFieldMetadataId: personIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataIdX: personCityFieldId,
|
||||
orderByX: 'FIELD_DESC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: personObject?.id ?? null,
|
||||
},
|
||||
|
||||
@@ -356,13 +460,16 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Contact Roles',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 4, columnSpan: 6 },
|
||||
configuration: {
|
||||
graphType: 'PIE',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataId: PERSON_STANDARD_FIELD_IDS.jobTitle,
|
||||
orderBy: 'VALUE_DESC',
|
||||
},
|
||||
configuration:
|
||||
isDefined(personIdFieldId) && isDefined(personJobTitleFieldId)
|
||||
? {
|
||||
graphType: 'PIE',
|
||||
aggregateFieldMetadataId: personIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
groupByFieldMetadataId: personJobTitleFieldId,
|
||||
orderBy: 'VALUE_DESC',
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: personObject?.id ?? null,
|
||||
},
|
||||
{
|
||||
@@ -374,11 +481,13 @@ export const getPageLayoutWidgetDataSeeds = (
|
||||
title: 'Open Tasks',
|
||||
type: WidgetType.GRAPH,
|
||||
gridPosition: { row: 0, column: 6, rowSpan: 6, columnSpan: 6 },
|
||||
configuration: {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
},
|
||||
configuration: isDefined(taskIdFieldId)
|
||||
? {
|
||||
graphType: 'NUMBER',
|
||||
aggregateFieldMetadataId: taskIdFieldId,
|
||||
aggregateOperation: AggregateOperations.COUNT,
|
||||
}
|
||||
: null,
|
||||
objectMetadataId: taskObject?.id ?? null,
|
||||
},
|
||||
];
|
||||
|
||||
+1
@@ -84,6 +84,7 @@ export class DevSeederService {
|
||||
this.coreDataSource.getRepository(ObjectMetadataEntity);
|
||||
const objectMetadataItems = await objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: { fields: true },
|
||||
});
|
||||
|
||||
await seedPageLayoutWidgets(
|
||||
|
||||
Reference in New Issue
Block a user