Advanced 35 min read

Project: Account Management Dashboard

By the end of this lesson, you'll be able to:

  • Combine sorting, row actions, inline editing, search, and pagination in one datatable
  • Persist inline edits using updateRecord
  • Apply the performance practices from this module

Prerequisites: "Custom Data Types and Performance Considerations"

What We're Building

An accountManagementDashboard component that:

  1. Lists Accounts with sortable Name, Industry, and Annual Revenue columns (Lessons 1-2).
  2. Offers a search box that filters the visible rows (Lesson 3).
  3. Supports inline editing of Industry, persisted via updateRecord (Module 6).
  4. Has a row action to delete an Account, backed by deleteRecord (Module 6) and a confirmation modal (Module 9).
  5. Follows the performance practices from Lesson 4 — a stable columns class field and hide-checkbox-column since bulk selection isn't needed here.

The Component

import { LightningElement, wire } from 'lwc';
import { updateRecord, deleteRecord } from 'lightning/uiRecordApi';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccounts from '@salesforce/apex/AccountSearchController.search';
import ConfirmDialog from 'c/confirmDialog';

const COLUMNS = [
    { label: 'Name', fieldName: 'accountName', type: 'text', sortable: true },
    { label: 'Industry', fieldName: 'industry', type: 'text', editable: true, sortable: true },
    { label: 'Annual Revenue', fieldName: 'revenue', type: 'currency', sortable: true },
    {
        type: 'action',
        typeAttributes: { rowActions: [{ label: 'Delete', name: 'delete' }] },
    },
];

export default class AccountManagementDashboard extends LightningElement {
    columns = COLUMNS;
    searchTerm = '';
    sortedBy;
    sortedDirection;
    draftValues = [];
    wiredAccounts;

    @wire(getAccounts, { searchTerm: '' })
    wiredResult(result) {
        this.wiredAccounts = result;
    }

    get accounts() {
        return this.wiredAccounts?.data ?? [];
    }

    get filteredAccounts() {
        if (!this.searchTerm) {
            return this.accounts;
        }
        const term = this.searchTerm.toLowerCase();
        return this.accounts.filter(acc => acc.accountName.toLowerCase().includes(term));
    }

    handleSearchChange(event) {
        this.searchTerm = event.target.value;
    }

    handleSort(event) {
        const { fieldName, sortDirection } = event.detail;
        this.sortedBy = fieldName;
        this.sortedDirection = sortDirection;
    }

    async handleSave(event) {
        const draftValues = event.detail.draftValues;
        try {
            await Promise.all(draftValues.map(draft => updateRecord({ fields: { Id: draft.accountId ?? draft.id, Industry: draft.industry } })));
            this.draftValues = [];
            await refreshApex(this.wiredAccounts);
            this.dispatchEvent(new ShowToastEvent({ title: 'Accounts Updated', variant: 'success' }));
        } catch (error) {
            this.dispatchEvent(new ShowToastEvent({
                title: 'Could Not Save Changes',
                message: error.body?.message ?? 'An unexpected error occurred.',
                variant: 'error',
            }));
        }
    }

    async handleRowAction(event) {
        if (event.detail.action.name !== 'delete') {
            return;
        }

        const result = await ConfirmDialog.open({ size: 'small', message: 'Delete this account?' });
        if (result !== 'confirm') {
            return;
        }

        try {
            await deleteRecord(event.detail.row.accountId ?? event.detail.row.id);
            await refreshApex(this.wiredAccounts);
            this.dispatchEvent(new ShowToastEvent({ title: 'Account Deleted', variant: 'success' }));
        } catch (error) {
            this.dispatchEvent(new ShowToastEvent({
                title: 'Could Not Delete Account',
                message: error.body?.message ?? 'An unexpected error occurred.',
                variant: 'error',
            }));
        }
    }
}
<lightning-input label="Search Accounts" onchange={handleSearchChange}></lightning-input>
<lightning-datatable
    key-field="accountId"
    data={filteredAccounts}
    columns={columns}
    sorted-by={sortedBy}
    sorted-direction={sortedDirection}
    onsort={handleSort}
    draft-values={draftValues}
    onsave={handleSave}
    onrowaction={handleRowAction}
    hide-checkbox-column>
</lightning-datatable>

COLUMNS is defined once as a module-level constant, then assigned to a plain columns class field (Lesson 4's performance practice) rather than recomputed on every render. Promise.all (Module 4) persists every edited row concurrently rather than one at a time.

Exercise

Add hide-checkbox-column to the markup above if it were missing, and explain as a comment why it is appropriate here.

Show hint

This dashboard never uses bulk row selection.

JAVASCRIPT

Exercise

Challenge: explain, as a comment, why handleSave uses Promise.all instead of awaiting each updateRecord call one at a time in a loop.

Show hint

Recall Module 4's Promises lesson and Module 7's bulkification lesson.

JAVASCRIPT

Project: Account Management Dashboard Quiz

1. Why is COLUMNS defined as a module-level constant assigned to a class field?

2. What handles persisting inline-edited Industry values?

3. What confirms a delete action before it proceeds?

4. Why is hide-checkbox-column used on this datatable?

5. What does Promise.all(draftValues.map(...)) achieve in handleSave?

Log in to submit the quiz and save your score.

My Notes

Log in to keep private notes on this lesson.

Questions about this lesson

No questions yet — be the first to ask.

Log in to ask a question about this lesson.

Summary

A hands-on capstone assembling every datatable technique from this module into one realistic, production-quality Account management dashboard.