Skip to content

Commit 563ed5a

Browse files
authored
fix: replacing select with multiselect for content type picker modal [INTEG-3378] (#10380)
* fix: replacing select with multiselect for content type picker modal, removing `SelectedContentTypes` type * cleanup
1 parent 8234498 commit 563ed5a

File tree

5 files changed

+76
-64
lines changed

5 files changed

+76
-64
lines changed

apps/google-docs/src/hooks/useGoogleDocPicker.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export function useGoogleDocsPicker(
4343
await loadPickerApi();
4444

4545
const google = (window as any).google;
46-
const gapi = (window as any).gapi;
4746

4847
// Only show Google Docs
4948
const view = new google.picker.View(google.picker.ViewId.DOCS);

apps/google-docs/src/hooks/useProgressTracking.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useState, useCallback } from 'react';
2-
import { SelectedContentType } from '../locations/Page/components/modals/step_2/SelectContentTypeModal';
2+
import { ContentTypeProps } from 'contentful-management';
33

44
export const useProgressTracking = () => {
55
const [documentId, setDocumentId] = useState<string>('');
6-
const [selectedContentTypes, setSelectedContentTypes] = useState<SelectedContentType[]>([]);
6+
const [selectedContentTypes, setSelectedContentTypes] = useState<ContentTypeProps[]>([]);
77
const [pendingCloseAction, setPendingCloseAction] = useState<(() => void) | null>(null);
88

99
const hasProgress = documentId.trim().length > 0;

apps/google-docs/src/locations/Page/components/mainpage/ModalOrchestrator.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import { ReviewEntriesModal } from '../modals/step_4/ReviewEntriesModal';
88
import { ErrorEntriesModal } from '../modals/step_4/ErrorEntriesModal';
99
import { createEntriesFromPreview, EntryCreationResult } from '../../../../services/entryService';
1010
import SelectDocumentModal from '../modals/step_1/SelectDocumentModal';
11-
import {
12-
SelectedContentType,
13-
ContentTypePickerModal,
14-
} from '../modals/step_2/SelectContentTypeModal';
11+
import { ContentTypePickerModal } from '../modals/step_2/SelectContentTypeModal';
1512
import { PreviewModal } from '../modals/step_3/PreviewModal';
13+
import { ContentTypeProps } from 'contentful-management';
1614

1715
export interface ModalOrchestratorHandle {
1816
startFlow: () => void;
@@ -109,22 +107,24 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
109107
openModal(ModalType.CONTENT_TYPE_PICKER);
110108
};
111109

112-
const handleContentTypeSelected = async (contentTypes: SelectedContentType[]) => {
113-
const ids = contentTypes.map((ct) => ct.id);
110+
const handleContentTypeSelected = async (contentTypes: ContentTypeProps[]) => {
111+
const ids = contentTypes.map((ct) => ct.sys.id);
114112
await submit(ids);
115113
};
116114

117-
const handlePreviewModalConfirm = async (contentTypes: SelectedContentType[]) => {
115+
const handlePreviewModalConfirm = async (contentTypeIds: string[]) => {
118116
if (!previewEntries || previewEntries.length === 0) {
119117
sdk.notifier.error('No entries to create');
120118
return;
121119
}
122-
123120
setIsCreatingEntries(true);
124121
try {
125-
const ids = contentTypes.map((ct) => ct.id);
126122
const entries = previewEntries.map((p) => p.entry);
127-
const entryResult: EntryCreationResult = await createEntriesFromPreview(sdk, entries, ids);
123+
const entryResult: EntryCreationResult = await createEntriesFromPreview(
124+
sdk,
125+
entries,
126+
contentTypeIds
127+
);
128128

129129
const createdCount = entryResult.createdEntries.length;
130130

@@ -203,7 +203,9 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
203203
isOpen={modalStates.isPreviewModalOpen}
204204
onClose={() => closeModal(ModalType.PREVIEW)}
205205
previewEntries={previewEntries}
206-
onCreateEntries={() => handlePreviewModalConfirm(selectedContentTypes)}
206+
onCreateEntries={() =>
207+
handlePreviewModalConfirm(selectedContentTypes.map((ct) => ct.sys.id))
208+
}
207209
isLoading={isSubmitting}
208210
isCreatingEntries={isCreatingEntries}
209211
/>

apps/google-docs/src/locations/Page/components/modals/step_2/SelectContentTypeModal.tsx

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,22 @@ import {
66
Modal,
77
Paragraph,
88
Pill,
9-
Select,
9+
Multiselect,
1010
Spinner,
1111
} from '@contentful/f36-components';
1212
import { PageAppSDK } from '@contentful/app-sdk';
1313
import { ContentTypeProps } from 'contentful-management';
14-
15-
export interface SelectedContentType {
16-
id: string;
17-
name: string;
18-
}
14+
import { css } from '@emotion/css';
1915

2016
interface ContentTypePickerModalProps {
2117
sdk: PageAppSDK;
2218
isOpen: boolean;
2319
onClose: () => void;
24-
onSelect: (contentTypes: SelectedContentType[]) => void;
20+
onSelect: (contentTypes: ContentTypeProps[]) => void;
2521
isSubmitting: boolean;
26-
selectedContentTypes: SelectedContentType[];
22+
selectedContentTypes: ContentTypeProps[];
2723
setSelectedContentTypes: (
28-
contentTypes: SelectedContentType[] | ((prev: SelectedContentType[]) => SelectedContentType[])
24+
contentTypes: ContentTypeProps[] | ((prev: ContentTypeProps[]) => ContentTypeProps[])
2925
) => void;
3026
}
3127

@@ -39,6 +35,7 @@ export const ContentTypePickerModal = ({
3935
setSelectedContentTypes,
4036
}: ContentTypePickerModalProps) => {
4137
const [contentTypes, setContentTypes] = useState<ContentTypeProps[]>([]);
38+
const [filteredContentTypes, setFilteredContentTypes] = useState<ContentTypeProps[]>([]);
4239
const [isLoading, setIsLoading] = useState<boolean>(false);
4340
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState<boolean>(false);
4441
const [hasFetchError, setHasFetchError] = useState<boolean>(false);
@@ -68,6 +65,7 @@ export const ContentTypePickerModal = ({
6865
environmentId: environment.sys.id,
6966
});
7067
setContentTypes(contentTypesResponse.items || []);
68+
setFilteredContentTypes(contentTypesResponse.items || []);
7169
} catch (error) {
7270
console.error('Failed to fetch content types:', error);
7371
setHasFetchError(true);
@@ -87,23 +85,35 @@ export const ContentTypePickerModal = ({
8785
}
8886
}, [isOpen]);
8987

90-
const handleAddContentType = (contentTypeId: string) => {
91-
if (!contentTypeId || isSubmitting) return;
88+
const onSearchValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
89+
const searchTerm = e.target.value.toLowerCase().trim();
90+
if (!searchTerm) {
91+
setFilteredContentTypes(contentTypes);
92+
return;
93+
}
94+
const filtered = contentTypes.filter((ct) => ct.name.toLowerCase().includes(searchTerm));
95+
setFilteredContentTypes(filtered);
96+
};
9297

93-
const contentType = contentTypes.find((ct) => ct.sys.id === contentTypeId);
94-
if (contentType && !selectedContentTypes.some((ct) => ct.id === contentTypeId)) {
98+
const getPlaceholderText = () => {
99+
if (isLoading) return 'Loading content types...';
100+
if (contentTypes.length === 0) return 'No content types in space';
101+
if (selectedContentTypes.length === 0) return 'Select one or more';
102+
return `${selectedContentTypes.length} selected`;
103+
};
104+
105+
const handleSelectContentType = (e: React.ChangeEvent<HTMLInputElement>) => {
106+
const { checked, value } = e.target;
107+
if (checked) {
95108
setSelectedContentTypes([
96109
...selectedContentTypes,
97-
{ id: contentType.sys.id, name: contentType.name },
110+
contentTypes.find((ct) => ct.sys.id === value) as ContentTypeProps,
98111
]);
112+
} else {
113+
setSelectedContentTypes(selectedContentTypes.filter((selected) => selected.sys.id !== value));
99114
}
100115
};
101116

102-
const handleRemoveContentType = (contentTypeId: string) => {
103-
if (isSubmitting) return;
104-
setSelectedContentTypes(selectedContentTypes.filter((ct) => ct.id !== contentTypeId));
105-
};
106-
107117
const handleClose = () => {
108118
if (isSubmitting) return; // Prevent closing during submission
109119
onClose();
@@ -118,14 +128,6 @@ export const ContentTypePickerModal = ({
118128
onSelect(selectedContentTypes);
119129
};
120130

121-
const availableContentTypes = useMemo(
122-
() =>
123-
contentTypes.filter(
124-
(ct) => !selectedContentTypes.some((selected) => selected.id === ct.sys.id)
125-
),
126-
[contentTypes, selectedContentTypes]
127-
);
128-
129131
return (
130132
<Modal title="Select content type(s)" isShown={isOpen} onClose={handleClose} size="medium">
131133
{() => (
@@ -137,23 +139,28 @@ export const ContentTypePickerModal = ({
137139
</Paragraph>
138140
<FormControl isRequired isInvalid={isInvalidSelectionError || showFetchError}>
139141
<FormControl.Label>Content type</FormControl.Label>
140-
<Select
141-
id="content-type-select"
142-
name="content-type-select"
143-
value=""
144-
onChange={(e) => {
145-
handleAddContentType(e.target.value);
142+
<Multiselect
143+
searchProps={{
144+
searchPlaceholder: 'Search content types',
145+
onSearchValueChange,
146146
}}
147-
isDisabled={isLoading || availableContentTypes.length === 0 || isSubmitting}>
148-
<Select.Option value="" isDisabled>
149-
{isLoading ? 'Loading content types...' : 'Select one or more'}
150-
</Select.Option>
151-
{availableContentTypes.map((ct) => (
152-
<Select.Option key={ct.sys.id} value={ct.sys.id}>
147+
currentSelection={selectedContentTypes.map((ct) => ct.sys.id)}
148+
placeholder={getPlaceholderText()}>
149+
{filteredContentTypes.map((ct) => (
150+
<Multiselect.Option
151+
className={css({ padding: `0.25rem` })}
152+
key={ct.sys.id}
153+
value={ct.sys.id}
154+
itemId={ct.sys.id}
155+
isChecked={selectedContentTypes.some(
156+
(selected) => selected.sys.id === ct.sys.id
157+
)}
158+
isDisabled={isLoading || isSubmitting}
159+
onSelectItem={handleSelectContentType}>
153160
{ct.name}
154-
</Select.Option>
161+
</Multiselect.Option>
155162
))}
156-
</Select>
163+
</Multiselect>
157164
{showFetchError && (
158165
<FormControl.ValidationMessage>
159166
Unable to load content types.
@@ -170,9 +177,18 @@ export const ContentTypePickerModal = ({
170177
<Flex flexWrap="wrap" gap="spacingXs" marginTop="spacingS">
171178
{selectedContentTypes.map((ct) => (
172179
<Pill
173-
key={ct.id}
180+
key={ct.sys.id}
174181
label={ct.name}
175-
onClose={isSubmitting ? undefined : () => handleRemoveContentType(ct.id)}
182+
onClose={
183+
isSubmitting
184+
? undefined
185+
: () =>
186+
setSelectedContentTypes(
187+
selectedContentTypes.filter(
188+
(selected) => selected.sys.id !== ct.sys.id
189+
)
190+
)
191+
}
176192
/>
177193
))}
178194
</Flex>

apps/google-docs/src/locations/Page/components/modals/step_3/PreviewModal.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22
import { Box, Button, Flex, Modal, Paragraph, Text } from '@contentful/f36-components';
33
import { EntryToCreate } from '../../../../../../functions/agents/documentParserAgent/schema';
4-
import { SelectedContentType } from '../step_2/SelectContentTypeModal';
54
import tokens from '@contentful/f36-tokens';
65

76
export interface PreviewEntry {
@@ -13,7 +12,7 @@ interface PreviewModalProps {
1312
isOpen: boolean;
1413
onClose: () => void;
1514
previewEntries: PreviewEntry[];
16-
onCreateEntries: (contentTypes: SelectedContentType[]) => void;
15+
onCreateEntries: (contentTypeIds: string[]) => void;
1716
isCreatingEntries: boolean;
1817
isLoading: boolean;
1918
}
@@ -87,11 +86,7 @@ export const PreviewModal: React.FC<PreviewModalProps> = ({
8786
</Button>
8887
<Button
8988
onClick={() =>
90-
onCreateEntries(
91-
previewEntries.map((item) => ({
92-
id: item.entry.contentTypeId,
93-
})) as SelectedContentType[]
94-
)
89+
onCreateEntries(previewEntries.map((preview) => preview.entry.contentTypeId))
9590
}
9691
variant="primary"
9792
isDisabled={isLoading || previewEntries.length === 0}>

0 commit comments

Comments
 (0)