|
| 1 | +/* |
| 2 | + * Copyright Red Hat, Inc. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +// HACK: Backstage scaffolder >=1.36.1 (commit 527cf88) updated validation.ts |
| 18 | +// to check for "bitbucketCloud"/"bitbucketServer" types, but RepoUrlPicker.tsx |
| 19 | +// still only checks hostType === "bitbucket". Since |
| 20 | +// integrationApi.byHost("bitbucket.org") returns a BitbucketCloudIntegration |
| 21 | +// (type "bitbucketCloud"), the BitbucketRepoPicker (workspace/project fields) |
| 22 | +// never renders. This wrapper fixes the type mismatch by intercepting the |
| 23 | +// scmIntegrationsApi within the component's React context. |
| 24 | +// |
| 25 | +// Remove once upstream fixes RepoUrlPicker to handle the new type strings. |
| 26 | +// See https://redhat.atlassian.net/browse/FLPATH-4033 |
| 27 | + |
| 28 | +import { createElement, useMemo } from 'react'; |
| 29 | +import { |
| 30 | + type ApiHolder, |
| 31 | + type ApiRef, |
| 32 | + getComponentData, |
| 33 | + useApi, |
| 34 | + useApiHolder, |
| 35 | +} from '@backstage/core-plugin-api'; |
| 36 | +import { ApiProvider } from '@backstage/core-app-api'; |
| 37 | +import { scmIntegrationsApiRef } from '@backstage/integration-react'; |
| 38 | +import { |
| 39 | + type FieldExtensionComponentProps, |
| 40 | + type FieldExtensionOptions, |
| 41 | +} from '@backstage/plugin-scaffolder-react'; |
| 42 | +import { RepoUrlPickerFieldExtension } from '@backstage/plugin-scaffolder'; |
| 43 | + |
| 44 | +type IntegrationsApi = |
| 45 | + typeof scmIntegrationsApiRef extends ApiRef<infer T> ? T : never; |
| 46 | + |
| 47 | +// Extract the original RepoUrlPicker component from the field extension's |
| 48 | +// attached component data. This avoids importing from internal dist paths |
| 49 | +// which are blocked by the package's "exports" field. |
| 50 | +const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; |
| 51 | +const fieldExtensionData = getComponentData<FieldExtensionOptions>( |
| 52 | + createElement(RepoUrlPickerFieldExtension), |
| 53 | + FIELD_EXTENSION_KEY, |
| 54 | +); |
| 55 | + |
| 56 | +if (!fieldExtensionData?.component) { |
| 57 | + throw new Error( |
| 58 | + 'Failed to extract RepoUrlPicker component from RepoUrlPickerFieldExtension. ' + |
| 59 | + 'The Backstage scaffolder field extension data key may have changed.', |
| 60 | + ); |
| 61 | +} |
| 62 | + |
| 63 | +const OriginalRepoUrlPicker = fieldExtensionData.component; |
| 64 | + |
| 65 | +const BITBUCKET_TYPES = new Set(['bitbucketCloud', 'bitbucketServer']); |
| 66 | + |
| 67 | +/** |
| 68 | + * Wraps the IntegrationsApi so that byHost() remaps "bitbucketCloud" and |
| 69 | + * "bitbucketServer" to "bitbucket", making the upstream RepoUrlPicker render |
| 70 | + * the BitbucketRepoPicker component. |
| 71 | + */ |
| 72 | +/** @internal Exported for testing only. */ |
| 73 | +export function wrapIntegrationsApi(api: IntegrationsApi): IntegrationsApi { |
| 74 | + return new Proxy(api, { |
| 75 | + get(target, prop, receiver) { |
| 76 | + if (prop !== 'byHost') { |
| 77 | + return Reflect.get(target, prop, receiver); |
| 78 | + } |
| 79 | + |
| 80 | + return (host: string) => { |
| 81 | + const integration = target.byHost(host); |
| 82 | + if (!integration || !BITBUCKET_TYPES.has(integration.type)) { |
| 83 | + return integration; |
| 84 | + } |
| 85 | + |
| 86 | + // TODO: Remove this type remapping once upstream decouples |
| 87 | + // "bitbucketCloud" / "bitbucketServer" in RepoUrlPicker. |
| 88 | + // See https://redhat.atlassian.net/browse/FLPATH-4033 |
| 89 | + return new Proxy(integration, { |
| 90 | + get(innerTarget, innerProp, innerReceiver) { |
| 91 | + if (innerProp === 'type') { |
| 92 | + return 'bitbucket'; |
| 93 | + } |
| 94 | + return Reflect.get(innerTarget, innerProp, innerReceiver); |
| 95 | + }, |
| 96 | + }); |
| 97 | + }; |
| 98 | + }, |
| 99 | + }); |
| 100 | +} |
| 101 | + |
| 102 | +/** |
| 103 | + * Creates an ApiHolder that intercepts scmIntegrationsApiRef to return a |
| 104 | + * wrapped version, delegating all other API lookups to the parent holder. |
| 105 | + */ |
| 106 | +function createWrappedApiHolder( |
| 107 | + parentHolder: ApiHolder, |
| 108 | + wrappedIntegrations: IntegrationsApi, |
| 109 | +): ApiHolder { |
| 110 | + return { |
| 111 | + get<T>(ref: ApiRef<T>): T | undefined { |
| 112 | + if (ref === scmIntegrationsApiRef) { |
| 113 | + return wrappedIntegrations as unknown as T; |
| 114 | + } |
| 115 | + return parentHolder.get(ref); |
| 116 | + }, |
| 117 | + }; |
| 118 | +} |
| 119 | + |
| 120 | +/** |
| 121 | + * Wrapper around Backstage's RepoUrlPicker that fixes the Bitbucket Cloud/Server |
| 122 | + * type mismatch. Provides a scoped API context override so the inner |
| 123 | + * RepoUrlPicker sees "bitbucket" as the host type and renders the |
| 124 | + * BitbucketRepoPicker with workspace/project fields. |
| 125 | + */ |
| 126 | +export function RepoUrlPickerWithBitbucketFix( |
| 127 | + props: FieldExtensionComponentProps<string>, |
| 128 | +) { |
| 129 | + const parentHolder = useApiHolder(); |
| 130 | + const integrationsApi = useApi(scmIntegrationsApiRef); |
| 131 | + |
| 132 | + const wrappedHolder = useMemo( |
| 133 | + () => |
| 134 | + createWrappedApiHolder( |
| 135 | + parentHolder, |
| 136 | + wrapIntegrationsApi(integrationsApi), |
| 137 | + ), |
| 138 | + [parentHolder, integrationsApi], |
| 139 | + ); |
| 140 | + |
| 141 | + return ( |
| 142 | + <ApiProvider apis={wrappedHolder}> |
| 143 | + <OriginalRepoUrlPicker |
| 144 | + {...(props as FieldExtensionComponentProps<unknown>)} |
| 145 | + /> |
| 146 | + </ApiProvider> |
| 147 | + ); |
| 148 | +} |
0 commit comments