Skip to content

Commit

Permalink
Merge pull request #779 from jetstreamapp/feat/778
Browse files Browse the repository at this point in the history
Add option to delete individual records
  • Loading branch information
paustint authored Mar 15, 2024
2 parents 21ae74d + f4e7520 commit abb0433
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ import {
useObservable,
} from '@jetstream/shared/ui-utils';
import { getRecordIdFromAttributes, getSObjectNameFromAttributes, splitArrayToMaxSize } from '@jetstream/shared/utils';
import { AsyncJob, CloneEditView, MapOf, Maybe, Record, SalesforceOrgUi, SobjectCollectionResponse } from '@jetstream/types';
import {
AsyncJob,
AsyncJobNew,
CloneEditView,
MapOf,
Maybe,
SalesforceOrgUi,
Record as SalesforceRecord,
SobjectCollectionResponse,
} from '@jetstream/types';
import {
AutoFullHeightContainer,
ButtonGroupContainer,
Expand All @@ -30,6 +39,7 @@ import {
Toolbar,
ToolbarItemActions,
ToolbarItemGroup,
useConfirmation,
} from '@jetstream/ui';
import classNames from 'classnames';
import React, { Fragment, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
Expand Down Expand Up @@ -91,12 +101,12 @@ export const QueryResults: FunctionComponent<QueryResultsProps> = React.memo(()
const [parsedQuery, setParsedQuery] = useState<Maybe<Query>>(null);
const [queryResults, setQueryResults] = useState<IQueryResults | null>(null);
const [recordCount, setRecordCount] = useState<number | null>(null);
const [records, setRecords] = useState<Record[] | null>(null);
const [records, setRecords] = useState<SalesforceRecord[] | null>(null);
const [nextRecordsUrl, setNextRecordsUrl] = useState<Maybe<string>>(null);
const [fields, setFields] = useState<string[] | null>(null);
const [subqueryFields, setSubqueryFields] = useState<Maybe<MapOf<string[]>>>(null);
const [filteredRows, setFilteredRows] = useState<Record[]>([]);
const [selectedRows, setSelectedRows] = useState<Record[]>([]);
const [filteredRows, setFilteredRows] = useState<SalesforceRecord[]>([]);
const [selectedRows, setSelectedRows] = useState<SalesforceRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const selectedOrg = useRecoilValue<SalesforceOrgUi>(selectedOrgState);
Expand All @@ -108,6 +118,7 @@ export const QueryResults: FunctionComponent<QueryResultsProps> = React.memo(()
fromJetstreamEvents.getObservable('jobFinished').pipe(filter((ev: AsyncJob) => ev.type === 'BulkDelete'))
);
const { notifyUser } = useBrowserNotifications(serverUrl, window.electron?.isFocused);
const confirm = useConfirmation();

const [cloneEditViewRecord, setCloneEditViewRecord] = useState<{
action: CloneEditView;
Expand Down Expand Up @@ -404,6 +415,35 @@ export const QueryResults: FunctionComponent<QueryResultsProps> = React.memo(()
}
}

async function handleDelete(record?: SalesforceRecord) {
const label = record.Name || record.Name || record.Id || getRecordIdFromAttributes(record);
await confirm({
content: (
<div className="slds-m-around_medium">
<p className="slds-align_absolute-center slds-m-bottom_small">
Are you sure you want to <span className="slds-text-color_destructive slds-p-left_xx-small">delete {label}</span>?
</p>
<p>
<strong>This record will be deleted from Salesforce.</strong> If you want to recover deleted records you can use the Salesforce
recycle bin.
</p>
</div>
),
header: 'Confirm Delete',
confirmText: 'Delete',
cancelText: 'Cancel',
})
.then(() => {
const jobs: AsyncJobNew[] = [{ type: 'BulkDelete', title: `Delete Record - ${label}`, org: selectedOrg, meta: [record] }];
fromJetstreamEvents.emit({ type: 'newJob', payload: jobs });
trackEvent(ANALYTICS_KEYS.query_BulkDelete, { numRecords: selectedRows.length, source: 'ROW_ACTION' });
})
.catch((ex) => {
logger.info(ex);
// user cancelled
});
}

function handleGetAsApex(record: any) {
setGetRecordAsApex({
record: record,
Expand Down Expand Up @@ -688,6 +728,9 @@ export const QueryResults: FunctionComponent<QueryResultsProps> = React.memo(()
handleCloneEditView(record, 'view', source);
}}
onUpdateRecords={handleUpdateRecords}
onDelete={(record) => {
handleDelete(record);
}}
onGetAsApex={(record) => {
handleGetAsApex(record);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { logger } from '@jetstream/shared/client-logger';
import { ANALYTICS_KEYS } from '@jetstream/shared/constants';
import { pluralizeIfMultiple } from '@jetstream/shared/utils';
import { AsyncJobNew, Maybe, SalesforceOrgUi } from '@jetstream/types';
import { DropDown, Tooltip, getSfdcRetUrl, salesforceLoginAndRedirect, useConfirmation } from '@jetstream/ui';
import { DropDown, getSfdcRetUrl, salesforceLoginAndRedirect, useConfirmation } from '@jetstream/ui';
import { Fragment, FunctionComponent, useState } from 'react';
import { Query } from 'soql-parser-js';
import { useAmplitude } from '../../core/analytics';
Expand Down Expand Up @@ -41,10 +41,21 @@ export const QueryResultsMoreActions: FunctionComponent<QueryResultsMoreActionsP
const confirm = useConfirmation();
const [openModal, setOpenModal] = useState<false | 'bulk-update' | 'apex'>(false);

function handleBulkRowAction(id: 'bulk-delete' | 'get-as-apex' | 'open-in-new-tab') {
function handleAction(id: 'bulk-delete' | 'get-as-apex' | 'open-in-new-tab' | 'bulk-update' | 'new-record') {
logger.log({ id, selectedRows });
switch (id) {
case 'bulk-update': {
setOpenModal('bulk-update');
break;
}
case 'new-record': {
onCreateNewRecord();
break;
}
case 'bulk-delete': {
if (!selectedRows) {
return;
}
const recordCountText = `${selectedRows.length} ${pluralizeIfMultiple('Record', selectedRows)}`;
confirm({
content: (
Expand All @@ -58,25 +69,29 @@ export const QueryResultsMoreActions: FunctionComponent<QueryResultsMoreActionsP
</p>
</div>
),
}).then(() => {
const jobs: AsyncJobNew[] = [
{
type: 'BulkDelete',
title: `Delete ${recordCountText}`,
org: selectedOrg,
meta: selectedRows,
},
];
fromJetstreamEvents.emit({ type: 'newJob', payload: jobs });
trackEvent(ANALYTICS_KEYS.query_BulkDelete, { numRecords: selectedRows.length });
});
})
.then(() => {
const jobs: AsyncJobNew[] = [{ type: 'BulkDelete', title: `Delete ${recordCountText}`, org: selectedOrg, meta: selectedRows }];
fromJetstreamEvents.emit({ type: 'newJob', payload: jobs });
trackEvent(ANALYTICS_KEYS.query_BulkDelete, { numRecords: selectedRows.length, source: 'HEADER_ACTION' });
})
.catch((ex) => {
logger.info(ex);
// user cancelled
});
break;
}
case 'get-as-apex': {
if (!selectedRows) {
return;
}
setOpenModal('apex');
break;
}
case 'open-in-new-tab': {
if (!selectedRows) {
return;
}
(selectedRows.length <= 15
? Promise.resolve()
: confirm({
Expand Down Expand Up @@ -110,64 +125,13 @@ export const QueryResultsMoreActions: FunctionComponent<QueryResultsMoreActionsP
}
}

function handleAction(item: 'bulk-update' | 'new-record') {
switch (item) {
case 'bulk-update':
setOpenModal('bulk-update');
break;
case 'new-record':
onCreateNewRecord();
break;
default:
break;
}
}

function handleBulkUpdateModalClose(didUpdate = false) {
setOpenModal(false);
didUpdate && refreshRecords();
}

return (
<Fragment>
<Tooltip content={!sObject || selectedRows.length === 0 ? 'Select one or more records to enable record actions' : undefined}>
<DropDown
className="slds-m-right_xx-small"
dropDownClassName="slds-dropdown_actions"
position="right"
disabled={disabled || selectedRows.length === 0}
leadingIcon={{ icon: 'table', type: 'utility', description: 'More Actions' }}
actionText="Selected record actions"
items={[
{
id: 'bulk-delete',
value: 'Delete Selected Records',
icon: {
icon: 'delete',
type: 'utility',
},
},
{
id: 'get-as-apex',
value: 'Turn records into Apex',
icon: {
icon: 'apex',
type: 'utility',
},
},
{
id: 'open-in-new-tab',
value: 'Open selected records in Salesforce',
icon: {
icon: 'new_window',
type: 'utility',
},
},
]}
onSelected={(item) => handleBulkRowAction(item as 'bulk-delete' | 'get-as-apex' | 'open-in-new-tab')}
/>
</Tooltip>

<DropDown
className="slds-m-right_xx-small"
dropDownClassName="slds-dropdown_actions"
Expand All @@ -176,10 +140,56 @@ export const QueryResultsMoreActions: FunctionComponent<QueryResultsMoreActionsP
actionText="Record actions"
disabled={disabled}
items={[
{ id: 'bulk-update', value: 'Bulk update records', disabled: !sObject || !totalRecordCount || !parsedQuery },
{ id: 'new-record', value: 'Create new record', disabled: !sObject || !parsedQuery },
{
id: 'bulk-update',
subheader: 'Actions',
value: 'Bulk update records',
disabled: !sObject || !totalRecordCount || !parsedQuery,
icon: {
icon: 'upload',
type: 'utility',
},
},
{
id: 'new-record',
value: 'Create new record',
disabled: !sObject || !parsedQuery,
trailingDivider: true,
icon: {
icon: 'record_create',
type: 'utility',
},
},
{
id: 'bulk-delete',
subheader: 'Selected Record Actions',
value: 'Delete Selected Records',
disabled: disabled || selectedRows.length === 0,
icon: {
icon: 'delete',
type: 'utility',
},
},
{
id: 'get-as-apex',
value: 'Convert selected records to Apex',
disabled: disabled || selectedRows.length === 0,
icon: {
icon: 'apex',
type: 'utility',
},
},
{
id: 'open-in-new-tab',
value: 'Open selected records in Salesforce',
disabled: disabled || selectedRows.length === 0,
icon: {
icon: 'new_window',
type: 'utility',
},
},
]}
onSelected={(item) => handleAction(item as 'bulk-update' | 'new-record')}
onSelected={(item) => handleAction(item as 'bulk-delete' | 'get-as-apex' | 'open-in-new-tab' | 'bulk-update' | 'new-record')}
/>

{openModal === 'bulk-update' && sObject && totalRecordCount && parsedQuery && (
Expand Down
10 changes: 10 additions & 0 deletions libs/icon-factory/src/lib/icon-factory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import StandardIcon_PortalRolesAndSubordinates from './icons/standard/PortalRole
import StandardIcon_ProductConsumed from './icons/standard/ProductConsumed';
import StandardIcon_Record from './icons/standard/Record';
import StandardIcon_RecordCreate from './icons/standard/RecordCreate';
import StandardIcon_RecordDelete from './icons/standard/RecordDelete';
import StandardIcon_RecordLookup from './icons/standard/RecordLookup';
import StandardIcon_RecordUpdate from './icons/standard/RecordUpdate';
import StandardIcon_RelatedList from './icons/standard/RelatedList';
import StandardIcon_Settings from './icons/standard/Settings';
import UtilityIcon_Add from './icons/utility/Add';
Expand Down Expand Up @@ -102,7 +104,10 @@ import UtilityIcon_Play from './icons/utility/Play';
import UtilityIcon_Preview from './icons/utility/Preview';
import UtilityIcon_PromptEdit from './icons/utility/PromptEdit';
import UtilityIcon_QuotationMarks from './icons/utility/QuotationMarks';
import UtilityIcon_RecordCreate from './icons/utility/RecordCreate';
import UtilityIcon_RecordDelete from './icons/utility/RecordDelete';
import UtilityIcon_RecordLookup from './icons/utility/RecordLookup';
import UtilityIcon_RecordUpdate from './icons/utility/RecordUpdate';
import UtilityIcon_Refresh from './icons/utility/Refresh';
import UtilityIcon_RemoveFormatting from './icons/utility/RemoveFormatting';
import UtilityIcon_Richtextbulletedlist from './icons/utility/Richtextbulletedlist';
Expand Down Expand Up @@ -175,7 +180,9 @@ const standardIcons = {
portal_roles_and_subordinates: StandardIcon_PortalRolesAndSubordinates,
product_consumed: StandardIcon_ProductConsumed,
record_create: StandardIcon_RecordCreate,
record_delete: StandardIcon_RecordDelete,
record_lookup: StandardIcon_RecordLookup,
record_update: StandardIcon_RecordUpdate,
record: StandardIcon_Record,
related_list: StandardIcon_RelatedList,
settings: StandardIcon_Settings,
Expand Down Expand Up @@ -261,7 +268,10 @@ const utilityIcons = {
preview: UtilityIcon_Preview,
prompt_edit: UtilityIcon_PromptEdit,
quotation_marks: UtilityIcon_QuotationMarks,
record_create: UtilityIcon_RecordCreate,
record_delete: UtilityIcon_RecordDelete,
record_lookup: UtilityIcon_RecordLookup,
record_update: UtilityIcon_RecordUpdate,
refresh: UtilityIcon_Refresh,
remove_formatting: UtilityIcon_RemoveFormatting,
richtextbulletedlist: UtilityIcon_Richtextbulletedlist,
Expand Down
5 changes: 2 additions & 3 deletions libs/ui/src/lib/confirmation-dialog/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Maybe } from '@jetstream/types';
import { OverlayProvider } from '@react-aria/overlays';
import React, { Fragment, FunctionComponent } from 'react';
import Modal from '../modal/Modal';
import { OverlayProvider } from '@react-aria/overlays';
import { Maybe } from '@jetstream/types';

export interface ConfirmationDialogProps {
isOpen: boolean;
Expand All @@ -15,7 +15,6 @@ export interface ConfirmationDialogProps {
}

export interface ConfirmationDialogServiceProviderOptions {
rejectOnCancel?: boolean; // if true, then a cancellation will result in a rejected promise
header?: Maybe<string | JSX.Element>;
tagline?: Maybe<string | JSX.Element>;
content: React.ReactNode;
Expand Down
Loading

0 comments on commit abb0433

Please sign in to comment.