ef499b6d47
## Summary - Re-enable one lint rule that was temporarily disabled during the ESLint-to-Oxlint migration: - **`twenty/sort-css-properties-alphabetically`** in twenty-front — 578 violations auto-fixed across 390 files - Document why **`typescript/consistent-type-imports`** cannot be auto-fixed in twenty-server: NestJS relies on `emitDecoratorMetadata` for DI, so converting constructor parameter imports to `import type` erases them at compile time and breaks dependency injection at runtime - Right-size CI runners, reducing 8-core usage from 18 jobs to 3: | Change | Jobs | Rationale | |--------|------|-----------| | **Keep 8-core** | `ci-merge-queue/e2e-test`, `ci-front/front-sb-build`, `ci-front/front-build` | Heavy builds needing max CPU + memory (10GB NODE_OPTIONS, full Storybook webpack bundling) | | **8-core → 4-core** | `ci-server` (build, lint-typecheck, validation, test, integration-test), `ci-front/front-sb-test`, `ci-zapier/server-setup`, `ci-sdk/sdk-e2e-test` | Already sharded into 10-12 parallel instances, I/O-bound (DB/Redis), or moderate single builds | | **8-core → 2-core** | `ci-emails/emails-test` | Trivially lightweight (build + curl health check) | | **Removed** | `ci-front/front-chromatic-deployment` | Dead code — permanently disabled with `if: false` | - Fix merge queue CI issues: - **Concurrency**: Use `merge_group.base_ref` instead of unique merge group ref so new queue entries cancel previous runs - **Required status checks**: Add `merge_group` trigger to all 6 required CI workflows (front, server, shared, website, docker-compose, sdk) with `changed-files-check` auto-skipped for merge_group events — status check jobs auto-pass without re-running full CI - **Build caching**: Add Nx build cache restore/save to E2E test job with fallback to `main` branch cache for faster frontend and server builds ## Test plan - [ ] CI passes on this PR (verifies lint rule auto-fix works) - [ ] Verify 4-core runner jobs complete within their 30-minute timeouts - [ ] Verify merge queue status checks auto-pass (ci-front-status-check, ci-server-status-check, etc.) - [ ] Verify merge queue E2E concurrency cancels previous runs when a new PR enters the queue
177 lines
5.8 KiB
TypeScript
177 lines
5.8 KiB
TypeScript
import { useContext, useState } from 'react';
|
|
import { styled } from '@linaria/react';
|
|
import { type Node, type NodeProps } from '@xyflow/react';
|
|
import { Link } from 'react-router-dom';
|
|
|
|
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
|
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|
import { ObjectFieldRow } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewField';
|
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
|
|
import { ObjectFieldRowWithoutRelation } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewFieldWithoutRelation';
|
|
import '@xyflow/react/dist/style.css';
|
|
import { SettingsPath } from 'twenty-shared/types';
|
|
import { isDefined, getSettingsPath } from 'twenty-shared/utils';
|
|
import { IconChevronDown, IconChevronUp, useIcons } from 'twenty-ui/display';
|
|
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
|
|
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
type SettingsDataModelOverviewObjectNode = Node<ObjectMetadataItem, 'object'>;
|
|
type SettingsDataModelOverviewObjectProps =
|
|
NodeProps<SettingsDataModelOverviewObjectNode>;
|
|
|
|
const StyledNode = styled.div`
|
|
background-color: ${themeCssVariables.background.secondary};
|
|
border: 1px solid ${themeCssVariables.border.color.medium};
|
|
border-radius: ${themeCssVariables.border.radius.md};
|
|
box-shadow: ${themeCssVariables.boxShadow.light};
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
padding: ${themeCssVariables.spacing[2]};
|
|
width: 220px;
|
|
`;
|
|
|
|
const StyledHeader = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
`;
|
|
|
|
const StyledObjectName = styled.div`
|
|
border: 0;
|
|
border-radius: 4px 4px 0 0;
|
|
display: flex;
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
position: relative;
|
|
text-align: center;
|
|
`;
|
|
|
|
const StyledInnerCard = styled.div`
|
|
background-color: ${themeCssVariables.background.primary};
|
|
border: 1px solid ${themeCssVariables.border.color.light};
|
|
border-radius: ${themeCssVariables.border.radius.sm};
|
|
color: ${themeCssVariables.font.color.tertiary};
|
|
display: flex;
|
|
flex-flow: column nowrap;
|
|
gap: ${themeCssVariables.spacing['0.5']};
|
|
padding: ${themeCssVariables.spacing[2]} 0 ${themeCssVariables.spacing[2]} 0;
|
|
`;
|
|
|
|
const StyledCardRow = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
height: 24px;
|
|
`;
|
|
|
|
const StyledCardRowOther = styled.div`
|
|
align-items: center;
|
|
cursor: pointer;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
height: 24px;
|
|
padding: 0 ${themeCssVariables.spacing[2]};
|
|
|
|
&:hover {
|
|
background-color: ${themeCssVariables.background.tertiary};
|
|
}
|
|
`;
|
|
|
|
const StyledCardRowText = styled.div``;
|
|
|
|
const StyledObjectInstanceCount = styled.div`
|
|
color: ${themeCssVariables.font.color.tertiary};
|
|
`;
|
|
|
|
const StyledObjectLinkContainer = styled.div`
|
|
> a {
|
|
align-items: center;
|
|
color: ${themeCssVariables.font.color.primary};
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
text-decoration: none;
|
|
|
|
&:hover {
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
}
|
|
}
|
|
`;
|
|
|
|
export const SettingsDataModelOverviewObject = ({
|
|
data: objectMetadataItem,
|
|
}: SettingsDataModelOverviewObjectProps) => {
|
|
const { theme } = useContext(ThemeContext);
|
|
const { getIcon } = useIcons();
|
|
const [otherFieldsExpanded, setOtherFieldsExpanded] = useState(false);
|
|
|
|
const { totalCount } = useFindManyRecords({
|
|
objectNameSingular: objectMetadataItem.nameSingular,
|
|
});
|
|
|
|
const fields = objectMetadataItem.fields.filter(
|
|
(x) => !isHiddenSystemField(x) && x.isActive,
|
|
);
|
|
|
|
const countNonRelation = fields.filter(
|
|
(x) => x.type !== FieldMetadataType.RELATION,
|
|
).length;
|
|
|
|
const Icon = getIcon(objectMetadataItem.icon);
|
|
|
|
return (
|
|
<StyledNode>
|
|
<StyledHeader>
|
|
<StyledObjectName onMouseEnter={() => {}} onMouseLeave={() => {}}>
|
|
<StyledObjectLinkContainer>
|
|
<Link
|
|
to={getSettingsPath(SettingsPath.Objects, {
|
|
objectNamePlural: objectMetadataItem.namePlural,
|
|
})}
|
|
>
|
|
{isDefined(Icon) && <Icon size={theme.icon.size.md} />}
|
|
{objectMetadataItem.labelPlural}
|
|
</Link>
|
|
</StyledObjectLinkContainer>
|
|
<StyledObjectInstanceCount> · {totalCount}</StyledObjectInstanceCount>
|
|
</StyledObjectName>
|
|
<SettingsItemTypeTag item={objectMetadataItem} />
|
|
</StyledHeader>
|
|
|
|
<StyledInnerCard>
|
|
{fields
|
|
.filter((x) => x.type === FieldMetadataType.RELATION)
|
|
.map((field) => (
|
|
<StyledCardRow key={field.id}>
|
|
<ObjectFieldRow field={field} />
|
|
</StyledCardRow>
|
|
))}
|
|
{countNonRelation > 0 && (
|
|
<>
|
|
<StyledCardRowOther
|
|
onClick={() => setOtherFieldsExpanded(!otherFieldsExpanded)}
|
|
>
|
|
{otherFieldsExpanded ? (
|
|
<IconChevronUp size={theme.icon.size.md} />
|
|
) : (
|
|
<IconChevronDown size={theme.icon.size.md} />
|
|
)}
|
|
<StyledCardRowText>{countNonRelation} fields</StyledCardRowText>
|
|
</StyledCardRowOther>
|
|
{otherFieldsExpanded &&
|
|
fields
|
|
.filter((x) => x.type !== FieldMetadataType.RELATION)
|
|
.map((field) => (
|
|
<StyledCardRow key={field.id}>
|
|
<ObjectFieldRowWithoutRelation field={field} />
|
|
</StyledCardRow>
|
|
))}
|
|
</>
|
|
)}
|
|
</StyledInnerCard>
|
|
</StyledNode>
|
|
);
|
|
};
|