Add record table widget to dashboards (#18747)
## Demo https://github.com/user-attachments/assets/584de452-544a-41f8-ae9f-4be9e9d0cd9f ## Problem - Dashboards only supported chart widgets — tabular record data had no inline widget type - `RecordTable` was tightly coupled to the record index: HTML IDs, CSS variables, and hover portals were global strings with no per-instance scoping, so multiple tables on the same page would collide - `updateRecordTableCSSVariable`, `RECORD_TABLE_HTML_ID`, and cell portal IDs were hardcoded — placing two tables caused hover portals and CSS column widths to bleed across instances - Grid drag-select captured record UUIDs as cell IDs, producing `NaN` layout coordinates and a full-page freeze on second widget creation ## Fix - `RECORD_TABLE` is now a valid widget type across the full stack — server DTOs, DB enum migration, universal config mapping, GraphQL codegen, shared types (`RecordTableConfigurationDto`, `WidgetType`, `addRecordTableWidgetType` migration) - A record table widget can be placed on a dashboard and boots from a View ID with no record index dependency — `StandaloneRecordTableProvider` + `StandaloneRecordTableViewLoadEffect` (wraps existing `RecordTableWithWrappers` unchanged) - Selecting a data source auto-creates a dedicated View with up to 6 initial fields; switching source or deleting the widget cleans up the View — `useCreateViewForRecordTableWidget` + `useDeleteViewForRecordTableWidget` - The settings panel exposes source, field visibility/reorder, filter conditions, sort rules, and editable widget title — `SidePanelPageLayoutRecordTableSettings` + sub-pages, matching chart widget pattern - Filters, sorts, and aggregate operations update the table in real time but only persist to the View on explicit dashboard save — `useSaveRecordTableWidgetsViewDataOnDashboardSave` (diff + flush on save) - Headers are always non-interactive (no dropdown, no cursor pointer); columns are resizable only in edit mode; cells are non-editable in both modes — `isRecordTableColumnHeadersReadOnlyComponentState`, `isRecordTableColumnResizableComponentState`, `isRecordTableCellsNonEditableComponentState` (Jotai component states) - Hover portals and CSS column widths no longer bleed between multiple table widgets — `getRecordTableHtmlId(tableId)`, `getRecordTableCellId(tableId, …)`, `updateRecordTableCSSVariable(tableId, …)` scope all DOM IDs and CSS variables per instance - Clicking inside a widget's content area no longer opens the settings panel — `WidgetCardContent` stops click propagation when editable, limiting settings-open to the card header and chrome - Second widget creation no longer freezes the page — `PageLayoutGridLayout` drag-select filters by `cell-` prefix to exclude record UUIDs from grid cell detection ## Follow-up fixes **Widget save flow** - Saving a dashboard silently dropped record table widget changes (column visibility, order, filters, sorts, aggregates) because widget data save was bundled inside the layout save and only ran when layout structure changed - Widget data now persists independently via `useSavePageLayoutWidgetsData`, called in all save paths (dashboard save, record page save, layout customization save); saves are also skipped when nothing has changed **Drag-and-drop / checkbox columns in widget** - Record table widgets showed the drag handle column and checkbox selection column even though row reordering and multi-select are meaningless in a read-only widget - Two new component states (`isRecordTableDragColumnHiddenComponentState`, `isRecordTableCheckboxColumnHiddenComponentState`) hide each column independently; widget tables now display only data columns **Sticky column layout** - Sticky positioning of the first three columns used `:nth-of-type` CSS selectors — when drag or checkbox columns were hidden, the selector targeted the wrong column and the first data column didn't stick - Sticky CSS now targets semantic class names (`RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME`, etc.) so sticky behavior is correct regardless of which columns are hidden **Save/Cancel buttons during edit mode** - Save and Cancel command-menu buttons were unpinned during dashboard edit mode because the pin logic excluded all items while `isPageInEditMode` was true - Items whose availability expression contains `isPageInEditMode` are now exempted from the unpin rule; Save/Cancel stay pinned during editing **Title input auto-focus** - Selecting "Record Table" as widget type auto-focused the title input, interrupting the configuration flow - `focusTitleInput` is now `false` when navigating to record table settings **Morph relation field error** - A field with missing `morphRelations` metadata crashed the page with a "refresh" error from `mapObjectMetadataToGraphQLQuery` - Now returns an empty array and silently omits the field from the query instead of crashing **`updateRecordMutation` prop removal** - `RecordTableWithWrappers` required callers to pass an `updateRecordMutation` callback, duplicating `useUpdateOneRecord` at every usage site - The mutation is now owned inside `RecordTableContextProvider` via `RecordTableUpdateContext`; the prop is gone **Standalone → Widget module rename** - `record-table-standalone` module renamed to `record-table-widget` — `StandaloneRecordTable` → `RecordTableWidget`, `StandaloneRecordTableViewLoadEffect` → `RecordTableWidgetViewLoadEffect`, etc. **RecordTableRow cell extraction** - Row rendering logic (`RecordTableCellDragAndDrop`, `RecordTableCellCheckbox`, `RecordTableFieldsCells`, hotkey/arrow-key effects) was duplicated between `RecordTableRow` and `RecordTableRowVirtualizedFullData` - Extracted `RecordTableRowCells` (shared cell content) and `RecordTableStaticTr` (non-draggable `<tr>` wrapper); when drag column is hidden, rows render inside a static `<tr>` instead of the draggable wrapper **View load effect metadata tracking** - `RecordTableWidgetViewLoadEffect` now tracks `objectMetadataItem.updatedAt` alongside `viewId` to re-load states when metadata changes (e.g. field additions), preventing stale column data **Data source dropdown deduplication** - Extracted `filterReadableActiveObjectMetadataItems` util, shared by both chart and record table data source dropdowns — removes duplicated permission-filtering logic **RECORD_TABLE view identifier mapping (server)** - Added `RECORD_TABLE` case to `fromPageLayoutWidgetConfigurationToUniversalConfiguration` and `fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration` so widget views are properly mapped during workspace import/export **GraphQL error handler typing (server)** - `formatError` parameter changed from `any` to `unknown`; `workspaceQueryRunnerGraphqlApiExceptionHandler` broadened from `QueryFailedErrorWithCode` to `Error | QueryFailedError` — removes unsafe type casts **Save hook signature** - `useSaveRecordTableWidgetsViewDataOnDashboardSave` no longer takes `pageLayoutId` in constructor; receives it as a callback parameter, eliminating the need for `useAtomComponentStateCallbackState` **Customize Dashboard hidden during edit mode** - The "Customize Dashboard" command was still visible while already editing — its `conditionalAvailabilityExpression` now includes `not isPageInEditMode` **Fields dropdown split** - `RecordTableFieldsDropdownContent` (300+ lines) split into `RecordTableFieldsDropdownVisibleFieldsContent` and `RecordTableFieldsDropdownHiddenFieldsContent` **Checkbox placeholder cleanup** - Removed unnecessary `StyledRecordTableTdContainer` wrapper from `RecordTableCellCheckboxPlaceholder`
This commit is contained in:
@@ -101,6 +101,7 @@ export const SidePanelPageInfo = ({ pageChip }: SidePanelPageInfoProps) => {
|
||||
SidePanelPages.PageLayoutIframeSettings,
|
||||
SidePanelPages.PageLayoutTabSettings,
|
||||
SidePanelPages.PageLayoutFieldsSettings,
|
||||
SidePanelPages.PageLayoutRecordTableSettings,
|
||||
].includes(pageChip.page?.page)
|
||||
: false;
|
||||
|
||||
|
||||
+31
@@ -12,6 +12,7 @@ import {
|
||||
IconFrame,
|
||||
IconList,
|
||||
IconPlus,
|
||||
IconTable,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
@@ -174,6 +175,36 @@ export const usePageLayoutHeaderInfo = ({
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutRecordTableSettings: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = isDefined(editedTitle)
|
||||
? editedTitle
|
||||
: isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
|
||||
? widgetInEditMode.title
|
||||
: '';
|
||||
|
||||
return {
|
||||
headerIcon: IconTable,
|
||||
headerIconColor: iconColor,
|
||||
headerType: t`Record Table`,
|
||||
title,
|
||||
isReadonly: false,
|
||||
tab: undefined,
|
||||
widgetInEditMode,
|
||||
};
|
||||
}
|
||||
|
||||
case SidePanelPages.PageLayoutWidgetTypeSelect: {
|
||||
return {
|
||||
headerIcon: IconPlus,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SidePanelNavigationMenuItemEditPage } from '@/navigation-menu-item/edit
|
||||
import { SidePanelNewSidebarItemPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemPage';
|
||||
import { SidePanelPageLayoutChartSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutChartSettings';
|
||||
import { SidePanelPageLayoutFieldsSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutFieldsSettings';
|
||||
import { SidePanelPageLayoutRecordTableSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordTableSettings';
|
||||
import { SidePanelPageLayoutIframeSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutIframeSettings';
|
||||
import { SidePanelPageLayoutTabSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutTabSettings';
|
||||
import { SidePanelPageLayoutWidgetTypeSelect } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutWidgetTypeSelect';
|
||||
@@ -62,6 +63,10 @@ export const SIDE_PANEL_PAGES_CONFIG = new Map<SidePanelPages, React.ReactNode>(
|
||||
SidePanelPages.PageLayoutFieldsSettings,
|
||||
<SidePanelPageLayoutFieldsSettings />,
|
||||
],
|
||||
[
|
||||
SidePanelPages.PageLayoutRecordTableSettings,
|
||||
<SidePanelPageLayoutRecordTableSettings />,
|
||||
],
|
||||
[SidePanelPages.ViewFrontComponent, <SidePanelFrontComponentPage />],
|
||||
[
|
||||
SidePanelPages.NavigationMenuItemEdit,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { SidePanelNewSidebarItemViewPickerSubPage } from '@/navigation-menu-item
|
||||
import { SidePanelNewSidebarItemViewSystemPickerSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemPickerSubPage';
|
||||
import { SidePanelChartFilterSubPage } from '@/side-panel/pages/page-layout/components/SidePanelChartFilterSubPage';
|
||||
import { SidePanelFieldsLayoutSubPage } from '@/side-panel/pages/page-layout/components/SidePanelFieldsLayoutSubPage';
|
||||
import { SidePanelRecordTableFilterSubPage } from '@/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableFilterSubPage';
|
||||
import { SidePanelRecordTableSortSubPage } from '@/side-panel/pages/page-layout/components/record-table-settings/SidePanelRecordTableSortSubPage';
|
||||
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
|
||||
import React from 'react';
|
||||
|
||||
@@ -16,6 +18,14 @@ export const SIDE_PANEL_SUB_PAGES_CONFIG = new Map<
|
||||
>([
|
||||
[SidePanelSubPages.PageLayoutGraphFilter, <SidePanelChartFilterSubPage />],
|
||||
[SidePanelSubPages.PageLayoutFieldsLayout, <SidePanelFieldsLayoutSubPage />],
|
||||
[
|
||||
SidePanelSubPages.PageLayoutRecordTableFilter,
|
||||
<SidePanelRecordTableFilterSubPage />,
|
||||
],
|
||||
[
|
||||
SidePanelSubPages.PageLayoutRecordTableSort,
|
||||
<SidePanelRecordTableSortSubPage />,
|
||||
],
|
||||
[
|
||||
SidePanelSubPages.NewSidebarItemViewObjectPicker,
|
||||
<SidePanelNewSidebarItemViewObjectPickerSubPage />,
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
|
||||
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
|
||||
import { RecordTableDataSourceDropdownContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableDataSourceDropdownContent';
|
||||
import { RecordTableFieldsDropdownContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableFieldsDropdownContent';
|
||||
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconArrowsSort,
|
||||
IconDatabase,
|
||||
IconEye,
|
||||
IconFilter,
|
||||
} from 'twenty-ui/display';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledSettingsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SidePanelPageLayoutRecordTableSettings = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { configuration } = widgetInEditMode;
|
||||
|
||||
const isRecordTableConfiguration =
|
||||
configuration.configurationType === WidgetConfigurationType.RECORD_TABLE;
|
||||
|
||||
const viewId =
|
||||
isRecordTableConfiguration &&
|
||||
'viewId' in configuration &&
|
||||
isDefined(configuration.viewId)
|
||||
? (configuration.viewId as string)
|
||||
: undefined;
|
||||
|
||||
const hasViewId = isDefined(viewId);
|
||||
|
||||
const selectableItemIds = [
|
||||
'record-table-source',
|
||||
...(hasViewId
|
||||
? ['record-table-fields', 'record-table-filter', 'record-table-sort']
|
||||
: []),
|
||||
];
|
||||
|
||||
const handleFilterClick = () => {
|
||||
navigateToSidePanelSubPage(SidePanelSubPages.PageLayoutRecordTableFilter);
|
||||
};
|
||||
|
||||
const handleSortClick = () => {
|
||||
navigateToSidePanelSubPage(SidePanelSubPages.PageLayoutRecordTableSort);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<WidgetComponentInstanceContext.Provider
|
||||
value={{ instanceId: widgetInEditMode.id }}
|
||||
>
|
||||
<StyledSettingsContainer>
|
||||
<SidePanelList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
>
|
||||
<SidePanelGroup heading={t`Data`}>
|
||||
<SelectableListItem itemId="record-table-source">
|
||||
<CommandMenuItemDropdown
|
||||
Icon={IconDatabase}
|
||||
label={t`Source`}
|
||||
id="record-table-source"
|
||||
dropdownId="record-table-source"
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<RecordTableDataSourceDropdownContent />
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownPlacement="bottom-end"
|
||||
hasSubMenu
|
||||
contextualTextPosition="right"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
{hasViewId && (
|
||||
<>
|
||||
<SelectableListItem itemId="record-table-fields">
|
||||
<CommandMenuItemDropdown
|
||||
Icon={IconEye}
|
||||
label={t`Fields`}
|
||||
id="record-table-fields"
|
||||
dropdownId="record-table-fields"
|
||||
dropdownComponents={
|
||||
<RecordTableFieldsDropdownContent
|
||||
viewId={viewId}
|
||||
objectMetadataId={widgetInEditMode.objectMetadataId!}
|
||||
/>
|
||||
}
|
||||
dropdownPlacement="bottom-end"
|
||||
hasSubMenu
|
||||
contextualTextPosition="right"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId="record-table-filter"
|
||||
onEnter={handleFilterClick}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id="record-table-filter"
|
||||
label={t`Filter`}
|
||||
Icon={IconFilter}
|
||||
hasSubMenu
|
||||
onClick={handleFilterClick}
|
||||
contextualTextPosition="right"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId="record-table-sort"
|
||||
onEnter={handleSortClick}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id="record-table-sort"
|
||||
label={t`Sort`}
|
||||
Icon={IconArrowsSort}
|
||||
hasSubMenu
|
||||
onClick={handleSortClick}
|
||||
contextualTextPosition="right"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</>
|
||||
)}
|
||||
</SidePanelGroup>
|
||||
</SidePanelList>
|
||||
</StyledSettingsContainer>
|
||||
<WidgetSettingsFooter pageLayoutId={pageLayoutId} />
|
||||
</WidgetComponentInstanceContext.Provider>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+39
@@ -3,6 +3,7 @@ import { FIND_MANY_FRONT_COMPONENTS } from '@/front-components/graphql/queries/f
|
||||
import { useCreatePageLayoutFrontComponentWidget } from '@/page-layout/hooks/useCreatePageLayoutFrontComponentWidget';
|
||||
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
|
||||
import { useCreatePageLayoutIframeWidget } from '@/page-layout/hooks/useCreatePageLayoutIframeWidget';
|
||||
import { useCreatePageLayoutRecordTableWidget } from '@/page-layout/hooks/useCreatePageLayoutRecordTableWidget';
|
||||
import { useCreatePageLayoutStandaloneRichTextWidget } from '@/page-layout/hooks/useCreatePageLayoutStandaloneRichTextWidget';
|
||||
import { useOpportunityDefaultChartConfig } from '@/page-layout/hooks/useOpportunityDefaultChartConfig';
|
||||
import { useRemovePageLayoutWidgetAndPreservePosition } from '@/page-layout/hooks/useRemovePageLayoutWidgetAndPreservePosition';
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
IconApps,
|
||||
IconChartPie,
|
||||
IconFrame,
|
||||
IconTable,
|
||||
} from 'twenty-ui/display';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
@@ -78,6 +80,9 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
|
||||
tabListInstanceId,
|
||||
});
|
||||
|
||||
const { createPageLayoutRecordTableWidget } =
|
||||
useCreatePageLayoutRecordTableWidget(pageLayoutId);
|
||||
|
||||
const { removePageLayoutWidgetAndPreservePosition } =
|
||||
useRemovePageLayoutWidgetAndPreservePosition(pageLayoutId);
|
||||
|
||||
@@ -176,6 +181,28 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
|
||||
closeSidePanelMenu();
|
||||
};
|
||||
|
||||
const handleNavigateToRecordTableSettings = () => {
|
||||
if (
|
||||
isExistingWidgetMissingOrDifferentType(
|
||||
existingWidget?.type,
|
||||
WidgetType.RECORD_TABLE,
|
||||
)
|
||||
) {
|
||||
if (isDefined(pageLayoutEditingWidgetId)) {
|
||||
removePageLayoutWidgetAndPreservePosition(pageLayoutEditingWidgetId);
|
||||
}
|
||||
|
||||
const newRecordTableWidget = createPageLayoutRecordTableWidget();
|
||||
|
||||
setPageLayoutEditingWidgetId(newRecordTableWidget.id);
|
||||
}
|
||||
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordTableSettings,
|
||||
focusTitleInput: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateFrontComponentWidget = (frontComponent: FrontComponent) => {
|
||||
if (
|
||||
isExistingWidgetMissingOrDifferentType(
|
||||
@@ -199,6 +226,7 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
|
||||
|
||||
const selectableItemIds = [
|
||||
'chart',
|
||||
'record-table',
|
||||
'iframe',
|
||||
'rich-text',
|
||||
...frontComponentsWithSelectItemId.map(({ selectItemId }) => selectItemId),
|
||||
@@ -218,6 +246,17 @@ export const SidePanelPageLayoutWidgetTypeSelect = () => {
|
||||
onClick={handleNavigateToGraphTypeSelect}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId="record-table"
|
||||
onEnter={handleNavigateToRecordTableSettings}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconTable}
|
||||
label={t`Record Table`}
|
||||
id="record-table"
|
||||
onClick={handleNavigateToRecordTableSettings}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId="iframe"
|
||||
onEnter={handleNavigateToIframeSettings}
|
||||
|
||||
+4
-11
@@ -3,6 +3,7 @@ import { useResetChartDraftFiltersSettings } from '@/side-panel/pages/page-layou
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
@@ -49,17 +50,9 @@ export const ChartDataSourceDropdownContent = () => {
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const objectsWithReadAccess = objectMetadataItems.filter(
|
||||
(objectMetadataItem) => {
|
||||
const objectPermissions =
|
||||
objectPermissionsByObjectMetadataId[objectMetadataItem.id];
|
||||
|
||||
return (
|
||||
isDefined(objectPermissions) &&
|
||||
objectPermissions.canReadObjectRecords &&
|
||||
objectMetadataItem.isActive
|
||||
);
|
||||
},
|
||||
const objectsWithReadAccess = filterReadableActiveObjectMetadataItems(
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
);
|
||||
|
||||
const regularObjects = objectsWithReadAccess
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { useCreateViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useCreateViewForRecordTableWidget';
|
||||
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
export const RecordTableDataSourceDropdownContent = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const currentObjectMetadataItemId = widgetInEditMode?.objectMetadataId as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
const dropdownId = useAvailableComponentInstanceIdOrThrow(
|
||||
DropdownComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectedItemId = useAtomComponentStateValue(
|
||||
selectedItemIdComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const { createViewForRecordTableWidget } =
|
||||
useCreateViewForRecordTableWidget(pageLayoutId);
|
||||
|
||||
const { deleteViewForRecordTableWidget } =
|
||||
useDeleteViewForRecordTableWidget();
|
||||
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const readableActiveObjectMetadataItems =
|
||||
filterReadableActiveObjectMetadataItems(
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
);
|
||||
|
||||
const objectsWithReadAccess = readableActiveObjectMetadataItems.filter(
|
||||
(objectMetadataItem) => !objectMetadataItem.isSystem,
|
||||
);
|
||||
|
||||
const sortedObjects = objectsWithReadAccess.sort((first, second) =>
|
||||
first.labelPlural.localeCompare(second.labelPlural),
|
||||
);
|
||||
|
||||
const filteredObjects = filterBySearchQuery({
|
||||
items: sortedObjects,
|
||||
searchQuery,
|
||||
getSearchableValues: (item) => [item.labelPlural, item.namePlural],
|
||||
});
|
||||
|
||||
const currentViewId =
|
||||
widgetInEditMode?.configuration &&
|
||||
'viewId' in widgetInEditMode.configuration
|
||||
? (widgetInEditMode.configuration.viewId as string | undefined)
|
||||
: undefined;
|
||||
|
||||
const handleSelectSource = async (newObjectMetadataItemId: string) => {
|
||||
if (currentObjectMetadataItemId === newObjectMetadataItemId) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(currentViewId)) {
|
||||
await deleteViewForRecordTableWidget(currentViewId);
|
||||
}
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
objectMetadataId: newObjectMetadataItemId,
|
||||
configToUpdate: {
|
||||
viewId: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === newObjectMetadataItemId,
|
||||
);
|
||||
|
||||
if (isDefined(selectedObjectMetadataItem)) {
|
||||
await createViewForRecordTableWidget(selectedObjectMetadataItem);
|
||||
|
||||
if (isDefined(widgetInEditMode)) {
|
||||
updatePageLayoutWidget(widgetInEditMode.id, {
|
||||
title: selectedObjectMetadataItem.labelPlural,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={t`Search objects`}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={filteredObjects.map(
|
||||
(objectMetadataItem) => objectMetadataItem.id,
|
||||
)}
|
||||
>
|
||||
{filteredObjects.map((objectMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={objectMetadataItem.id}
|
||||
itemId={objectMetadataItem.id}
|
||||
onEnter={() => {
|
||||
handleSelectSource(objectMetadataItem.id);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={objectMetadataItem.labelPlural}
|
||||
selected={currentObjectMetadataItemId === objectMetadataItem.id}
|
||||
focused={selectedItemId === objectMetadataItem.id}
|
||||
LeftIcon={getIcon(objectMetadataItem.icon)}
|
||||
onClick={() => {
|
||||
handleSelectSource(objectMetadataItem.id);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { RecordTableFieldsDropdownHiddenFieldsContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableFieldsDropdownHiddenFieldsContent';
|
||||
import { RecordTableFieldsDropdownVisibleFieldsContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableFieldsDropdownVisibleFieldsContent';
|
||||
import { useState } from 'react';
|
||||
|
||||
type RecordTableFieldsDropdownContentProps = {
|
||||
viewId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export const RecordTableFieldsDropdownContent = ({
|
||||
viewId,
|
||||
objectMetadataId,
|
||||
}: RecordTableFieldsDropdownContentProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const [showHiddenFields, setShowHiddenFields] = useState(false);
|
||||
|
||||
return (
|
||||
<RecordFieldsComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordIndexId }}
|
||||
>
|
||||
{showHiddenFields ? (
|
||||
<RecordTableFieldsDropdownHiddenFieldsContent
|
||||
objectMetadataId={objectMetadataId}
|
||||
recordIndexId={recordIndexId}
|
||||
onBack={() => setShowHiddenFields(false)}
|
||||
/>
|
||||
) : (
|
||||
<RecordTableFieldsDropdownVisibleFieldsContent
|
||||
objectMetadataId={objectMetadataId}
|
||||
recordIndexId={recordIndexId}
|
||||
onShowHiddenFields={() => setShowHiddenFields(true)}
|
||||
/>
|
||||
)}
|
||||
</RecordFieldsComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFieldMetadataItems';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
|
||||
import { useUpsertRecordField } from '@/object-record/record-field/hooks/useUpsertRecordField';
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft, IconEye, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { v4 } from 'uuid';
|
||||
import { sortByProperty } from '~/utils/array/sortByProperty';
|
||||
|
||||
type RecordTableFieldsDropdownHiddenFieldsContentProps = {
|
||||
objectMetadataId: string;
|
||||
recordIndexId: string;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const RecordTableFieldsDropdownHiddenFieldsContent = ({
|
||||
objectMetadataId,
|
||||
recordIndexId,
|
||||
onBack,
|
||||
}: RecordTableFieldsDropdownHiddenFieldsContentProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { upsertRecordField } = useUpsertRecordField(recordIndexId);
|
||||
|
||||
const { updateRecordField } = useUpdateRecordField(recordIndexId);
|
||||
|
||||
const currentRecordFields = useAtomComponentStateValue(
|
||||
currentRecordFieldsComponentState,
|
||||
);
|
||||
|
||||
const { activeFieldMetadataItems } = useActiveFieldMetadataItems({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const visibleFieldMetadataItemIds = currentRecordFields
|
||||
.filter((recordField) => recordField.isVisible)
|
||||
.map((recordField) => recordField.fieldMetadataItemId);
|
||||
|
||||
const hiddenFieldMetadataItems = activeFieldMetadataItems.filter(
|
||||
(fieldMetadataItem) =>
|
||||
!visibleFieldMetadataItemIds.includes(fieldMetadataItem.id),
|
||||
);
|
||||
|
||||
const handleShowField = (fieldMetadataId: string) => {
|
||||
const existingRecordField = currentRecordFields.find(
|
||||
(recordField) => recordField.fieldMetadataItemId === fieldMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(existingRecordField)) {
|
||||
updateRecordField(fieldMetadataId, { isVisible: true });
|
||||
} else {
|
||||
const lastPosition =
|
||||
currentRecordFields.toSorted(sortByProperty('position', 'desc'))?.[0]
|
||||
?.position ?? 0;
|
||||
|
||||
const newRecordField: RecordField = {
|
||||
id: v4(),
|
||||
fieldMetadataItemId: fieldMetadataId,
|
||||
size: 100,
|
||||
isVisible: true,
|
||||
position: lastPosition + 1,
|
||||
};
|
||||
|
||||
upsertRecordField(newRecordField);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownContent>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={onBack}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t`Hidden Fields`}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{hiddenFieldMetadataItems.map((fieldMetadataItem) => (
|
||||
<MenuItem
|
||||
key={fieldMetadataItem.id}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconEye,
|
||||
onClick: () => handleShowField(fieldMetadataItem.id),
|
||||
},
|
||||
]}
|
||||
text={fieldMetadataItem.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { useReorderVisibleRecordFields } from '@/object-record/record-field/hooks/useReorderVisibleRecordFields';
|
||||
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconEyeOff, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemDraggable, MenuItemNavigate } from 'twenty-ui/navigation';
|
||||
import { sortByProperty } from '~/utils/array/sortByProperty';
|
||||
|
||||
type RecordTableFieldsDropdownVisibleFieldsContentProps = {
|
||||
objectMetadataId: string;
|
||||
recordIndexId: string;
|
||||
onShowHiddenFields: () => void;
|
||||
};
|
||||
|
||||
export const RecordTableFieldsDropdownVisibleFieldsContent = ({
|
||||
objectMetadataId,
|
||||
recordIndexId,
|
||||
onShowHiddenFields,
|
||||
}: RecordTableFieldsDropdownVisibleFieldsContentProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { getFieldMetadataItemByIdOrThrow } =
|
||||
useGetFieldMetadataItemByIdOrThrow();
|
||||
|
||||
const { reorderVisibleRecordFields } =
|
||||
useReorderVisibleRecordFields(recordIndexId);
|
||||
|
||||
const { updateRecordField } = useUpdateRecordField(recordIndexId);
|
||||
|
||||
const fieldMetadataItemLabelIdentifier =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
|
||||
const visibleRecordFields = useAtomComponentSelectorValue(
|
||||
visibleRecordFieldsComponentSelector,
|
||||
);
|
||||
|
||||
const nonDraggableRecordField = visibleRecordFields.find(
|
||||
(recordField) =>
|
||||
recordField.fieldMetadataItemId === fieldMetadataItemLabelIdentifier?.id,
|
||||
);
|
||||
|
||||
const draggableRecordFields = visibleRecordFields
|
||||
.filter(
|
||||
(recordField) =>
|
||||
nonDraggableRecordField?.fieldMetadataItemId !==
|
||||
recordField.fieldMetadataItemId,
|
||||
)
|
||||
.toSorted(sortByProperty('position'));
|
||||
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
if (
|
||||
!result.destination ||
|
||||
result.destination.index === 1 ||
|
||||
result.source.index === 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
reorderVisibleRecordFields({
|
||||
fromIndex: result.source.index - 1,
|
||||
toIndex: result.destination.index - 1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleHideField = (fieldMetadataId: string) => {
|
||||
updateRecordField(fieldMetadataId, { isVisible: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
{isDefined(fieldMetadataItemLabelIdentifier) && (
|
||||
<MenuItemDraggable
|
||||
LeftIcon={getIcon(fieldMetadataItemLabelIdentifier.icon)}
|
||||
text={fieldMetadataItemLabelIdentifier.label}
|
||||
accent="placeholder"
|
||||
gripMode="always"
|
||||
isDragDisabled
|
||||
/>
|
||||
)}
|
||||
{draggableRecordFields.length > 0 && (
|
||||
<DraggableList
|
||||
onDragEnd={handleDragEnd}
|
||||
draggableItems={
|
||||
<>
|
||||
{draggableRecordFields.map((recordField, index) => {
|
||||
const fieldIndex =
|
||||
index +
|
||||
(isDefined(fieldMetadataItemLabelIdentifier) ? 1 : 0);
|
||||
|
||||
const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow(
|
||||
recordField.fieldMetadataItemId,
|
||||
);
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={recordField.fieldMetadataItemId}
|
||||
draggableId={recordField.fieldMetadataItemId}
|
||||
index={fieldIndex + 1}
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconEyeOff,
|
||||
onClick: () =>
|
||||
handleHideField(
|
||||
recordField.fieldMetadataItemId,
|
||||
),
|
||||
},
|
||||
]}
|
||||
text={fieldMetadataItem.label}
|
||||
gripMode="always"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItemNavigate
|
||||
onClick={onShowHiddenFields}
|
||||
LeftIcon={IconEyeOff}
|
||||
text={t`Hidden Fields`}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { useRecordTableWidgetViewFieldItems } from '@/page-layout/widgets/record-table/hooks/useRecordTableWidgetViewFieldItems';
|
||||
import { useReorderRecordTableWidgetFields } from '@/page-layout/widgets/record-table/hooks/useReorderRecordTableWidgetFields';
|
||||
import { useToggleRecordTableWidgetFieldVisibility } from '@/page-layout/widgets/record-table/hooks/useToggleRecordTableWidgetFieldVisibility';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useMemo } from 'react';
|
||||
import { IconEye, IconEyeOff, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemDraggable } from 'twenty-ui/navigation';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledFieldListContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const StyledSectionLabel = styled.div`
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-size: ${themeCssVariables.font.size.xs};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
padding: ${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
type RecordTableSettingsFieldVisibilityProps = {
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsFieldVisibility = ({
|
||||
viewId,
|
||||
}: RecordTableSettingsFieldVisibilityProps) => {
|
||||
const { recordTableWidgetViewFieldItems } =
|
||||
useRecordTableWidgetViewFieldItems(viewId);
|
||||
|
||||
const { toggleRecordTableWidgetFieldVisibility } =
|
||||
useToggleRecordTableWidgetFieldVisibility();
|
||||
|
||||
const { reorderRecordTableWidgetFields } =
|
||||
useReorderRecordTableWidgetFields();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const visibleFieldItems = useMemo(
|
||||
() =>
|
||||
recordTableWidgetViewFieldItems.filter(
|
||||
(item) => item.viewField.isVisible,
|
||||
),
|
||||
[recordTableWidgetViewFieldItems],
|
||||
);
|
||||
|
||||
const hiddenFieldItems = useMemo(
|
||||
() =>
|
||||
recordTableWidgetViewFieldItems.filter(
|
||||
(item) => !item.viewField.isVisible,
|
||||
),
|
||||
[recordTableWidgetViewFieldItems],
|
||||
);
|
||||
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
const { source, destination } = result;
|
||||
|
||||
if (!destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
reorderRecordTableWidgetFields(
|
||||
source.index,
|
||||
destination.index,
|
||||
visibleFieldItems,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledFieldListContainer>
|
||||
<StyledSectionLabel>Visible</StyledSectionLabel>
|
||||
{visibleFieldItems.length > 0 && (
|
||||
<DraggableList
|
||||
onDragEnd={handleDragEnd}
|
||||
draggableItems={
|
||||
<>
|
||||
{visibleFieldItems.map((fieldItem, index) => (
|
||||
<DraggableItem
|
||||
key={fieldItem.viewField.id}
|
||||
draggableId={fieldItem.viewField.id}
|
||||
index={index}
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
LeftIcon={getIcon(fieldItem.fieldMetadataItem.icon)}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconEyeOff,
|
||||
onClick: () => {
|
||||
toggleRecordTableWidgetFieldVisibility(
|
||||
fieldItem.viewField.id,
|
||||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
text={fieldItem.fieldMetadataItem.label}
|
||||
gripMode="always"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{hiddenFieldItems.length > 0 && (
|
||||
<>
|
||||
<StyledSectionLabel>Hidden</StyledSectionLabel>
|
||||
{hiddenFieldItems.map((fieldItem) => (
|
||||
<MenuItemDraggable
|
||||
key={fieldItem.viewField.id}
|
||||
LeftIcon={getIcon(fieldItem.fieldMetadataItem.icon)}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconEye,
|
||||
onClick: () => {
|
||||
toggleRecordTableWidgetFieldVisibility(
|
||||
fieldItem.viewField.id,
|
||||
true,
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
text={fieldItem.fieldMetadataItem.label}
|
||||
accent="placeholder"
|
||||
isDragDisabled
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</StyledFieldListContainer>
|
||||
);
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { AdvancedFilterSidePanelContainer } from '@/object-record/advanced-filter/side-panel/components/AdvancedFilterSidePanelContainer';
|
||||
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { RecordTableSettingsFiltersInitializeStateEffect } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFiltersInitializeStateEffect';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledFilterSettingsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
type RecordTableSettingsFiltersProps = {
|
||||
viewId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsFilters = ({
|
||||
viewId,
|
||||
objectMetadataId,
|
||||
}: RecordTableSettingsFiltersProps) => {
|
||||
const { view } = useViewById(viewId);
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
viewId,
|
||||
);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledFilterSettingsContainer>
|
||||
<InputLabel>{t`Conditions`}</InputLabel>
|
||||
<RecordFilterGroupsComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordIndexId }}
|
||||
>
|
||||
<RecordFiltersComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordIndexId }}
|
||||
>
|
||||
<AdvancedFilterSidePanelContainer
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isWorkflowFindRecords={false}
|
||||
/>
|
||||
<RecordTableSettingsFiltersInitializeStateEffect
|
||||
view={view}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
</RecordFilterGroupsComponentInstanceContext.Provider>
|
||||
</StyledFilterSettingsContainer>
|
||||
);
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useSetAdvancedFilterDropdownStates } from '@/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { getFilterableFields } from '@/views/utils/getFilterableFields';
|
||||
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordTableSettingsFiltersInitializeStateEffectProps = {
|
||||
view: View;
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsFiltersInitializeStateEffect = ({
|
||||
view,
|
||||
objectMetadataItem,
|
||||
}: RecordTableSettingsFiltersInitializeStateEffectProps) => {
|
||||
const setCurrentRecordFilters = useSetAtomComponentState(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const setCurrentRecordFilterGroups = useSetAtomComponentState(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
);
|
||||
|
||||
const { setAdvancedFilterDropdownStates } =
|
||||
useSetAdvancedFilterDropdownStates();
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const currentRecordFilterGroups = useAtomComponentStateValue(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
);
|
||||
|
||||
const [hasInitializedFilters, setHasInitializedFilters] = useState(false);
|
||||
|
||||
const stateAlreadyHasFilters =
|
||||
currentRecordFilters.length > 0 || currentRecordFilterGroups.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitializedFilters) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stateAlreadyHasFilters) {
|
||||
setHasInitializedFilters(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const filterableFields = getFilterableFields(objectMetadataItem);
|
||||
const recordFilters = mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterableFields,
|
||||
);
|
||||
|
||||
setCurrentRecordFilters(recordFilters);
|
||||
|
||||
if (isDefined(view.viewFilterGroups) && view.viewFilterGroups.length > 0) {
|
||||
setCurrentRecordFilterGroups(
|
||||
mapViewFilterGroupsToRecordFilterGroups(view.viewFilterGroups),
|
||||
);
|
||||
}
|
||||
|
||||
setHasInitializedFilters(true);
|
||||
}, [
|
||||
view,
|
||||
objectMetadataItem,
|
||||
hasInitializedFilters,
|
||||
stateAlreadyHasFilters,
|
||||
setCurrentRecordFilters,
|
||||
setCurrentRecordFilterGroups,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitializedFilters) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAdvancedFilterDropdownStates();
|
||||
}, [
|
||||
currentRecordFilters,
|
||||
hasInitializedFilters,
|
||||
setAdvancedFilterDropdownStates,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems';
|
||||
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { RecordTableSettingsSortsContent } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSortsContent';
|
||||
import { RecordTableSettingsSortsInitializeStateEffect } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSortsInitializeStateEffect';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { ViewSortDirection } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledSortSettingsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
type RecordTableSettingsSortsProps = {
|
||||
viewId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsSorts = ({
|
||||
viewId,
|
||||
objectMetadataId,
|
||||
}: RecordTableSettingsSortsProps) => {
|
||||
const { view } = useViewById(viewId);
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
viewId,
|
||||
);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortableFieldOptions = objectMetadataItem.fields
|
||||
.filter(filterSortableFieldMetadataItems)
|
||||
.map((field) => ({
|
||||
Icon: getIcon(field.icon),
|
||||
label: field.label,
|
||||
value: field.id,
|
||||
}));
|
||||
|
||||
const directionOptions: Array<SelectOption<ViewSortDirection>> = [
|
||||
{
|
||||
label: t`Ascending`,
|
||||
value: ViewSortDirection.ASC,
|
||||
},
|
||||
{
|
||||
label: t`Descending`,
|
||||
value: ViewSortDirection.DESC,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledSortSettingsContainer>
|
||||
<InputLabel>{t`Sorts`}</InputLabel>
|
||||
<RecordSortsComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordIndexId }}
|
||||
>
|
||||
<RecordTableSettingsSortsContent
|
||||
sortableFieldOptions={sortableFieldOptions}
|
||||
directionOptions={directionOptions}
|
||||
/>
|
||||
<RecordTableSettingsSortsInitializeStateEffect view={view} />
|
||||
</RecordSortsComponentInstanceContext.Provider>
|
||||
</StyledSortSettingsContainer>
|
||||
);
|
||||
};
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconArrowsSort, IconTrash } from 'twenty-ui/display';
|
||||
import { Button, type SelectOption } from 'twenty-ui/input';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ViewSortDirection } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledSortItemContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledAddButtonContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
type RecordTableSettingsSortsContentProps = {
|
||||
sortableFieldOptions: Array<SelectOption<string>>;
|
||||
directionOptions: Array<SelectOption<ViewSortDirection>>;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsSortsContent = ({
|
||||
sortableFieldOptions,
|
||||
directionOptions,
|
||||
}: RecordTableSettingsSortsContentProps) => {
|
||||
const [currentRecordSorts, setCurrentRecordSorts] = useAtomComponentState(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const handleAddSort = () => {
|
||||
const firstAvailableField = sortableFieldOptions[0]?.value;
|
||||
|
||||
if (!isDefined(firstAvailableField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSort: RecordSort = {
|
||||
id: uuidv4(),
|
||||
fieldMetadataId: firstAvailableField,
|
||||
direction: ViewSortDirection.ASC,
|
||||
};
|
||||
|
||||
setCurrentRecordSorts([...currentRecordSorts, newSort]);
|
||||
};
|
||||
|
||||
const handleRemoveSort = (sortId: string) => {
|
||||
setCurrentRecordSorts(
|
||||
currentRecordSorts.filter((sort) => sort.id !== sortId),
|
||||
);
|
||||
};
|
||||
|
||||
const handleFieldChange = (sortId: string, fieldMetadataId: string) => {
|
||||
setCurrentRecordSorts(
|
||||
currentRecordSorts.map((sort) =>
|
||||
sort.id === sortId ? { ...sort, fieldMetadataId } : sort,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleDirectionChange = (
|
||||
sortId: string,
|
||||
direction: ViewSortDirection,
|
||||
) => {
|
||||
setCurrentRecordSorts(
|
||||
currentRecordSorts.map((sort) =>
|
||||
sort.id === sortId ? { ...sort, direction } : sort,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentRecordSorts.map((sort) => (
|
||||
<StyledSortItemContainer key={sort.id}>
|
||||
<Select
|
||||
dropdownId={`record-table-widget-sort-field-${sort.id}`}
|
||||
fullWidth
|
||||
value={sort.fieldMetadataId}
|
||||
options={sortableFieldOptions}
|
||||
onChange={(value) => handleFieldChange(sort.id, value)}
|
||||
withSearchInput
|
||||
/>
|
||||
<Select
|
||||
dropdownId={`record-table-widget-sort-direction-${sort.id}`}
|
||||
fullWidth
|
||||
value={sort.direction}
|
||||
options={directionOptions}
|
||||
onChange={(value) =>
|
||||
handleDirectionChange(sort.id, value as ViewSortDirection)
|
||||
}
|
||||
/>
|
||||
<Button onClick={() => handleRemoveSort(sort.id)} Icon={IconTrash} />
|
||||
</StyledSortItemContainer>
|
||||
))}
|
||||
<StyledAddButtonContainer>
|
||||
<Button
|
||||
Icon={IconArrowsSort}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
onClick={handleAddSort}
|
||||
ariaLabel={t`Add sort`}
|
||||
title={t`Add sort`}
|
||||
/>
|
||||
</StyledAddButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { type ViewSortDirection } from '~/generated-metadata/graphql';
|
||||
|
||||
type RecordTableSettingsSortsInitializeStateEffectProps = {
|
||||
view: View;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsSortsInitializeStateEffect = ({
|
||||
view,
|
||||
}: RecordTableSettingsSortsInitializeStateEffectProps) => {
|
||||
const setCurrentRecordSorts = useSetAtomComponentState(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const currentRecordSorts = useAtomComponentStateValue(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const [hasInitializedSorts, setHasInitializedSorts] = useState(false);
|
||||
|
||||
const stateAlreadyHasSorts = currentRecordSorts.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitializedSorts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stateAlreadyHasSorts) {
|
||||
setHasInitializedSorts(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const recordSorts: RecordSort[] = (view.viewSorts ?? []).map(
|
||||
(viewSort) => ({
|
||||
id: viewSort.id,
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
direction: viewSort.direction as ViewSortDirection,
|
||||
}),
|
||||
);
|
||||
|
||||
setCurrentRecordSorts(recordSorts);
|
||||
setHasInitializedSorts(true);
|
||||
}, [view, hasInitializedSorts, stateAlreadyHasSorts, setCurrentRecordSorts]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { RecordTableSettingsFieldVisibility } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFieldVisibility';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const SidePanelRecordTableFieldsSubPage = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { configuration } = widgetInEditMode;
|
||||
|
||||
const isRecordTableConfiguration =
|
||||
configuration.configurationType === WidgetConfigurationType.RECORD_TABLE;
|
||||
|
||||
const viewId =
|
||||
isRecordTableConfiguration &&
|
||||
'viewId' in configuration &&
|
||||
isDefined(configuration.viewId)
|
||||
? (configuration.viewId as string)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RecordTableSettingsFieldVisibility viewId={viewId} />;
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { RecordTableSettingsFilters } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsFilters';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const SidePanelRecordTableFilterSubPage = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { configuration } = widgetInEditMode;
|
||||
|
||||
const isRecordTableConfiguration =
|
||||
configuration.configurationType === WidgetConfigurationType.RECORD_TABLE;
|
||||
|
||||
const viewId =
|
||||
isRecordTableConfiguration &&
|
||||
'viewId' in configuration &&
|
||||
isDefined(configuration.viewId)
|
||||
? (configuration.viewId as string)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(viewId) || !isDefined(widgetInEditMode.objectMetadataId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableSettingsFilters
|
||||
viewId={viewId}
|
||||
objectMetadataId={widgetInEditMode.objectMetadataId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { RecordTableSettingsSorts } from '@/side-panel/pages/page-layout/components/record-table-settings/RecordTableSettingsSorts';
|
||||
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
|
||||
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const SidePanelRecordTableSortSubPage = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStore();
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { configuration } = widgetInEditMode;
|
||||
|
||||
const isRecordTableConfiguration =
|
||||
configuration.configurationType === WidgetConfigurationType.RECORD_TABLE;
|
||||
|
||||
const viewId =
|
||||
isRecordTableConfiguration &&
|
||||
'viewId' in configuration &&
|
||||
isDefined(configuration.viewId)
|
||||
? (configuration.viewId as string)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(viewId) || !isDefined(widgetInEditMode.objectMetadataId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableSettingsSorts
|
||||
viewId={viewId}
|
||||
objectMetadataId={widgetInEditMode.objectMetadataId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+2
-1
@@ -5,4 +5,5 @@ export type PageLayoutSidePanelPage =
|
||||
| SidePanelPages.PageLayoutGraphTypeSelect
|
||||
| SidePanelPages.PageLayoutIframeSettings
|
||||
| SidePanelPages.PageLayoutTabSettings
|
||||
| SidePanelPages.PageLayoutFieldsSettings;
|
||||
| SidePanelPages.PageLayoutFieldsSettings
|
||||
| SidePanelPages.PageLayoutRecordTableSettings;
|
||||
|
||||
+3
@@ -6,6 +6,7 @@ import {
|
||||
IconChartPie,
|
||||
IconFrame,
|
||||
IconList,
|
||||
IconTable,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
export const getPageLayoutIcon = (page: PageLayoutSidePanelPage) => {
|
||||
@@ -20,6 +21,8 @@ export const getPageLayoutIcon = (page: PageLayoutSidePanelPage) => {
|
||||
return IconAppWindow;
|
||||
case SidePanelPages.PageLayoutFieldsSettings:
|
||||
return IconList;
|
||||
case SidePanelPages.PageLayoutRecordTableSettings:
|
||||
return IconTable;
|
||||
default:
|
||||
assertUnreachable(page);
|
||||
}
|
||||
|
||||
+2
@@ -15,6 +15,8 @@ export const getPageLayoutPageTitle = (page: PageLayoutSidePanelPage) => {
|
||||
return t`Tab Settings`;
|
||||
case SidePanelPages.PageLayoutFieldsSettings:
|
||||
return t`Fields Settings`;
|
||||
case SidePanelPages.PageLayoutRecordTableSettings:
|
||||
return t`Record Table Settings`;
|
||||
default:
|
||||
assertUnreachable(page);
|
||||
}
|
||||
|
||||
+7
@@ -13,6 +13,7 @@ import {
|
||||
type LineChartConfiguration,
|
||||
type NotesConfiguration,
|
||||
type PieChartConfiguration,
|
||||
type RecordTableConfiguration,
|
||||
type StandaloneRichTextConfiguration,
|
||||
type TasksConfiguration,
|
||||
type TimelineConfiguration,
|
||||
@@ -79,6 +80,12 @@ type WidgetConfigurationTypenameMap = {
|
||||
PieChartConfiguration: Omit<PieChartConfiguration, 'configurationType'> & {
|
||||
configurationType: WidgetConfigurationType.PIE_CHART;
|
||||
};
|
||||
RecordTableConfiguration: Omit<
|
||||
RecordTableConfiguration,
|
||||
'configurationType'
|
||||
> & {
|
||||
configurationType: WidgetConfigurationType.RECORD_TABLE;
|
||||
};
|
||||
StandaloneRichTextConfiguration: Omit<
|
||||
StandaloneRichTextConfiguration,
|
||||
'configurationType'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export enum SidePanelSubPages {
|
||||
PageLayoutGraphFilter = 'page-layout-graph-filter',
|
||||
PageLayoutFieldsLayout = 'page-layout-fields-layout',
|
||||
PageLayoutRecordTableFilter = 'page-layout-record-table-filter',
|
||||
PageLayoutRecordTableSort = 'page-layout-record-table-sort',
|
||||
NewSidebarItemViewObjectPicker = 'new-sidebar-item-view-object-picker',
|
||||
NewSidebarItemViewPicker = 'new-sidebar-item-view-picker',
|
||||
NewSidebarItemViewSystemPicker = 'new-sidebar-item-view-system-picker',
|
||||
|
||||
@@ -10,6 +10,10 @@ export const getSidePanelSubPageTitle = (
|
||||
return t`Filters`;
|
||||
case SidePanelSubPages.PageLayoutFieldsLayout:
|
||||
return t`Layout`;
|
||||
case SidePanelSubPages.PageLayoutRecordTableFilter:
|
||||
return t`Filters`;
|
||||
case SidePanelSubPages.PageLayoutRecordTableSort:
|
||||
return t`Sorts`;
|
||||
case SidePanelSubPages.NewSidebarItemViewObjectPicker:
|
||||
return t`Pick an object`;
|
||||
case SidePanelSubPages.NewSidebarItemViewPicker:
|
||||
|
||||
Reference in New Issue
Block a user