Skip to content

Adding CRUD Actions

Beyond basic list functionality, it's easy in molten to add actions to Create, Update, and Delete items in your list if those are things your API supports.

Below is the code for basic actions to perform these operations continuing the pet shop example.

Create Action

File CreateAction.js

  import { Dialogs } from '@leverege/ui-elements'
  import { Config } from '@leverege/plugin'
  import CreatePet from '../views/CreatePet'

  // given a Relationship instance, make a create action that can be installed into molten
  export default relationship => ( { 
    id : 'action.pet.CreateItem',
    name : 'Create Pet Item',
    layout : { sort : 'item.add' },
    handles : cxt => true,
    appearance : ( ) => {
      return { 
        name : 'Create Pet', 
        icon : Config.get(
          'BlueprintActions',
          'createIcon',
          'https://storage.googleapis.com/molten-ui-assets/create-action.png'
        )
      }
    },
    perform : ( { context } ) => {
      const { clientProps : { actions }, reloadData } = context

      Dialogs.show( {
        component : CreatePet,
        props : { reloadData, actions, relationship }
      } )
    }
  } )

File CreatePet.jsx

  import React from 'react'
  import { Dialog, Toast } from '@leverege/ui-elements'
  import { GlobalState } from '@leverege/ui-redux'

  import CreateEditForm from './CreateEditForm'

  export default function CreatePet( props ) {
    const { show, onClose, actions, value, reloadData } = props

    const onSubmit = async ( { value } ) => {
      try {
        await GlobalState.dispatch( actions.create( value ) )
        Toast.success( 'Successfully Created Pet' )
      } catch ( err ) {
        console.error( err )
        Toast.error( 'Failed to Create Pet' )
      }
      await reloadData?.()
      onClose()
    }

    return (
      <Dialog show={show} onClose={onClose}>
        <CreateEditForm
          {...props}
          value={value}
          title="Create Pet"
          submitText="Submit"
          onSubmit={onSubmit}
          onCancel={onClose} />
      </Dialog>
    )
  }  

File CreateEditForm.jsx

  import React from 'react'
  import { Pane, Button, TextInput, NumericInput, PropertyGrid, Content } from '@leverege/ui-elements'
  import { TitleBar } from '@leverege/ui-plugin'
  import { GeoPointEditor } from '@leverege/ui-geo-elements'

  export default class CreateEditForm extends React.Component {

    constructor( props ) {
      super( props )
      const { value } = props
      this.state = {
        newValue : value
      }
    }

    onCancel = ( evt ) => {
      const { onCancel, eventData } = this.props
      return onCancel?.( { data : eventData, value : null, originalEvent : evt } )
    }

    onSubmit = ( evt ) => {
      const { onSubmit, eventData } = this.props
      const { newValue } = this.state

      return onSubmit?.( { data : eventData, value : newValue, originalEvent : evt } )
    }

    onAgeChange = ( evt ) => {
      const { newValue } = this.state
      this.setState( {
        newValue : { ...newValue, age : evt?.value }
      } )
    }

    onNameChange = ( evt ) => {
      const { newValue } = this.state
      this.setState( {
        newValue : { ...newValue, name : evt?.value }
      } )
    }

    onPositionChange = ( evt ) => {
      const { newValue } = this.state
      this.setState( {
        newValue : { ...newValue, position : evt?.value }
      } )
    }

    render() {
      const { value, submitText, title, titleIcon } = this.props
      const { newValue } = this.state
      return (
        <Content relative>
          <Content.Header variant="formHeader">
            <TitleBar variant="dialogTitle" title={title} icon={titleIcon} />
          </Content.Header>
          <Content.Area variant="formBody">
            <Pane>
              <PropertyGrid>
                <PropertyGrid.Item label="Name">
                  <TextInput value={newValue?.name} onChange={this.onNameChange} />
                </PropertyGrid.Item>
                <PropertyGrid.Item label="Age">
                  <NumericInput value={newValue?.age} onChange={this.onAgeChange} />
                </PropertyGrid.Item>
                <PropertyGrid.Item label="Position">
                  <GeoPointEditor value={newValue?.position} onChange={this.onPositionChange} />
                </PropertyGrid.Item>
              </PropertyGrid>
            </Pane>
          </Content.Area>
          <Content.Footer variant="formButtons" layout="flex:rowMEnd">
            <Pane layout="flex:rowM">
              <Button variant="secondary" onClick={this.onCancel}>Cancel</Button>
              <Button disabled={newValue === value} variant="primary" onClick={this.onSubmit}>{submitText}</Button>
            </Pane>
          </Content.Footer>
        </Content>

      )
    }
  }

Update Action

File UpdateAction.js

  import { Dialogs } from '@leverege/ui-elements'
  import { Config, Context } from '@leverege/plugin'
  import { DataSources } from '@leverege/ui-attributes'
  import UpdatePet from '../views/UpdatePet'

  // given a Relationship instance, make an update action that can be installed into molten
  export default relationship => ( { 
    id : 'blueprint.action.pet.UpdateItem',
    name : 'Update Pet Item',
    layout : { sort : 'item.update' },
    handles : ( cxt ) => { 
      const targets = Context.getTargetsOfType( cxt, 'petApi.pet' )
      return !( targets == null || targets.length === 0 || targets.length > 1 )
    },
    appearance : ( ) => {
      return { 
        name : 'Update Pet', 
        icon : Config.get(
          'BlueprintActions',
          'updateIcon',
          'https://storage.googleapis.com/molten-ui-assets/update-action.png'
        )
      }
    },
    perform : ( { context } ) => {
      const { reloadData } = context
      const target = Context.getTargetOfType( context, 'petApi.pet' )
      const actions = DataSources.getActions( target )

      Dialogs.show( {
        component : UpdatePet,
        props : { reloadData, actions, relationship, value : target?.data }
      } )
    }
  } )

File UpdatePet.jsx

  import React from 'react'
  import { GlobalState } from '@leverege/ui-redux'
  import { Dialog, Toast } from '@leverege/ui-elements'

  import CreateEditForm from './CreateEditForm'

  export default function UpdatePet( props ) {
    const { show, onClose, actions, value, reloadData } = props

    const onSubmit = async ( { value } ) => {
      try {
        await GlobalState.dispatch( actions.update( value ) )
        Toast.success( 'Successfully Updated Pet' )
      } catch ( err ) {
        console.error( err )
        Toast.error( 'Failed to Update Pet' )
      }
      await reloadData?.()
      onClose()
    }

    return (
      <Dialog show={show} onClose={onClose}>
        <CreateEditForm
          {...props}
          value={value}
          title="Update Pet"
          submitText="Update"
          onSubmit={onSubmit}
          onCancel={onClose} />
      </Dialog>
    )
  }

File CreateEditForm.jsx

  import React from 'react'
  import { Pane, Button, TextInput, NumericInput, PropertyGrid, Content } from '@leverege/ui-elements'
  import { TitleBar } from '@leverege/ui-plugin'
  import { GeoPointEditor } from '@leverege/ui-geo-elements'

  export default class CreateEditForm extends React.Component {

    constructor( props ) {
      super( props )
      const { value } = props
      this.state = {
        newValue : value
      }
    }

    onCancel = ( evt ) => {
      const { onCancel, eventData } = this.props
      return onCancel?.( { data : eventData, value : null, originalEvent : evt } )
    }

    onSubmit = ( evt ) => {
      const { onSubmit, eventData } = this.props
      const { newValue } = this.state

      return onSubmit?.( { data : eventData, value : newValue, originalEvent : evt } )
    }

    onAgeChange = ( evt ) => {
      const { newValue } = this.state
      this.setState( {
        newValue : { ...newValue, age : evt?.value }
      } )
    }

    onNameChange = ( evt ) => {
      const { newValue } = this.state
      this.setState( {
        newValue : { ...newValue, name : evt?.value }
      } )
    }

    onPositionChange = ( evt ) => {
      const { newValue } = this.state
      this.setState( {
        newValue : { ...newValue, position : evt?.value }
      } )
    }

    render() {
      const { value, submitText, title, titleIcon } = this.props
      const { newValue } = this.state
      return (
        <Content relative>
          <Content.Header variant="formHeader">
            <TitleBar variant="dialogTitle" title={title} icon={titleIcon} />
          </Content.Header>
          <Content.Area variant="formBody">
            <Pane>
              <PropertyGrid>
                <PropertyGrid.Item label="Name">
                  <TextInput value={newValue?.name} onChange={this.onNameChange} />
                </PropertyGrid.Item>
                <PropertyGrid.Item label="Age">
                  <NumericInput value={newValue?.age} onChange={this.onAgeChange} />
                </PropertyGrid.Item>
                <PropertyGrid.Item label="Position">
                  <GeoPointEditor value={newValue?.position} onChange={this.onPositionChange} />
                </PropertyGrid.Item>
              </PropertyGrid>
            </Pane>
          </Content.Area>
          <Content.Footer variant="formButtons" layout="flex:rowMEnd">
            <Pane layout="flex:rowM">
              <Button variant="secondary" onClick={this.onCancel}>Cancel</Button>
              <Button disabled={newValue === value} variant="primary" onClick={this.onSubmit}>{submitText}</Button>
            </Pane>
          </Content.Footer>
        </Content>

      )
    }
  }

Delete Action

File DeleteAction.js

import { Context, Config } from '@leverege/plugin'
import { Dialogs } from '@leverege/ui-elements'
import DeletePet from '../views/DeletePet'

export default ( relationship, { objectType, name, namePlural } ) => {
  const { path, attribute } = relationship
  const sectionName = `${name} Actions`

  return {
    id : `action.${path}.DeleteItem`,
    name : `Delete ${namePlural} Items`,
    layout : { sort : 'item.zzz', sectionName },
    handles : ( cxt ) => { 
      const targets = Context.getTargetsOfType( cxt, objectType )
      return !( targets == null || targets.length === 0 )
    },
    appearance : ( { context, action } ) => { 
      const targets = Context.getTargetsOfType( context, objectType )
      const num = targets.length
      return { 
        name : num < 2 ? `Delete ${name}` : `Delete ${num} ${namePlural}`,
        icon : Config.get(
          'PetActions',
          'deleteIcon',
          'https://storage.googleapis.com/molten-ui-assets/delete-action.png'
        ),
        disabled : num === 0
      }
    },
    perform : ( { context } ) => { 
      const { clientProps : { actions, targetKey } } = context
      const targets = Context.getTargetsOfType( context, objectType )
      if ( targets.length === 0 ) {
        return
      }
      Dialogs.show( {
        component : DeletePet,
        props : {
          targets,
          actions,
          attribute,
          isItem : false,
          selectionKey : targetKey,
        } } )
    }
  }
}

File DeletePet.jsx

  /* eslint-disable no-await-in-loop */
  import React from 'react'
  import { DataSources } from '@leverege/ui-attributes'
  import { GlobalState, Selection } from '@leverege/ui-redux'
  import { Dialog, Toast } from '@leverege/ui-elements'

  /**
  * Removes the userIds from the given action.
  */
  async function onRemove( { data } ) {

    // Get parameters
    const { targets, selectionKey, onClose } = data
    const num = targets.length
    if ( num === 0 ) {
      onClose()
      return
    }
    // Try to remove users
    let success = 0
    let failure = 0
    let err = null

    for ( let n = 0; n < num; n++ ) {
      try { 
        const tgt = targets[n]
        const actions = DataSources.getActions( tgt )
        await GlobalState.dispatch( actions.delete() )
        success++
        if ( selectionKey ) {
          GlobalState.dispatch( Selection.remove( selectionKey, tgt.id ) )
        }
      } catch ( error ) {
        failure++
        if ( err == null ) {
          err = error
        }
        // eslint-disable-next-line no-console
        console.error( error )
      }
    }

    // Successfully removed
    if ( failure === 0 ) {
      Toast.success( `${success === 1 ? '' : num} Pet${num === 1 ? '' : 's'} Deleted` )
    } else if ( success === 0 ) {
      Toast.error( [ `Failed to Delete ${failure === 1 ? '' : num} Pet`, err.message ] )
    } else {
      Toast.warn( [ `Deleted ${success}/${num} Pet. Failed to delete ${failure}.`, err.message ] )
    }
    onClose()
  }

  /**
  * Remove users dialog.
  */
  export default function DeletePet( props ) {

    // Get parameters
    const { onClose, show, targets } = props

    const num = targets?.length || 0
    const title = num < 2 ? 'Pet' : `${num} Pets`
    const button = 'Delete'
    const titleType = 'Delete'
    const extra = `This will permanently delete the ${title} and cannot be undone.`

    // Render component
    return (
      <Dialog.Question
        show={show}
        eventData={props}
        title={`${titleType} ${title}?`}
        message={`Are you sure you want to DELETE ${num} Pet${num === 1 ? '' : 's'}? ${extra}`}
        okay={button}
        okayVariant="primaryDestructive"
        onCancel={onClose}
        onOkay={onRemove}/>
    )
  }

Setup

File PluginSetup.js

import DeleteAction from './DeleteAction'
import CreateAction from './CreateAction'
import UpdateAction from './UpdateAction'
import Relationship from '../../../src/dataSource/Relationship'

const objectType = 'petApi.pet'
const name = 'Pet'
const namePlural = 'Pets'

exports.install = ( molten ) => {

  const relationship = new Relationship( { 
    apiName : 'petApi',
    name : 'pets',
    objectType,
    path : 'petApi.pets',
    refPath : '/pets',
    urlPath : '/pets',
  } )

  molten.addPlugin( 'Action', DeleteAction( relationship, { objectType, name, namePlural } ) )
  molten.addPlugin( 'Action', CreateAction( relationship, { objectType, name, namePlural } ) )
  molten.addPlugin( 'Action', UpdateAction( relationship, { objectType, name, namePlural } ) )
}