-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[AJ-1277] select and delete rows #5090
Changes from all commits
4ea75a6
2f29d0f
c6be257
ece9d2d
f101dfe
0253f4e
95b8fed
55fc585
9f23736
f9e609f
64bfba1
4fb0a71
512c57f
405e9be
267dc5a
c433dd9
799adf1
731d503
151869f
0dab7b7
d379ddd
e3b019b
93a95e3
c9d731e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import _ from 'lodash/fp'; | ||
import { useState } from 'react'; | ||
import { b, div, h } from 'react-hyperscript-helpers'; | ||
import { absoluteSpinnerOverlay, DeleteConfirmationModal } from 'src/components/common'; | ||
import { Ajax } from 'src/libs/ajax'; | ||
import colors from 'src/libs/colors'; | ||
import { reportError } from 'src/libs/error'; | ||
import * as Utils from 'src/libs/utils'; | ||
|
||
export const RecordDeleter = ({ onDismiss, onSuccess, dataProvider, collectionId, selectedRecords, runningSubmissionsCount }) => { | ||
const [additionalDeletions, setAdditionalDeletions] = useState([]); | ||
const [deleting, setDeleting] = useState(false); | ||
|
||
const selectedKeys = _.keys(selectedRecords); | ||
|
||
const doDelete = async () => { | ||
const recordsToDelete = _.flow( | ||
_.map(({ name: entityName, entityType }) => ({ entityName, entityType })), | ||
(records) => _.concat(additionalDeletions, records) | ||
)(selectedRecords); | ||
|
||
const recordTypes = _.uniq(_.map(({ entityType }) => entityType, selectedRecords)); | ||
if (recordTypes.length > 1) { | ||
await reportError('Something went wrong; more than one recordType is represented in the selection. This should not happen.'); | ||
} | ||
const recordType = recordTypes[0]; | ||
setDeleting(true); | ||
|
||
mspector marked this conversation as resolved.
Show resolved
Hide resolved
|
||
try { | ||
await Ajax().WorkspaceData.deleteRecords(dataProvider.proxyUrl, collectionId, recordType, { | ||
record_ids: recordsToDelete, | ||
}); | ||
onSuccess(); | ||
} catch (error) { | ||
if (error.status !== 409) { | ||
await reportError('Error deleting data entries', error); | ||
return onDismiss(); | ||
} | ||
|
||
// Handle 409 error by filtering additional deletions that need to be deleted first | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this in case of references? Or why would they need to be deleted first? |
||
setAdditionalDeletions(await filterAdditionalDeletions(error, recordsToDelete)); | ||
setDeleting(false); | ||
} | ||
}; | ||
|
||
const filterAdditionalDeletions = async (error, recordsToDelete) => { | ||
const errorEntities = await error.json(); | ||
|
||
return _.filter( | ||
errorEntities, | ||
(errorEntity) => | ||
!_.some( | ||
recordsToDelete, | ||
(selectedEntity) => selectedEntity.entityType === errorEntity.entityType && selectedEntity.entityName === errorEntity.entityName | ||
) | ||
); | ||
}; | ||
|
||
const moreToDelete = !!additionalDeletions.length; | ||
|
||
const total = selectedKeys.length + additionalDeletions.length; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this |
||
return h( | ||
DeleteConfirmationModal, | ||
{ | ||
objectType: 'data', | ||
title: `Delete ${total} ${total > 1 ? 'entries' : 'entry'}`, | ||
onConfirm: doDelete, | ||
onDismiss, | ||
}, | ||
[ | ||
runningSubmissionsCount > 0 && | ||
b({ style: { display: 'block', margin: '1rem 0' } }, [ | ||
`WARNING: ${runningSubmissionsCount} workflows are currently running in this workspace. ` + | ||
'Deleting the following entries could cause workflows using them to fail.', | ||
]), | ||
moreToDelete && | ||
b({ style: { display: 'block', margin: '1rem 0' } }, [ | ||
'In order to delete the selected data entries, the following entries that reference them must also be deleted.', | ||
]), | ||
// Size the scroll container to cut off the last row to hint that there's more content to be scrolled into view | ||
// Row height calculation is font size * line height + padding + border | ||
div( | ||
{ style: { maxHeight: 'calc((1em * 1.15 + 1.2rem + 1px) * 10.5)', overflowY: 'auto', margin: '0 -1.25rem' } }, | ||
_.map( | ||
([i, entity]) => | ||
div( | ||
{ | ||
style: { | ||
borderTop: i === 0 && runningSubmissionsCount === 0 ? undefined : `1px solid ${colors.light()}`, | ||
padding: '0.6rem 1.25rem', | ||
}, | ||
}, | ||
moreToDelete ? `${entity.entityName} (${entity.entityType})` : entity | ||
), | ||
Utils.toIndexPairs(moreToDelete ? additionalDeletions : selectedKeys) | ||
) | ||
), | ||
deleting && absoluteSpinnerOverlay, | ||
] | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,20 @@ | ||
import _ from 'lodash/fp'; | ||
import { Fragment, useState } from 'react'; | ||
import { h } from 'react-hyperscript-helpers'; | ||
import { div, h } from 'react-hyperscript-helpers'; | ||
import { ButtonSecondary } from 'src/components/common'; | ||
import { icon } from 'src/components/icons'; | ||
import { MenuButton } from 'src/components/MenuButton'; | ||
import { MenuTrigger } from 'src/components/PopupTrigger'; | ||
import { Ajax } from 'src/libs/ajax'; | ||
import { DataTableProvider } from 'src/libs/ajax/data-table-providers/DataTableProvider'; | ||
import { RecordTypeSchema, wdsToEntityServiceMetadata } from 'src/libs/ajax/data-table-providers/WdsDataTableProvider'; | ||
import colors from 'src/libs/colors'; | ||
import Events, { extractWorkspaceDetails } from 'src/libs/events'; | ||
import { isGoogleWorkspace, WorkspaceWrapper as Workspace } from 'src/workspaces/utils'; | ||
import * as WorkspaceUtils from 'src/workspaces/utils'; | ||
|
||
import DataTable from '../shared/DataTable'; | ||
import { RecordDeleter } from './RecordDeleter'; | ||
|
||
export interface WDSContentProps { | ||
workspace: Workspace; | ||
|
@@ -26,11 +35,49 @@ export const WDSContent = ({ | |
}: WDSContentProps) => { | ||
const googleProject = isGoogleWorkspace(workspace) ? workspace.workspace.googleProject : undefined; | ||
// State | ||
const [refreshKey] = useState(0); | ||
const [refreshKey, setRefreshKey] = useState(0); | ||
const [selectedRecords, setSelectedRecords] = useState({}); | ||
const [deletingRecords, setDeletingRecords] = useState(false); | ||
|
||
// Render | ||
const [entityMetadata, setEntityMetadata] = useState(() => wdsToEntityServiceMetadata(wdsSchema)); | ||
|
||
const recordsSelected = !_.isEmpty(selectedRecords); | ||
|
||
// This is a (mostly) copy/paste from the EntitiesContent component. | ||
// Maintainers of the future should consider abstracting it into its own component or shared function. | ||
const renderEditMenu = () => { | ||
return h( | ||
MenuTrigger, | ||
{ | ||
side: 'bottom', | ||
closeOnClick: true, | ||
content: h(Fragment, [ | ||
h( | ||
MenuButton, | ||
{ | ||
disabled: !recordsSelected, | ||
tooltip: !recordsSelected && 'Select rows to delete in the table', | ||
onClick: () => setDeletingRecords(true), | ||
}, | ||
['Delete selected rows'] | ||
), | ||
]), | ||
}, | ||
[ | ||
h( | ||
ButtonSecondary, | ||
{ | ||
tooltip: 'Edit data', | ||
...WorkspaceUtils.getWorkspaceEditControlProps(workspace as WorkspaceUtils.WorkspaceAccessInfo), | ||
style: { marginRight: '1.5rem' }, | ||
}, | ||
[icon('edit', { style: { marginRight: '0.5rem' } }), 'Edit'] | ||
), | ||
] | ||
); | ||
}; | ||
|
||
// dataProvider contains the proxyUrl for an instance of WDS | ||
return h(Fragment, [ | ||
h(DataTable, { | ||
|
@@ -45,19 +92,39 @@ export const WDSContent = ({ | |
workspace, | ||
snapshotName: undefined, | ||
selectionModel: { | ||
selected: [], | ||
setSelected: () => [], | ||
selected: selectedRecords, | ||
setSelected: setSelectedRecords, | ||
}, | ||
setEntityMetadata, | ||
childrenBefore: undefined, | ||
enableSearch: false, | ||
controlPanelStyle: { | ||
background: colors.light(1), | ||
borderBottom: `1px solid ${colors.grey(0.4)}`, | ||
}, | ||
border: false, | ||
loadMetadata, | ||
childrenBefore: () => | ||
div( | ||
{ style: { display: 'flex', alignItems: 'center', flex: 'none' } }, | ||
dataProvider.features.supportsRowSelection ? [renderEditMenu()] : [] | ||
), | ||
}), | ||
deletingRecords && | ||
h(RecordDeleter, { | ||
onDismiss: () => setDeletingRecords(false), | ||
onSuccess: () => { | ||
setDeletingRecords(false); | ||
setSelectedRecords({}); | ||
setRefreshKey(_.add(1)); | ||
Ajax().Metrics.captureEvent(Events.workspaceDataDelete, extractWorkspaceDetails(workspace.workspace)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that we are capturing the deletion event |
||
loadMetadata(); | ||
}, | ||
dataProvider, | ||
collectionId: workspace.workspace.workspaceId, | ||
selectedRecords, | ||
selectedRecordType: recordType, | ||
runningSubmissionsCount: workspace?.workspaceSubmissionStats?.runningSubmissionsCount, | ||
}), | ||
]); | ||
}; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason why this is
.js
instead of.ts
? I'd love for new files to be.ts
so we don't have to convert them later!