Skip to content

Commit fc9ccb0

Browse files
ryan953scttcpergetsantry[bot]
authored
ref: Convert ContextPickerModal to an FC (#83625)
Test Notes: This modal is behind Settings Search, the cmd+K search, and Help Search in the sidebar. Search works as expected with one special case I discovered: Suggestions may include the `configUrl` field. In this case clicking on the suggestion will trigger that url to load and be rendered as a 2nd-level search list. To see this in action, go to Org Settings and search for something like "Github". One of the results will be the GitHub Integration, where you can connect to a github org. Clicking on that result will open the 2nd screen with a list of available github orgs to choose. | First search | list from `configUrl` | | --- | --- | | <img width="820" alt="SCR-20250117-iogq" src="https://github.com/user-attachments/assets/4b65213a-f90e-427b-96c7-e5387b992de3" /> | <img width="696" alt="SCR-20250117-iolm" src="https://github.com/user-attachments/assets/a6b9161c-b7db-4643-b67b-5795b6cd9c8d" /> Also before, if there was nothing returned from the `configUrl` url, only an empty array for example, we'd see an empty modal: <img width="830" alt="SCR-20250117-jdyq" src="https://github.com/user-attachments/assets/ed8003f5-0099-48d7-aea8-1e59f3115293" /> This PR adds a call to `sharedProps.onFinish(sharedProps.nextPath);` which will redirect the user to the root of the config page instead of showing the blank modal. In my testing the root is something like `/settings/integrations/github/`. When the modal is not blank, and there is a list of 2nd-level things to pick from, the url would've been constructed as ` `"/settings/integrations/github/" + pickedItem.value + "/"`. So redirecting just into the root is a little bit of a guess and might break in some cases. --------- Co-authored-by: Scott Cooper <[email protected]> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 7c60d3c commit fc9ccb0

File tree

1 file changed

+84
-87
lines changed

1 file changed

+84
-87
lines changed

Diff for: static/app/components/contextPickerModal.tsx

+84-87
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
import {Component, Fragment} from 'react';
1+
import {Component, type Dispatch, Fragment, type SetStateAction, useState} from 'react';
22
import {components} from 'react-select';
33
import styled from '@emotion/styled';
44
import type {Query} from 'history';
55

66
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
7-
import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
87
import type {StylesConfig} from 'sentry/components/forms/controls/selectControl';
98
import SelectControl from 'sentry/components/forms/controls/selectControl';
109
import IdBadge from 'sentry/components/idBadge';
1110
import Link from 'sentry/components/links/link';
11+
import LoadingError from 'sentry/components/loadingError';
1212
import LoadingIndicator from 'sentry/components/loadingIndicator';
1313
import {t, tct} from 'sentry/locale';
1414
import ConfigStore from 'sentry/stores/configStore';
1515
import OrganizationsStore from 'sentry/stores/organizationsStore';
1616
import OrganizationStore from 'sentry/stores/organizationStore';
17+
import {useLegacyStore} from 'sentry/stores/useLegacyStore';
1718
import {space} from 'sentry/styles/space';
1819
import type {Integration} from 'sentry/types/integrations';
1920
import type {Organization} from 'sentry/types/organization';
2021
import type {Project} from 'sentry/types/project';
2122
import Projects from 'sentry/utils/projects';
23+
import {useApiQuery} from 'sentry/utils/queryClient';
2224
import replaceRouterParams from 'sentry/utils/replaceRouterParams';
2325
import IntegrationIcon from 'sentry/views/settings/organizationIntegrations/integrationIcon';
2426

@@ -61,7 +63,7 @@ type Props = SharedProps & {
6163
/**
6264
* Organization slug
6365
*/
64-
organization: string;
66+
organization: string | undefined;
6567

6668
/**
6769
* List of available organizations
@@ -128,7 +130,7 @@ class ContextPickerModal extends Component<Props> {
128130
navigateIfFinish = (
129131
organizations: Array<{slug: string}>,
130132
projects: Array<{slug: string}>,
131-
latestOrg: string = this.props.organization
133+
latestOrg = this.props.organization
132134
) => {
133135
const {needProject, onFinish, nextPath, integrationConfigs} = this.props;
134136
const {isSuperuser} = ConfigStore.get('user') || {};
@@ -411,103 +413,98 @@ type ContainerProps = SharedProps & {
411413
* List of slugs we want to be able to choose from
412414
*/
413415
projectSlugs?: string[];
414-
} & DeprecatedAsyncComponent['props'];
416+
};
415417

416-
type ContainerState = {
417-
organizations: Organization[];
418-
integrationConfigs?: Integration[];
419-
selectedOrganization?: string;
420-
} & DeprecatedAsyncComponent['state'];
421-
422-
class ContextPickerModalContainer extends DeprecatedAsyncComponent<
423-
ContainerProps,
424-
ContainerState
425-
> {
426-
getDefaultState() {
427-
const storeState = OrganizationStore.get();
428-
return {
429-
...super.getDefaultState(),
430-
organizations: OrganizationsStore.getAll(),
431-
selectedOrganization: storeState.organization?.slug,
432-
};
433-
}
418+
export default function ContextPickerModalContainer(props: ContainerProps) {
419+
const {configUrl, projectSlugs, ...sharedProps} = props;
434420

435-
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
436-
const {configUrl} = this.props;
437-
if (configUrl) {
438-
return [['integrationConfigs', configUrl]];
439-
}
440-
return [];
441-
}
421+
const {organizations} = useLegacyStore(OrganizationsStore);
442422

443-
componentWillUnmount() {
444-
this.unlistener?.();
445-
}
423+
const {organization} = useLegacyStore(OrganizationStore);
424+
const [selectedOrgSlug, setSelectedOrgSlug] = useState(organization?.slug);
446425

447-
unlistener = OrganizationsStore.listen(
448-
(organizations: Organization[]) => this.setState({organizations}),
449-
undefined
450-
);
451-
452-
handleSelectOrganization = (organizationSlug: string) => {
453-
this.setState({selectedOrganization: organizationSlug});
454-
};
455-
456-
renderModal({
457-
projects,
458-
initiallyLoaded,
459-
integrationConfigs,
460-
}: {
461-
initiallyLoaded?: boolean;
462-
integrationConfigs?: Integration[];
463-
projects?: Project[];
464-
}) {
426+
if (configUrl) {
465427
return (
466-
<ContextPickerModal
467-
{...this.props}
468-
projects={projects || []}
469-
loading={!initiallyLoaded}
470-
organizations={this.state.organizations}
471-
organization={this.state.selectedOrganization!}
472-
onSelectOrganization={this.handleSelectOrganization}
473-
integrationConfigs={integrationConfigs || []}
474-
allowAllProjectsSelection={this.props.allowAllProjectsSelection}
428+
<ConfigUrlContainer
429+
configUrl={configUrl}
430+
selectedOrgSlug={selectedOrgSlug}
431+
setSelectedOrgSlug={setSelectedOrgSlug}
432+
{...sharedProps}
475433
/>
476434
);
477435
}
436+
if (selectedOrgSlug) {
437+
return (
438+
<Projects
439+
orgId={selectedOrgSlug}
440+
allProjects={!projectSlugs?.length}
441+
slugs={projectSlugs}
442+
>
443+
{({projects, initiallyLoaded}) => (
444+
<ContextPickerModal
445+
{...sharedProps}
446+
projects={projects as Project[]}
447+
loading={!initiallyLoaded}
448+
organizations={organizations}
449+
organization={selectedOrgSlug}
450+
onSelectOrganization={setSelectedOrgSlug}
451+
integrationConfigs={[]}
452+
/>
453+
)}
454+
</Projects>
455+
);
456+
}
478457

479-
render() {
480-
const {projectSlugs, configUrl} = this.props;
458+
return (
459+
<ContextPickerModal
460+
{...sharedProps}
461+
projects={[]}
462+
loading
463+
organizations={organizations}
464+
organization={selectedOrgSlug!}
465+
onSelectOrganization={setSelectedOrgSlug}
466+
integrationConfigs={[]}
467+
/>
468+
);
469+
}
481470

482-
if (configUrl && this.state.loading) {
483-
return <LoadingIndicator />;
484-
}
485-
if (this.state.integrationConfigs?.length) {
486-
return this.renderModal({
487-
integrationConfigs: this.state.integrationConfigs,
488-
initiallyLoaded: !this.state.loading,
489-
});
490-
}
491-
if (this.state.selectedOrganization) {
492-
return (
493-
<Projects
494-
orgId={this.state.selectedOrganization}
495-
allProjects={!projectSlugs?.length}
496-
slugs={projectSlugs}
497-
>
498-
{({projects, initiallyLoaded}) =>
499-
this.renderModal({projects: projects as Project[], initiallyLoaded})
500-
}
501-
</Projects>
502-
);
503-
}
471+
function ConfigUrlContainer(
472+
props: SharedProps & {
473+
configUrl: string;
474+
selectedOrgSlug: string | undefined;
475+
setSelectedOrgSlug: Dispatch<SetStateAction<string | undefined>>;
476+
}
477+
) {
478+
const {configUrl, selectedOrgSlug, setSelectedOrgSlug, ...sharedProps} = props;
479+
480+
const {organizations} = useLegacyStore(OrganizationsStore);
504481

505-
return this.renderModal({});
482+
const {data, isError, isPending, refetch} = useApiQuery<Integration[]>([configUrl], {
483+
staleTime: Infinity,
484+
});
485+
486+
if (isPending) {
487+
return <LoadingIndicator />;
488+
}
489+
if (isError) {
490+
return <LoadingError onRetry={refetch} />;
506491
}
492+
if (!data.length) {
493+
sharedProps.onFinish(sharedProps.nextPath);
494+
}
495+
return (
496+
<ContextPickerModal
497+
{...sharedProps}
498+
projects={[]}
499+
loading={isPending}
500+
organizations={organizations}
501+
organization={selectedOrgSlug!}
502+
onSelectOrganization={setSelectedOrgSlug}
503+
integrationConfigs={data}
504+
/>
505+
);
507506
}
508507

509-
export default ContextPickerModalContainer;
510-
511508
const StyledSelectControl = styled(SelectControl)`
512509
margin-top: ${space(1)};
513510
`;

0 commit comments

Comments
 (0)