Skip to content

Commit d92e8de

Browse files
JuliRossiFBanfi
andauthored
Bulk edit v1.3 [] (#10111)
* Bulk-Edit-App: Adding status filter + refactors + some fixes [INTEG-3116] (#10097) * Adding status filter + refactors + some fixes * removing unused status multiselect component * removing unnecessary rename * using Multiselect.Option * refactor GenericMultiselect styles * renaming generic multiselect test * adding tests for status case in generic multiselect * fix and comments refactors * refactor disable CustomMultiselectAll styles and disabled * removing unnecessary component + general refactors and renames * removing custom component unnecessary prop * include refactors * rename in style file * Bulk-Edit-App: search bar + reset button + pending refactors [MAPS-34] (#10109) * adding search entries filter + tests + sticking filters * adding reset filters logic + tests + warning fixes * refactor dividing status label from color in entryUtils.ts * adding new empty state differentiation * fix bug on SearchFilter when changing page * fix bug: disable CT list when entries loading * disabling sort button * renaming files * Bulk-Edit-App: Fix v1.3 comments [MAPS-50] (#10123) * renaming in style file * adding filtering for pure numbers * removing old-unused placeholders props * improving filter multiselect performance * fix in object-value comparison * comments corrections * moving validation --------- Co-authored-by: Franco Banfi <[email protected]>
1 parent 62aae42 commit d92e8de

22 files changed

+1133
-198
lines changed

apps/bulk-edit/package-lock.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/bulk-edit/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"contentful-management": "^11.52.0",
1616
"emotion": "10.0.27",
1717
"react": "18.3.1",
18-
"react-dom": "18.3.1"
18+
"react-dom": "18.3.1",
19+
"use-debounce": "^10.0.6"
1920
},
2021
"scripts": {
2122
"start": "vite",

apps/bulk-edit/src/locations/Page/components/ColumnMultiselect.tsx

Lines changed: 0 additions & 72 deletions
This file was deleted.

apps/bulk-edit/src/locations/Page/components/ContentTypeSidebar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ interface ContentTypeSidebarProps {
88
contentTypes: ContentTypeProps[];
99
selectedContentTypeId: string | undefined;
1010
onContentTypeSelect: (id: string) => void;
11+
disabled?: boolean;
1112
}
1213

1314
export const ContentTypeSidebar: React.FC<ContentTypeSidebarProps> = ({
1415
contentTypes,
1516
selectedContentTypeId,
1617
onContentTypeSelect,
18+
disabled = false,
1719
}) => {
1820
return (
1921
<Flex style={styles.sidebar} padding="spacingM" flexDirection="column" gap="spacingXs">
@@ -27,7 +29,8 @@ export const ContentTypeSidebar: React.FC<ContentTypeSidebarProps> = ({
2729
as="button"
2830
key={ct.sys.id}
2931
isActive={ct.sys.id === selectedContentTypeId}
30-
onClick={() => onContentTypeSelect(ct.sys.id)}
32+
onClick={disabled ? undefined : () => onContentTypeSelect(ct.sys.id)}
33+
isDisabled={disabled}
3134
testId="content-type-nav-item">
3235
{ct.name}
3336
</NavList.Item>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import tokens from '@contentful/f36-tokens';
2+
import { css } from 'emotion';
3+
4+
export const emptyEntryBannerStyles = {
5+
container: css({
6+
background: tokens.gray100,
7+
borderRadius: tokens.borderRadiusMedium,
8+
}),
9+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import { Flex, Text } from '@contentful/f36-components';
3+
import { emptyEntryBannerStyles } from './EmptyEntryBanner.styles';
4+
5+
interface EmptyStateProps {
6+
hasEntries: boolean;
7+
hasInitialEntries: boolean;
8+
}
9+
10+
export const EmptyEntryBanner: React.FC<EmptyStateProps> = ({ hasEntries, hasInitialEntries }) => {
11+
if (hasEntries) {
12+
return null;
13+
}
14+
15+
if (!hasInitialEntries) {
16+
return (
17+
<Flex
18+
alignItems="center"
19+
justifyContent="center"
20+
flexDirection="column"
21+
padding="spacing2Xl"
22+
className={emptyEntryBannerStyles.container}>
23+
<Text fontSize="fontSizeL" fontWeight="fontWeightDemiBold">
24+
No entries found.
25+
</Text>
26+
</Flex>
27+
);
28+
}
29+
30+
return (
31+
<Flex
32+
alignItems="center"
33+
justifyContent="center"
34+
flexDirection="column"
35+
padding="spacing2Xl"
36+
className={emptyEntryBannerStyles.container}>
37+
<Text fontSize="fontSizeL" fontWeight="fontWeightDemiBold">
38+
We couldn't find any matches.
39+
</Text>
40+
<Text fontSize="fontSizeM">
41+
Try adjusting your filters or resetting them to broaden your search.
42+
</Text>
43+
</Flex>
44+
);
45+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import tokens from '@contentful/f36-tokens';
2+
import { css } from 'emotion';
3+
4+
export const optionStyles = css({
5+
fontSize: tokens.fontSizeS,
6+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Flex } from '@contentful/f36-components';
2+
import { Multiselect } from '@contentful/f36-multiselect';
3+
import { useMemo } from 'react';
4+
import { truncate } from '../utils/entryUtils';
5+
import { optionStyles } from './FilterMultiselect.styles';
6+
import { FilterOption } from '../types';
7+
8+
interface FilterMultiselectProps {
9+
id?: string;
10+
options: FilterOption[];
11+
selectedItems: FilterOption[];
12+
setSelectedItems: (items: FilterOption[]) => void;
13+
disabled?: boolean;
14+
placeholderConfig: {
15+
noneSelected: string;
16+
allSelected: string;
17+
};
18+
style?: React.CSSProperties | undefined;
19+
}
20+
21+
const FilterMultiselect = ({
22+
id,
23+
options,
24+
selectedItems,
25+
setSelectedItems,
26+
disabled,
27+
placeholderConfig,
28+
style,
29+
}: FilterMultiselectProps) => {
30+
const getPlaceholderText = () => {
31+
if (selectedItems.length === 0) return placeholderConfig.noneSelected;
32+
if (selectedItems.length === options.length) return placeholderConfig.allSelected;
33+
if (selectedItems.length === 1) {
34+
return selectedItems[0].label;
35+
}
36+
const firstLabel = selectedItems[0].label;
37+
return `${firstLabel} and ${selectedItems.length - 1} more`;
38+
};
39+
40+
const toggleAll = (e: React.ChangeEvent<HTMLInputElement>) => {
41+
const checked = e.target.checked;
42+
if (checked) {
43+
setSelectedItems(options);
44+
} else {
45+
setSelectedItems([]);
46+
}
47+
};
48+
49+
const selectedValuesSet = useMemo(
50+
() => new Set(selectedItems.map((item) => item.value)),
51+
[selectedItems]
52+
);
53+
54+
const areAllSelected = useMemo(() => {
55+
return options.every((option) => selectedValuesSet.has(option.value));
56+
}, [selectedValuesSet, options]);
57+
58+
return (
59+
<Flex gap="spacing2Xs" flexDirection="column" style={{ ...style }}>
60+
<Multiselect
61+
placeholder={getPlaceholderText()}
62+
triggerButtonProps={{ size: 'small', isDisabled: disabled }}
63+
popoverProps={{ isFullWidth: true }}>
64+
<Multiselect.SelectAll
65+
itemId={`selectAll-${id}`}
66+
onSelectItem={toggleAll}
67+
isDisabled={disabled}
68+
isChecked={areAllSelected}
69+
/>
70+
{options.map((option) => (
71+
<Multiselect.Option
72+
isDisabled={disabled}
73+
className={optionStyles}
74+
key={option.value}
75+
label={truncate(option.label, 30)}
76+
value={option.value}
77+
itemId={`option-${id}-${option.value}`}
78+
isChecked={selectedValuesSet.has(option.value)}
79+
onSelectItem={(e) => {
80+
const checked = e.target.checked;
81+
if (checked) {
82+
setSelectedItems([...selectedItems, option]);
83+
} else {
84+
setSelectedItems(selectedItems.filter((item) => item.value !== option.value));
85+
}
86+
}}
87+
/>
88+
))}
89+
</Multiselect>
90+
</Flex>
91+
);
92+
};
93+
94+
export default FilterMultiselect;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { css } from 'emotion';
2+
import { SIDEBAR_WIDTH, STICKY_SPACER_SPACING } from '../utils/constants';
3+
4+
export const styles = {
5+
container: css({
6+
position: 'sticky',
7+
left: SIDEBAR_WIDTH + STICKY_SPACER_SPACING,
8+
zIndex: 1,
9+
width: 1400,
10+
}),
11+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { Flex, Text, TextInput } from '@contentful/f36-components';
3+
import { SearchIcon } from '@contentful/f36-icons';
4+
import { useDebounce } from 'use-debounce';
5+
import { styles } from './SearchBar.styles';
6+
7+
interface SearchBarProps {
8+
searchQuery: string;
9+
onSearchChange: (query: string) => void;
10+
isDisabled?: boolean;
11+
debounceDelay?: number;
12+
}
13+
14+
export const SearchBar: React.FC<SearchBarProps> = ({
15+
searchQuery,
16+
onSearchChange,
17+
isDisabled = false,
18+
debounceDelay = 300,
19+
}) => {
20+
const [inputValue, setInputValue] = useState(searchQuery);
21+
const [debouncedValue] = useDebounce(inputValue, debounceDelay);
22+
const prevDebouncedValueRef = useRef<string>(debouncedValue);
23+
24+
useEffect(() => {
25+
setInputValue(searchQuery);
26+
}, [searchQuery]);
27+
28+
useEffect(() => {
29+
if (prevDebouncedValueRef.current !== debouncedValue) {
30+
onSearchChange(debouncedValue);
31+
prevDebouncedValueRef.current = debouncedValue;
32+
}
33+
}, [debouncedValue, onSearchChange]);
34+
35+
return (
36+
<Flex flexDirection="column" gap="spacingM" className={styles.container}>
37+
<Text fontSize="fontSizeM" fontWeight="fontWeightMedium">
38+
Search entries
39+
</Text>
40+
<TextInput
41+
placeholder="Search"
42+
value={inputValue}
43+
onChange={(e) => setInputValue(e.target.value)}
44+
icon={<SearchIcon />}
45+
isDisabled={isDisabled}
46+
/>
47+
</Flex>
48+
);
49+
};

0 commit comments

Comments
 (0)