feat: add link to relation filtered table in Record Show Page (#3261)
* feat: add link to relation filtered table in Record Show Page Closes #3125 * refactor: use generateFindManyRecordsQuery for optimization * Fixes from review * Minor fixes --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -117,6 +117,7 @@
|
|||||||
"pg-boss": "^9.0.3",
|
"pg-boss": "^9.0.3",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"prism-react-renderer": "^2.1.0",
|
"prism-react-renderer": "^2.1.0",
|
||||||
|
"qs": "^6.11.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-data-grid": "7.0.0-beta.13",
|
"react-data-grid": "7.0.0-beta.13",
|
||||||
"react-datepicker": "^4.11.0",
|
"react-datepicker": "^4.11.0",
|
||||||
|
|||||||
@ -83,12 +83,14 @@ export const useObjectMetadataItem = (
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
const findManyRecordsQuery = useGenerateFindManyRecordsQuery({
|
const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery();
|
||||||
|
const findManyRecordsQuery = generateFindManyRecordsQuery({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
depth,
|
depth,
|
||||||
});
|
});
|
||||||
|
|
||||||
const findOneRecordQuery = useGenerateFindOneRecordQuery({
|
const generateFindOneRecordQuery = useGenerateFindOneRecordQuery();
|
||||||
|
const findOneRecordQuery = generateFindOneRecordQuery({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
depth,
|
depth,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
|||||||
return [...acc, formatFieldMetadataItemAsFilterDefinition({ field })];
|
return [...acc, formatFieldMetadataItemAsFilterDefinition({ field })];
|
||||||
}, [] as FilterDefinition[]);
|
}, [] as FilterDefinition[]);
|
||||||
|
|
||||||
const formatFieldMetadataItemAsFilterDefinition = ({
|
export const formatFieldMetadataItemAsFilterDefinition = ({
|
||||||
field,
|
field,
|
||||||
}: {
|
}: {
|
||||||
field: ObjectMetadataItem['fields'][0];
|
field: ObjectMetadataItem['fields'][0];
|
||||||
|
|||||||
@ -4,16 +4,16 @@ import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMa
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
export const useGenerateFindManyRecordsQuery = ({
|
export const useGenerateFindManyRecordsQuery = () => {
|
||||||
objectMetadataItem,
|
|
||||||
depth,
|
|
||||||
}: {
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
depth?: number;
|
|
||||||
}) => {
|
|
||||||
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
||||||
|
|
||||||
return gql`
|
return ({
|
||||||
|
objectMetadataItem,
|
||||||
|
depth,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
depth?: number;
|
||||||
|
}) => gql`
|
||||||
query FindMany${capitalize(
|
query FindMany${capitalize(
|
||||||
objectMetadataItem.namePlural,
|
objectMetadataItem.namePlural,
|
||||||
)}($filter: ${capitalize(
|
)}($filter: ${capitalize(
|
||||||
|
|||||||
@ -1,34 +1,30 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
||||||
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
export const useGenerateFindOneRecordQuery = ({
|
export const useGenerateFindOneRecordQuery = () => {
|
||||||
objectMetadataItem,
|
|
||||||
depth,
|
|
||||||
}: {
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
depth?: number;
|
|
||||||
}) => {
|
|
||||||
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
||||||
|
|
||||||
if (!objectMetadataItem) {
|
return ({
|
||||||
return EMPTY_QUERY;
|
objectMetadataItem,
|
||||||
}
|
depth,
|
||||||
|
}: {
|
||||||
return gql`
|
objectMetadataItem: Pick<ObjectMetadataItem, 'nameSingular' | 'fields'>;
|
||||||
query FindOne${objectMetadataItem.nameSingular}($objectRecordId: UUID!) {
|
depth?: number;
|
||||||
${objectMetadataItem.nameSingular}(filter: {
|
}) =>
|
||||||
id: {
|
gql`
|
||||||
eq: $objectRecordId
|
query FindOne${objectMetadataItem.nameSingular}($objectRecordId: UUID!) {
|
||||||
|
${objectMetadataItem.nameSingular}(filter: {
|
||||||
|
id: {
|
||||||
|
eq: $objectRecordId
|
||||||
|
}
|
||||||
|
}){
|
||||||
|
id
|
||||||
|
${objectMetadataItem.fields
|
||||||
|
.map((field) => mapFieldMetadataToGraphQLQuery(field, depth))
|
||||||
|
.join('\n')}
|
||||||
}
|
}
|
||||||
}){
|
|
||||||
id
|
|
||||||
${objectMetadataItem.fields
|
|
||||||
.map((field) => mapFieldMetadataToGraphQLQuery(field, depth))
|
|
||||||
.join('\n')}
|
|
||||||
}
|
}
|
||||||
}
|
`;
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import qs from 'qs';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
|
||||||
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
|
||||||
import { usePersistField } from '@/object-record/field/hooks/usePersistField';
|
import { usePersistField } from '@/object-record/field/hooks/usePersistField';
|
||||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||||
@ -23,7 +26,10 @@ import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
|||||||
import { Card } from '@/ui/layout/card/components/Card';
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { FilterQueryParams } from '@/views/hooks/internal/useFiltersFromQueryParams';
|
||||||
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
|
||||||
const StyledAddDropdown = styled(Dropdown)`
|
const StyledAddDropdown = styled(Dropdown)`
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@ -54,8 +60,23 @@ const StyledHeader = styled.header<{ isDropdownOpen?: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTitle = styled.div`
|
const StyledTitle = styled.div`
|
||||||
|
align-items: flex-end;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitleLabel = styled.div`
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
padding: ${({ theme }) => theme.spacing(0, 1)};
|
`;
|
||||||
|
|
||||||
|
const StyledLink = styled(Link)`
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordRelationFieldCardSection = () => {
|
export const RecordRelationFieldCardSection = () => {
|
||||||
@ -191,33 +212,54 @@ export const RecordRelationFieldCardSection = () => {
|
|||||||
|
|
||||||
if (!relationLabelIdentifierFieldMetadata) return null;
|
if (!relationLabelIdentifierFieldMetadata) return null;
|
||||||
|
|
||||||
|
const filterQueryParams: FilterQueryParams = {
|
||||||
|
filter: {
|
||||||
|
[relationFieldMetadataItem?.name || '']: {
|
||||||
|
[ViewFilterOperand.Is]: [entityId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const filterLinkHref = `/objects/${
|
||||||
|
relationObjectMetadataItem.namePlural
|
||||||
|
}?${qs.stringify(filterQueryParams)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
<StyledHeader isDropdownOpen={isDropdownOpen}>
|
<StyledHeader isDropdownOpen={isDropdownOpen}>
|
||||||
<StyledTitle>{fieldDefinition.label}</StyledTitle>
|
<StyledTitle>
|
||||||
<StyledAddDropdown
|
<StyledTitleLabel>{fieldDefinition.label}</StyledTitleLabel>
|
||||||
dropdownId={dropdownId}
|
{parseFieldRelationType(relationFieldMetadataItem) ===
|
||||||
dropdownPlacement="right-start"
|
'TO_ONE_OBJECT' && (
|
||||||
onClose={handleCloseRelationPickerDropdown}
|
<StyledLink to={filterLinkHref}>
|
||||||
clickableComponent={
|
All ({relationRecords.length})
|
||||||
<LightIconButton
|
</StyledLink>
|
||||||
className="displayOnHover"
|
)}
|
||||||
Icon={IconPlus}
|
</StyledTitle>
|
||||||
accent="tertiary"
|
<DropdownScope dropdownScopeId={dropdownId}>
|
||||||
/>
|
<StyledAddDropdown
|
||||||
}
|
dropdownId={dropdownId}
|
||||||
dropdownComponents={
|
dropdownPlacement="right-start"
|
||||||
<SingleEntitySelectMenuItemsWithSearch
|
onClose={handleCloseRelationPickerDropdown}
|
||||||
EmptyIcon={IconForbid}
|
clickableComponent={
|
||||||
entitiesToSelect={entities.entitiesToSelect}
|
<LightIconButton
|
||||||
loading={entities.loading}
|
className="displayOnHover"
|
||||||
onEntitySelected={handleRelationPickerEntitySelected}
|
Icon={IconPlus}
|
||||||
/>
|
accent="tertiary"
|
||||||
}
|
/>
|
||||||
dropdownHotkeyScope={{
|
}
|
||||||
scope: dropdownId,
|
dropdownComponents={
|
||||||
}}
|
<SingleEntitySelectMenuItemsWithSearch
|
||||||
/>
|
EmptyIcon={IconForbid}
|
||||||
|
entitiesToSelect={entities.entitiesToSelect}
|
||||||
|
loading={entities.loading}
|
||||||
|
onEntitySelected={handleRelationPickerEntitySelected}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownHotkeyScope={{
|
||||||
|
scope: dropdownId,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownScope>
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
{!!relationRecords.length && (
|
{!!relationRecords.length && (
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { useFiltersFromQueryParams } from '@/views/hooks/internal/useFiltersFromQueryParams';
|
||||||
|
import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates';
|
||||||
|
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||||
|
|
||||||
|
export const FilterQueryParamsEffect = () => {
|
||||||
|
const { hasFiltersQueryParams, getFiltersFromQueryParams } =
|
||||||
|
useFiltersFromQueryParams();
|
||||||
|
const { currentViewFiltersState, onViewFiltersChangeState } =
|
||||||
|
useViewScopedStates();
|
||||||
|
const setCurrentViewFilters = useSetRecoilState(currentViewFiltersState);
|
||||||
|
const onViewFiltersChange = useRecoilValue(onViewFiltersChangeState);
|
||||||
|
const { resetViewBar } = useViewBar();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasFiltersQueryParams) return;
|
||||||
|
|
||||||
|
getFiltersFromQueryParams().then((filtersFromParams) => {
|
||||||
|
if (Array.isArray(filtersFromParams)) {
|
||||||
|
setCurrentViewFilters(filtersFromParams);
|
||||||
|
onViewFiltersChange?.(filtersFromParams);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resetViewBar();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
getFiltersFromQueryParams,
|
||||||
|
hasFiltersQueryParams,
|
||||||
|
onViewFiltersChange,
|
||||||
|
resetViewBar,
|
||||||
|
setCurrentViewFilters,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { ObjectFilterDropdownButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton';
|
import { ObjectFilterDropdownButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton';
|
||||||
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
||||||
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
|
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { TopBar } from '@/ui/layout/top-bar/TopBar';
|
import { TopBar } from '@/ui/layout/top-bar/TopBar';
|
||||||
|
import { FilterQueryParamsEffect } from '@/views/components/FilterQueryParamsEffect';
|
||||||
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
||||||
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
|
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
|
||||||
import { useViewBar } from '@/views/hooks/useViewBar';
|
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||||
@ -45,6 +47,7 @@ export const ViewBar = ({
|
|||||||
const { upsertViewSort, upsertViewFilter } = useViewBar({
|
const { upsertViewSort, upsertViewFilter } = useViewBar({
|
||||||
viewBarId: viewBarId,
|
viewBarId: viewBarId,
|
||||||
});
|
});
|
||||||
|
const { objectNamePlural } = useParams();
|
||||||
|
|
||||||
const filterDropdownId = 'view-filter';
|
const filterDropdownId = 'view-filter';
|
||||||
const sortDropdownId = 'view-sort';
|
const sortDropdownId = 'view-sort';
|
||||||
@ -65,6 +68,7 @@ export const ViewBar = ({
|
|||||||
sortDropdownId={sortDropdownId}
|
sortDropdownId={sortDropdownId}
|
||||||
onSortSelect={upsertViewSort}
|
onSortSelect={upsertViewSort}
|
||||||
/>
|
/>
|
||||||
|
{!!objectNamePlural && <FilterQueryParamsEffect />}
|
||||||
|
|
||||||
<TopBar
|
<TopBar
|
||||||
className={className}
|
className={className}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
|
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
|
||||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||||
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
||||||
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
||||||
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
||||||
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
|
||||||
@ -132,19 +133,20 @@ export const ViewBarDetails = ({
|
|||||||
<StyledBar>
|
<StyledBar>
|
||||||
<StyledFilterContainer>
|
<StyledFilterContainer>
|
||||||
<StyledChipcontainer>
|
<StyledChipcontainer>
|
||||||
{currentViewSorts?.map((sort) => {
|
{currentViewSorts?.map((sort) => (
|
||||||
return <EditableSortChip viewSort={sort} />;
|
<EditableSortChip key={sort.id} viewSort={sort} />
|
||||||
})}
|
))}
|
||||||
{!!currentViewSorts?.length && !!currentViewFilters?.length && (
|
{!!currentViewSorts?.length && !!currentViewFilters?.length && (
|
||||||
<StyledSeperatorContainer>
|
<StyledSeperatorContainer>
|
||||||
<StyledSeperator />
|
<StyledSeperator />
|
||||||
</StyledSeperatorContainer>
|
</StyledSeperatorContainer>
|
||||||
)}
|
)}
|
||||||
{currentViewFilters?.map((viewFilter) => {
|
{currentViewFilters?.map((viewFilter) => (
|
||||||
return (
|
<ObjectFilterDropdownScope
|
||||||
<ObjectFilterDropdownScope
|
key={viewFilter.id}
|
||||||
filterScopeId={viewFilter.fieldMetadataId}
|
filterScopeId={viewFilter.fieldMetadataId}
|
||||||
>
|
>
|
||||||
|
<DropdownScope dropdownScopeId={viewFilter.fieldMetadataId}>
|
||||||
<ViewBarFilterEffect
|
<ViewBarFilterEffect
|
||||||
filterDropdownId={viewFilter.fieldMetadataId}
|
filterDropdownId={viewFilter.fieldMetadataId}
|
||||||
onFilterSelect={upsertViewFilter}
|
onFilterSelect={upsertViewFilter}
|
||||||
@ -156,9 +158,9 @@ export const ViewBarDetails = ({
|
|||||||
}}
|
}}
|
||||||
viewFilterDropdownId={viewFilter.fieldMetadataId}
|
viewFilterDropdownId={viewFilter.fieldMetadataId}
|
||||||
/>
|
/>
|
||||||
</ObjectFilterDropdownScope>
|
</DropdownScope>
|
||||||
);
|
</ObjectFilterDropdownScope>
|
||||||
})}
|
))}
|
||||||
</StyledChipcontainer>
|
</StyledChipcontainer>
|
||||||
{hasFilterButton && (
|
{hasFilterButton && (
|
||||||
<StyledAddFilterContainer>
|
<StyledAddFilterContainer>
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export const ViewBarFilterEffect = ({
|
|||||||
filterDefinitionUsedInDropdown,
|
filterDefinitionUsedInDropdown,
|
||||||
setObjectFilterDropdownSelectedRecordIds,
|
setObjectFilterDropdownSelectedRecordIds,
|
||||||
isObjectFilterDropdownUnfolded,
|
isObjectFilterDropdownUnfolded,
|
||||||
} = useFilterDropdown({ filterDropdownId: filterDropdownId });
|
} = useFilterDropdown({ filterDropdownId });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (availableFilterDefinitions) {
|
if (availableFilterDefinitions) {
|
||||||
|
|||||||
@ -0,0 +1,160 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import qs from 'qs';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||||
|
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||||
|
import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||||
|
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||||
|
import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
import { assertNotNull } from '~/utils/assert';
|
||||||
|
|
||||||
|
const filterQueryParamsSchema = z.object({
|
||||||
|
filter: z.record(
|
||||||
|
z.record(
|
||||||
|
z.nativeEnum(ViewFilterOperand),
|
||||||
|
z.string().or(z.array(z.string())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FilterQueryParams = z.infer<typeof filterQueryParamsSchema>;
|
||||||
|
|
||||||
|
export const useFiltersFromQueryParams = () => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { objectNamePlural = '' } = useParams();
|
||||||
|
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||||
|
objectNamePlural,
|
||||||
|
});
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
|
||||||
|
const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery();
|
||||||
|
|
||||||
|
const filterParamsValidation = filterQueryParamsSchema.safeParse(
|
||||||
|
qs.parse(searchParams.toString()),
|
||||||
|
);
|
||||||
|
const filterQueryParams = useMemo(
|
||||||
|
() =>
|
||||||
|
filterParamsValidation.success ? filterParamsValidation.data.filter : {},
|
||||||
|
[filterParamsValidation],
|
||||||
|
);
|
||||||
|
const hasFiltersQueryParams = filterParamsValidation.success;
|
||||||
|
|
||||||
|
const getFiltersFromQueryParams = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
async () => {
|
||||||
|
if (!hasFiltersQueryParams) return [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(filterQueryParams).map<Promise<ViewFilter | null>>(
|
||||||
|
async ([fieldName, filterFromURL]) => {
|
||||||
|
const [filterOperandFromURL, filterValueFromURL] =
|
||||||
|
Object.entries(filterFromURL)[0];
|
||||||
|
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
|
(field) => field.name === fieldName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fieldMetadataItem) return null;
|
||||||
|
|
||||||
|
const filterDefinition =
|
||||||
|
formatFieldMetadataItemAsFilterDefinition({
|
||||||
|
field: fieldMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filterDefinition) return null;
|
||||||
|
|
||||||
|
const relationObjectMetadataNameSingular =
|
||||||
|
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata
|
||||||
|
.nameSingular;
|
||||||
|
|
||||||
|
const relationObjectMetadataNamePlural =
|
||||||
|
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata
|
||||||
|
.namePlural;
|
||||||
|
|
||||||
|
const relationObjectMetadataItem =
|
||||||
|
relationObjectMetadataNameSingular
|
||||||
|
? snapshot
|
||||||
|
.getLoadable(
|
||||||
|
objectMetadataItemFamilySelector({
|
||||||
|
objectName: relationObjectMetadataNameSingular,
|
||||||
|
objectNameType: 'singular',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.getValue()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const relationRecordNames = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
relationObjectMetadataNamePlural &&
|
||||||
|
relationObjectMetadataItem &&
|
||||||
|
Array.isArray(filterValueFromURL)
|
||||||
|
) {
|
||||||
|
const queryResult = await apolloClient.query<
|
||||||
|
Record<string, { edges: { node: ObjectRecord }[] }>
|
||||||
|
>({
|
||||||
|
query: generateFindManyRecordsQuery({
|
||||||
|
objectMetadataItem: relationObjectMetadataItem,
|
||||||
|
}),
|
||||||
|
variables: {
|
||||||
|
filter: { id: { in: filterValueFromURL } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const relationRecordNamesFromQuery = queryResult.data?.[
|
||||||
|
relationObjectMetadataNamePlural
|
||||||
|
]?.edges.map(
|
||||||
|
({ node: record }) =>
|
||||||
|
getObjectRecordIdentifier({
|
||||||
|
objectMetadataItem: relationObjectMetadataItem,
|
||||||
|
record,
|
||||||
|
}).name,
|
||||||
|
);
|
||||||
|
|
||||||
|
relationRecordNames.push(...relationRecordNamesFromQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterValueAsString = Array.isArray(filterValueFromURL)
|
||||||
|
? JSON.stringify(filterValueFromURL)
|
||||||
|
: filterValueFromURL;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `tmp-${[
|
||||||
|
fieldName,
|
||||||
|
filterOperandFromURL,
|
||||||
|
filterValueFromURL,
|
||||||
|
].join('-')}`,
|
||||||
|
fieldMetadataId: fieldMetadataItem.id,
|
||||||
|
operand: filterOperandFromURL as ViewFilterOperand,
|
||||||
|
value: filterValueAsString,
|
||||||
|
displayValue:
|
||||||
|
relationRecordNames?.join(', ') ?? filterValueAsString,
|
||||||
|
definition: filterDefinition,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).filter(assertNotNull);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
apolloClient,
|
||||||
|
filterQueryParams,
|
||||||
|
generateFindManyRecordsQuery,
|
||||||
|
hasFiltersQueryParams,
|
||||||
|
objectMetadataItem.fields,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasFiltersQueryParams,
|
||||||
|
getFiltersFromQueryParams,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -43,13 +43,7 @@ export const useViewFilters = (viewScopeId: string) => {
|
|||||||
viewScopeId,
|
viewScopeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!currentViewId) {
|
if (!currentViewId || !currentViewFilters || !savedViewFiltersByKey) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!currentViewFilters) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!savedViewFiltersByKey) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -85,7 +85,10 @@ export const useViewBar = (props?: UseViewProps) => {
|
|||||||
|
|
||||||
const changeViewInUrl = useCallback(
|
const changeViewInUrl = useCallback(
|
||||||
(viewId: string) => {
|
(viewId: string) => {
|
||||||
setSearchParams({ view: viewId });
|
setSearchParams((previousSearchParams) => {
|
||||||
|
previousSearchParams.set('view', viewId);
|
||||||
|
return previousSearchParams;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[setSearchParams],
|
[setSearchParams],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user