3.2 KB94 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {PrimitiveAtom} from 'jotai';
9import type {ComponentProps} from 'react';
10import type {Operation} from './operations/Operation';
11
12import {Button} from 'isl-components/Button';
13import {Icon} from 'isl-components/Icon';
14import {atom, useAtom} from 'jotai';
15import {isPromise} from 'shared/utils';
16import {atomFamilyWeak} from './jotaiUtils';
17import {useRunOperation} from './operationsState';
18import {useMostRecentPendingOperation} from './previews';
19
20/**
21 * Wrapper around VSCodeButton intended for buttons which runOperations.
22 * It remembers what Operation it spawns, and leaves the button disabled
23 * if that operation is the most recent pending operation (queued or running).
24 * If any further operations have been queued, then button will be re-enabled
25 * (to allow queueing it again which may be valid).
26 *
27 * Note: do not use "useRunOperation" directly in the "runOperation", instead return the operation instance.
28 *
29 * runOperation may also return an Array of operations, if it queues multiple.
30 * If the pending operation is ANY of those operations, the button will be disabled.
31 *
32 * Provide a `contextKey` to describe what this button is doing, to correlate with its resulting operation.
33 * Generally this is just the name of the operation, but for operations that e.g. depend on a commit,
34 * it may also include the commit hash so not every instance of this button is disabled.
35 */
36export function OperationDisabledButton({
37 contextKey,
38 runOperation,
39 disabled,
40 children,
41 icon,
42 ...rest
43}: {
44 contextKey: string;
45 runOperation: () =>
46 | Operation
47 | Array<Operation>
48 | undefined
49 | Promise<Operation | Array<Operation> | undefined>;
50 children?: React.ReactNode;
51 disabled?: boolean;
52 icon?: React.ReactNode;
53 className?: string;
54} & (Omit<ComponentProps<typeof Button>, 'icon' | 'primary'> & {kind?: string})) {
55 const actuallyRunOperation = useRunOperation();
56 const pendingOperation = useMostRecentPendingOperation();
57 const [triggeredOperationId, setTriggeredOperationId] = useAtom(
58 operationButtonDisableState(contextKey),
59 );
60 const isRunningThisOperation =
61 pendingOperation != null && triggeredOperationId?.includes(pendingOperation.id);
62
63 return (
64 <Button
65 {...rest}
66 disabled={isRunningThisOperation || disabled}
67 onClick={async () => {
68 const opOrOpsResult = runOperation();
69 let opOrOps;
70 if (isPromise(opOrOpsResult)) {
71 opOrOps = await opOrOpsResult;
72 } else {
73 opOrOps = opOrOpsResult;
74 }
75 if (opOrOps == null) {
76 return;
77 }
78 const ops = Array.isArray(opOrOps) ? opOrOps : [opOrOps];
79 for (const op of ops) {
80 actuallyRunOperation(op);
81 }
82 setTriggeredOperationId(ops.map(op => op.id));
83 }}>
84 {isRunningThisOperation ? <Icon icon="loading" slot="start" /> : (icon ?? null)}
85 {children}
86 </Button>
87 );
88}
89
90const operationButtonDisableState = atomFamilyWeak<
91 string,
92 PrimitiveAtom<Array<string> | undefined>
93>((_param: string | undefined) => atom<Array<string> | undefined>(undefined));
94