Skip to content

Commit 7225f6d

Browse files
Merge pull request #45391 from code-dot-org/bethany/all-code-docs-table
Create a levelbuilder-facing table of all code docs
2 parents c777fbb + 2b3c84c commit 7225f6d

File tree

6 files changed

+570
-1
lines changed

6 files changed

+570
-1
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import React, {useEffect, useState} from 'react';
2+
import PropTypes from 'prop-types';
3+
import * as Table from 'reactabular-table';
4+
import queryString from 'query-string';
5+
import Button from '@cdo/apps/templates/Button';
6+
import StylizedBaseDialog from '@cdo/apps/componentLibrary/StylizedBaseDialog';
7+
import PaginationWrapper from '@cdo/apps/templates/PaginationWrapper';
8+
import CloneProgrammingExpressionDialog from './CloneProgrammingExpressionDialog';
9+
10+
const DEFAULT_VALUE = 'all';
11+
12+
/*
13+
* A component that fetches programming expressions and displays them in a
14+
* table. It includes filters for which expressions are fetched as well as the
15+
* ability to destroy and clone expressions.
16+
* This is a levelbuilder-facing component.
17+
*/
18+
export default function ProgrammingExpressionsTable({
19+
allProgrammingEnvironments,
20+
allCategories,
21+
hidden
22+
}) {
23+
const [selectedEnvironment, setSelectedEnvironment] = useState(DEFAULT_VALUE);
24+
const [selectedCategory, setSelectedCategory] = useState(DEFAULT_VALUE);
25+
const [programmingExpressions, setProgrammingExpressions] = useState([]);
26+
const [
27+
categoriesAvailableForSelect,
28+
setCategoriesAvailableForSelect
29+
] = useState(allCategories);
30+
const [itemToClone, setItemToClone] = useState(null);
31+
32+
const [itemToDelete, setItemToDelete] = useState(null);
33+
const [currentPage, setCurrentPage] = useState(1);
34+
const [numPages, setNumPages] = useState(1);
35+
const [error, setError] = useState(null);
36+
37+
const actionsCellFormatter = (actions, {rowData}) => {
38+
return (
39+
<div style={styles.actionsColumn}>
40+
<Button
41+
icon="edit"
42+
text=""
43+
href={rowData.editPath}
44+
target="_blank"
45+
__useDeprecatedTag
46+
color="teal"
47+
/>
48+
<Button
49+
onClick={() => setItemToClone(rowData)}
50+
text=""
51+
icon="clone"
52+
color="gray"
53+
style={{margin: 0}}
54+
/>
55+
<Button
56+
onClick={() => setItemToDelete(rowData)}
57+
text=""
58+
icon="trash"
59+
color="red"
60+
style={{margin: 0}}
61+
/>
62+
</div>
63+
);
64+
};
65+
66+
const fetchExpressions = (environmentId, categoryId, page) => {
67+
const data = {};
68+
if (environmentId !== DEFAULT_VALUE) {
69+
data.programmingEnvironmentId = environmentId;
70+
}
71+
if (categoryId !== DEFAULT_VALUE) {
72+
data.categoryId = categoryId;
73+
}
74+
data.page = page;
75+
const url =
76+
'/programming_expressions/get_filtered_expressions?' +
77+
queryString.stringify(data);
78+
let success = false;
79+
fetch(url)
80+
.then(response => {
81+
if (response.ok) {
82+
success = true;
83+
} else {
84+
setError(response.statusText);
85+
}
86+
return response.json();
87+
})
88+
.then(data => {
89+
if (success) {
90+
setProgrammingExpressions(data.expressions);
91+
setNumPages(data.numPages);
92+
}
93+
})
94+
.catch(error => {
95+
setError(error);
96+
});
97+
};
98+
99+
const destroyExpression = (destroyPath, callback) => {
100+
fetch(destroyPath, {
101+
method: 'DELETE',
102+
headers: {'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')}
103+
}).then(response => {
104+
if (response.ok) {
105+
setItemToDelete(null);
106+
fetchExpressions(selectedEnvironment, selectedCategory, currentPage);
107+
} else {
108+
setError(response.statusText);
109+
}
110+
});
111+
};
112+
113+
useEffect(() => {
114+
fetchExpressions(selectedEnvironment, selectedCategory, currentPage);
115+
}, [selectedEnvironment, selectedCategory, currentPage]);
116+
117+
const onEnvironmentSelect = e => {
118+
const newSelectedEnvironment = e.target.value;
119+
setSelectedEnvironment(newSelectedEnvironment);
120+
if (newSelectedEnvironment === DEFAULT_VALUE) {
121+
setCategoriesAvailableForSelect(allCategories);
122+
} else {
123+
setCategoriesAvailableForSelect(
124+
allCategories.filter(
125+
cat => String(cat.envId) === String(newSelectedEnvironment)
126+
)
127+
);
128+
if (String(selectedCategory.envId) !== newSelectedEnvironment) {
129+
setSelectedCategory(DEFAULT_VALUE);
130+
}
131+
}
132+
setCurrentPage(1);
133+
};
134+
135+
const getColumns = () => {
136+
return [
137+
{
138+
property: 'actions',
139+
header: {
140+
label: 'Actions',
141+
props: {
142+
style: {width: '10%'}
143+
}
144+
},
145+
cell: {
146+
formatters: [actionsCellFormatter]
147+
}
148+
},
149+
150+
{
151+
property: 'name',
152+
header: {
153+
label: 'Name'
154+
}
155+
},
156+
{
157+
property: 'environmentTitle',
158+
header: {
159+
label: 'IDE'
160+
}
161+
},
162+
{
163+
property: 'categoryName',
164+
header: {
165+
label: 'Category'
166+
}
167+
}
168+
];
169+
};
170+
171+
const renderFilters = () => {
172+
return (
173+
<>
174+
<select
175+
onChange={onEnvironmentSelect}
176+
value={selectedEnvironment}
177+
style={{marginRight: 7}}
178+
>
179+
<option value={DEFAULT_VALUE}>All IDEs</option>
180+
{allProgrammingEnvironments.map(env => (
181+
<option key={env.id} value={env.id}>
182+
{env.title || env.name}
183+
</option>
184+
))}
185+
</select>
186+
<select
187+
onChange={e => {
188+
setSelectedCategory(e.target.value);
189+
setCurrentPage(1);
190+
}}
191+
value={selectedCategory}
192+
>
193+
<option value={DEFAULT_VALUE}>All Categories</option>
194+
{categoriesAvailableForSelect.map(category => (
195+
<option key={category.id} value={category.id}>
196+
{category.formattedName}
197+
</option>
198+
))}
199+
</select>
200+
</>
201+
);
202+
};
203+
204+
const renderDialogs = () => {
205+
return (
206+
<>
207+
{!!itemToDelete && (
208+
<StylizedBaseDialog
209+
body={`Are you sure you want to remove ${itemToDelete.name ||
210+
itemToDelete.key} and its associated code doc?`}
211+
handleConfirmation={() => {
212+
destroyExpression(
213+
`/programming_expressions/${itemToDelete.id}`,
214+
() => {
215+
setItemToDelete(null);
216+
fetchExpressions(
217+
selectedEnvironment,
218+
selectedCategory,
219+
currentPage
220+
);
221+
}
222+
);
223+
}}
224+
handleClose={() => setItemToDelete(null)}
225+
isOpen
226+
/>
227+
)}
228+
{!!itemToClone && (
229+
<CloneProgrammingExpressionDialog
230+
itemToClone={itemToClone}
231+
programmingEnvironmentsForSelect={allProgrammingEnvironments}
232+
categoriesForSelect={allCategories}
233+
onClose={() => {
234+
setItemToClone(null);
235+
fetchExpressions();
236+
}}
237+
/>
238+
)}
239+
</>
240+
);
241+
};
242+
243+
if (hidden) {
244+
return null;
245+
}
246+
return (
247+
<>
248+
{renderFilters()}
249+
{error && <div>{error}</div>}
250+
<Table.Provider columns={getColumns()} style={{width: '100%'}}>
251+
<Table.Header />
252+
<Table.Body rows={programmingExpressions} rowKey="id" />
253+
</Table.Provider>
254+
<PaginationWrapper
255+
totalPages={numPages}
256+
currentPage={currentPage}
257+
onChangePage={setCurrentPage}
258+
/>
259+
{renderDialogs()}
260+
</>
261+
);
262+
}
263+
264+
ProgrammingExpressionsTable.propTypes = {
265+
allProgrammingEnvironments: PropTypes.arrayOf(PropTypes.object).isRequired,
266+
allCategories: PropTypes.arrayOf(PropTypes.object).isRequired,
267+
hidden: PropTypes.bool
268+
};
269+
270+
const styles = {
271+
actionsColumn: {
272+
display: 'flex',
273+
justifyContent: 'flex-start',
274+
backgroundColor: 'white'
275+
}
276+
};

0 commit comments

Comments
 (0)