Source: customDatatable/customDatatable.js

import { refreshApex } from '@salesforce/apex';
import getColumns from '@salesforce/apex/CustomDatatableUtil.convertFieldSetToColumns';
import getRecords from '@salesforce/apex/CustomDatatableUtil.getRecordsWithFieldSet';
import { NavigationMixin } from 'lightning/navigation';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { deleteRecord, updateRecord } from 'lightning/uiRecordApi';
import { LightningElement, api, track, wire } from 'lwc';

/**
 * A custom datatable with different configuration options.
 * @alias CustomDatatable
 * @extends LightningElement
 * @hideconstructor
 *
 * @example
 * <c-custom-datatable
 *   object-api-name="Case"
 *   field-set-api-name="CaseFieldSet"
 *   where-conditions="Status = 'New'"
 *   hide-checkbox-column
 *   show-row-number-column
 * ></c-custom-datatable>
 */
export default class CustomDatatable extends NavigationMixin(LightningElement) {
  /**
   * If show card option is active, the card icon is displayed in the header before the card title.
   * It should contain the SLDS name of the icon.
   * Specify the name in the format 'utility:down' where 'utility' is the category and 'down' the icon to be displayed.
   * @type {string}
   * @default ''
   * @example 'standard:case'
   */
  @api cardIcon = '';

  /**
   * If show card option is active, the card title can include text and is displayed in the header above the table.
   * @type {string}
   * @default ''
   */
  @api cardTitle = '';

  /**
   * Specifies how column widths are calculated. Set to 'fixed' for columns with equal widths.
   * Set to 'auto' for column widths that are based on the width of the column content and the table width.
   * @type {string}
   * @default 'fixed'
   */
  @api columnWidthsMode = 'fixed';

  /**
   * Specifies the default sorting direction on an unsorted column. Valid options include 'asc' and 'desc'.
   * @type {string}
   * @default 'asc'
   */
  @api defaultSortDirection = 'asc';

  /**
   * API name of the field set that specifies which fields are displayed in the table.
   * @type {string}
   */
  @api fieldSetApiName = '';

  /**
   * If present, the checkbox column for row selection is hidden.
   * @type {boolean}
   * @default false
   */
  @api hideCheckboxColumn = false;

  /**
   * If present, the table header is hidden.
   * @type {boolean}
   * @default false
   */
  @api hideTableHeader = false;

  /**
   * If present, the table is wrapped with the correct page header to fit better into the related list layout.
   * @type {boolean}
   * @default false
   */
  @api isUsedAsRelatedList = false;

  /**
   * Required field for better table performance. Associates each row with a unique Id.
   * @type {string}
   * @default 'Id'
   */
  @api keyField = 'Id';

  /**
   * The maximum width for all columns. The default is 1000px.
   * @type {number}
   * @default 1000
   */
  @api maxColumnWidth = 1000;

  /**
   * The maximum number of rows that can be selected.
   * Checkboxes are used for selection by default, and radio buttons are used when maxRowSelection is 1.
   * @type {number}
   * @default 50
   */
  @api maxRowSelection = 50;

  /**
   * The minimum width for all columns. The default is 50px.
   * @type {number}
   * @default 50
   */
  @api minColumnWidth = 50;

  /**
   * API name of the object that will be displayed in the table.
   * @type {string}
   */
  @api objectApiName = '';

  /**
   * If present, then all datatable fields are not editable.
   * @type {boolean}
   * @default false
   */
  @api readOnly = false;

  /**
   * If present, column resizing is disabled.
   * @type {boolean}
   * @default false
   */
  @api resizeColumnDisabled = false;

  /**
   * Determines where to start counting the row number.
   * @type {number}
   * @default 0
   */
  @api rowNumberOffset = 0;

  /**
   * If present, the table is wrapped in a lightning card to fit better into the overall page layout.
   * @type {boolean}
   * @default false
   */
  @api showCard = false;

  /**
   * If present, the last column contains a delete record action.
   * @type {boolean}
   * @default false
   */
  @api showDeleteRowAction = false;

  /**
   * If present, the last column contains a edit record action.
   * @type {boolean}
   * @default false
   */
  @api showEditRowAction = false;

  /**
   * If present, a delete action button is available when multiple records are selected.
   * This is only available if the checkbox column is visible and the table is either displayed with a Lightning Card
   * or as a Related List.
   * @type {boolean}
   * @default false
   */
  @api showMultipleRowDeleteAction = false;

  /**
   * If present, the row numbers are shown in the first column.
   * @type {boolean}
   * @default false
   */
  @api showRowNumberColumn = false;

  /**
   * If present, the last column contains a view record action.
   * @type {boolean}
   * @default false
   */
  @api showViewRowAction = false;

  /**
   * If present, the footer that displays the Save and Cancel buttons is hidden during inline editing.
   * @type {boolean}
   * @default false
   */
  @api suppressBottomBar = false;

  /**
   * Optional where clause conditions for loaded data records.
   * @type {string}
   * @default ''
   * @example Status = 'New'
   */
  @api whereConditions = '';

  @track columns = [];
  @track draftValues = [];
  @track records = [];
  @track selectedRecords = [];
  @track wiredRecords = [];

  isLoading = true;
  hasSelectedRecords = false;

  @wire(getColumns, { objectName: '$objectApiName', fieldSetName: '$fieldSetApiName', readOnly: '$readOnly' })
  wiredGetColumns({ data }) {
    if (data) {
      this.isLoading = false;
      this.columns = data.slice();
      this.addRowActions();
    }
  }

  @wire(getRecords, {
    objectName: '$objectApiName',
    fieldSetName: '$fieldSetApiName',
    whereConditions: '$whereConditions'
  })
  wiredGetRecords(result) {
    this.wiredRecords = result;
    if (result.data) {
      this.records = result.data;
    }
  }

  addRowActions() {
    const actions = [];
    if (this.showViewRowAction) actions.push({ label: 'View', name: 'view' });
    if (this.showEditRowAction) actions.push({ label: 'Edit', name: 'edit' });
    if (this.showDeleteRowAction) actions.push({ label: 'Delete', name: 'delete' });
    if (actions.length) {
      this.columns.push({ type: 'action', typeAttributes: { rowActions: actions, menuAlignment: 'right' } });
    }
  }

  handleRowAction(event) {
    const actionName = event.detail.action.name;
    const row = event.detail.row;
    switch (actionName) {
      case 'view':
        this.viewCurrentRecord(row);
        break;
      case 'edit':
        this.editCurrentRecord(row);
        break;
      case 'delete':
        this.deleteCurrentRecord(row);
        break;
      default:
    }
  }

  viewCurrentRecord(row) {
    this[NavigationMixin.Navigate]({
      type: 'standard__recordPage',
      attributes: {
        recordId: row.Id,
        actionName: 'view'
      }
    });
  }

  editCurrentRecord(row) {
    this[NavigationMixin.Navigate]({
      type: 'standard__recordPage',
      attributes: {
        recordId: row.Id,
        actionName: 'edit'
      }
    });
  }

  deleteCurrentRecord(row) {
    this.isLoading = true;
    deleteRecord(row.Id)
      .then(() => {
        this.showToast('Success', 'Record deleted', 'success');
        return refreshApex(this.wiredRecords).then(() => {
          this.isLoading = false;
        });
      })
      .catch((error) => {
        this.showToast('Error deleting record', error.body.message, 'error');
      });
  }

  handleSave(event) {
    const recordInputs = event.detail.draftValues.slice().map((draft) => {
      const fields = { ...draft };
      return { fields };
    });

    const promises = recordInputs.map((recordInput) => updateRecord(recordInput));
    Promise.all(promises)
      .then(() => {
        this.showToast('Success', 'Records updated', 'success');
        return refreshApex(this.wiredRecords).then(() => {
          this.draftValues = [];
        });
      })
      .catch((error) => {
        this.showToast('Error updating records', error.body.message, 'error');
      });
  }

  getSelectedRecords(event) {
    this.selectedRecords = event.detail.selectedRows;
    this.hasSelectedRecords = this.selectedRecords.length > 0;
  }

  deleteSelectedRecords() {
    this.isLoading = true;
    const records = this.selectedRecords;
    const promises = records.map((record) => deleteRecord(record.Id));
    Promise.all(promises)
      .then(() => {
        this.showToast('Success', 'Records deleted', 'success');
        return refreshApex(this.wiredRecords).then(() => {
          this.isLoading = false;
        });
      })
      .catch((error) => {
        this.showToast('Error deleting records', error.body.message, 'error');
      });
  }

  showToast(title, message, variant) {
    this.dispatchEvent(new ShowToastEvent({ title: title, message: message, variant: variant }));
  }
}