Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display and update fields from fromManyObjects relations in Show card #5801

Merged
merged 8 commits into from
Jun 11, 2024
Next Next commit
Extract MultiRecordSelect from MultipleObjectRecordSelect
  • Loading branch information
ijreilly committed Jun 7, 2024
commit c2bf742f311eae15ddde71921b83b38ad15704a4
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useDebouncedCallback } from 'use-debounce';

import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
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 { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { isDefined } from '~/utils/isDefined';

export const StyledSelectableItem = styled(SelectableItem)`
height: 100%;
width: 100%;
`;
export const MultiRecordSelect = ({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic was mostly extracted from MultipleObjectRecordSelect to be shared with RelationManyFieldInput

onChange,
onSubmit,
selectedObjectRecords,
allRecords,
loading,
searchFilter,
setSearchFilter,
}: {
onChange?: (
changedRecordForSelect: ObjectRecordForSelect,
newSelectedValue: boolean,
) => void;
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
selectedObjectRecords: ObjectRecordForSelect[];
allRecords: ObjectRecordForSelect[];
loading: boolean;
searchFilter: string;
setSearchFilter: (searchFilter: string) => void;
}) => {
const containerRef = useRef<HTMLDivElement>(null);

const [internalSelectedRecords, setInternalSelectedRecords] = useState<
ObjectRecordForSelect[]
>([]);

useEffect(() => {
if (!loading) {
setInternalSelectedRecords(selectedObjectRecords);
}
}, [selectedObjectRecords, loading]);

const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
leading: true,
});

const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSetSearchFilter(event.currentTarget.value);
};

const handleSelectChange = (
changedRecordForSelect: ObjectRecordForSelect,
newSelectedValue: boolean,
) => {
const newSelectedRecords = newSelectedValue
? [...internalSelectedRecords, changedRecordForSelect]
: internalSelectedRecords.filter(
(selectedRecord) =>
selectedRecord.record.id !== changedRecordForSelect.record.id,
);

setInternalSelectedRecords(newSelectedRecords);

onChange?.(changedRecordForSelect, newSelectedValue);
};

const entitiesInDropdown = useMemo(
() =>
[...(allRecords ?? [])].filter((entity) =>
isNonEmptyString(entity.recordIdentifier.id),
),
[allRecords],
);

const selectableItemIds = entitiesInDropdown.map(
(entity) => entity.record.id,
);

return (
<>
<MultipleObjectRecordOnClickOutsideEffect
containerRef={containerRef}
onClickOutside={() => {
onSubmit?.(internalSelectedRecords);
}}
/>
<DropdownMenu ref={containerRef} data-select-disable>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{loading ? (
<MenuItem text="Loading..." />
) : (
<>
<SelectableList
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
selectableItemIdArray={selectableItemIds}
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
onEnter={(recordId) => {
const recordIsSelected = internalSelectedRecords?.some(
(selectedRecord) => selectedRecord.record.id === recordId,
);

const correspondingRecordForSelect = entitiesInDropdown?.find(
(entity) => entity.record.id === recordId,
);

if (isDefined(correspondingRecordForSelect)) {
handleSelectChange(
correspondingRecordForSelect,
!recordIsSelected,
);
}
}}
>
{entitiesInDropdown?.map((objectRecordForSelect) => (
<MultipleObjectRecordSelectItem
key={objectRecordForSelect.record.id}
objectRecordForSelect={objectRecordForSelect}
onSelectedChange={(newSelectedValue) =>
handleSelectChange(
objectRecordForSelect,
newSelectedValue,
)
}
selected={internalSelectedRecords?.some(
(selectedRecord) => {
return (
selectedRecord.record.id ===
objectRecordForSelect.record.id
);
},
)}
/>
))}
</SelectableList>
{entitiesInDropdown?.length === 0 && (
<MenuItem text="No result" />
)}
</>
)}
</DropdownMenuItemsContainer>
</DropdownMenu>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemo, useState } from 'react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useDebouncedCallback } from 'use-debounce';

import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
import {
ObjectRecordForSelect,
SelectedObjectRecordId,
useMultiObjectSearch,
} from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
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 { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { isDefined } from '~/utils/isDefined';

export const StyledSelectableItem = styled(SelectableItem)`
height: 100%;
width: 100%;
`;

export type EntitiesForMultipleObjectRecordSelect = {
filteredSelectedObjectRecords: ObjectRecordForSelect[];
objectRecordsToSelect: ObjectRecordForSelect[];
loading: boolean;
};

export const MultipleObjectRecordSelect = ({
onChange,
onSubmit,
Expand All @@ -41,13 +22,10 @@ export const MultipleObjectRecordSelect = ({
changedRecordForSelect: ObjectRecordForSelect,
newSelectedValue: boolean,
) => void;
onCancel?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
selectedObjectRecordIds: SelectedObjectRecordId[];
}) => {
const containerRef = useRef<HTMLDivElement>(null);

const [searchFilter, setSearchFilter] = useState<string>('');
const [searchFilter, setSearchFilter] = useState<string>(''); // Put in recoil state?

const {
filteredSelectedObjectRecords,
Expand All @@ -73,122 +51,18 @@ export const MultipleObjectRecordSelect = ({
[selectedObjectRecords, selectedObjectRecordIds],
);

const [internalSelectedRecords, setInternalSelectedRecords] = useState<
ObjectRecordForSelect[]
>([]);

useEffect(() => {
if (!loading) {
setInternalSelectedRecords(selectedObjectRecordsForSelect);
}
}, [selectedObjectRecordsForSelect, loading]);

const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
leading: true,
});

const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSetSearchFilter(event.currentTarget.value);
};

const handleSelectChange = (
changedRecordForSelect: ObjectRecordForSelect,
newSelectedValue: boolean,
) => {
const newSelectedRecords = newSelectedValue
? [...internalSelectedRecords, changedRecordForSelect]
: internalSelectedRecords.filter(
(selectedRecord) =>
selectedRecord.record.id !== changedRecordForSelect.record.id,
);

setInternalSelectedRecords(newSelectedRecords);

onChange?.(changedRecordForSelect, newSelectedValue);
};

const entitiesInDropdown = useMemo(
() =>
[
return (
<MultiRecordSelect
onChange={onChange}
onSubmit={onSubmit}
selectedObjectRecords={selectedObjectRecordsForSelect}
allRecords={[
...(filteredSelectedObjectRecords ?? []),
...(objectRecordsToSelect ?? []),
].filter((entity) => isNonEmptyString(entity.recordIdentifier.id)),
[filteredSelectedObjectRecords, objectRecordsToSelect],
);

const selectableItemIds = entitiesInDropdown.map(
(entity) => entity.record.id,
);

return (
<>
<MultipleObjectRecordOnClickOutsideEffect
containerRef={containerRef}
onClickOutside={() => {
onSubmit?.(internalSelectedRecords);
}}
/>
<DropdownMenu ref={containerRef} data-select-disable>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{loading ? (
<MenuItem text="Loading..." />
) : (
<>
<SelectableList
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
selectableItemIdArray={selectableItemIds}
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
onEnter={(recordId) => {
const recordIsSelected = internalSelectedRecords?.some(
(selectedRecord) => selectedRecord.record.id === recordId,
);

const correspondingRecordForSelect = entitiesInDropdown?.find(
(entity) => entity.record.id === recordId,
);

if (isDefined(correspondingRecordForSelect)) {
handleSelectChange(
correspondingRecordForSelect,
!recordIsSelected,
);
}
}}
>
{entitiesInDropdown?.map((objectRecordForSelect) => (
<MultipleObjectRecordSelectItem
key={objectRecordForSelect.record.id}
objectRecordForSelect={objectRecordForSelect}
onSelectedChange={(newSelectedValue) =>
handleSelectChange(
objectRecordForSelect,
newSelectedValue,
)
}
selected={internalSelectedRecords?.some(
(selectedRecord) => {
return (
selectedRecord.record.id ===
objectRecordForSelect.record.id
);
},
)}
/>
))}
</SelectableList>
{entitiesInDropdown?.length === 0 && (
<MenuItem text="No result" />
)}
</>
)}
</DropdownMenuItemsContainer>
</DropdownMenu>
</>
]}
loading={loading}
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
/>
);
};