Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
45f9f46
feat: extend timeout to 50 minutes and add in timing markers (#10608)
ryunsong-contentful Feb 24, 2026
e6a57e3
fix: update AGENT_ANALYZER_ID constant to reflect new workflow agent …
harikakondur Feb 25, 2026
f2ba1f7
fix: update local agents API URL to use constant for base URL
harikakondur Feb 26, 2026
e9b83fa
fix(google-docs): add pagination for content type fetching [INTEG-349…
harikakondur Mar 5, 2026
281450d
chore: add env example
harikakondur Mar 6, 2026
be83fbf
Merge branch 'google-docs-quality' of https://github.com/contentful/a…
harikakondur Mar 6, 2026
ec9e046
chore: update readme
harikakondur Mar 6, 2026
394e607
fix: cleanup test modal [INTEG-3496] (#10682)
harikakondur Mar 9, 2026
ca47e8b
cleaning unnecessary steps
FBanfi Mar 12, 2026
44fde50
adding close button for first step
FBanfi Mar 12, 2026
f9253a2
adding select tabs step
FBanfi Mar 12, 2026
cca6f84
removing unused custom hook and usages
FBanfi Mar 12, 2026
1463987
Merge branch 'google-docs-hil' into google-docs-modal-cleanup
FBanfi Mar 13, 2026
c7810c3
solving conflicts in merge
FBanfi Mar 13, 2026
3cda72e
Merge branch 'google-docs-hil' into google-docs-modal-cleanup
FBanfi Mar 13, 2026
afb542b
adding select tabs step
FBanfi Mar 12, 2026
d8aa5b0
Merge remote-tracking branch 'origin/google-docs-tabs' into google-do…
FBanfi Mar 16, 2026
c7c169b
removing unused hook
FBanfi Mar 16, 2026
65e1bd6
adding select tabs modal tests
FBanfi Mar 16, 2026
7df0c36
wip changing tabs modal step
FBanfi Mar 16, 2026
38149d5
refactors and fixing multiselect bug
FBanfi Mar 17, 2026
f308aff
Merge branch 'google-docs-hil' into google-docs-tabs
FBanfi Mar 17, 2026
841af57
refactoring selected tabs and content types modals
FBanfi Mar 18, 2026
265fb2c
fixing build errors
FBanfi Mar 18, 2026
dcc3794
adding truncation in content types selection
FBanfi Mar 18, 2026
e27fe00
addressing PR comments
FBanfi Mar 18, 2026
bfa4c34
Merge branch 'google-docs-hil' into google-docs-tabs
FBanfi Mar 19, 2026
1529c55
changing onClose from tabs step
FBanfi Mar 19, 2026
53f6333
addressing pr comments
FBanfi Mar 19, 2026
66ac557
changing selected tabs setter usage
FBanfi Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/google-docs/src/hooks/useModalManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ export enum ModalType {
CONTENT_TYPE_PICKER = 'contentTypePicker',
CONFIRM_CANCEL = 'confirmCancel',
ERROR_PREVIEW = 'errorPreview',
SELECT_TABS = 'selectTabs',
}

export const useModalManagement = () => {
const [isUploadModalOpen, setIsUploadModalOpen] = useState<boolean>(false);
const [isContentTypePickerOpen, setIsContentTypePickerOpen] = useState<boolean>(false);
const [isConfirmCancelModalOpen, setIsConfirmCancelModalOpen] = useState<boolean>(false);
const [isErrorPreviewModalOpen, setIsErrorPreviewModalOpen] = useState<boolean>(false);
const [isSelectTabsModalOpen, setIsSelectTabsModalOpen] = useState<boolean>(false);

const openModal = (modalType: ModalType) => {
switch (modalType) {
Expand All @@ -27,6 +29,9 @@ export const useModalManagement = () => {
case ModalType.ERROR_PREVIEW:
setIsErrorPreviewModalOpen(true);
break;
case ModalType.SELECT_TABS:
setIsSelectTabsModalOpen(true);
break;
}
};

Expand All @@ -44,6 +49,9 @@ export const useModalManagement = () => {
case ModalType.ERROR_PREVIEW:
setIsErrorPreviewModalOpen(false);
break;
case ModalType.SELECT_TABS:
setIsSelectTabsModalOpen(false);
break;
}
};

Expand All @@ -53,6 +61,7 @@ export const useModalManagement = () => {
isContentTypePickerOpen,
isConfirmCancelModalOpen,
isErrorPreviewModalOpen,
isSelectTabsModalOpen,
},
openModal,
closeModal,
Expand Down
23 changes: 23 additions & 0 deletions apps/google-docs/src/hooks/useMultiselectReflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useRef, type RefObject } from 'react';

export const useMultiselectScrollReflow = <T>(selection: T[]): RefObject<HTMLUListElement> => {
const listRef = useRef<HTMLUListElement>(null);

useEffect(() => {
if (listRef.current) {
const element = listRef.current;
const currentScroll = element.scrollTop;
const maxScroll = element.scrollHeight - element.clientHeight;

if (currentScroll >= maxScroll) {
element.scrollTop = currentScroll - 1;
} else {
element.scrollTop = currentScroll + 1;
}

element.scrollTop = currentScroll;
}
}, [selection]);

return listRef;
};
9 changes: 9 additions & 0 deletions apps/google-docs/src/hooks/useProgressTracking.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { useState, useCallback } from 'react';
import { ContentTypeProps } from 'contentful-management';
import { DocumentTabProps } from '../utils/types';

export const useProgressTracking = () => {
const [documentId, setDocumentId] = useState<string>('');
const [selectedContentTypes, setSelectedContentTypes] = useState<ContentTypeProps[]>([]);
const [pendingCloseAction, setPendingCloseAction] = useState<(() => void) | null>(null);
const [availableTabs, setAvailableTabs] = useState<DocumentTabProps[]>([]);
const [selectedTabs, setSelectedTabs] = useState<DocumentTabProps[]>([]);

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

const resetProgress = useCallback(() => {
setDocumentId('');
setSelectedContentTypes([]);
setAvailableTabs([]);
setSelectedTabs([]);
}, []);

return {
documentId,
setDocumentId,
selectedContentTypes,
setSelectedContentTypes,
availableTabs,
setAvailableTabs,
selectedTabs,
setSelectedTabs,
hasProgress,
resetProgress,
pendingCloseAction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { useModalManagement, ModalType } from '../../../../hooks/useModalManagem
import { useProgressTracking } from '../../../../hooks/useProgressTracking';
import { ErrorModal } from '../modals/ErrorModal';
import SelectDocumentModal from '../modals/step_1/SelectDocumentModal';
import { ContentTypePickerModal } from '../modals/step_2/SelectContentTypeModal';
import { LoadingModal } from '../modals/LoadingModal';
import { ERROR_MESSAGES } from '../../../../utils/constants/messages';
import { SelectTabsModal } from '../modals/step_3/SelectTabsModal';
import { DocumentTabProps } from '../../../../utils/types';
import { ContentTypePickerModal } from '../modals/step_2/ContentTypePickerModal';

export interface ModalOrchestratorHandle {
startFlow: () => void;
Expand All @@ -26,6 +28,10 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
setDocumentId,
selectedContentTypes,
setSelectedContentTypes,
availableTabs,
setAvailableTabs,
selectedTabs,
setSelectedTabs,
hasProgress,
resetProgress: resetProgressTracking,
pendingCloseAction,
Expand All @@ -41,6 +47,7 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
resetProgressTracking();
closeModal(ModalType.UPLOAD);
closeModal(ModalType.CONTENT_TYPE_PICKER);
closeModal(ModalType.SELECT_TABS);
};

const handleUploadModalCloseRequest = (docId?: string) => {
Expand Down Expand Up @@ -74,6 +81,18 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
}
};

const handleSelectTabsCloseRequest = () => {
if (hasProgress) {
closeModal(ModalType.SELECT_TABS);
setPendingCloseAction(() => () => {
resetProgress();
});
openModal(ModalType.CONFIRM_CANCEL);
} else {
closeModal(ModalType.SELECT_TABS);
}
};

const handleConfirmCancel = () => {
closeModal(ModalType.CONFIRM_CANCEL);
if (pendingCloseAction) {
Expand All @@ -94,7 +113,13 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
// The modal already updates `selectedContentTypes` via `setSelectedContentTypes`, so we don't need to set it here.
void contentTypeIdsCsv;
// setSelectedContentTypes(contentTypes);
// TODO : open content tabs step
openModal(ModalType.SELECT_TABS);
};

const handleSelectTabsContinue = (tabs: DocumentTabProps[]) => {
setSelectedTabs(tabs);
closeModal(ModalType.SELECT_TABS);
// TODO: add preview step and redirect to it, using selectedTabs
};

const handleErrorPreviewModalClose = () => {
Expand Down Expand Up @@ -124,6 +149,16 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
setSelectedContentTypes={setSelectedContentTypes}
/>

<SelectTabsModal
isOpen={modalStates.isSelectTabsModalOpen}
onContinue={handleSelectTabsContinue}
onClose={handleSelectTabsCloseRequest}
availableTabs={availableTabs}
setAvailableTabs={setAvailableTabs}
selectedTabs={selectedTabs}
setSelectedTabs={setSelectedTabs}
/>

<ConfirmCancelModal
isOpen={modalStates.isConfirmCancelModalOpen}
onConfirm={handleConfirmCancel}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { css } from '@emotion/css';
import tokens from '@contentful/f36-tokens';

export const multiselect = css({
maxWidth: '74%',
});

export const pillsContainer = css({
marginRight: tokens.spacingXl,
maxHeight: '100px',
overflowY: 'scroll',
'&::-webkit-scrollbar': {
width: tokens.spacingXs,
height: tokens.spacingXs,
},
'&::-webkit-scrollbar-thumb': {
background: tokens.gray400,
borderRadius: tokens.borderRadiusSmall,
},
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState, useMemo, useRef } from 'react';
import { useEffect, useState, useMemo } from 'react';
import {
Button,
Flex,
Expand All @@ -11,6 +11,9 @@ import {
import { PageAppSDK } from '@contentful/app-sdk';
import { ContentTypeProps } from 'contentful-management';
import { css } from '@emotion/css';
import { useMultiselectScrollReflow } from '../../../../../hooks/useMultiselectReflow';
import { multiselect, pillsContainer } from './ContentTypePickerModal.styles';
import { truncateLabel } from '../../../../../utils/utils';

interface ContentTypePickerModalProps {
sdk: PageAppSDK;
Expand Down Expand Up @@ -41,7 +44,7 @@ export const ContentTypePickerModal = ({
const [isLoading, setIsLoading] = useState<boolean>(false);
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState<boolean>(false);
const [hasFetchError, setHasFetchError] = useState<boolean>(false);
const multiselectListRef = useRef<HTMLUListElement>(null);
const multiselectListRef = useMultiselectScrollReflow(selectedContentTypes);

const isInvalidSelection = useMemo(
() => selectedContentTypes.length === 0,
Expand Down Expand Up @@ -110,23 +113,6 @@ export const ContentTypePickerModal = ({
}
}, [isOpen, setSelectedContentTypes]);

useEffect(() => {
// Recalculate the Multiselect dropdown position when selection changes
if (multiselectListRef.current) {
const element = multiselectListRef.current;
const currentScroll = element.scrollTop;
const maxScroll = element.scrollHeight - element.clientHeight;

if (currentScroll >= maxScroll) {
element.scrollTop = currentScroll - 1;
} else {
element.scrollTop = currentScroll + 1;
}

element.scrollTop = currentScroll;
}
}, [selectedContentTypes]);

const onSearchValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value.toLowerCase().trim();
setFilteredContentTypes(
Expand Down Expand Up @@ -170,24 +156,29 @@ export const ContentTypePickerModal = ({
};

return (
<Modal title="Select content type(s)" isShown={isOpen} onClose={handleClose} size="medium">
<Modal title="Select content type(s)" isShown={isOpen} onClose={handleClose} size="large">
{() => (
<>
<Modal.Header title="Select content type(s)" onClose={handleClose} />
<Modal.Content className={css({ minHeight: '300px' })}>
<Modal.Content>
<Paragraph marginBottom="spacingM" color="gray700">
Select the content type(s) you would like to use with this document.
</Paragraph>
<FormControl isRequired isInvalid={isInvalidSelectionError || showFetchError}>
<FormControl
isRequired
isInvalid={isInvalidSelectionError || showFetchError}
marginBottom="none">
<FormControl.Label>Content type</FormControl.Label>
<Multiselect
className={multiselect}
searchProps={{
searchPlaceholder: 'Search content types',
onSearchValueChange,
}}
currentSelection={selectedContentTypes.map((ct) => ct.name)}
placeholder={getPlaceholderText()}
popoverProps={{
listMaxHeight: 300,
listRef: multiselectListRef,
}}>
{filteredContentTypes.map((ct) => (
Expand Down Expand Up @@ -218,11 +209,11 @@ export const ContentTypePickerModal = ({
</FormControl>

{selectedContentTypes.length > 0 && (
<Flex flexWrap="wrap" gap="spacingXs" marginTop="spacingS">
<Flex flexWrap="wrap" gap="spacingXs" marginTop="spacingS" className={pillsContainer}>
{selectedContentTypes.map((ct) => (
<Pill
key={ct.sys.id}
label={ct.name}
label={truncateLabel(ct.name)}
onClose={
isSubmitting
? undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { css } from '@emotion/css';
import tokens from '@contentful/f36-tokens';

export const multiselect = css({
maxWidth: '68%',
});

export const multiselectOption = css({
padding: tokens.spacing2Xs,
});

export const pillsContainer = css({
marginRight: tokens.spacingXl,
maxHeight: '100px',
overflowY: 'scroll',
'&::-webkit-scrollbar': {
width: tokens.spacingXs,
height: tokens.spacingXs,
},
'&::-webkit-scrollbar-thumb': {
background: tokens.gray400,
borderRadius: tokens.borderRadiusSmall,
},
});

export const modalControls = css({
paddingTop: '0',
});
Loading
Loading