Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import {
2 namedOperations,
3 useGQLBulkActionExecutionMutation,
4 useGQLBulkActionsFormDataQuery,
5} from '@/graphql/generated';
6import { stripTypename } from '@/graphql/inputHelpers';
7import { ItemIdentifier } from '@roostorg/types';
8import { Select } from 'antd';
9import orderBy from 'lodash/orderBy';
10import { useCallback, useMemo, useState } from 'react';
11
12import { selectFilterByLabelOption } from '@/webpages/dashboard/components/antDesignUtils';
13import CoopButton from '@/webpages/dashboard/components/CoopButton';
14import CoopModal from '@/webpages/dashboard/components/CoopModal';
15import PolicyDropdown from '@/webpages/dashboard/components/PolicyDropdown';
16
17const { Option } = Select;
18
19export default function ItemAction(props: {
20 itemIdentifier: ItemIdentifier;
21 title?: string;
22}) {
23 const { itemIdentifier, title = 'Take action on this item' } = props;
24
25 const { data: queryData } = useGQLBulkActionsFormDataQuery();
26 const [bulkAction, { loading }] = useGQLBulkActionExecutionMutation({
27 refetchQueries: [
28 namedOperations.Query.ItemActionHistory,
29 namedOperations.Query.GetRecentDecisions,
30 ],
31 onCompleted: (data) => {
32 const results = data?.bulkExecuteActions?.results ?? [];
33 const anyFailed = results.some((r) => r.success === false);
34 if (anyFailed) {
35 setModalBody(
36 'One or more actions failed. The callback URL may have returned an error. If your org requires a policy for decisions, select a policy and try again.',
37 );
38 } else {
39 setModalBody('Actions submitted successfully.');
40 }
41 setShowModal(true);
42 },
43 onError: () => {
44 setModalBody('Error submitting actions. Please try again.');
45 setShowModal(true);
46 },
47 });
48
49 const [selectedPolicyIds, setSelectedPolicyIds] = useState<string[]>([]);
50 const [selectedActionIds, setSelectedActionIds] = useState<string[]>([]);
51 const [showModal, setShowModal] = useState(false);
52 const [modalBody, setModalBody] = useState<string>('');
53
54 const eligibleActions = (queryData?.myOrg?.actions ?? []).filter((it) =>
55 it.itemTypes.map((it) => it.id).includes(itemIdentifier.typeId),
56 );
57
58 const selectOnChange = useCallback(
59 (actionIds: string[]) => setSelectedActionIds(actionIds),
60 [],
61 );
62
63 const selectDropdownRender = useCallback(
64 (menu: React.ReactElement) => {
65 if (eligibleActions.length === 0) {
66 return (
67 <div>
68 <div className="text-coop-alert-red">No actions available</div>
69 {menu}
70 </div>
71 );
72 }
73 return menu;
74 },
75 [eligibleActions.length],
76 );
77
78 const policies = queryData?.myOrg?.policies;
79 const policiesMemo = useMemo(
80 () => (policies ? policies.map((p) => stripTypename(p)) : []),
81 [policies],
82 );
83
84 const policiesDropdownOnChange = useCallback(
85 (policyIds: string | readonly string[]) => {
86 if (Array.isArray(policyIds)) {
87 setSelectedPolicyIds(policyIds.map((id) => id.toString()));
88 } else {
89 // NB: This cast is required because of a longstanding typescript
90 // issue. See https://github.com/microsoft/TypeScript/issues/17002 for
91 // more details.
92 const policyId = policyIds satisfies
93 | string
94 | readonly string[] as string;
95 setSelectedPolicyIds([policyId]);
96 }
97 },
98 [],
99 );
100
101 const buttonOnClick = useCallback(
102 async () =>
103 bulkAction({
104 variables: {
105 input: {
106 itemTypeId: itemIdentifier.typeId,
107 actionIds: selectedActionIds,
108 itemIds: [itemIdentifier.id],
109 policyIds: selectedPolicyIds,
110 },
111 },
112 }),
113 [
114 bulkAction,
115 itemIdentifier.id,
116 itemIdentifier.typeId,
117 selectedActionIds,
118 selectedPolicyIds,
119 ],
120 );
121
122 const modalOnClose = useCallback(() => setShowModal(false), []);
123
124 if (eligibleActions.length === 0) {
125 return null;
126 }
127
128 return (
129 <div className="flex flex-col">
130 <div className="flex flex-col items-start mb-2">
131 <div className="text-base font-semibold">{title}</div>
132 </div>
133 <div className="flex flex-row gap-4">
134 <div className="flex flex-col items-start">
135 <div>
136 <Select
137 className="w-80"
138 mode="multiple"
139 maxTagCount={1}
140 placeholder="Select action"
141 dropdownMatchSelectWidth={false}
142 filterOption={selectFilterByLabelOption}
143 onChange={selectOnChange}
144 dropdownRender={selectDropdownRender}
145 >
146 {orderBy(eligibleActions, ['name']).map((action) => (
147 <Option key={action.id} value={action.id} label={action.name}>
148 {action.name}
149 </Option>
150 ))}
151 </Select>
152 </div>
153 </div>
154 <div className="flex flex-col items-start">
155 <div>
156 <PolicyDropdown
157 className="w-80"
158 policies={policiesMemo}
159 maxTagCount={1}
160 onChange={policiesDropdownOnChange}
161 selectedPolicyIds={selectedPolicyIds}
162 multiple={
163 queryData?.myOrg?.allowMultiplePoliciesPerAction ?? false
164 }
165 />
166 </div>
167 </div>
168 <CoopButton
169 title="Submit Actions"
170 size="small"
171 onClick={buttonOnClick}
172 loading={loading}
173 disabled={selectedActionIds.length === 0}
174 />
175 </div>
176 <CoopModal visible={showModal} onClose={modalOnClose}>
177 {modalBody}
178 </CoopModal>
179 </div>
180 );
181}