Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136daecbf5 |
@@ -4089,7 +4089,6 @@ input UpdateObjectPayload {
|
||||
labelIdentifierFieldMetadataId: UUID
|
||||
imageIdentifierFieldMetadataId: UUID
|
||||
isLabelSyncedWithName: Boolean
|
||||
isSearchable: Boolean
|
||||
}
|
||||
|
||||
input UpdateViewFieldInput {
|
||||
|
||||
@@ -6475,7 +6475,7 @@ export interface UpdateOneObjectInput {update: UpdateObjectPayload,
|
||||
/** The id of the object to update */
|
||||
id: Scalars['UUID']}
|
||||
|
||||
export interface UpdateObjectPayload {labelSingular?: (Scalars['String'] | null),labelPlural?: (Scalars['String'] | null),nameSingular?: (Scalars['String'] | null),namePlural?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),color?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),labelIdentifierFieldMetadataId?: (Scalars['UUID'] | null),imageIdentifierFieldMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),isSearchable?: (Scalars['Boolean'] | null)}
|
||||
export interface UpdateObjectPayload {labelSingular?: (Scalars['String'] | null),labelPlural?: (Scalars['String'] | null),nameSingular?: (Scalars['String'] | null),namePlural?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),color?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),labelIdentifierFieldMetadataId?: (Scalars['UUID'] | null),imageIdentifierFieldMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface UpdateViewFieldInput {
|
||||
/** The id of the view field to update */
|
||||
|
||||
@@ -10303,9 +10303,6 @@ export default {
|
||||
"isLabelSyncedWithName": [
|
||||
6
|
||||
],
|
||||
"isSearchable": [
|
||||
6
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
@@ -5326,7 +5326,6 @@ export type UpdateObjectPayload = {
|
||||
imageIdentifierFieldMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
isActive?: InputMaybe<Scalars['Boolean']>;
|
||||
isLabelSyncedWithName?: InputMaybe<Scalars['Boolean']>;
|
||||
isSearchable?: InputMaybe<Scalars['Boolean']>;
|
||||
labelIdentifierFieldMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
labelPlural?: InputMaybe<Scalars['String']>;
|
||||
labelSingular?: InputMaybe<Scalars['String']>;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SEARCH_QUERY } from '@/command-menu/graphql/queries/search';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
type SearchQuery,
|
||||
@@ -19,10 +19,15 @@ export const useMentionSearch = () => {
|
||||
|
||||
const searchableObjectMetadataItems = useMemo(
|
||||
() =>
|
||||
filterReadableActiveObjectMetadataItems(
|
||||
activeObjectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
).filter((item) => !item.isSystem && item.isSearchable),
|
||||
activeObjectMetadataItems.filter(
|
||||
(item) =>
|
||||
!item.isSystem &&
|
||||
item.isSearchable &&
|
||||
getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: item.id,
|
||||
}).canReadObjectRecords === true,
|
||||
),
|
||||
[activeObjectMetadataItems, objectPermissionsByObjectMetadataId],
|
||||
);
|
||||
|
||||
|
||||
+19
-32
@@ -1,19 +1,18 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { SidePanelObjectFilterDropdown } from '@/side-panel/components/SidePanelObjectFilterDropdown';
|
||||
import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState';
|
||||
import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { SidePanelNewSidebarItemRecordItem } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordItem';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { SearchDocument } from '~/generated/graphql';
|
||||
@@ -28,30 +27,21 @@ type SearchRecordBase = {
|
||||
export const SidePanelNewSidebarItemRecordSubPage = () => {
|
||||
const { t } = useLingui();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const [recordSearchInput, setRecordSearchInput] = useState('');
|
||||
const [deferredRecordSearchInput] = useDebounce(recordSearchInput, 300);
|
||||
const coreClient = useApolloCoreClient();
|
||||
const { readableObjectMetadataItems } = useReadableObjectMetadataItems();
|
||||
const [selectedObjectNameSingular, setSelectedObjectNameSingular] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const sidePanelShowHiddenObjects = useAtomStateValue(
|
||||
sidePanelShowHiddenObjectsState,
|
||||
);
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const includedObjectNameSingulars = useMemo(() => {
|
||||
if (isDefined(selectedObjectNameSingular)) {
|
||||
return [selectedObjectNameSingular];
|
||||
}
|
||||
|
||||
return readableObjectMetadataItems
|
||||
.filter((item) => sidePanelShowHiddenObjects || item.isSearchable)
|
||||
.map((item) => item.nameSingular);
|
||||
}, [
|
||||
readableObjectMetadataItems,
|
||||
selectedObjectNameSingular,
|
||||
sidePanelShowHiddenObjects,
|
||||
]);
|
||||
const nonReadableObjectMetadataItemsNameSingular = objectMetadataItems
|
||||
.filter(
|
||||
(objectMetadataItem) =>
|
||||
!getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
})?.canReadObjectRecords,
|
||||
)
|
||||
.map((objectMetadataItem) => objectMetadataItem.nameSingular);
|
||||
|
||||
const { data: searchData, loading: recordSearchLoading } = useQuery(
|
||||
SearchDocument,
|
||||
@@ -60,7 +50,10 @@ export const SidePanelNewSidebarItemRecordSubPage = () => {
|
||||
variables: {
|
||||
searchInput: deferredRecordSearchInput ?? '',
|
||||
limit: MAX_SEARCH_RESULTS,
|
||||
includedObjectNameSingulars,
|
||||
excludedObjectNameSingulars: [
|
||||
'workspaceMember',
|
||||
...nonReadableObjectMetadataItemsNameSingular,
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -91,12 +84,6 @@ export const SidePanelNewSidebarItemRecordSubPage = () => {
|
||||
searchPlaceholder={t`Search records...`}
|
||||
searchValue={recordSearchInput}
|
||||
onSearchChange={setRecordSearchInput}
|
||||
rightElement={
|
||||
<SidePanelObjectFilterDropdown
|
||||
selectedObjectNameSingular={selectedObjectNameSingular}
|
||||
onSelectObject={setSelectedObjectNameSingular}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SidePanelAddToNavigationDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
|
||||
@@ -2,8 +2,8 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState';
|
||||
import { type ObjectPathInfo } from '@/navigation/types/ObjectPathInfo';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
@@ -17,26 +17,29 @@ export const useDefaultHomePagePath = () => {
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
const { alphaSortedActiveNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const readableNonSystemObjectMetadataItems = useMemo(
|
||||
() =>
|
||||
filterReadableActiveObjectMetadataItems(
|
||||
activeObjectMetadataItems,
|
||||
const readableAlphaSortedActiveNonSystemObjectMetadataItems = useMemo(() => {
|
||||
return alphaSortedActiveNonSystemObjectMetadataItems.filter((item) => {
|
||||
const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
)
|
||||
.filter((item) => !item.isSystem)
|
||||
.sort((a, b) => a.nameSingular.localeCompare(b.nameSingular)),
|
||||
[activeObjectMetadataItems, objectPermissionsByObjectMetadataId],
|
||||
);
|
||||
objectMetadataId: item.id,
|
||||
});
|
||||
return objectPermissions?.canReadObjectRecords;
|
||||
});
|
||||
}, [
|
||||
alphaSortedActiveNonSystemObjectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
]);
|
||||
|
||||
const getActiveObjectMetadataItemMatchingId = useCallback(
|
||||
(objectMetadataId: string) => {
|
||||
return readableNonSystemObjectMetadataItems.find(
|
||||
return readableAlphaSortedActiveNonSystemObjectMetadataItems.find(
|
||||
(item) => item.id === objectMetadataId,
|
||||
);
|
||||
},
|
||||
[readableNonSystemObjectMetadataItems],
|
||||
[readableAlphaSortedActiveNonSystemObjectMetadataItems],
|
||||
);
|
||||
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
@@ -51,7 +54,8 @@ export const useDefaultHomePagePath = () => {
|
||||
);
|
||||
|
||||
const firstObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {
|
||||
const [firstObjectMetadataItem] = readableNonSystemObjectMetadataItems;
|
||||
const [firstObjectMetadataItem] =
|
||||
readableAlphaSortedActiveNonSystemObjectMetadataItems;
|
||||
|
||||
if (!isDefined(firstObjectMetadataItem)) {
|
||||
return null;
|
||||
@@ -60,7 +64,7 @@ export const useDefaultHomePagePath = () => {
|
||||
const view = getFirstView(firstObjectMetadataItem?.id);
|
||||
|
||||
return { objectMetadataItem: firstObjectMetadataItem, view };
|
||||
}, [getFirstView, readableNonSystemObjectMetadataItems]);
|
||||
}, [getFirstView, readableAlphaSortedActiveNonSystemObjectMetadataItems]);
|
||||
|
||||
const getDefaultObjectPathInfo = useCallback(() => {
|
||||
const lastVisitedObjectMetadataItemId = store.get(
|
||||
@@ -93,7 +97,7 @@ export const useDefaultHomePagePath = () => {
|
||||
return AppPath.SignInUp;
|
||||
}
|
||||
|
||||
if (isEmpty(readableNonSystemObjectMetadataItems)) {
|
||||
if (isEmpty(readableAlphaSortedActiveNonSystemObjectMetadataItems)) {
|
||||
return getSettingsPath(SettingsPath.ProfilePage);
|
||||
}
|
||||
|
||||
@@ -114,7 +118,7 @@ export const useDefaultHomePagePath = () => {
|
||||
}, [
|
||||
currentUser,
|
||||
getDefaultObjectPathInfo,
|
||||
readableNonSystemObjectMetadataItems,
|
||||
readableAlphaSortedActiveNonSystemObjectMetadataItems,
|
||||
]);
|
||||
|
||||
return { defaultHomePagePath };
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
|
||||
export const useReadableObjectMetadataItems = () => {
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const readableObjectMetadataItems = useMemo(
|
||||
() =>
|
||||
filterReadableActiveObjectMetadataItems(
|
||||
activeObjectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
),
|
||||
[activeObjectMetadataItems, objectPermissionsByObjectMetadataId],
|
||||
);
|
||||
|
||||
return { readableObjectMetadataItems };
|
||||
};
|
||||
-16
@@ -3,9 +3,7 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/Enriche
|
||||
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
|
||||
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
|
||||
import { SettingsUpdateDataModelObjectAboutForm } from '@/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm';
|
||||
import { SettingsObjectSearchSection } from '@/settings/data-model/object-details/components/tabs/SettingsObjectSearchSection';
|
||||
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
@@ -116,20 +114,6 @@ export const ObjectSettings = ({
|
||||
/>
|
||||
</Section>
|
||||
</StyledFormSectionContainer>
|
||||
<AdvancedSettingsWrapper>
|
||||
<StyledFormSectionContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Search`}
|
||||
description={t`Configure how this object appears in search results`}
|
||||
/>
|
||||
<SettingsObjectSearchSection
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
</Section>
|
||||
</StyledFormSectionContainer>
|
||||
</AdvancedSettingsWrapper>
|
||||
{!isReadOnly && (
|
||||
<StyledFormSectionContainer>
|
||||
<Section>
|
||||
|
||||
-194
@@ -1,194 +0,0 @@
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { SEARCH_VECTOR_FIELD_NAME } from '@/object-record/constants/SearchVectorFieldName';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { SettingsObjectFieldDataType } from '@/settings/data-model/object-details/components/SettingsObjectFieldDataType';
|
||||
import { type SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
|
||||
import { IconEye, IconSearch, useIcons } from 'twenty-ui/display';
|
||||
import { Card } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type SettingsObjectSearchSectionProps = {
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
isReadOnly: boolean;
|
||||
};
|
||||
|
||||
type IndexedFieldEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string | null;
|
||||
weight: number;
|
||||
fieldType: string;
|
||||
};
|
||||
|
||||
const StyledSearchSectionContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledNameLabel = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const INDEXED_FIELDS_GRID_TEMPLATE_COLUMNS = 'minmax(0, 1fr) 100px 148px';
|
||||
|
||||
// TODO: This is very DIRTY ; let's migrate searchVector to be proper tables
|
||||
// Already tracked here: https://github.com/twentyhq/core-team-issues/issues/1428
|
||||
const extractIndexedFields = (
|
||||
objectMetadataItem: EnrichedObjectMetadataItem,
|
||||
): IndexedFieldEntry[] => {
|
||||
const searchVectorField = objectMetadataItem.fields.find(
|
||||
(field) => field.name === SEARCH_VECTOR_FIELD_NAME,
|
||||
);
|
||||
|
||||
const asExpression = (
|
||||
searchVectorField?.settings as { asExpression?: string } | null
|
||||
)?.asExpression;
|
||||
|
||||
if (!asExpression) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const columnNames = [
|
||||
...new Set(
|
||||
Array.from(asExpression.matchAll(/"([^"]+)"/g), (match) => match[1]),
|
||||
),
|
||||
];
|
||||
|
||||
const seenFieldIds = new Set<string>();
|
||||
const entries: IndexedFieldEntry[] = [];
|
||||
|
||||
for (const columnName of columnNames) {
|
||||
const field = objectMetadataItem.fields.find(
|
||||
(fieldItem) =>
|
||||
fieldItem.name === columnName || columnName.startsWith(fieldItem.name),
|
||||
);
|
||||
|
||||
if (
|
||||
field &&
|
||||
field.name !== SEARCH_VECTOR_FIELD_NAME &&
|
||||
!seenFieldIds.has(field.id)
|
||||
) {
|
||||
seenFieldIds.add(field.id);
|
||||
|
||||
entries.push({
|
||||
id: field.id,
|
||||
label: field.label,
|
||||
icon: field.icon,
|
||||
weight: 1,
|
||||
fieldType: field.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
export const SettingsObjectSearchSection = ({
|
||||
objectMetadataItem,
|
||||
isReadOnly,
|
||||
}: SettingsObjectSearchSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
|
||||
const [isSearchable, setIsSearchable] = useState(
|
||||
objectMetadataItem.isSearchable,
|
||||
);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const indexedFields = useMemo(
|
||||
() => extractIndexedFields(objectMetadataItem),
|
||||
[objectMetadataItem],
|
||||
);
|
||||
|
||||
const filteredIndexedFields = searchTerm
|
||||
? indexedFields.filter((entry) =>
|
||||
entry.label.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
: indexedFields;
|
||||
|
||||
const handleToggleSearchable = async (value: boolean) => {
|
||||
setIsSearchable(value);
|
||||
await updateOneObjectMetadataItem({
|
||||
idToUpdate: objectMetadataItem.id,
|
||||
updatePayload: { isSearchable: value },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSearchSectionContent>
|
||||
{!isReadOnly && (
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconEye}
|
||||
title={t`Include in default search`}
|
||||
description={t`If disabled, use advanced search filters to find these records`}
|
||||
checked={isSearchable}
|
||||
advancedMode
|
||||
onChange={handleToggleSearchable}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{indexedFields.length > 0 && (
|
||||
<>
|
||||
<SettingsTextInput
|
||||
instanceId="indexed-fields-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search across indexed fields...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
<Table>
|
||||
<TableRow
|
||||
gridTemplateColumns={INDEXED_FIELDS_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Weight`}</TableHeader>
|
||||
<TableHeader>{t`Data type`}</TableHeader>
|
||||
</TableRow>
|
||||
{filteredIndexedFields.map((entry) => {
|
||||
const FieldIcon = getIcon(entry.icon);
|
||||
return (
|
||||
<TableRow
|
||||
key={entry.id}
|
||||
gridTemplateColumns={INDEXED_FIELDS_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableCell
|
||||
color={theme.font.color.primary}
|
||||
gap={theme.spacing[2]}
|
||||
>
|
||||
<FieldIcon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
<StyledNameLabel>{entry.label}</StyledNameLabel>
|
||||
</TableCell>
|
||||
<TableCell>{entry.weight}</TableCell>
|
||||
<TableCell>
|
||||
<SettingsObjectFieldDataType
|
||||
value={entry.fieldType as SettingsFieldType}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</StyledSearchSectionContent>
|
||||
);
|
||||
};
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFilter } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
|
||||
import { SidePanelObjectFilterDropdownContent } from '@/side-panel/components/SidePanelObjectFilterDropdownContent';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
|
||||
export const OBJECT_FILTER_DROPDOWN_ID = 'side-panel-object-filter-dropdown';
|
||||
|
||||
type SidePanelObjectFilterDropdownProps = {
|
||||
selectedObjectNameSingular: string | null;
|
||||
onSelectObject: (objectNameSingular: string | null) => void;
|
||||
};
|
||||
|
||||
export const SidePanelObjectFilterDropdown = ({
|
||||
selectedObjectNameSingular,
|
||||
onSelectObject,
|
||||
}: SidePanelObjectFilterDropdownProps) => {
|
||||
const { t } = useLingui();
|
||||
const isFilterActive = isDefined(selectedObjectNameSingular);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={OBJECT_FILTER_DROPDOWN_ID}
|
||||
dropdownPlacement="bottom-end"
|
||||
clickableComponent={
|
||||
<IconButton
|
||||
Icon={IconFilter}
|
||||
variant="tertiary"
|
||||
accent={isFilterActive ? 'blue' : 'default'}
|
||||
size="small"
|
||||
ariaLabel={t`Filter by object type`}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<SidePanelObjectFilterDropdownContent
|
||||
selectedObjectNameSingular={selectedObjectNameSingular}
|
||||
onSelectObject={onSelectObject}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS } from 'twenty-shared/constants';
|
||||
import { IconCube, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemSelectAvatar, MenuItemToggle } from 'twenty-ui/navigation';
|
||||
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemStyleIcon';
|
||||
import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems';
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/side-panel/components/SidePanelObjectFilterDropdown';
|
||||
import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
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 { 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 { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
|
||||
const ALL_OBJECTS_ITEM_ID = 'all-objects';
|
||||
|
||||
type SidePanelObjectFilterDropdownContentProps = {
|
||||
selectedObjectNameSingular: string | null;
|
||||
onSelectObject: (objectNameSingular: string | null) => void;
|
||||
};
|
||||
|
||||
export const SidePanelObjectFilterDropdownContent = ({
|
||||
selectedObjectNameSingular,
|
||||
onSelectObject,
|
||||
}: SidePanelObjectFilterDropdownContentProps) => {
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const [filterSearch, setFilterSearch] = useState('');
|
||||
const [sidePanelShowHiddenObjects, setSidePanelShowHiddenObjects] =
|
||||
useAtomState(sidePanelShowHiddenObjectsState);
|
||||
const { readableObjectMetadataItems } = useReadableObjectMetadataItems();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const searchFilter = filterSearch.toLowerCase();
|
||||
|
||||
const displayedObjects = readableObjectMetadataItems.filter((item) => {
|
||||
if (
|
||||
OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS.includes(
|
||||
item.nameSingular as (typeof OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS)[number],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sidePanelShowHiddenObjects && !item.isSearchable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.labelPlural.toLowerCase().includes(searchFilter);
|
||||
});
|
||||
|
||||
const handleSelect = (objectNameSingular: string | null) => {
|
||||
onSelectObject(objectNameSingular);
|
||||
closeDropdown(OBJECT_FILTER_DROPDOWN_ID);
|
||||
};
|
||||
|
||||
const selectableItemIdArray = [
|
||||
ALL_OBJECTS_ITEM_ID,
|
||||
...displayedObjects.map((item) => item.nameSingular),
|
||||
];
|
||||
|
||||
const selectedItemId = useAtomComponentStateValue(
|
||||
selectedItemIdComponentState,
|
||||
OBJECT_FILTER_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownContent>
|
||||
<DropdownMenuHeader>{t`Object`}</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
value={filterSearch}
|
||||
onChange={(event) => setFilterSearch(event.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<SelectableList
|
||||
selectableListInstanceId={OBJECT_FILTER_DROPDOWN_ID}
|
||||
focusId={OBJECT_FILTER_DROPDOWN_ID}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableListItem
|
||||
itemId={ALL_OBJECTS_ITEM_ID}
|
||||
onEnter={() => handleSelect(null)}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
avatar={
|
||||
<NavigationMenuItemStyleIcon Icon={IconCube} color="gray" />
|
||||
}
|
||||
text={t`All objects`}
|
||||
selected={selectedObjectNameSingular === null}
|
||||
onClick={() => handleSelect(null)}
|
||||
focused={selectedItemId === ALL_OBJECTS_ITEM_ID}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
{displayedObjects.map((objectMetadataItem) => {
|
||||
const ObjectIcon = getIcon(objectMetadataItem.icon);
|
||||
const iconColor = getObjectColorWithFallback(objectMetadataItem);
|
||||
|
||||
return (
|
||||
<SelectableListItem
|
||||
key={objectMetadataItem.id}
|
||||
itemId={objectMetadataItem.nameSingular}
|
||||
onEnter={() => handleSelect(objectMetadataItem.nameSingular)}
|
||||
>
|
||||
<MenuItemSelectAvatar
|
||||
avatar={
|
||||
<NavigationMenuItemStyleIcon
|
||||
Icon={ObjectIcon}
|
||||
color={iconColor}
|
||||
/>
|
||||
}
|
||||
text={objectMetadataItem.labelPlural}
|
||||
selected={
|
||||
selectedObjectNameSingular ===
|
||||
objectMetadataItem.nameSingular
|
||||
}
|
||||
onClick={() => handleSelect(objectMetadataItem.nameSingular)}
|
||||
focused={selectedItemId === objectMetadataItem.nameSingular}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconCube}
|
||||
onToggleChange={() =>
|
||||
setSidePanelShowHiddenObjects(!sidePanelShowHiddenObjects)
|
||||
}
|
||||
toggled={sidePanelShowHiddenObjects}
|
||||
text={t`Show hidden objects`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
@@ -53,7 +53,6 @@ type SidePanelSubViewWithSearchProps = {
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
searchInputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||
rightElement?: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
@@ -62,7 +61,6 @@ export const SidePanelSubViewWithSearch = ({
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
searchInputProps,
|
||||
rightElement,
|
||||
children,
|
||||
}: SidePanelSubViewWithSearchProps) => (
|
||||
<StyledSubViewContainer>
|
||||
@@ -75,7 +73,6 @@ export const SidePanelSubViewWithSearch = ({
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...searchInputProps}
|
||||
/>
|
||||
{rightElement}
|
||||
</StyledSearchContainer>
|
||||
{children != null && (
|
||||
<StyledScrollableListWrapper>{children}</StyledScrollableListWrapper>
|
||||
|
||||
+18
-18
@@ -1,17 +1,16 @@
|
||||
import { useSwitchToNewAIChat } from '@/ai/hooks/useSwitchToNewAIChat';
|
||||
import { SidePanelObjectFilterDropdown } from '@/side-panel/components/SidePanelObjectFilterDropdown';
|
||||
import { useOpenAskAIPageInSidePanel } from '@/side-panel/hooks/useOpenAskAIPageInSidePanel';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconEdit } from 'twenty-ui/display';
|
||||
import { IconEdit, IconSparkles } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useSwitchToNewAIChat } from '@/ai/hooks/useSwitchToNewAIChat';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledIconButtonContainer = styled.div`
|
||||
@@ -22,19 +21,11 @@ export const SidePanelTopBarRightCornerIcon = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
const { openAskAIPage } = useOpenAskAIPageInSidePanel();
|
||||
const { switchToNewChat } = useSwitchToNewAIChat();
|
||||
const [sidePanelSearchObjectFilter, setSidePanelSearchObjectFilter] =
|
||||
useAtomState(sidePanelSearchObjectFilterState);
|
||||
|
||||
const isOnSearchPage = sidePanelPage === SidePanelPages.SearchRecords;
|
||||
|
||||
if (isOnSearchPage) {
|
||||
return (
|
||||
<SidePanelObjectFilterDropdown
|
||||
selectedObjectNameSingular={sidePanelSearchObjectFilter}
|
||||
onSelectObject={setSidePanelSearchObjectFilter}
|
||||
/>
|
||||
);
|
||||
if (isMobile || !isAiEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOnAskAIPage = [
|
||||
@@ -42,8 +33,17 @@ export const SidePanelTopBarRightCornerIcon = () => {
|
||||
SidePanelPages.ViewPreviousAIChats,
|
||||
].includes(sidePanelPage);
|
||||
|
||||
if (isMobile || !isAiEnabled || !isOnAskAIPage) {
|
||||
return null;
|
||||
if (!isOnAskAIPage) {
|
||||
return (
|
||||
<StyledIconButtonContainer>
|
||||
<IconButton
|
||||
onClick={() => openAskAIPage({ resetNavigationStack: false })}
|
||||
Icon={IconSparkles}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
</StyledIconButtonContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
-4
@@ -7,9 +7,7 @@ import { sidePanelNavigationMorphItemsByPageState } from '@/side-panel/states/si
|
||||
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState';
|
||||
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
|
||||
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
@@ -106,8 +104,6 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
|
||||
});
|
||||
store.set(isSidePanelOpenedState.atom, false);
|
||||
store.set(sidePanelSearchState.atom, '');
|
||||
store.set(sidePanelSearchObjectFilterState.atom, null);
|
||||
store.set(sidePanelShowHiddenObjectsState.atom, false);
|
||||
store.set(sidePanelNavigationMorphItemsByPageState.atom, new Map());
|
||||
store.set(sidePanelNavigationStackState.atom, []);
|
||||
resetSelectedItem();
|
||||
|
||||
@@ -5,7 +5,6 @@ import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
|
||||
import { emitSidePanelOpenEvent } from '@/ui/layout/side-panel/utils/emitSidePanelOpenEvent';
|
||||
@@ -81,7 +80,6 @@ export const useSidePanelMenu = () => {
|
||||
const isSidePanelOpened = store.get(isSidePanelOpenedState.atom);
|
||||
|
||||
store.set(sidePanelSearchState.atom, '');
|
||||
store.set(sidePanelSearchObjectFilterState.atom, null);
|
||||
|
||||
if (isSidePanelOpened) {
|
||||
closeSidePanelMenu();
|
||||
|
||||
+23
-28
@@ -3,17 +3,16 @@ import { CommandLink } from '@/command-menu-item/display/components/CommandLink'
|
||||
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
|
||||
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
|
||||
import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems';
|
||||
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
|
||||
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular, AppPath } from 'twenty-shared/types';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
@@ -21,37 +20,34 @@ import { SearchDocument } from '~/generated/graphql';
|
||||
|
||||
export const useSidePanelSearchRecords = () => {
|
||||
const sidePanelSearch = useAtomStateValue(sidePanelSearchState);
|
||||
const sidePanelSearchObjectFilter = useAtomStateValue(
|
||||
sidePanelSearchObjectFilterState,
|
||||
);
|
||||
const sidePanelShowHiddenObjects = useAtomStateValue(
|
||||
sidePanelShowHiddenObjectsState,
|
||||
);
|
||||
const coreClient = useApolloCoreClient();
|
||||
|
||||
const [deferredSidePanelSearch] = useDebounce(sidePanelSearch, 300);
|
||||
const { readableObjectMetadataItems } = useReadableObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const includedObjectNameSingulars = useMemo(() => {
|
||||
if (isDefined(sidePanelSearchObjectFilter)) {
|
||||
return [sidePanelSearchObjectFilter];
|
||||
}
|
||||
const nonReadableObjectMetadataItemsNameSingular = useMemo(() => {
|
||||
return Object.values(objectMetadataItems)
|
||||
.filter((objectMetadataItem) => {
|
||||
const objectPermission = getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
return readableObjectMetadataItems
|
||||
.filter((item) => sidePanelShowHiddenObjects || item.isSearchable)
|
||||
.map((item) => item.nameSingular);
|
||||
}, [
|
||||
readableObjectMetadataItems,
|
||||
sidePanelSearchObjectFilter,
|
||||
sidePanelShowHiddenObjects,
|
||||
]);
|
||||
return !objectPermission?.canReadObjectRecords;
|
||||
})
|
||||
.map((objectMetadataItem) => objectMetadataItem.nameSingular);
|
||||
}, [objectMetadataItems, objectPermissionsByObjectMetadataId]);
|
||||
|
||||
const { data: searchData, loading } = useQuery(SearchDocument, {
|
||||
client: coreClient,
|
||||
variables: {
|
||||
searchInput: deferredSidePanelSearch ?? '',
|
||||
limit: MAX_SEARCH_RESULTS,
|
||||
includedObjectNameSingulars,
|
||||
excludedObjectNameSingulars: [
|
||||
'workspaceMember',
|
||||
...nonReadableObjectMetadataItemsNameSingular,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -81,7 +77,7 @@ export const useSidePanelSearchRecords = () => {
|
||||
),
|
||||
shouldBeRegistered: () => true,
|
||||
description:
|
||||
readableObjectMetadataItems.find(
|
||||
objectMetadataItems.find(
|
||||
(item) => item.nameSingular === searchRecord.objectNameSingular,
|
||||
)?.labelSingular ?? searchRecord.objectNameSingular,
|
||||
};
|
||||
@@ -96,8 +92,7 @@ export const useSidePanelSearchRecords = () => {
|
||||
component: (
|
||||
<Command
|
||||
onClick={() => {
|
||||
searchRecord.objectNameSingular ===
|
||||
CoreObjectNameSingular.Task
|
||||
searchRecord.objectNameSingular === 'task'
|
||||
? openRecordInSidePanel({
|
||||
recordId: searchRecord.recordId,
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
@@ -127,7 +122,7 @@ export const useSidePanelSearchRecords = () => {
|
||||
};
|
||||
},
|
||||
);
|
||||
}, [searchData, openRecordInSidePanel, readableObjectMetadataItems]);
|
||||
}, [searchData, openRecordInSidePanel, objectMetadataItems]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const sidePanelSearchObjectFilterState = createAtomState<string | null>({
|
||||
key: 'side-panel/sidePanelSearchObjectFilterState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const sidePanelShowHiddenObjectsState = createAtomState<boolean>({
|
||||
key: 'side-panel/sidePanelShowHiddenObjectsState',
|
||||
defaultValue: false,
|
||||
});
|
||||
+14
@@ -1,4 +1,6 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery';
|
||||
import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep';
|
||||
import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
@@ -13,6 +15,11 @@ export const useDeleteWorkflowVersionStep = () => {
|
||||
|
||||
const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache();
|
||||
|
||||
const { findOneRecordQuery: findOneWorkflowVersionQuery } =
|
||||
useFindOneRecordQuery({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const [mutate] = useMutation<
|
||||
DeleteWorkflowVersionStepMutation,
|
||||
DeleteWorkflowVersionStepMutationVariables
|
||||
@@ -25,6 +32,13 @@ export const useDeleteWorkflowVersionStep = () => {
|
||||
) => {
|
||||
const result = await mutate({
|
||||
variables: { input },
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [
|
||||
{
|
||||
query: findOneWorkflowVersionQuery,
|
||||
variables: { objectRecordId: input.workflowVersionId },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const workflowVersionStepChanges = result?.data?.deleteWorkflowVersionStep;
|
||||
|
||||
+3
-3
@@ -233,9 +233,9 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{apiKey?.name && (
|
||||
{isDefined(apiKey) && (
|
||||
<SubMenuTopBarContainer
|
||||
title={apiKey?.name}
|
||||
title={apiKey.name || t`Unnamed API Key`}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
@@ -245,7 +245,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
children: t`APIs & Webhooks`,
|
||||
href: getSettingsPath(SettingsPath.ApiWebhooks),
|
||||
},
|
||||
{ children: apiKey?.name },
|
||||
{ children: apiKey.name || t`Unnamed API Key` },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
|
||||
+5
-1
@@ -75,6 +75,10 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
formValues.expirationDate ?? 30,
|
||||
).toISOString();
|
||||
|
||||
if (!formValues.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleIdToUse = formValues.roleId;
|
||||
|
||||
if (!roleIdToUse) {
|
||||
@@ -137,7 +141,7 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
]}
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!isDefined(canSave)}
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => {
|
||||
navigateSettings(SettingsPath.ApiWebhooks);
|
||||
}}
|
||||
|
||||
+47
-70
@@ -10,7 +10,6 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
@@ -77,28 +76,20 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const standardCommandMenuItems =
|
||||
await this.computeStandardCommandMenuItemsToCreate(
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
);
|
||||
await this.computeStandardCommandMenuItemsToCreate(workspaceId);
|
||||
|
||||
const triggerWorkflowVersionCommandMenuItems =
|
||||
await this.computeTriggerWorkflowVersionCommandMenuItemsToCreate(
|
||||
workspaceId,
|
||||
workspaceCustomFlatApplication,
|
||||
);
|
||||
|
||||
const totalCount =
|
||||
standardCommandMenuItems.length +
|
||||
triggerWorkflowVersionCommandMenuItems.length;
|
||||
const allCommandMenuItemsToCreate = [
|
||||
...standardCommandMenuItems,
|
||||
...triggerWorkflowVersionCommandMenuItems,
|
||||
];
|
||||
|
||||
if (totalCount === 0) {
|
||||
if (allCommandMenuItemsToCreate.length === 0) {
|
||||
this.logger.log(
|
||||
`No missing command menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
@@ -107,33 +98,46 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${totalCount} missing command menu item(s) for workspace ${workspaceId} (${standardCommandMenuItems.length} standard, ${triggerWorkflowVersionCommandMenuItems.length} trigger workflow version)`,
|
||||
`Found ${allCommandMenuItemsToCreate.length} missing command menu item(s) for workspace ${workspaceId} (${standardCommandMenuItems.length} standard, ${triggerWorkflowVersionCommandMenuItems.length} trigger workflow version)`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create ${totalCount} command menu item(s) for workspace ${workspaceId}`,
|
||||
`[DRY RUN] Would create ${allCommandMenuItemsToCreate.length} command menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (standardCommandMenuItems.length > 0) {
|
||||
await this.createCommandMenuItems({
|
||||
workspaceId,
|
||||
flatCommandMenuItemsToCreate: standardCommandMenuItems,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
});
|
||||
}
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
if (triggerWorkflowVersionCommandMenuItems.length > 0) {
|
||||
await this.createCommandMenuItems({
|
||||
workspaceId,
|
||||
flatCommandMenuItemsToCreate: triggerWorkflowVersionCommandMenuItems,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
});
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: allCommandMenuItemsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to backfill command menu items:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to backfill command menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
@@ -142,14 +146,18 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled ${totalCount} command menu item(s) for workspace ${workspaceId}`,
|
||||
`Successfully backfilled ${allCommandMenuItemsToCreate.length} command menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async computeStandardCommandMenuItemsToCreate(
|
||||
workspaceId: string,
|
||||
twentyStandardFlatApplication: FlatApplication,
|
||||
): Promise<FlatCommandMenuItem[]> {
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
shouldIncludeRecordPageLayouts: true,
|
||||
@@ -187,7 +195,6 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
|
||||
private async computeTriggerWorkflowVersionCommandMenuItemsToCreate(
|
||||
workspaceId: string,
|
||||
workspaceCustomFlatApplication: FlatApplication,
|
||||
): Promise<FlatCommandMenuItem[]> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
@@ -237,6 +244,11 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = [];
|
||||
|
||||
for (const workflowVersion of manualTriggerVersions) {
|
||||
@@ -299,41 +311,6 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
);
|
||||
}
|
||||
|
||||
private async createCommandMenuItems({
|
||||
workspaceId,
|
||||
flatCommandMenuItemsToCreate,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatCommandMenuItemsToCreate: FlatCommandMenuItem[];
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: flatCommandMenuItemsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to backfill command menu items:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to backfill command menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveManualTriggerAvailability(
|
||||
trigger: WorkflowManualTrigger,
|
||||
workspaceId: string,
|
||||
|
||||
@@ -84,24 +84,6 @@ export const mockFlatObjectMetadatas: FlatObjectMetadata[] = [
|
||||
universalIdentifier: 'non-searchable-object-universal-id',
|
||||
applicationId: workspaceId,
|
||||
}),
|
||||
getFlatObjectMetadataMock({
|
||||
id: '20202020-6a7c-4e3f-9b2d-1d8f7a3e5c4b',
|
||||
nameSingular: 'message',
|
||||
namePlural: 'messages',
|
||||
labelSingular: 'Message',
|
||||
labelPlural: 'Messages',
|
||||
description: 'Message',
|
||||
icon: 'IconMessage',
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isSearchable: false,
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
workspaceId,
|
||||
fieldIds: [],
|
||||
universalIdentifier: 'message-universal-id',
|
||||
applicationId: workspaceId,
|
||||
}),
|
||||
];
|
||||
|
||||
export const mockFlatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata> = {
|
||||
|
||||
-18
@@ -65,24 +65,6 @@ describe('SearchService', () => {
|
||||
|
||||
expect(objectMetadataItems).toEqual([mockFlatObjectMetadatas[1]]);
|
||||
});
|
||||
it('should allow non-searchable objects when explicitly included', () => {
|
||||
const objectMetadataItems = service.filterObjectMetadataItems({
|
||||
flatObjectMetadatas: mockFlatObjectMetadatas,
|
||||
includedObjectNameSingulars: ['non-searchable-object'],
|
||||
excludedObjectNameSingulars: [],
|
||||
});
|
||||
|
||||
expect(objectMetadataItems).toEqual([mockFlatObjectMetadatas[3]]);
|
||||
});
|
||||
it('should block objects with channel visibility constraints even when explicitly included', () => {
|
||||
const objectMetadataItems = service.filterObjectMetadataItems({
|
||||
flatObjectMetadatas: mockFlatObjectMetadatas,
|
||||
includedObjectNameSingulars: ['message'],
|
||||
excludedObjectNameSingulars: [],
|
||||
});
|
||||
|
||||
expect(objectMetadataItems).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLabelIdentifierColumns', () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import chunk from 'lodash.chunk';
|
||||
import { OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS } from 'twenty-shared/constants';
|
||||
import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types';
|
||||
import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils';
|
||||
import { Brackets, type ObjectLiteral } from 'typeorm';
|
||||
@@ -139,36 +138,20 @@ export class SearchService {
|
||||
includedObjectNameSingulars: string[];
|
||||
excludedObjectNameSingulars: string[];
|
||||
}) {
|
||||
const hasExplicitInclusion = includedObjectNameSingulars.length > 0;
|
||||
|
||||
return flatObjectMetadatas.filter(
|
||||
({ nameSingular, isSearchable, isActive }) => {
|
||||
if (!isActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasExplicitInclusion) {
|
||||
if (
|
||||
OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS.includes(
|
||||
nameSingular as (typeof OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS)[number],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
includedObjectNameSingulars.includes(nameSingular) &&
|
||||
!excludedObjectNameSingulars.includes(nameSingular)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isSearchable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
return false;
|
||||
}
|
||||
if (excludedObjectNameSingulars.includes(nameSingular)) {
|
||||
return false;
|
||||
}
|
||||
if (includedObjectNameSingulars.length > 0) {
|
||||
return includedObjectNameSingulars.includes(nameSingular);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
-2
@@ -7,7 +7,6 @@ export const FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES = {
|
||||
'icon',
|
||||
'isActive',
|
||||
'isLabelSyncedWithName',
|
||||
'isSearchable',
|
||||
'labelPlural',
|
||||
'labelSingular',
|
||||
'namePlural',
|
||||
@@ -19,7 +18,6 @@ export const FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES = {
|
||||
'description',
|
||||
'icon',
|
||||
'isActive',
|
||||
'isSearchable',
|
||||
'labelPlural',
|
||||
'labelSingular',
|
||||
],
|
||||
|
||||
-5
@@ -76,11 +76,6 @@ export class UpdateObjectPayload {
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isLabelSyncedWithName?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
isSearchable?: boolean;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// TODO: These objects are tied to connected accounts and have channel-based visibility
|
||||
// settings (MessageChannelVisibility / CalendarChannelVisibility) that control what data
|
||||
// workspace members can see. The global search service does not yet enforce these visibility
|
||||
// rules, so we exclude these objects from explicit search inclusion to prevent leaking
|
||||
// restricted content (e.g. message subjects, calendar event titles).
|
||||
// Once the search service properly joins channel tables and applies visibility filtering,
|
||||
// this list can be removed.
|
||||
export const OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS = [
|
||||
'blocklist',
|
||||
'connectedAccount',
|
||||
'message',
|
||||
'messageThread',
|
||||
'messageChannel',
|
||||
'messageParticipant',
|
||||
'messageFolder',
|
||||
'messageChannelMessageAssociation',
|
||||
'messageChannelMessageAssociationMessageFolder',
|
||||
'messageThreadSubscriber',
|
||||
'calendarEvent',
|
||||
'calendarChannel',
|
||||
'calendarChannelEventAssociation',
|
||||
'calendarEventParticipant',
|
||||
] as const;
|
||||
@@ -32,7 +32,6 @@ export { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from './LabelIdentifierFieldMet
|
||||
export { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from './MultiItemFieldDefaultMaxValues';
|
||||
export { MULTI_ITEM_FIELD_MIN_MAX_VALUES } from './MultiItemFieldMinMaxValues';
|
||||
export { MUTATION_MAX_MERGE_RECORDS } from './MutationMaxMergeRecords';
|
||||
export { OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS } from './ObjectsWithChannelVisibilityConstraints';
|
||||
export { PermissionFlagType } from './PermissionFlagType';
|
||||
export { PermissionsOnAllObjectRecords } from './PermissionsOnAllObjectRecords';
|
||||
export { QUERY_DEFAULT_LIMIT_RECORDS } from './QueryDefaultLimitRecords';
|
||||
|
||||
Reference in New Issue
Block a user