diff --git a/force-app/main/default/flexipages/GraphQL.flexipage-meta.xml b/force-app/main/default/flexipages/GraphQL.flexipage-meta.xml
index 60be5bbde..b163b86c0 100644
--- a/force-app/main/default/flexipages/GraphQL.flexipage-meta.xml
+++ b/force-app/main/default/flexipages/GraphQL.flexipage-meta.xml
@@ -11,6 +11,12 @@
graphqlContacts
+
+
+ graphqlMutations
+ graphqlMutations
+
+
region2
Region
diff --git a/force-app/main/default/lwc/graphqlMutations/__tests__/data/graphqlContactResponse.json b/force-app/main/default/lwc/graphqlMutations/__tests__/data/graphqlContactResponse.json
new file mode 100644
index 000000000..a598f38f0
--- /dev/null
+++ b/force-app/main/default/lwc/graphqlMutations/__tests__/data/graphqlContactResponse.json
@@ -0,0 +1,71 @@
+{
+ "uiapi": {
+ "query": {
+ "Contact": {
+ "edges": [
+ {
+ "node": {
+ "Id": "0031700000pJRRSAA4",
+ "FirstName": {
+ "value": "Amy"
+ },
+ "LastName": {
+ "value": "Taylor"
+ },
+ "Title": {
+ "value": "VP of Engineering"
+ },
+ "Phone": {
+ "value": "4152568563"
+ },
+ "Email": {
+ "value": "amy@demo.net"
+ }
+ }
+ },
+ {
+ "node": {
+ "Id": "0031700000pJRRTAA4",
+ "FirstName": {
+ "value": "Michael"
+ },
+ "LastName": {
+ "value": "Jones"
+ },
+ "Title": {
+ "value": "VP of Sales"
+ },
+ "Phone": {
+ "value": "4158526633"
+ },
+ "Email": {
+ "value": "michael@demo.net"
+ }
+ }
+ },
+ {
+ "node": {
+ "Id": "0031700000pJRRUAA4",
+ "FirstName": {
+ "value": "Jennifer"
+ },
+ "LastName": {
+ "value": "Wu"
+ },
+ "Title": {
+ "value": "CEO"
+ },
+ "Phone": {
+ "value": "4158521463"
+ },
+ "Email": {
+ "value": "jennifer@demo.net"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+}
+
diff --git a/force-app/main/default/lwc/graphqlMutations/__tests__/graphqlMutations.test.js b/force-app/main/default/lwc/graphqlMutations/__tests__/graphqlMutations.test.js
new file mode 100644
index 000000000..44322073e
--- /dev/null
+++ b/force-app/main/default/lwc/graphqlMutations/__tests__/graphqlMutations.test.js
@@ -0,0 +1,278 @@
+import { createElement } from '@lwc/engine-dom';
+import GraphqlMutations from 'c/graphqlMutations';
+import { graphql, executeMutation } from 'lightning/graphql';
+import { ShowToastEventName } from 'lightning/platformShowToastEvent';
+
+// Mock realistic GraphQL contact data
+const mockGraphQLContactResponse = require('./data/graphqlContactResponse.json');
+
+// Mock lightning/graphql with wire adapter and executeMutation
+jest.mock('lightning/graphql', () => {
+ const actual = jest.requireActual('lightning/graphql');
+ return {
+ ...actual,
+ executeMutation: jest.fn(),
+ gql: jest.fn((strings, ...values) =>
+ strings.reduce(
+ (acc, str, i) => acc + str + (i < values.length ? values[i] : ''),
+ ''
+ )
+ )
+ };
+});
+
+const DRAFT_VALUES = [
+ {
+ Id: '0031700000pJRRSAA4',
+ FirstName: 'Amy',
+ LastName: 'Taylor',
+ Title: 'VP of Engineering',
+ Phone: '4152568563',
+ Email: 'amy@new_demo.net'
+ },
+ {
+ Id: '0031700000pJRRTAA4',
+ FirstName: 'Michael',
+ LastName: 'Jones',
+ Title: 'VP of Sales',
+ Phone: '4158526633',
+ Email: 'michael@new_demo.net'
+ }
+];
+
+describe('c-graphql-mutations', () => {
+ afterEach(() => {
+ // The jsdom instance is shared across test cases in a single file so reset the DOM
+ while (document.body.firstChild) {
+ document.body.removeChild(document.body.firstChild);
+ }
+ // Prevent data saved on mocks from leaking between tests
+ jest.clearAllMocks();
+ });
+
+ // Helper function to wait until the microtask queue is empty
+ async function flushPromises() {
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
+ return new Promise((resolve) => setTimeout(resolve, 0));
+ }
+
+ describe('graphql @wire data', () => {
+ it('renders three rows in the lightning datatable', async () => {
+ const element = createElement('c-graphql-mutations', {
+ is: GraphqlMutations
+ });
+ document.body.appendChild(element);
+
+ // Emit data from @wire
+ graphql.emit(mockGraphQLContactResponse);
+
+ // Wait for any asynchronous DOM updates
+ await flushPromises();
+
+ const tableEl = element.shadowRoot.querySelector('lightning-datatable');
+
+ // Validate the datatable is populated with correct number of records
+ expect(tableEl.data.length).toBe(3);
+
+ // Validate the record to have rendered with correct data
+ expect(tableEl.data[0].FirstName).toBe('Amy');
+ expect(tableEl.data[0].LastName).toBe('Taylor');
+ expect(tableEl.data[0].Email).toBe('amy@demo.net');
+ });
+
+ it('is accessible when data is returned', async () => {
+ const element = createElement('c-graphql-mutations', {
+ is: GraphqlMutations
+ });
+ document.body.appendChild(element);
+
+ // Emit data from @wire
+ graphql.emit(mockGraphQLContactResponse);
+
+ // Wait for any asynchronous DOM updates
+ await flushPromises();
+
+ // Check accessibility
+ await expect(element).toBeAccessible();
+ });
+ });
+
+
+ describe('save handler with executeMutation', () => {
+ it('calls executeMutation when save is triggered', async () => {
+ // Mock executeMutation to return success
+ executeMutation.mockResolvedValue({
+ errors: null
+ });
+
+ const element = createElement('c-graphql-mutations', {
+ is: GraphqlMutations
+ });
+ document.body.appendChild(element);
+
+ const refreshGraphQL = jest.fn().mockResolvedValue();
+ // Emit data from @wire
+ graphql.emit(mockGraphQLContactResponse, () => true, refreshGraphQL);
+
+ // Wait for any asynchronous DOM updates
+ await flushPromises();
+
+ // Simulate save event on datatable
+ const tableEl = element.shadowRoot.querySelector('lightning-datatable');
+ tableEl.dispatchEvent(
+ new CustomEvent('save', {
+ detail: {
+ draftValues: DRAFT_VALUES
+ }
+ })
+ );
+
+ // Wait for any asynchronous DOM updates - need extra time for async operations
+ await flushPromises();
+
+ const calledQuery = executeMutation.mock.calls[0][0].query;
+ expect(calledQuery).toBe(`mutation ContactUpdateExample($input0: ContactUpdateInput!, $input1: ContactUpdateInput!){uiapi (input: { allOrNone: false }) { query0: ContactUpdate(input: $input0)
+ {
+ success
+ } query1: ContactUpdate(input: $input1)
+ {
+ success
+ } } }`);
+ });
+
+ it('displays a success toast after records are updated', async () => {
+ // Mock executeMutation to return success
+ executeMutation.mockResolvedValue({
+ errors: null
+ });
+
+ const element = createElement('c-graphql-mutations', {
+ is: GraphqlMutations
+ });
+ document.body.appendChild(element);
+
+ // Mock handler for toast event
+ const toastHandler = jest.fn();
+ element.addEventListener(ShowToastEventName, toastHandler);
+
+ const refreshGraphQL = jest.fn().mockResolvedValue();
+ // Emit data from @wire
+ graphql.emit(mockGraphQLContactResponse, () => true, refreshGraphQL);
+
+ // Wait for any asynchronous DOM updates
+ await flushPromises();
+
+ // Simulate save event on datatable
+ const tableEl = element.shadowRoot.querySelector('lightning-datatable');
+ tableEl.dispatchEvent(
+ new CustomEvent('save', {
+ detail: {
+ draftValues: DRAFT_VALUES
+ }
+ })
+ );
+
+ // Wait for any asynchronous DOM updates - need extra time for async operations
+ await flushPromises();
+
+ // Validate the toast event is called with success
+ expect(toastHandler).toHaveBeenCalled();
+ expect(toastHandler.mock.calls[0][0].detail.variant).toBe('success');
+ expect(toastHandler.mock.calls[0][0].detail.title).toBe('Success');
+
+ // Validate refreshGraphQL is called and the draft values are reset
+ expect(refreshGraphQL).toHaveBeenCalled();
+ expect(tableEl.draftValues).toEqual([]);
+ });
+
+ it('displays an error toast when graphql mutation returns errors', async () => {
+ // Mock executeMutation to return errors
+ executeMutation.mockResolvedValue({
+ errors: ['GraphQL error occurred']
+ });
+
+ const element = createElement('c-graphql-mutations', {
+ is: GraphqlMutations
+ });
+ document.body.appendChild(element);
+
+ // Mock handler for toast event
+ const toastHandler = jest.fn();
+ element.addEventListener(ShowToastEventName, toastHandler);
+
+ const refreshGraphQL = jest.fn().mockResolvedValue();
+ // Emit data from @wire
+ graphql.emit(mockGraphQLContactResponse, () => true, refreshGraphQL);
+
+ // Wait for any asynchronous DOM updates
+ await flushPromises();
+
+ // Simulate save event on datatable
+ const tableEl = element.shadowRoot.querySelector('lightning-datatable');
+ tableEl.dispatchEvent(
+ new CustomEvent('save', {
+ detail: {
+ draftValues: DRAFT_VALUES
+ }
+ })
+ );
+
+ // Wait for any asynchronous DOM updates - need extra time for async operations
+ await flushPromises();
+
+ // Validate the toast event is called with error
+ expect(toastHandler).toHaveBeenCalled();
+ expect(toastHandler.mock.calls[0][0].detail.variant).toBe('error');
+ expect(toastHandler.mock.calls[0][0].detail.title).toBe('GraphQl Error');
+
+ // Validate refreshGraphQL is NOT called when there's an error
+ expect(refreshGraphQL).not.toHaveBeenCalled();
+ });
+
+ it('displays an error toast on executeMutation exception', async () => {
+ // Mock executeMutation to reject
+ executeMutation.mockRejectedValue({
+ body: {
+ message: 'Error executing mutation'
+ }
+ });
+
+ const element = createElement('c-graphql-mutations', {
+ is: GraphqlMutations
+ });
+ document.body.appendChild(element);
+
+ // Mock handler for toast event
+ const toastHandler = jest.fn();
+ element.addEventListener(ShowToastEventName, toastHandler);
+
+ const refreshGraphQL = jest.fn().mockResolvedValue();
+ // Emit data from @wire
+ graphql.emit(mockGraphQLContactResponse, () => true, refreshGraphQL);
+
+ // Wait for any asynchronous DOM updates
+ await flushPromises();
+
+ // Simulate save event on datatable
+ const tableEl = element.shadowRoot.querySelector('lightning-datatable');
+ tableEl.dispatchEvent(
+ new CustomEvent('save', {
+ detail: {
+ draftValues: DRAFT_VALUES
+ }
+ })
+ );
+
+ // Wait for any asynchronous DOM updates - need extra time for async operations
+ await flushPromises();
+
+ // Validate the toast event is called with error
+ expect(toastHandler).toHaveBeenCalled();
+ expect(toastHandler.mock.calls[0][0].detail.variant).toBe('error');
+ expect(toastHandler.mock.calls[0][0].detail.title).toBe(
+ 'Error while updating or refreshing records'
+ );
+ });
+ });
+});
+
diff --git a/force-app/main/default/lwc/graphqlMutations/graphqlMutations.html b/force-app/main/default/lwc/graphqlMutations/graphqlMutations.html
new file mode 100644
index 000000000..f1ddd6dcc
--- /dev/null
+++ b/force-app/main/default/lwc/graphqlMutations/graphqlMutations.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mutate data in a table with inline editing using GraphQL Mutations
+
+
+
diff --git a/force-app/main/default/lwc/graphqlMutations/graphqlMutations.js b/force-app/main/default/lwc/graphqlMutations/graphqlMutations.js
new file mode 100644
index 000000000..fcc8b3e7c
--- /dev/null
+++ b/force-app/main/default/lwc/graphqlMutations/graphqlMutations.js
@@ -0,0 +1,178 @@
+import { LightningElement, wire } from 'lwc';
+import { gql, graphql, executeMutation } from 'lightning/graphql';
+
+import { ShowToastEvent } from 'lightning/platformShowToastEvent';
+import FIRSTNAME_FIELD from '@salesforce/schema/Contact.FirstName';
+import LASTNAME_FIELD from '@salesforce/schema/Contact.LastName';
+import TITLE_FIELD from '@salesforce/schema/Contact.Title';
+import PHONE_FIELD from '@salesforce/schema/Contact.Phone';
+import EMAIL_FIELD from '@salesforce/schema/Contact.Email';
+
+const COLS = [
+ {
+ label: 'Name',
+ fieldName: FIRSTNAME_FIELD.fieldApiName,
+ editable: true
+ },
+ {
+ label: 'Last Name',
+ fieldName: LASTNAME_FIELD.fieldApiName,
+ editable: true
+ },
+ { label: 'Title', fieldName: TITLE_FIELD.fieldApiName, editable: true },
+ {
+ label: 'Phone',
+ fieldName: PHONE_FIELD.fieldApiName,
+ type: 'phone',
+ editable: true
+ },
+ {
+ label: 'Email',
+ fieldName: EMAIL_FIELD.fieldApiName,
+ type: 'email',
+ editable: true
+ }
+];
+export default class GraphqlMutations extends LightningElement {
+ columns = COLS;
+ draftValues = [];
+ refreshGraphQL;
+ contacts;
+ errors;
+
+ // Using GraphQL to get contacts
+ @wire(graphql, {
+ query: gql`
+ query getContacts {
+ uiapi {
+ query {
+ Contact{
+ edges {
+ node {
+ Id
+ FirstName {
+ value
+ }
+ LastName {
+ value
+ }
+ Phone {
+ value
+ }
+ Title {
+ value
+ }
+ Email {
+ value
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `
+ })
+ wiredValues(result) {
+ this.isLoading = false;
+ this.account = undefined;
+ this.errors = undefined;
+
+ const { errors, data, refresh } = result;
+ // We hold a reference to the refresh function on the graphQL query result so we can call it later.
+ if (refresh) {
+ this.refreshGraphQL = refresh;
+ }
+ if (data) {
+ this.contacts = data.uiapi.query.Contact.edges.map((edge) => ({
+ Id: edge.node.Id,
+ FirstName: edge.node.FirstName.value,
+ LastName: edge.node.LastName.value,
+ Phone: edge.node.Phone.value,
+ Title: edge.node.Title.value,
+ Email: edge.node.Email.value
+ }));
+ }
+ if (errors) {
+ this.errors = errors;
+ }
+ }
+
+ async handleSave(event) {
+ // Convert datatable draft values into record objects
+ const params = event.detail.draftValues.slice().map((draftValue) => {
+ const fields = Object.assign({}, draftValue);
+ return { fields };
+ });
+
+ // Clear all datatable draft values
+ this.draftValues = [];
+
+ const variables = this.buildVariables(params);
+ const query = this.buildQuery(params);
+ try {
+ const result = await executeMutation({ query, variables });
+ if (result.errors) {
+ this.dispatchEvent(
+ new ShowToastEvent({
+ title: 'GraphQl Error',
+ message: 'Error in graphql mutation',
+ variant: 'error'
+ })
+ );
+ } else {
+ this.dispatchEvent(
+ new ShowToastEvent({
+ title: 'Success',
+ message: 'Contacts updated',
+ variant: 'success'
+ })
+ );
+ // Refresh data in the datatable
+ await this.refreshGraphQL?.();
+ }
+ } catch (error) {
+ this.dispatchEvent(
+ new ShowToastEvent({
+ title: 'Error while updating or refreshing records',
+ message: error.body.message,
+ variant: 'error'
+ })
+ );
+ }
+ }
+ buildVariables(params){
+ const variables = {}
+ // eslint-disable-next-line guard-for-in
+ for(let index in params){
+ const fields = params[index].fields;
+ let input = {Contact: {}};
+ for(let field of Object.keys(fields)){
+ if(field === 'Id'){
+ input.Id = fields[field];
+ } else {
+ input.Contact[field] = fields[field];
+ }
+ }
+ variables[`input${index}`] = input;
+ }
+ return variables;
+ }
+
+ buildQuery(params){
+ let header = '';
+ let body = '';
+ let query = 'mutation ContactUpdateExample(';
+ // eslint-disable-next-line guard-for-in
+ for(let index in params){
+ header+=`$input${index}: ContactUpdateInput!, `;
+ let queryBlock = ` query${index}: ContactUpdate(input: $input${index})
+ {
+ success
+ }`;
+ body += queryBlock;
+ }
+ query+=`${header.slice(0, -2)}){uiapi (input: { allOrNone: false }) {${body} } }`
+ return gql`${query.trim()}`;
+ }
+}
diff --git a/force-app/main/default/lwc/graphqlMutations/graphqlMutations.js-meta.xml b/force-app/main/default/lwc/graphqlMutations/graphqlMutations.js-meta.xml
new file mode 100644
index 000000000..6cc1e7780
--- /dev/null
+++ b/force-app/main/default/lwc/graphqlMutations/graphqlMutations.js-meta.xml
@@ -0,0 +1,10 @@
+
+
+ 58.0
+ true
+
+ lightning__AppPage
+ lightning__RecordPage
+ lightning__HomePage
+
+