addons/isl/src/OperationDisabledButton.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {PrimitiveAtom} from 'jotai';
b69ab319import type {ComponentProps} from 'react';
b69ab3110import type {Operation} from './operations/Operation';
b69ab3111
b69ab3112import {Button} from 'isl-components/Button';
b69ab3113import {Icon} from 'isl-components/Icon';
b69ab3114import {atom, useAtom} from 'jotai';
b69ab3115import {isPromise} from 'shared/utils';
b69ab3116import {atomFamilyWeak} from './jotaiUtils';
b69ab3117import {useRunOperation} from './operationsState';
b69ab3118import {useMostRecentPendingOperation} from './previews';
b69ab3119
b69ab3120/**
b69ab3121 * Wrapper around VSCodeButton intended for buttons which runOperations.
b69ab3122 * It remembers what Operation it spawns, and leaves the button disabled
b69ab3123 * if that operation is the most recent pending operation (queued or running).
b69ab3124 * If any further operations have been queued, then button will be re-enabled
b69ab3125 * (to allow queueing it again which may be valid).
b69ab3126 *
b69ab3127 * Note: do not use "useRunOperation" directly in the "runOperation", instead return the operation instance.
b69ab3128 *
b69ab3129 * runOperation may also return an Array of operations, if it queues multiple.
b69ab3130 * If the pending operation is ANY of those operations, the button will be disabled.
b69ab3131 *
b69ab3132 * Provide a `contextKey` to describe what this button is doing, to correlate with its resulting operation.
b69ab3133 * Generally this is just the name of the operation, but for operations that e.g. depend on a commit,
b69ab3134 * it may also include the commit hash so not every instance of this button is disabled.
b69ab3135 */
b69ab3136export function OperationDisabledButton({
b69ab3137 contextKey,
b69ab3138 runOperation,
b69ab3139 disabled,
b69ab3140 children,
b69ab3141 icon,
b69ab3142 ...rest
b69ab3143}: {
b69ab3144 contextKey: string;
b69ab3145 runOperation: () =>
b69ab3146 | Operation
b69ab3147 | Array<Operation>
b69ab3148 | undefined
b69ab3149 | Promise<Operation | Array<Operation> | undefined>;
b69ab3150 children?: React.ReactNode;
b69ab3151 disabled?: boolean;
b69ab3152 icon?: React.ReactNode;
b69ab3153 className?: string;
b69ab3154} & (Omit<ComponentProps<typeof Button>, 'icon' | 'primary'> & {kind?: string})) {
b69ab3155 const actuallyRunOperation = useRunOperation();
b69ab3156 const pendingOperation = useMostRecentPendingOperation();
b69ab3157 const [triggeredOperationId, setTriggeredOperationId] = useAtom(
b69ab3158 operationButtonDisableState(contextKey),
b69ab3159 );
b69ab3160 const isRunningThisOperation =
b69ab3161 pendingOperation != null && triggeredOperationId?.includes(pendingOperation.id);
b69ab3162
b69ab3163 return (
b69ab3164 <Button
b69ab3165 {...rest}
b69ab3166 disabled={isRunningThisOperation || disabled}
b69ab3167 onClick={async () => {
b69ab3168 const opOrOpsResult = runOperation();
b69ab3169 let opOrOps;
b69ab3170 if (isPromise(opOrOpsResult)) {
b69ab3171 opOrOps = await opOrOpsResult;
b69ab3172 } else {
b69ab3173 opOrOps = opOrOpsResult;
b69ab3174 }
b69ab3175 if (opOrOps == null) {
b69ab3176 return;
b69ab3177 }
b69ab3178 const ops = Array.isArray(opOrOps) ? opOrOps : [opOrOps];
b69ab3179 for (const op of ops) {
b69ab3180 actuallyRunOperation(op);
b69ab3181 }
b69ab3182 setTriggeredOperationId(ops.map(op => op.id));
b69ab3183 }}>
b69ab3184 {isRunningThisOperation ? <Icon icon="loading" slot="start" /> : (icon ?? null)}
b69ab3185 {children}
b69ab3186 </Button>
b69ab3187 );
b69ab3188}
b69ab3189
b69ab3190const operationButtonDisableState = atomFamilyWeak<
b69ab3191 string,
b69ab3192 PrimitiveAtom<Array<string> | undefined>
b69ab3193>((_param: string | undefined) => atom<Array<string> | undefined>(undefined));