snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 3s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { t } from 'twenty-sdk/front-component';
import {
IconChevronDown,
IconChevronUp,
IconFolder,
IconFolderRoot,
} from 'twenty-ui/icon';
import { ICON } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { Checkbox } from 'src/front-components/components/Checkbox';
import {
GRANOLA_FOLDER_TREE_INDENT_PIXELS,
GranolaFolderTreeBreadcrumb,
} from 'src/front-components/components/GranolaFolderTreeBreadcrumb';
import { type GranolaFolderTreeNode } from 'src/front-components/utils/compute-granola-folder-tree.util';
import { countNestedGranolaFolders } from 'src/front-components/utils/count-nested-granola-folders.util';
import { hasSelectedGranolaDescendant } from 'src/front-components/utils/has-selected-granola-descendant.util';
const StyledItem = styled.li`
position: relative;
`;
const StyledNestedList = styled.ul`
list-style: none;
margin: 0;
padding: 0;
`;
const StyledCollapsibleWrapper = styled.div<{ $isExpanded: boolean }>`
display: grid;
grid-template-rows: ${({ $isExpanded }) => ($isExpanded ? '1fr' : '0fr')};
transition: grid-template-rows
calc(${() => themeCssVariables.animation.duration.fast} * 1s) ease-out;
`;
const StyledCollapsibleContent = styled.div`
overflow: hidden;
`;
const StyledRow = styled.div<{ $depth: number }>`
align-items: center;
border-radius: ${() => themeCssVariables.border.radius.sm};
cursor: pointer;
display: flex;
height: 28px;
padding-left: ${({ $depth }) => $depth * GRANOLA_FOLDER_TREE_INDENT_PIXELS}px;
user-select: none;
&:hover {
background-color: ${() => themeCssVariables.background.transparent.lighter};
}
`;
const StyledFolderContent = styled.div`
align-items: center;
color: ${() => themeCssVariables.font.color.tertiary};
display: flex;
flex: 1;
gap: ${() => themeCssVariables.spacing[2]};
min-width: 0;
padding-left: ${() => themeCssVariables.spacing[1]};
padding-right: ${() => themeCssVariables.spacing[1]};
`;
const StyledFolderName = styled.span<{ $disabled: boolean }>`
color: ${({ $disabled }) =>
$disabled
? themeCssVariables.font.color.tertiary
: themeCssVariables.font.color.primary};
font-size: ${() => themeCssVariables.font.size.md};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledRightSection = styled.div`
align-items: center;
display: flex;
flex-shrink: 0;
gap: ${() => themeCssVariables.spacing[1]};
margin-left: auto;
`;
const StyledChildCount = styled.span`
color: ${() => themeCssVariables.font.color.tertiary};
font-size: ${() => themeCssVariables.font.size.sm};
min-width: ${() => themeCssVariables.spacing[3]};
text-align: right;
`;
const StyledExpandButton = styled.button`
align-items: center;
background: none;
border: none;
color: ${() => themeCssVariables.font.color.tertiary};
cursor: pointer;
display: flex;
height: ${() => themeCssVariables.spacing[4]};
justify-content: center;
padding: 0;
width: ${() => themeCssVariables.spacing[4]};
&:hover {
color: ${() => themeCssVariables.font.color.primary};
}
`;
type GranolaFolderTreeItemProps = {
node: GranolaFolderTreeNode;
depth?: number;
isLast?: boolean;
parentsIsLastList?: boolean[];
selectedFolderIds: Set<string>;
coveredFolderIds: Set<string>;
isSelectionFull: boolean;
onToggleFolder: (folderId: string, checked: boolean) => void;
};
export const GranolaFolderTreeItem = ({
node,
depth = 0,
isLast = false,
parentsIsLastList = [],
selectedFolderIds,
coveredFolderIds,
isSelectionFull,
onToggleFolder,
}: GranolaFolderTreeItemProps) => {
const [isExpanded, setIsExpanded] = useState(true);
const { folder, children } = node;
const hasChildren = children.length > 0;
const isSelected = selectedFolderIds.has(folder.id);
const isCovered = coveredFolderIds.has(folder.id);
const isChecked = isSelected || isCovered;
const isIndeterminate =
!isChecked && hasSelectedGranolaDescendant(node, selectedFolderIds);
const isOnlySelectedFolder = isSelected && selectedFolderIds.size === 1;
const isUnavailable = isCovered || (isSelectionFull && !isChecked);
const isDisabled = isUnavailable || isOnlySelectedFolder;
const FolderIcon = depth > 0 ? IconFolderRoot : IconFolder;
const childParentsIsLastList =
depth > 0 ? [...parentsIsLastList, isLast] : parentsIsLastList;
return (
<StyledItem>
<StyledRow
$depth={depth}
onClick={() => hasChildren && setIsExpanded(!isExpanded)}
>
{depth > 0 && (
<GranolaFolderTreeBreadcrumb
depth={depth}
isLast={isLast}
parentsIsLastList={parentsIsLastList}
/>
)}
<StyledFolderContent>
<FolderIcon size={ICON.size.md} stroke={ICON.stroke.sm} />
<StyledFolderName $disabled={isUnavailable} title={folder.name}>
{folder.name}
</StyledFolderName>
<StyledRightSection>
{hasChildren && (
<>
<StyledChildCount>
{countNestedGranolaFolders(node)}
</StyledChildCount>
<StyledExpandButton
type="button"
aria-label={
isExpanded ? t('Collapse folder') : t('Expand folder')
}
onClick={(event) => {
event.stopPropagation();
setIsExpanded(!isExpanded);
}}
>
{isExpanded ? (
<IconChevronUp size={ICON.size.md} />
) : (
<IconChevronDown size={ICON.size.md} />
)}
</StyledExpandButton>
</>
)}
<span onClick={(event) => event.stopPropagation()}>
<Checkbox
checked={isChecked}
indeterminate={isIndeterminate}
disabled={isDisabled}
aria-label={folder.name}
onChange={(checked) => onToggleFolder(folder.id, checked)}
/>
</span>
</StyledRightSection>
</StyledFolderContent>
</StyledRow>
{hasChildren && (
<StyledCollapsibleWrapper $isExpanded={isExpanded}>
<StyledCollapsibleContent>
<StyledNestedList>
{children.map((child, index) => (
<GranolaFolderTreeItem
key={child.folder.id}
node={child}
depth={depth + 1}
isLast={index === children.length - 1}
parentsIsLastList={childParentsIsLastList}
selectedFolderIds={selectedFolderIds}
coveredFolderIds={coveredFolderIds}
isSelectionFull={isSelectionFull}
onToggleFolder={onToggleFolder}
/>
))}
</StyledNestedList>
</StyledCollapsibleContent>
</StyledCollapsibleWrapper>
)}
</StyledItem>
);
};