Ember Patterns

Forms

Move your form to it’s own component e.g:

<KbForm::Container>
  {{#if @model.schedule.isRunning}}
    <KbSpinner @caption="Please wait..." />
  {{else}}
    {{#with @model.schedule.value as |schedule|}}
      <WorkflowDeleteForm
        @schedule={{schedule}}
        @routeTransition={{this.routeTransition}}
      />
    {{/with}}
  {{/if}}
</KbForm::Container>

Using ember-changeset and validations:

import Component from "@glimmer/component"
import lookupValidator from "ember-changeset-validations"
import ScheduleValidations from "../validations/schedule"
import { action } from "@ember/object"
import { Changeset } from "ember-changeset"
import { inject as service } from "@ember/service"
import { task } from "ember-concurrency"
import { tracked } from "@glimmer/tracking"

export default class WorkflowDeleteFormComponent extends Component {
  @service kbMessages
  @tracked changeset = null

  constructor(owner, args) {
    super(owner, args)
    console.log(this.args.schedule)
    this.changeset = new Changeset(
      this.args.schedule,
      lookupValidator(ScheduleValidations),
      ScheduleValidations
    )
  }

  @action
  submitForm() {
    this.saveTask.perform()
  }

  @task *saveTask() {
    let process_id = this.changeset.get("id")
    this.changeset.validate()
    if (this.changeset.isValid) {
      try {
        yield this.changeset.save()
        if (this.args.routeTransition) {
          this.args.routeTransition(process_id)
        } else {
          console.log(
            "WorkflowDeleteFormComponent ('components/workflow-delete-form.js') no 'routeTransition'"
          )
        }
      } catch (e) {
        this.kbMessages.addError("Cannot delete the workflow process", e)
      }
    }
  }
}

To allow empty dates, use the following code:

if (this.changeset.isValid) {
  try {
    if (this.changeset.get("startDate") == "") {
      this.changeset.set("startDate", null)
    }
    yield this.changeset.save()

In the form, highlight changeset and server side errors:

<KbForm::Form::Field::ErrorMessageChangeset
  @errorField={{this.changeset.error.deleted_comment}}
/>
<KbForm::Form::Field::ErrorMessageServer
  @errorField={{@schedule.errors.deleted_comment}}
/>

Tip

To return server side errors, check out the _deleted_comment code from ScheduledWorkflowUserViewSet (work/api.py). 08/04/2021, Not sure if this works with serializer errors or our custom_exception_handler (api/models.py).

Notifications

Use the KbMessages service: https://gitlab.com/kb/kb-base-ember-addons/-/blob/tailwind-2-update-ember/addon/services/kb-messages.js

import { inject as service } from "@ember/service"

@service kbMessages

try {
  // do something
} catch (e) {
  this.kbMessages.addError("Cannot delete the workflow process", e)
}

Page

import { inject as service } from "@ember/service"

@service kbPage

export default class WorkflowRoute extends Route {

  beforeModel(transition) {
    this.kbPage.setTitle("Workflow")
  }

Pagination (JSON API)

Example pagination (meta) returned from a JSON API view:

{page: 5, pages: 191, count: 3817}

Pagination

Configure KbRestPagination as the pagination_class for the viewset:

from api.api_utils import SoftDeleteViewSet

class IssueViewSet(SoftDeleteViewSet):
    authentication_classes = (TokenAuthentication,)
    permission_classes = (IsAuthenticated,)
    serializer_class = IssueSerializer

In your controller:

queryParams = ["page"]

@tracked page = 1

get recordsPerPage() {
  /**
   * Number of records per page (``perPage``).
   *
   * This is a fixed value in Django and Ember code.
   */
  return 20
}

@action
setPage(page) {
  this.page = page
}

In your route, refresh the page and add perPage / totalPagesParam parameters:

queryParams = {
  page: {
    refreshModel: true,
  },
  sort: {
    refreshModel: true,
  },
  company_name: {
    refreshModel: true,
  },
};

contactTask = task(async (params) => {
  let filter = {};

  if (params.company_name) {
    filter = { company_name: params.company_name };
  }
  try {
    let contact = await this.store.query('contact', {
      page: {
        number: params.page,
      },
      sort: params.sort,
      filter: filter,
    });
    return await contact;
  } catch (e) {
    this.kbMessages.addError('Cannot load contact', e);
  }
});

Add the KbPagination component in your template (just before the table):

<KbBase::Content>
  <KbTable::Container>
    {{#if @model.tickets.isRunning}}{{else}}
      <KbPagination
        @model={{@model.tickets.value}}
        @page={{this.page}}
        @recordsPerPage={{this.recordsPerPage}}
        @setPage={{this.setPage}}
      />
    {{/if}}
    <KbTable::Table>

Tip

If the KbPagination is inside a SlideOver, then add @isSlideOver={{true}}.

Slide Over

{{#if this.slideOverVisible}}
  <KbSlideOver>
    <KbSlideOver::Toggle
      @title="Add Contacts"
      @toggleSlideOver={{this.toggleSlideOver}}
    />

    // Content of Slide-over...

  </KbSlideOver>
{{/if}}

Tables / Data

Checklist

  1. Add reload: true to all findRecord and findAll methods For details, see Error Handling

  2. Add a try, catch to all store methods. Use kbMessages.addError in the catch (see example below).

  3. Test the store methods with an exception on the server (500 error).

  4. Check the user is notified if a store method returns no data.

Route:

import { inject as service } from "@ember/service"

@service kbMessages

model(params) {
  return {
    processes: this.processesTask.perform(params)
  }
}

@task *processesTask(params) {
  try {
    let processes = yield this.store.query("process", params)
    return yield processes
  } catch (e) {
    this.kbMessages.addError("Cannot load workflow processes", e)
  }
}

The components for tables can be found here Table. To add a spinner and nothing found to your table:

{{#if @model.processes.isRunning}}
  <KbTable::Body::Row>
    <KbTable::Body::Cell>
      <KbSpinner @caption="Please wait..." />
    </KbTable::Body::Cell>
    <KbTable::Body::Cell />
    <KbTable::Body::Cell />
    <KbTable::Body::Cell />
    <KbTable::Body::Cell />
    <KbTable::Body::Cell />
  </KbTable::Body::Row>
{{else}}
  <KbTable::Body>
    {{#each @model.processes.value as |process|}}
      <Workflow::TableRow @process={{process}} />
    {{else}}
      <KbTable::Body::Row>
        <KbTable::Body::Cell>
          {{#if this.workflow}}
            No workflow processes found ...
          {{else}}
            Please select a workflow...
          {{/if}}
        </KbTable::Body::Cell>
        <KbTable::Body::Cell />
        <KbTable::Body::Cell />
        <KbTable::Body::Cell />
        <KbTable::Body::Cell />
        <KbTable::Body::Cell />
      </KbTable::Body::Row>
    {{/each}}
  </KbTable::Body>
{{/if}}

Tip

For pagination, see Pagination (JSON API).