Files
twenty/packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx
T
Charles Bochet ef499b6d47 Re-enable disabled lint rules and right-size CI runners (#18461)
## 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
2026-03-06 13:33:02 +00:00

152 lines
5.0 KiB
TypeScript

import { SidePanelRouter } from '@/side-panel/components/SidePanelRouter';
import { SidePanelWidthEffect } from '@/side-panel/components/SidePanelWidthEffect';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useSidePanelCloseAnimationCompleteCleanup } from '@/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup';
import {
SIDE_PANEL_WIDTH_VAR,
sidePanelWidthState,
} from '@/side-panel/states/sidePanelWidthState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState';
import { ModalContainerContext } from '@/ui/layout/modal/contexts/ModalContainerContext';
import { ResizablePanelGap } from '@/ui/layout/resizable-panel/components/ResizablePanelGap';
import { SIDE_PANEL_CONSTRAINTS } from '@/side-panel/constants/SidePanelConstraints';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { styled } from '@linaria/react';
import { useCallback, useState } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledSidePanelWrapper = styled.div<{
isOpen: boolean;
isResizing: boolean;
}>`
flex-shrink: 0;
min-width: 0;
overflow: hidden;
transition: ${({ isResizing }) =>
isResizing
? 'none'
: `width ${themeCssVariables.animation.duration.normal}s`};
width: ${({ isOpen }) => (isOpen ? `var(${SIDE_PANEL_WIDTH_VAR})` : '0px')};
`;
const StyledSidePanel = styled.aside`
background: ${themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
`;
const StyledModalContainer = styled.div`
height: 100%;
left: 0;
pointer-events: none;
position: absolute;
top: 0;
width: 100%;
z-index: 1;
`;
const GAP_WIDTH = 8;
export const SidePanelForDesktop = () => {
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
const isSidePanelClosing = useAtomStateValue(isSidePanelClosingState);
const [sidePanelWidth, setSidePanelWidth] = useAtomState(sidePanelWidthState);
const { closeSidePanelMenu } = useSidePanelMenu();
const { sidePanelCloseAnimationCompleteCleanup } =
useSidePanelCloseAnimationCompleteCleanup();
const [modalContainer, setModalContainer] = useState<HTMLDivElement | null>(
null,
);
const [isResizing, setIsResizing] = useState(false);
const [shouldRenderContent, setShouldRenderContent] =
useState(isSidePanelOpened);
const setTableWidthResizeIsActive = useSetAtomState(
tableWidthResizeIsActiveState,
);
const shouldShowContent = isSidePanelOpened || shouldRenderContent;
const handleTransitionEnd = () => {
if (isSidePanelOpened) {
// Open animation completed - ensure content persists for close animation
setShouldRenderContent(true);
} else {
// Close animation completed
setShouldRenderContent(false);
if (isSidePanelClosing) {
sidePanelCloseAnimationCompleteCleanup();
}
}
};
const handleModalContainerRef = useCallback(
(element: HTMLDivElement | null) => {
setModalContainer(element);
},
[],
);
const handleWidthChange = useCallback(
(width: number) => {
setSidePanelWidth(width);
setIsResizing(false);
setTableWidthResizeIsActive(true);
},
[setSidePanelWidth, setTableWidthResizeIsActive],
);
const handleResizeStart = useCallback(() => {
setIsResizing(true);
setTableWidthResizeIsActive(false);
}, [setTableWidthResizeIsActive]);
const handleCollapse = useCallback(() => {
closeSidePanelMenu();
setIsResizing(false);
setTableWidthResizeIsActive(true);
}, [closeSidePanelMenu, setTableWidthResizeIsActive]);
return (
<>
<SidePanelWidthEffect />
<ResizablePanelGap
side="left"
constraints={SIDE_PANEL_CONSTRAINTS}
currentWidth={sidePanelWidth}
onWidthChange={handleWidthChange}
onCollapse={handleCollapse}
gapWidth={isSidePanelOpened ? GAP_WIDTH : 0}
cssVariableName={SIDE_PANEL_WIDTH_VAR}
onResizeStart={handleResizeStart}
/>
<StyledSidePanelWrapper
isOpen={isSidePanelOpened}
isResizing={isResizing}
onTransitionEnd={handleTransitionEnd}
data-side-panel=""
>
<StyledSidePanel>
<StyledModalContainer ref={handleModalContainerRef} />
<ModalContainerContext.Provider value={{ container: modalContainer }}>
{shouldShowContent && <SidePanelRouter />}
</ModalContainerContext.Provider>
</StyledSidePanel>
</StyledSidePanelWrapper>
</>
);
};