React based user interface for the DINA project.

1. Technology Stack

This application is developed using these technologies:

  • Yarn: Node.js package manager

  • TypeScript: Superset of JavaScript with static types.

  • React: JavaScript library for building user interfaces.

  • Next.js: Framework for building React-based applications.

  • Bootstrap: CSS framework.

  • Jest: Testing framework.

  • Docker: Application containerization

2. UI Design Guidelines

2.1. Page Layout

Navigation

All page components should be aligned with the navigation and footer elements. The PageLayout component should be used as the root element to enforce this structure:

<PageLayout titleId="splitSubsampleTitle" buttonBarContent={buttonBar}>
  ... Main content of the page
</PageLayout>

2.2. Order of Components

Navigation

The components on a page should follow this order:

  1. Navigation Bar

  2. Header

  3. Button Bar

  4. Main Content

  5. Footer

2.3. Button Bar

DINA design guidelines button bar

The Back button should always be positioned on the left side and displayed as a link since it serves as a navigation element.

Action buttons like Edit, Create, Delete and Revisions should be placed on the right side.

2.4. Header

DINA design guidelines header
  1. This is the main header text, depending on the page it will display:

    1. List page: The plural name of the entity (eg. Assemblages)

    2. View/Edit page: The name/identifier of the entity itself being edited. UUIDs should only be used if a name/identifier is not set.

  2. Tooltips can be added to the right to describe the entity itself. Should be referenced from the DINA user docs.

  3. Group name will be displayed on the right only for viewing and editing records. It should be all capital letters.

3. Dev tools

3.1. IDE

Visual Studio Code is recommended for development.

3.2. Browser Extensions

4. Getting Started

Before starting, you will need to setup your dev environment:

4.1. Node.js and Yarn

See dina-local-deployment prerequisites

You can check which versions you currently have installed with:

node -v
yarn -v

4.2. Installing dependencies

After you have downloaded all of the required tools, you will need to install the required dependencies needed to run the app.

It’s a good idea to run this command when checking out a new branch since the dependencies might have changed.

yarn

5. React Component Types

These are some common patterns we use when working with React components:

5.1. Pages

Stored in "packages/dina-ui/pages". The folder path and filename correspond to the pages’s URL, which is automatically routed by Next.js.

5.2. Page Layouts

e.g. ViewPageLayout and ListPageLayout. If a set of pages with a similar layout exists we can make page layout components to avoid duplicating code across page Components.

5.3. DinaForm

The main way to create forms in dina-ui. It’s a wrapper around Formik with some added props and error message handling.

5.4. Form Fields

Usually stored in "packages/common-ui/lib/formik-connected" with the Component name ending in "Field". These are form inputs with a "name" prop for the property in the form data the field edits.

e.g.

<DinaForm initialValues={{ myField: "initial text" }}>
  <TextField name="myField" className="col-md-6" />
</DinaForm>

6. Audit / Revisions UI

The "revisions" pages show edit history of specific records and user activity.

Audit records are fetched from the back-end which is implemented using Javers. See the DINA back-and auditing docs.

6.1. Adding audit support for one Resource Type

For tracking changes to a single record. This page should be linked to from the record’s details page.

You can re-use the RevisionsPage component to create a revisions page for the specified resource:

Example from dina-ui/pages/collection/material-sample/revisions.tsx:

import { COLLECTION_MODULE_REVISION_ROW_CONFIG } from "../../../components/revisions/revision-modules";
import { RevisionsPage } from "../../../components/revisions/RevisionsPageLayout";

export default () => (
  <RevisionsPage
    // The API route to the Javers snapshot list
    auditSnapshotPath="collection-api/audit-snapshot"
    // URL to the details page
    detailsPageLink="/collection/material-sample/view?id="
    // Query for the current state of the record
    queryPath="collection-api/material-sample"
    // The JSONAPI resource "type"
    resourceType="material-sample"
    // The row configs are specific to each module (Collection, Object Store, Agent, etc.)
    revisionRowConfigsByType={COLLECTION_MODULE_REVISION_ROW_CONFIG}
    // The field on the resource that gives the name. Usuallu this field is called "name"
    nameField="materialSampleName"
  />
);

6.2. Adding audit support for a user’s edits across a DINA module

For tracking all changes to any record by a user. This page should be linked to from the main UI navigation.

There is a separate "revisions-by-user" page per DINA module (Collection, Object Store, Agent, etc.) because the back-end is a separate application for each DINA module. There is no way to retrieve a user’s activity across all multiple back-ends in one query.

You can re-use the RevisionsByUserPage component to create a revisions page for the specified DINA module:

Example from dina-ui/pages/collection/revisions-by-user.tsx:

import { COLLECTION_MODULE_REVISION_ROW_CONFIG } from "../../components/revisions/revision-modules";
import RevisionsByUserPage from "../../components/revision-by-user/CommonRevisionsByUserPage";

export default function CollectionRevisionByUserPage() {
  return (
    <RevisionsByUserPage
      // The API route to the Javers snapshot list
      snapshotPath="collection-api/audit-snapshot"
      // The row configs are specific to each module (Collection, Object Store, Agent, etc.)
      revisionRowConfigsByType={COLLECTION_MODULE_REVISION_ROW_CONFIG}
    />
  );
}

6.3. Configuring how audit data is displayed in the "Show Changes" section.

The back-end provides historical data as unstructured JSON (in the "state" field), so the UI might not always be able to show a good looking UI based on the data it’s given because the data schema is unknown. Simple string or number fields are easily rendered as-is, but the last-resort fallback for an unknown object structure is to show stringified JSON in the UI. The Revision UI components are written in a way that unstructured data is displayed generically by looping through the key-value pairs of the given JSON, but specific field names can be manually overwritten in the code to improve the way that the UI displays those fields.

All edited fields will be rendered generically in the Revisions UI, but to manually register fields to be rendered in a specific way, go to:

dina-ui/components/revisions/revision-modules.ts

and add your typename into the map for the correct DINA module.

For examples of types added:

import { RevisionRowConfigsByType } from "./revision-row-config";
import { COLLECTING_EVENT_REVISION_ROW_CONFIG } from "./revision-row-configs/collectingevent-revision-row-config";
import { MATERIAL_SAMPLE_REVISION_ROW_CONFIG } from "./revision-row-configs/material-sample-revision-row-configs";
import { METADATA_REVISION_ROW_CONFIG } from "./revision-row-configs/metadata-revision-row-config";

/** Custom revision row behavior for Object Store Resources. */
export const OBJECT_STORE_MODULE_REVISION_ROW_CONFIG: RevisionRowConfigsByType = {
  metadata: METADATA_REVISION_ROW_CONFIG
};

/** Custom revision row behavior for Object Store Resources. */
export const COLLECTION_MODULE_REVISION_ROW_CONFIG: RevisionRowConfigsByType = {
  "collecting-event": COLLECTING_EVENT_REVISION_ROW_CONFIG,
  "material-sample": MATERIAL_SAMPLE_REVISION_ROW_CONFIG
};
  • In the Object Store module, mappings have been added for the "metadata" type.

  • In the Collection module, mappings have been added for the "collecting-event" and "material-sample" types.

See the implementation of MATERIAL_SAMPLE_REVISION_ROW_CONFIG for examples of field renderers.

7. Running Tests

7.1. Run all tests

Run yarn test from the top-level dina-ui repo.

7.2. Run individual tests

Either

  • From VS Code: Install the Jest Runner extension, which adds Run/Debug buttons to your test files.

  • From the command line:

    • Using just the file name: yarn jest QueryTable.test.tsx

    • Using the full path (when there is more than one file with this name): yarn jest packages/common-ui/lib/table/tests/QueryTable.test.tsx

8. Versioning

Dina UI’s version is stored in the dina-ui package’s package.json (Not the repo’s top-level package.json). You can update this version when releasing a new version of the application by running this command at the repo’s top level:

yarn --cwd ./packages/dina-ui version

9. Yarn Package Scripts

9.1. Top-level directory

Commands you can run from the root package:

9.1.1. yarn docs

Compiles the docs (written in AsciiDoc) as a single HTML file in the "generated-docs" directory.

9.1.2. yarn test

Runs the tests for all packages (without generating the test coverage reports).

9.1.3. yarn test:coverage

Runs the tests for all packages. Prints the test coverage report to stdout, and writes the coverage report files into the "coverage" directory.

9.2. Sub-package directories

Commands you can run from sub-packages:

9.2.1. yarn build

Generates the production build.

9.2.2. yarn dev

Runs the UI application at localhost:3000 in dev mode using Next.js. When you save a file, you can refresh the page to see your changes without restarting the app. Note that this does not let the UI connect to the backend because there is no HTTP proxy set up. See "https://github.com/poffm/dina-dev" for the full dev mode with a proxy.

9.2.3. yarn next

Runs the UI application at localhost:3000 in dev mode using Next.js. When you save a file, you can refresh the page to see your changes without restarting the app. Note that this does not let the UI connect to the backend because there is no HTTP proxy set up. See "https://github.com/poffm/dina-dev" for the full dev mode with a proxy.

9.2.4. yarn test

Runs the tests (without generating the test coverage report).

9.2.5. yarn test:coverage

Runs the tests. Prints the test coverage report to stdout, and writes the coverage report files into the "coverage" directory.

10. Building the app Docker images

From the top-level directory, run the yarn build and then the docker build:

yarn
yarn workspace dina-ui build
docker build -t dina-ui:dev .

Docker environment variables required to run:

dina-ui:

  • OBJECTSTORE_API_ADDRESS: The back-end API URL and port. e.g. objectstore-api:8080

  • USER_API_ADDRESS: The back-end API URL and port. e.g. user-api:8080

  • COLLECTION_API_ADDRESS: The back-end API URL and port. e.g. collection-api:8080

  • AGENT_API_ADDRESS: The back-end API URL and port. e.g. agent-api:8080

  • SEQDB_API_ADDRESS: The back-end API URL and port. e.g. seqdb-api:8080

  • DISABLE_BROWSER_CHECK: (optional) Whether to disable the browser check for blocking Internet Explorer. e.g. true

11. React Components

11.1. Forms

Forms can be made in this app using Formik .

Our custom formik-integrated input field components can be found in /components/formik-connected. These inputs should be easily re-usable throughout the app.

Example form that on submit either shows a success alert or an error message:

import { DinaForm, SubmitButton, TextField } from "../components";

export default function ExampleFormPage() {
  const onSubmit: DinaFormOnSubmit = async ({
    submittedValues,
    api: { save }
  }) => {
    if (!submittedValues.name) {
      throw new Error("Name must be set.");
    }

    // Makes a Create or Update call to the back-end.
    await save(
      [
        {
          resource: submittedValues,
          type: "my-type"
        }
      ],
      { apiBaseUrl: "/objectstore-api" }
    )
  }

  return (
    <div className="card card-body" style={{ width: "1000px" }}>
      <DinaForm initialValues={{}} onSubmit={onSubmit}>
        <TextField name="name" />
        <TextField name="description" />
        <SubmitButton />
      </DinaForm>
    </div>
  );
}

11.1.1. Getting a Field value from the form.

Sometimes you will want to get a field’s value in one component without re-rendering the surrounding component. You can listen to a Field’s state using FieldSpy:

import { DinaForm, SubmitButton, TextField, FieldSpy } from "common-ui";

export default function ExampleFormPage() {
  return (
    <div className="card card-body" style={{ width: "1000px" }}>
      <DinaForm initialValues={{}} onSubmit={onSubmit}>
        <TextField name="name" />
        <FieldSpy fieldName="name">
          {value => `Your name is ${value}`}
        </FieldSpy>
        <SubmitButton />
      </DinaForm>
    </div>
  );
}

11.1.2. Tooltips

The tooltip is found from the intl messages file using the key "field_{fieldName}_tooltip". As an example:

<TextField name="name" />
<TextField name="description" />

// In the dina-ui-en.ts file:
field_name_tooltip: "This message will appear in the name fields tooltip."
field_description_tooltip: "This message will appear in the description fields tooltip."

Tooltip ids can also be manually added using the customName property. This can be handy for fields that share a common name like "description". Here is an example of using a custom tooltip id:

<TextField name="name" customName="metadataName" />

// In the dina-ui-en.ts file:
field_metadataName_tooltip: "This will be the message that appears for the name fields tooltip."

You are also able to add images and links using the following properties on any of the Field components. Below is a table of all of the different supported properties for images and links.

Table 1. Table Tooltip properties
Property Name Description Example

tooltipImage

URL of a image to display in the tooltip. It is displayed under the tooltip texts (if supplied).

tooltipImage="/images/tooltip_image.png"

tooltipImageAlt

Accessability text which should be used when a tooltipImage is provided. Best practice is to use internationalization key so it works in multiple languages.

tooltipImageAlt="tooltip_image_alt"

tooltipLink

URL of a link to display at the bottom of the tooltip.

tooltipLink="https//www.google.com"

tooltipLinkText

Text identifier to appear in the tooltip link text. Defaults to a generic message if not supplied.

tooltipLinkText="tooltip_link_reference"

11.1.3. Text field with dropdown suggestions

Using the AutoSuggestTextField component, you can provide a dropdown of suggestions for a text field as the user types.

The AutoSuggestTextField component supports JSON API and elastic search for auto-suggestions, you can even provide both and fallback to the JSON API if the elastic search fails for example.

Important: This is used for retrieving text suggestions. The form will be returned a string. Not a UUID for a resource. Checkout the ResourceSelectField for that functionality.

JSON API example

To configure the JSON API auto-suggestions, you need to setup the jsonApiBackend prop:

<AutoSuggestTextField<Person>
  name="examplePersonNameField"
  jsonApiBackend={{
    query: searchValue => ({
      path: "agent-api/person",
      filter: {
        rsql: `name==*${searchValue}*`
      }
    }),
    option: person => person?.name
  }}
/>

The query defines the path and any filters you wish to use. You can also easily retrieve the current search value to filter by.

The option is used to determine the text to display for each suggestion. This is also the value that is returned to the form.

Elastic Search example

To configure the Elastic Search auto-suggestions, you need to setup the elasticSearchBackend prop:

<AutoSuggestTextField<Person>
  name="examplePersonNameField"
  elasticSearchBackend={{
    indexName: "dina_agent_index",
    searchField: "data.attributes.name",
    option: person => person?.name
  }}
/>

indexName is the name of the Elastic Search index to use. (required) searchField is the name of the field to search in, this needs to be the full path. (required) additionalField is another field to search by in addition to the search field. This also needs to the full path. (optional) restrictedField is used to filter by a specific field in the index. This also needs to the full path. (optional) restrictedFieldValue is the value to search against the restricted field. (optional) option is used to determine the text to display for each suggestion. This is also the value that is returned to the form. (required)

Typescript Support

The AutoSuggestTextField component supports Typescript. To use it, you need to import the AutoSuggestTextField component and pass it the type of the resource you are searching for.

For example for a Person (Kitsu Resource):

<AutoSuggestTextField<Person> />

Providing the type, will make the option section of the component more specific:

<AutoSuggestTextField<Person>
  name="examplePersonNameField"
  elasticSearchBackend={{
    indexName: "dina_agent_index",
    searchField: "data.attributes.name",
    option: person => person?.name // This part you can now say .name since the type was defined.
  }}
/>

If you are using an included search field, insure the type being used contains the field you are looking for. If it doesn’t exist on the type it will not be parsed.

Default to elastic search, fall back to JSON API example
<AutoSuggestTextField<Person>
  name="examplePersonNameField"
  elasticSearchBackend={{
    indexName: "dina_agent_index",
    searchField: "data.attributes.name",
    option: person => person?.name
  }}
  jsonApiBackend={{
    query: searchValue => ({
      path: "agent-api/person",
      filter: {
        rsql: `name==*${searchValue}*`
      }
    }),
    option: person => person?.name
  }}
  preferredBackend={"elastic-search"} // Default to elastic search
/>

In the example above, both "elastic-search" and "json-api" are supplied. Elastic search will be used first and it will keep using elastic search until any errors occur.

Once an error occurs it will switch to the other available backend, in this case the "json-api".

It’s important to make sure both providers work correctly since it will probably be rare that provider will fail.

Custom options example

You can provide your own suggestions directly using the customOptions prop:

<AutoSuggestTextField<Person>
  name="examplePersonNameField"
  customOptions={value => [
    "suggestion-1",
    "suggestion-2",
    "suggestion-" + value
  ]}
/>

In this example the following suggestions will be provided if the user types "3" into the text field:

  • suggestion-1

  • suggestion-2

  • suggestion-3

While you can provide static options, you can also call an API to populate these results or another function:

<AutoSuggestTextField
  name={fieldProps("matchValue", index)}
  blankSearchBackend={"preferred"}
  customOptions={value =>
    useElasticSearchDistinctTerm({
      fieldName:
        dataFromIndexMapping?.parentPath +
        "." +
        dataFromIndexMapping?.path +
        "." +
        dataFromIndexMapping?.label,
      groups: selectedGroups,
      relationshipType: dataFromIndexMapping?.parentName,
      indexName
    })?.filter(suggestion =>
      suggestion?.toLowerCase()?.includes(value?.toLowerCase())
    )
  }
/>
Blank Search provider example

You can even provide suggestions even if the text field is blank. Using the blankSearchProvider property, you can configure what happens during a blank search.

If you would like blank searches to occur with the current provider you can use the "preferred" option:

<AutoSuggestTextField<Person>
  name="examplePersonNameField"
  elasticSearchBackend={{
    indexName: "dina_agent_index",
    searchField: "data.attributes.name",
    option: person => person?.name
  }}
  jsonApiBackend={{
    query: searchValue => ({
      path: "agent-api/person",
      filter: {
        rsql: `name==*${searchValue}*`
      }
    }),
    option: person => person?.name
  }}
  preferredBackend={"elastic-search"}
  blankSearchBackend={"preferred"}
/>

In this example elastic search is the default provider. On a blank search elastic search is used unless it fails then the JSON API is used.

Preferred is useful if you don’t care which backend provider is used for a blank search.

Otherwise you can specify a specific backend to use for blank searches, even if it differs from the currently selected one.

<AutoSuggestTextField<Person>
  name="examplePersonNameField"
  elasticSearchBackend={{
    indexName: "dina_agent_index",
    searchField: "data.attributes.name",
    option: person => person?.name
  }}
  jsonApiBackend={{
    query: searchValue => ({
      path: "agent-api/person",
      filter: {
        rsql: `name==*${searchValue}*`
      }
    }),
    option: person => person?.name
  }}
  preferredBackend={"elastic-search"}
  blankSearchBackend={"json-api"}
/>

In the example above, elastic search is the default provider. On a blank search JSON API is used only if the search is blank.

If elastic search fails, then JSON API will be used for both blank and non-blank searches.

If you wish to not display any suggestions on a blank search, you can just remove the blankSearchBackend property and no suggestions will appear until a value is provided.

11.2. Resource Select Field Component

This is a searchable dropdown select component for selecting a back-end resource from a list.

Example usage for selecting a Group from a list:

import { DinaForm, ResourceSelectField } from "common-ui";
import { Group } from "../types/user-api/resources/Group";

/**
 * This page shows a dropdown for selecting a Group. You can type the name of a Group you're
 * looking for into the input, which sends a filtered query to the back-end API for a Group with
 * that name. When you select a Group, the onChange callback function sets the selected group into
 * the page's component state. The page displays a message with the name of the currently selected
 * Group.
 */
export default function ExamplePage() {
  return (
    <div className="card card-body" style={{ width: "500px" }}>
      <DinaForm initialValues={{ myGroup: null }}>
        {({ values: { myGroup } }) => (
          <div>
            <h2>Selected group: {myGroup ? myGroup.groupName : "None"}</h2>
            <ResourceSelectField<Group>
              name="myGroup"
              filter={groupName => ({ groupName })}
              model="group"
              optionLabel={group => group.groupName}
            />
          </div>
        )}
      </div>
    </div>
  );
}

11.3. FilterBuilderField component

The FilterBuilderField component provides a Formik input for creating complex filter queries. The FilterGroupModel value this input provides can be converted to an RSQL string to inluded in the API request.

Example usage for filtering PcrPrimers in a list page:

import { FilterParam } from "kitsu";
import { useState } from "react";
import { FilterBuilderField } from "../components/filter-builder/FilterBuilderField";
import { rsql } from "../components/filter-builder/rsql";
import Head from "../components/head";
import Nav from "../components/nav";
import { QueryTable } from "../components/table/QueryTable";
import { DinaForm, DinaFormOnSubmit } from "../components/formik-connected/DinaForm";

export default function ExamplePage() {
  const [filter, setFilter] = useState<FilterParam>();

  const onSubmit: DinaFormOnSubmit = ({ submittedvalues:  { filter } }) => {
    setFilter({ rsql: rsql(filter) });
  }

  return (
    <div>
      <Head />
      <Nav />
      <div className="container-fluid">
        <DinaForm initialValues={{}} onSubmit={onSubmit}>
          <FilterBuilderField
            filterAttributes={["name", "group.groupName"]}
            name="filter"
          />
          <button type="submit">search</button>
        </DinaForm>
        <QueryTable
          columns={["name", "type", "group.groupName"]}
          filter={filter}
          include="group"
          path="pcrPrimer"
        />
      </div>
    </div>
  );
}

12. API Client

12.1. useQuery Hook Function

useQuery is an API querying function using the React Hooks API. You can use it to easily query the back-end from inside a function component.

Example usage for fetching a single resource:

import { useQuery } from "../components";
import { PcrPrimer } from "../types/seqdb-api/resources/PcrPrimer";

function ExampleComponent() {
  const { error, loading, response } = useQuery<PcrPrimer>({ path: "pcrPrimer/1" });

  if (loading) {
    return <div>Loading...</div>;
  } else if (error) {
    return <ErrorInfo error={error} />;
  } else {
    return <div>Primer name: {response.data.name}</div>;
  }
}

Example usage for fetching a resource list:

import { useQuery } from "../components";
import { PcrPrimer } from "../types/seqdb-api/resources/PcrPrimer";

function ExampleComponent() {
  const { error, loading, response } = useQuery<PcrPrimer[]>({ path: "pcrPrimer" });

  if (loading) {
    return <div>Loading...</div>;
  } else if (error) {
    return <ErrorInfo error={error} />;
  } else {
    return (
      <div>
        {response.data.map(primer => (
          <PcrPrimerInfo key={primer.id} primer={primer} />
        ))}
      </div>
    );
  }
}

Using the "withResponse" helper function to render generic loading and error components with minimal code, instead of writing if/else blocks for each state:

import { useQuery, withResponse } from "../components";
import { PcrPrimer } from "../types/seqdb-api/resources/PcrPrimer";

function ExampleComponent() {
  const primerQuery = useQuery<PcrPrimer[]>({ path: "pcrPrimer" });

  return withResponse(primerQuery, ({ data: primers }) => (
    <div>
      {primers.map(primer => (
        <PcrPrimerInfo key={primer.id} primer={primer} />
      ))}
    </div>
  ));
}

12.2. Performing write operations

This application uses React’s hooks and context API to provide a "save" function which allows you to perform write operations against the back-end API. "save" accepts an array of operations to perform, so you can submit multiple resource operations in a single transaction in a single HTTP request.

If one operation fails, the entire transaction is cancelled, and "save" throws an Error, which provides an aggregation message of all of the error messages. This function relies on the back-end to support JSONAPI’s jsonpatch extension (implemented by crnk-operations).

Here is a simple example of a component that uses the "save" function from the context to provide a button that creates a new Region with a random name and symbol:

import React from "react";
import { useApiClient } from "common-ui";

export funcion NewRegionButton() {
  const { save } = useApiClient();

  async function createRegion() {
    await save(
      [
        {
          resource: {
            name: `new-region-${Math.random()}`,
            seq: "",
            symbol: `${Math.random()}`,
            type: "PRIMER"
          },
          type: "region"
        }
      ],
      { apiBaseUrl: "/seqdb-api" }
    );
  };

  return <button onClick={this.createRegion}>Create Region</button>;
}

13. Internationalization

13.1. Internationalization / Multiple Languages Support

This application can display messages in multiple languages using react-intl.

13.1.1. Message components

When developing UI, you should not hard-code messages into the UI, like this:

<h2>PCR Primers</h2>

You should instead reference a message key:

import { DinaMessage } from "../../intl/dina-intl.ts""

...

<h2><DinaMessage id="pcrPrimerListTitle" /></h2>

13.1.2. formatMessage function

You can also use the "formatMessage" function if you want to reference a message outside of a JSX tree:

import { useDinaIntl } from "../../dina-intl.ts""

function MyComponent() {
  const { formatMessage } = useDinaIntl();

  useEffect(() => {
    alert(formatMessage("greeting"));
  }, []);

  return (
    <div>...</div>
  );
}

13.1.3. Defining messages

The actual text for these messages needs to be defined in a messages file, like dina-en.ts. Each UI app ( dina-ui is the only UI app at the moment ) should have its own messages files. common-ui has its own messages files for shared messages used by multiple apps.

dina-en.ts:

export const DINA_MESSAGES_ENGLISH = {
  ...
  greeting: "Hello!"
  ...
}

Type-checking is enabled so the message ID passed into "DinaMessage" or "formatMessage" MUST be a key in at least the English messages file. To add a French (or other language) translation for a message, the English version of the message must already be defined. The UI uses the English translation as a fallback if the message has no translation for the user’s locale.

Example from dina-fr.ts:

export const DINA_MESSAGES_FRENCH: Partial<typeof DINA_MESSAGES_ENGLISH> = {
  ...
  greeting: "Bonjour!", // If the key exists in the english messages file, Typescript allows it.
  otherKey: "C'est un autre message." // If "otherKey" isn't a key in the English messages, this should show an error in your IDE.
  ...
};

13.1.4. Field label messages

Some components, like QueryTable, FilterBuilder and FieldWrapper (which wraps our Formik input components) take in a list of field names ( e.g. ["name","version","group.groupName"] ) and display auto-generated title-case labels of these field names ( e.g. Name, Version, Group Group Name ). Sometimes the auto-generated labels need to be manually overridden, so you can define custom messages for these labels using the field_${fieldName} key format.

Example:

export const DINA_MESSAGES_ENGLISH = {
  ...
  field_name: "Name",
  "field_group.groupName": "Group Name",
  ...
}
export const DINA_MESSAGES_FRENCH: Partial<typeof DINA_MESSAGES_ENGLISH> = {
  ...
  field_name: "Nom",
  "field_group.groupName": "Nom de Groupe"
  ...
}
Field tooltip messages

You can add an additional tooltip message to go inside the Field Label after the field name. After adding the field label message, add a tooltip message by adding "_tooltip" to the end of the field label’s message.

Example:

export const DINAUI_MESSAGES_ENGLISH = {
  ...
  field_publiclyReleasable: "Publicly releasable",
  field_publiclyReleasable_tooltip:
    "Indicates if the object could be released publicly on a web page or open data portals.",
  ...
}

13.1.5. Export messages as CSV

Run the following command to export the messages in CSV format:

cd ./packages/scripts
yarn --silent export-intl-csv > messages.csv

13.1.6. Import CSV files to update Typescript message files

Run the following command after editing your CSV file:

cd ./packages/scripts
CSVFILE=messages.csv yarn import-intl-csv

14. WCAG compliance tools setup

14.1. Pa11y

  • Install Pa11y

git clone https://github.com/pa11y/pa11y.git
cd pa11y
  • Run Pa11y for multiple pages

copy below example multiple.js file to pa11y dir

// An example of running Pa11y on multiple URLS
'use strict';

const pa11y = require('.');

runExample();

// Async function required for us to use await
async function runExample() {
        try {

                // Put together some options to use in each test
                const options = {
                        log: {
                                debug: console.log,
                                error: console.error,
                                info: console.log
                        }
                };

                // Run tests against multiple URLs
                const results = await Promise.all([
                        pa11y('http://localhost:8000/product/list', options),
                        pa11y('http://localhost:8000/product/edit?id=1', options),
                        pa11y('http://localhost:8000/product/view?id=1', options)

                ]);

        } catch (error) {

                // Output an error if it occurred
                console.error(error.message);

        }
}

run below

node multiple.js > WCAGreports.txt

WCAGreports.txt will be generated for all the elements on all pages that have WCAG compliant issues.

Part of the example output report will be like below

 {
      code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.InputText.Name',
      context: '<input class="form-control" type="text" value="">',
      message: 'This textinput element does not have a name available to an accessibility API. Valid names are: label element, title undefined, aria-label undefined, aria-labelledby undefined.',
      type: 'error',
      typeCode: 1,
      selector: '#__next > div > div > div > div > form > div:nth-child(2) > div:nth-child(3) > div:nth-child(2) > div > input'
    },
    {
      code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.F68',
      context: '<input class="form-control" type="text" value="">',
      message: 'This form field should be labelled in some way. Use the label element (either with a "for" attribute or wrapped around the form field), or "title", "aria-label" or "aria-labelledby" attributes as appropriate.',
      type: 'error',
      typeCode: 1,
      selector: '#__next > div > div > div > div > form > div:nth-child(2) > div:nth-child(3) > div:nth-child(2) > div > input'
    },

14.2. Chrome Inspector

To have a visual feedback of which html element has violated the WCAG rule,use Chrome Inspector

  • Download the Chrome stable version

  • Launch a page, e.g, http://localhost:8000/product/list

  • Press F12 to lauch the chome devTools

  • Go to audit tab, check the "accessibility" checkbox and press "run audit" button

  • A report is shown for the page regarding the WCAG validation and the elements that have WCAG compliance issue.

  • Click on the element, you can visually see the issue on premise.

  • After fix specific issue, rerun the audit report to verify the issue is gone

15. Pa11y-ci automation

We used pa11y-ci to automate the WCAG compliance detection and pa11y-ci-reporter-html to generate report of configured level of severity with good readability.

15.1. Follow below steps in sequence

15.1.1. Go to packages/dina-ui for object store UI ( packages/seqdb-ui for seqdb UI )

cd packages/dina-ui

15.1.2. Check compliance for all configured pages and output json format results

yarn a11y-check

15.1.3. Generate user fridendly html report based on above results

yarn a11y-generate-report

Note for linux user, due to a bug in pa11y-ci-html-reporter npm package, will need to run below first:

sudo apt install dos2unix

dos2unix /home/shemy/git/dina-ui/node_modules/pa11y-ci-reporter-html/bin/pa11y-ci-reporter-html.js

15.1.4. Know issues

Due to the pa11y implementation, in order to get all compliance level report (A, AA are the currentl AAFC scope), one will need to change the .pa11yci file to manually switch between the A and AA to cover both,as the script takes one arg only.

e.g, modify the .pa11yci file to change Standards: "WCAG2AA" to Standards: "WCAG2A", as Standards: "WCAG2AA" is the default, and WCAG2AAA is out of scope as of writting the doc.

16. Translation task

16.1. To export to csv file

  • To get the net items for translation ( instead of sending all entries in the file with some items having blank French translation), add a filter in messages-csv-export.ts as in below line:

const rows = keys.filter(key => english.messages[key] && (french.messages[key] === undefined ||
      french.messages[key]?.trim().length === 0 ||
    french.messages[key] === null )
    ).map<CsvRow>(key => ({

run

cd ./packages/scripts
yarn --silent export-intl-csv > messages.csv
  • Open the csv and save as xlsx file, make sure the xlsx is able to be previewed

  • Go to the translation site and clone the previous project (or create a new project), upload the xlsx file and submit, when translation is received or done, an email will be sent to the contact email.

16.2. To import from translated csv file

  • Go to the translation site and download the target translated file

  • Make sure the column header has french instead of français

  • Open the file in windows excel and save it as CSV (Comma delimited) file.

  • Import the file into the /packages/script directory.

Run the following commands:

cd ./packages/scripts
CSVFILE=messages.csv yarn import-intl-csv
  • Verify there are no unexpected characters, e.g apostrophe are the same as existing ones etc.