Fix/record group index and seed (#9605)

- [x] [Disable group by on default view Options
menu](https://discord.com/channels/1130383047699738754/1328421803399446568)
- [x] Add default seed for view group
This commit is contained in:
Jérémy M
2025-01-15 09:37:15 +01:00
committed by GitHub
parent 1a5b3ef2f8
commit eaa68424f5
12 changed files with 393 additions and 143 deletions

View File

@ -1,5 +1,6 @@
import { Key } from 'ts-key-enum';
import {
AppTooltip,
IconFileExport,
IconFileImport,
IconLayout,
@ -55,6 +56,10 @@ export const ObjectOptionsDropdownMenuContent = () => {
recordGroupFieldMetadataComponentState,
);
const isGroupByEnabled =
(isDefined(currentView?.viewGroups) && currentView.viewGroups.length > 0) ||
currentView?.key !== 'INDEX';
useScopedHotkeys(
[Key.Escape],
() => {
@ -115,20 +120,34 @@ export const ObjectOptionsDropdownMenuContent = () => {
contextualText={`${visibleBoardFields.length} shown`}
hasSubMenu
/>
{viewType === ViewType.Kanban ||
(currentView?.key !== 'INDEX' && (
<MenuItem
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text="Group by"
contextualText={recordGroupFieldMetadata?.label}
hasSubMenu
/>
))}
<div id="group-by-menu-item">
<MenuItem
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text="Group by"
contextualText={
!isGroupByEnabled
? 'Not available on Default View'
: recordGroupFieldMetadata?.label
}
hasSubMenu
disabled={!isGroupByEnabled}
/>
</div>
{!isGroupByEnabled && (
<AppTooltip
anchorSelect={`#group-by-menu-item`}
content="Not available on Default View"
noArrow
place="bottom"
width="100%"
/>
)}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>

View File

@ -45,7 +45,9 @@ import { SURVEY_RESULTS_METADATA_SEEDS } from 'src/engine/seeder/metadata-seeds/
import { SeederService } from 'src/engine/seeder/seeder.service';
import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { createWorkspaceViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/create-workspace-views';
import { seedViewWithDemoData } from 'src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data';
import { opportunitiesTableByStageView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-table-by-stage.view';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
@ -230,6 +232,14 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
isWorkflowEnabled,
);
const devViewDefinitionsWithId = await createWorkspaceViews(
entityManager,
dataSourceMetadata.schema,
[opportunitiesTableByStageView(objectMetadataStandardIdToIdMap)],
);
viewDefinitionsWithId.push(...devViewDefinitionsWithId);
await seedWorkspaceFavorites(
viewDefinitionsWithId
.filter(

View File

@ -0,0 +1,131 @@
import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import { ViewDefinition } from 'src/engine/workspace-manager/standard-objects-prefill-data/types/view-definition.interface';
export const createWorkspaceViews = async (
entityManager: EntityManager,
schemaName: string,
viewDefinitions: ViewDefinition[],
) => {
const viewDefinitionsWithId = viewDefinitions.map((viewDefinition) => ({
...viewDefinition,
id: v4(),
}));
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.view`, [
'id',
'name',
'objectMetadataId',
'type',
'key',
'position',
'icon',
'kanbanFieldMetadataId',
])
.values(
viewDefinitionsWithId.map(
({
id,
name,
objectMetadataId,
type,
key,
position,
icon,
kanbanFieldMetadataId,
}) => ({
id,
name,
objectMetadataId,
type,
key,
position,
icon,
kanbanFieldMetadataId,
}),
),
)
.returning('*')
.execute();
for (const viewDefinition of viewDefinitionsWithId) {
if (viewDefinition.fields && viewDefinition.fields.length > 0) {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewField`, [
'fieldMetadataId',
'position',
'isVisible',
'size',
'viewId',
])
.values(
viewDefinition.fields.map((field) => ({
fieldMetadataId: field.fieldMetadataId,
position: field.position,
isVisible: field.isVisible,
size: field.size,
viewId: viewDefinition.id,
})),
)
.execute();
}
if (viewDefinition.filters && viewDefinition.filters.length > 0) {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewFilter`, [
'fieldMetadataId',
'displayValue',
'operand',
'value',
'viewId',
])
.values(
viewDefinition.filters.map((filter: any) => ({
fieldMetadataId: filter.fieldMetadataId,
displayValue: filter.displayValue,
operand: filter.operand,
value: filter.value,
viewId: viewDefinition.id,
})),
)
.execute();
}
if (
'groups' in viewDefinition &&
viewDefinition.groups &&
viewDefinition.groups.length > 0
) {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewGroup`, [
'fieldMetadataId',
'isVisible',
'fieldValue',
'position',
'viewId',
])
.values(
viewDefinition.groups.map((group: any) => ({
fieldMetadataId: group.fieldMetadataId,
isVisible: group.isVisible,
fieldValue: group.fieldValue,
position: group.position,
viewId: viewDefinition.id,
})),
)
.execute();
}
}
return viewDefinitionsWithId;
};

View File

@ -1,8 +1,8 @@
import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { createWorkspaceViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/create-workspace-views';
import { seedCompaniesAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/companies-all.view';
import { notesAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view';
import { opportunitiesAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/opportunities-all.view';
@ -37,124 +37,5 @@ export const seedViewWithDemoData = async (
: []),
];
const viewDefinitionsWithId = viewDefinitions.map((viewDefinition) => ({
...viewDefinition,
id: v4(),
}));
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.view`, [
'id',
'name',
'objectMetadataId',
'type',
'key',
'position',
'icon',
'kanbanFieldMetadataId',
])
.values(
viewDefinitionsWithId.map(
({
id,
name,
objectMetadataId,
type,
key,
position,
icon,
kanbanFieldMetadataId,
}) => ({
id,
name,
objectMetadataId,
type,
key,
position,
icon,
kanbanFieldMetadataId,
}),
),
)
.returning('*')
.execute();
for (const viewDefinition of viewDefinitionsWithId) {
if (viewDefinition.fields && viewDefinition.fields.length > 0) {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewField`, [
'fieldMetadataId',
'position',
'isVisible',
'size',
'viewId',
])
.values(
viewDefinition.fields.map((field) => ({
fieldMetadataId: field.fieldMetadataId,
position: field.position,
isVisible: field.isVisible,
size: field.size,
viewId: viewDefinition.id,
})),
)
.execute();
}
if (viewDefinition.filters && viewDefinition.filters.length > 0) {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewFilter`, [
'fieldMetadataId',
'displayValue',
'operand',
'value',
'viewId',
])
.values(
viewDefinition.filters.map((filter: any) => ({
fieldMetadataId: filter.fieldMetadataId,
displayValue: filter.displayValue,
operand: filter.operand,
value: filter.value,
viewId: viewDefinition.id,
})),
)
.execute();
}
if (
'groups' in viewDefinition &&
viewDefinition.groups &&
viewDefinition.groups.length > 0
) {
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.viewGroup`, [
'fieldMetadataId',
'isVisible',
'fieldValue',
'position',
'viewId',
])
.values(
viewDefinition.groups.map((group: any) => ({
fieldMetadataId: group.fieldMetadataId,
isVisible: group.isVisible,
fieldValue: group.fieldValue,
position: group.position,
viewId: viewDefinition.id,
})),
)
.execute();
}
}
return viewDefinitionsWithId;
return createWorkspaceViews(entityManager, schemaName, viewDefinitions);
};

View File

@ -0,0 +1,28 @@
export interface ViewDefinition {
id?: string;
name: string;
objectMetadataId: string;
type: string;
key: string | null;
position: number;
icon?: string;
kanbanFieldMetadataId?: string;
fields?: {
fieldMetadataId: string;
position: number;
isVisible: boolean;
size: number;
}[];
filters?: {
fieldMetadataId: string;
displayValue: string;
operand: string;
value: string;
}[];
groups?: {
fieldMetadataId: string;
isVisible: boolean;
fieldValue: string;
position: number;
}[];
}

View File

@ -12,7 +12,7 @@ export const opportunitiesByStageView = (
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity].id,
type: 'kanban',
key: null,
position: 1,
position: 2,
icon: 'IconLayoutKanban',
kanbanFieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity].fields[

View File

@ -0,0 +1,115 @@
import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map';
import { OPPORTUNITY_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';
export const opportunitiesTableByStageView = (
objectMetadataStandardIdToIdMap: ObjectMetadataStandardIdToIdMap,
) => {
return {
name: 'By Stage',
objectMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity].id,
type: 'table',
key: null,
position: 1,
icon: 'IconList',
kanbanFieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity].fields[
OPPORTUNITY_STANDARD_FIELD_IDS.stage
],
filters: [],
fields: [
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.name],
position: 0,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.amount],
position: 1,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.createdBy],
position: 2,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.closeDate],
position: 3,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.company],
position: 4,
isVisible: true,
size: 150,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.pointOfContact],
position: 5,
isVisible: true,
size: 150,
},
],
groups: [
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.stage],
isVisible: true,
fieldValue: 'NEW',
position: 0,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.stage],
isVisible: true,
fieldValue: 'SCREENING',
position: 1,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.stage],
isVisible: true,
fieldValue: 'MEETING',
position: 2,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.stage],
isVisible: true,
fieldValue: 'PROPOSAL',
position: 3,
},
{
fieldMetadataId:
objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.opportunity]
.fields[OPPORTUNITY_STANDARD_FIELD_IDS.stage],
isVisible: true,
fieldValue: 'CUSTOMER',
position: 4,
},
],
};
};

View File

@ -16,7 +16,7 @@ export enum TooltipDelay {
mediumDelay = '500ms',
}
const StyledAppTooltip = styled(Tooltip)`
const StyledAppTooltip = styled(Tooltip)<{ width?: string }>`
backdrop-filter: ${({ theme }) => theme.blur.strong};
background-color: ${({ theme }) => RGBA(theme.color.gray80, 0.8)};
border-radius: ${({ theme }) => theme.border.radius.sm};
@ -27,7 +27,7 @@ const StyledAppTooltip = styled(Tooltip)`
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
max-width: 40%;
max-width: ${({ width }) => width || '40%'};
overflow: visible;
padding: ${({ theme }) => theme.spacing(2)};
@ -49,6 +49,7 @@ export type AppTooltipProps = {
delay?: TooltipDelay;
positionStrategy?: PositionStrategy;
clickable?: boolean;
width?: string;
};
export const AppTooltip = ({
@ -63,6 +64,7 @@ export const AppTooltip = ({
positionStrategy,
children,
clickable,
width,
}: AppTooltipProps) => {
const delayInMs =
delay === TooltipDelay.noDelay
@ -86,6 +88,7 @@ export const AppTooltip = ({
positionStrategy,
children,
clickable,
width,
}}
/>
);

View File

@ -102,6 +102,48 @@ export const Hoverable: Story = {
),
};
export const WithWidth: Story = {
args: {
place: TooltipPosition.Top,
delay: TooltipDelay.mediumDelay,
content: 'Tooltip with custom width',
hidden: false,
anchorSelect: '#width-text',
width: '200px',
},
decorators: [ComponentDecorator],
render: ({
anchorSelect,
className,
content,
delay,
noArrow,
offset,
place,
positionStrategy,
width,
}) => (
<>
<p id="width-text" data-testid="tooltip">
Hover me to see custom width!
</p>
<Tooltip
{...{
anchorSelect,
className,
content,
delay,
noArrow,
offset,
place,
positionStrategy,
width,
}}
/>
</>
),
};
export const Catalog: CatalogStory<Story, typeof Tooltip> = {
args: { hidden: false, content: 'Tooltip Test' },
play: async ({ canvasElement }) => {

View File

@ -30,6 +30,7 @@ export type MenuItemProps = {
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
testId?: string;
disabled?: boolean;
text: ReactNode;
contextualText?: ReactNode;
hasSubMenu?: boolean;
@ -49,6 +50,7 @@ export const MenuItem = ({
text,
contextualText,
hasSubMenu = false,
disabled = false,
}: MenuItemProps) => {
const theme = useTheme();
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
@ -64,7 +66,8 @@ export const MenuItem = ({
return (
<StyledHoverableMenuItemBase
data-testid={testId ?? undefined}
onClick={handleMenuItemClick}
onClick={disabled ? undefined : handleMenuItemClick}
disabled={disabled}
className={className}
accent={accent}
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}

View File

@ -100,6 +100,11 @@ export const Catalog: CatalogStory<Story, typeof MenuItem> = {
}
},
},
{
name: 'disabled',
values: [true, false],
props: (disabled: boolean) => ({ disabled }),
},
],
options: {
elementContainer: {

View File

@ -1,6 +1,7 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { isUndefined } from '@sniptt/guards';
import { HOVER_BACKGROUND } from '@ui/theme';
import { MenuItemAccent } from '../../types/MenuItemAccent';
@ -9,6 +10,7 @@ export type MenuItemBaseProps = {
isKeySelected?: boolean;
isHoverBackgroundDisabled?: boolean;
hovered?: boolean;
disabled?: boolean;
};
export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
@ -35,10 +37,16 @@ export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
${({ theme, isKeySelected }) =>
isKeySelected ? `background: ${theme.background.transparent.light};` : ''}
${({ isHoverBackgroundDisabled }) =>
isHoverBackgroundDisabled ?? HOVER_BACKGROUND};
${({ isHoverBackgroundDisabled, disabled }) =>
(disabled || isHoverBackgroundDisabled) ?? HOVER_BACKGROUND};
${({ theme, accent, disabled }) => {
if (isUndefined(disabled) && disabled !== false) {
return css`
opacity: 0.4;
`;
}
${({ theme, accent }) => {
switch (accent) {
case 'danger': {
return css`
@ -112,6 +120,7 @@ export const StyledDraggableItem = styled.div`
`;
export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{
disabled?: boolean;
isIconDisplayedOnHoverOnly?: boolean;
cursor?: 'drag' | 'default' | 'not-allowed';
}>`
@ -136,7 +145,11 @@ export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{
transition: opacity ${({ theme }) => theme.animation.duration.instant}s ease;
}
cursor: ${({ cursor }) => {
cursor: ${({ cursor, disabled }) => {
if (!isUndefined(disabled) && disabled !== false) {
return 'not-allowed';
}
switch (cursor) {
case 'drag':
return 'grab';