# Developer Guide This is a guide for people who develop Chaise. ## Table of contents - [Reading Material](#reading-material) - [Idioms](#idioms) * [Naming conventions](#naming-conventions) * [General](#general-1) * [React/TypeScript](#reacttypescript-1) * [Lint](#lint) * [CSS/SCSS](#cssscss-1) * [Font Awesome](#font-awesome) * [Handling time](#handling-time) - [Folder structure](#folder-structure) - [Building and installation](#building-and-installation) * [Make targets](#make-targets) * [NPM](#npm) - [Structure of an App](#structure-of-an-app) - [Using Chaise through npm](#using-chaise-through-npm) - [Error handling](#error-handling) * [How it works](#how-it-works) * [Guidelines](#guidelines) * [Guidelines for promise chains](#guidelines-for-promise-chains) - [Context and provider pattern](#context-and-provider-pattern) - [Performance](#performance) * [Debugging](#debugging) * [Memoization](#memoization) ## Reading Material In this section, we've included all the guides and tools that we think are useful for learning different concepts. ### General - [caniuse.com](https://caniuse.com/): can be used to quickly figure out if a feature can be used based on our browser support or not. - [Guide to Chrome DevTools](https://www.keycdn.com/blog/chrome-devtools): Very useful for debugging. ### React/TypeScript - [Intro to React, Redux, and TypeScript](https://blog.isquaredsoftware.com/presentations/react-redux-ts-intro-2020-12) - [Code style](https://github.com/typescript-cheatsheets/react) - [React design patterns](https://blog.logrocket.com/react-component-design-patterns-2022/) ### CSS/SCSS - [W3schools CSS tutorial](https://www.w3schools.com/css/): this is a good starting point for refreshing your CSS knowledge. - [MDN CSS reference](https://developer.mozilla.org/en-US/docs/Web/CSS): a very thorough CSS reference that you can use to learn more about different CSS features. - [Sass basics](https://sass-lang.com/guide): a very good starting point for learning Sass basics (we're using SCSS syntax.) - [CSS Printing Guide - 1](https://www.smashingmagazine.com/2018/05/print-stylesheets-in-2018/) : A-Z about CSS print rules - [CSS Printing Guide - 2](https://www.smashingmagazine.com/2015/01/designing-for-print-with-css/) : Essentials for CSS Print rules (part - 2) - [Overriding inline styles](https://css-tricks.com/override-inline-styles-with-css/) : Inline styles have the highest priority, but they too can be overwritten when the element is accessed, as shown in the document. - [Important Information on CSS position](https://css-tricks.com/almanac/properties/p/position/) : Adding scrolling to a collapsible navbar can be tricky. This link explains how you can add scrolling without affecting any level of dropdown menus. - [Calculating position of element](https://javascript.info/size-and-scroll) : This link gives an in-depth understanding of how we can manually calculate the position of any element dynamically in the DOM ## Idioms The rules that should be followed while writing code. ### Naming conventions - Use kebab-case for filenames (all lower case with `-` for breaking words). - Related to React/Typescript, - Use PascalCase for type names, class names, and enum values. - Use camelCase for function names. - Use camelCase for property names and local variables. - Use `_` as a prefix for private properties and functions. - Use whole words in names when possible. - Related to SASS/SCSS, - Use kebab-case (all lower case with `-` for breaking words). Avoid using camelCase or `_`. - Use meaningful names that can be easily understood without knowing the whole page. Try to convey what an ID or class is about while being as brief as possible. - Related to annotations, - All the keys and properties are case sensitive and written in lower case. - Annotation keys are using kebab-case (all lower case with `-` for breaking words). - Annotation properties use snake case (all lower case with `_` for breaking words). - Properties that enforce a boolean state should be defined based on the opposite default value. For example, if a button is displayed by default and we want to add a property to force its state, we have to add `hide_button`. - Related to markdown and templating, - The helper functions are case sensitive and use camelCase. ### General - Stick with the indentation size of 2. If a file is not using size 2, just change the whole file. - Use single quotes (`'some string'`) for defining strings. - Use semicolons everywhere. Even though it's unnecessary, use a semicolon after each line for consistency. ### React/TypeScript - Use functional components in most cases (class components should only be considered for special cases.) - Avoid using `any` type as much as you can. - Don't use relative paths while importing. Instead, use the `@isrd-isi-edu/chaise` alias, which points to the root. For example: ```js import { ConfigService } from '@isrd-isi-edu/chaise/src/services/config'; ``` - Create a `type` for `props` of components. - Regarding [Render logic](https://blog.isquaredsoftware.com/presentations/react-redux-ts-intro-2020-12/#/36), - It must be “pure“, without any “side effects” - No AJAX calls, mutating data, random values - Rendering should only be based on current props and state - Render any dependent items into temporary variables, such as conditional components or lists. - Regarding `useState` and `setState` hooks: - Creating callbacks “closes over” variable values at time of render - Can cause bugs due to “stale state” if not careful! - Be careful with multiple update calls in a row! - `setState` can accept an “updater” callback instead of a value. - Updater will be called with the latest value as its argument, and should return a new value. Safer to use this if multiple updates depend on each other - Also can avoid having to capture value from parent scope ```typescript // ❌ Two bugs here! // 1) "Closed over" old value of `counter` // 2) Updates will be batched together const onClick = () => { setCounter(counter + 1); setCounter(counter + 1); } const onClickFixed = () => { // ✅ Can pass an "updater" function to setState, // which gets the latest value as its argument setCounter(prevCounter => prevCounter + 1); setCounter(prevCounter => prevCounter + 1); }; ``` - When to use different React hooks, `useState`, `useRef`, and `useStateRef` - `useState`: - common to both `useState` and `useRef`, remembers it's value after a rerender of the component - used to bind component state with rendering the component - state update is asynchronous, the new state value won't be updated until after rerender - modifying the state will queue a rerender of the component which will have the new state value that was set before rerender - `useRef`: - common to both `useState` and `useRef`, remembers it's value after a rerender of the component - does not trigger rerenders of the component or `useEffect` of component - ref update is synchronous, meaning the new value is immediately available in other functions - better for accessing mutable values that are independent of the React component state - useful when mutating a value that is used in another function later in the stack before a rerender would occur - for instance, in a function used as a callback for a promise - `.current` is a mutable value - `useStateRef`: - when a value is needed in functions and is used for triggering component rerenders, use this custom hook - intended to be synchronous - Calling functions after `useState` update and browser repaint - When the set function of a `useState` hook is called, a browser repaint is triggered followed by each `useEffect` and `useLayoutEffect` being checked for changes - If a change occurred that triggers a `useEffect` or `useLayoutEffect` hook, the defined function for that hook will run after the browser repaint with the updated values for the `useState` hook. - This is useful for displaying feedback to the user before triggering some functionality that might take some time to process (cloning forms in recordedit or submitting many at once) - List items must have keys, which tell React list item identity - Should be unique per list - Ideally, use item IDs - Fallback, use array indices, but only if data won’t reorder - Take a look at `example.tsx` for a sample react component. - Since we're using [`StrictMode`](https://reactjs.org/docs/strict-mode.html), React will double-invoke the functions related to rendering content to find issues. So to debug the performance and rendering issues, we should make sure that we are always using production mode. - While importing `react-bootstrap` components, import individual components using `react-bootstrap/Button` rather than the entire library ([source](https://react-bootstrap.github.io/getting-started/introduction/#importing-components)). ```ts // ❌ bad import { Button } from 'react-bootstrap'; // ✅ good import Button from 'react-bootstrap/Button'; ``` - Before implementing your special component take a look at `components` folder and see if any of the existing reusable components can satisfy your case. The following are list of more common components that we have: - `AppWrapper`: Wrapper for all the chaise-like application. Refer to [this section](#structure-of-an-app) for more information. - `DisplayValue`: This component can be used to inject HTML content into the DOM. Use this component instead of `dangerouslySetInnerHTML`. - `ChaiseTooltip`: Display toolips for elements. - `ClearInputBtn`: A clear button that should be attached to all inputs on the page. - `CondWrapper`: Can be used to conditionally add wrappers to another component. - `InputSwitch`: Display appropriate input for a given column. - `SearchInput`: Display a search input. - `Spinner`: A spinner to show progress on the page. - `SplitView`: Adds a resizable side panel to the page. - `Title`: Given a `displayname` object will return the proper title to be displayed on the page. ### Lint - Make sure the `ESLint` extension is installed for Visual Studio Code. - Makefile commands related to linter: - `make lint`: Run linter and only show the errors. - `make lint-w-warn`: Run linter and show both warning and errors. - You can ask linter to escape a file or line, but you should not use this unless you're 100% sure what you're doing is correct: ``` // to ignore the rule for the next line: // eslint-disable-next-line NAME_OF_THE_RULE // to ignore the rule for the whole file: /* eslint NAME_OF_THE_RULE: 0 */ ``` - Using the previously described method, you can also change rules locally, but we generally recommend against it . ### CSS/SCSS - General-purpose styles should be part of the `app.scss` (or included as part of `app.scss`.) - Styles specific to a component should be included in a separate file and imported into the component TSX file. - It's better if you use classes instead of ids for writing CSS rules. - Avoid adding duplicated rules. If there's a rule in a file that is applied to the element that you're trying to add a new style to, add it there. - Avoid using `!important` as much as you can (Unless there's a bootstrap rule that you're trying to override.) - Comment your rule to make it easier for others to figure out why a rule was added. - If you're doing some calculations, don't just use the end result. Right the calculations so later we can easily figure out why you chose that value. - Use variables if you're using the same value more than once, and these values should be the same all the time (Just because you're using the value `10` in two different rules doesn't mean they should share the same variable. Use a variable if these two rules MUST be using the same value. So if later you changed one to `20`, the other one should be updated as well). The following is an example of using variables: ```scss $border-width: 2px; .chaise-btn { border: $border-width solid; } .chaise-btn-btn-group > > .chaise-btn:not(:first-child):not([disabled]) { margin-left: $border-width * -1; } ``` - If the variables you want to define are used in multiple places, add them to `variables.scss`. And make sure to use `@use` for using this variable in other places. For example: ```scss // _variables.scss $my-variable: 5px; // _file1.scss @use 'variables'; .sample-element { margin-right: variables.$my-variable; } // _file2.scss @use 'variables'; .another-element { margin-right: variables.$my-variable * -1; } ``` - You can also opt to define a map instead of simple variables. To do so, - Add a new file under `src/assets/scss/maps`, and define your map in there. ```scss // _my-new-map.scss $my-map: ( 'value1': #333, 'value2': #ccc ); ``` - Import your file at the end of `src/assets/scss/_variables.scss`. ```scss // _variables.scss // ... @import 'maps/my-new-map'; ``` - Use `map-get` for accessing the map values: ```scss // _file.scss @use 'sass:map'; @use 'variables'; // ... .my-element { color: map-get(variables.$my-new-map, 'value1'); } ``` - How each browser renders printing styles is different from the other. Mac and Windows behave differently for the same browser type (Firefox, Chrome, etc.). Hence we need to keep in mind the following while writing print rules in CSS. - If table borders or other line elements are not visible in the print preview or the PDF, check if there exists any overriding bootstrap rules. These bootstrap rules could be a `background-color`, `background-border`, etc., and they always take precedence over the custom CSS rules that are defined in the @media-print section of the CSS file. - If yes, we must override those rules with `!important` to get the desired effect. - A new class has been defined to apply custom styling to the case of Firefox browser in combination with MacOs, which can be found [here](../user-docs/custom-css.md). - Use the print mode in the rendering tab to see how the document looks when printed in the Chrome browser. On Firefox, this can be achieved by clicking on a small page icon in the "Inspect Element mode". - The print preview that is seen when doing a `Ctrl-P` on Windows or a `Cmd-P` on Mac doesn't necessarily give you the right picture of the document to be printed. To view what will be printed, either save to PDF file or chose to switch to the 'Print mode' as described above. - Scrolling can be persisted by using the `scrolling : scroll` option. #### Supporting styles and classes in configuration There are multiple places in chaise and deriva-webapps that we want to allow for users to be able to customize the look and feel of different apps and features. Below are some of the guidelines for when to expose a style property in configuration, attach ids and classes to elements that are made available for custom.css, and define other special classes for use in configs and annotations. - style properties that are directly exposed in configuration documents are usually quantifiable values or boolean values. - For instance width, max-width, font-size are all quatifiable properties that expect a specific numeric value (usually a number, decimal, or percentage) - there is a defined set of [special classes](https://github.com/informatics-isi-edu/ermrestjs/blob/master/docs/user-docs/markdown-formatting.md#special-classes) that can be attached to elements using markdown in annotations or certain configurations that allow for a list of classes to be defined - we want to include these classes when a style property might have multiple values (like an enum) instead of an integer. Think about the `align-items` or `overflow` CSS properties that could have multiple values - At a minimum, chaise and deriva-webapps should be attaching unique classes to elements to communicate different parts of the page or components to help data modelers write their own custom CSS by targeting these unique classes ### Font Awesome In font-awesome, each font/icon can either be solid, regular, or light. In some cases only one version is available in the free, open-source version that we're using. While using these types of fonts, the font-awesome website directs us to use `fa-solid` (`fas`) for sold, `fa-regular` (`far`) for regular, and `fa-light` (`fal`) for light. `fa-light` is not available in the free version, so we should not use it at all. From the font-awesome source, the only difference between `fa-regular` and `fa-solid` is font-weight: ```css .fa-regular, .fa-solid { font-family: "Font Awesome 6 Free"; } .fa-regular { font-weight: 400; } .fa-solid { font-weight: 900; } ``` This can cause some inconsistencies where `fa-regular`/`fa-solid` are used in places where we're manually changing the `font-weight`. For example, assume the following icon is used. ```html ``` And we're using the following CSS rule ```css .some-icon { font-weight: 400 !important; } ``` Even though by using `fa-solid` we were meant to use the solid version of the font, the CSS rule will make sure we're using the regular version instead. And in this case, the regular version of `fa-ellipsis-v` is not available in the free version of font-awesome that we're using. So, - We have to be careful where we're using font-awesome and avoid any manual changing of `font-family` or `font-weight` and let font-awesome handle it for us. - While changing font-awesome versions, we have to make sure the fonts that we're using are available. In some cases, we might want to change the font-weight group by updating the font-awesome classes that are used. ### Handling time Regarding `timestamp` and `timestamptz` column types: - A `timestamptz` value is stored as a single point in time in Postgres. When the value is retrieved, the value is in whatever time zone the database is in. - A `timestamp` value is stored as a string with the date and time; time zone info will be dropped in Postgres. - When submitting values for `timestamp` and `timestamptz` columns, the app should just submit the values as browser's local time. - When displaying `timestamp` value, display whatever is in the database (the date and time, no need to convert to local time because there's no time zone info attached anyway) - When displaying `timestamptz` value, convert that value to browser's local time. ## Folder structure The following is the overall structure of the project: ``` . ├── src │ ├── assets │ │ └── scss │ │ ├── app.scss │ │ └── _.scss │ ├── components │ │ └── .tsx │ ├── hooks │ │ └── .ts │ ├── libs │ │ └── .tsx │ ├── models │ │ └── .ts │ ├── pages │ │ ├── .tsx │ ├── providers │ │ └── .tsx │ ├── services │ │ └── .ts │ ├── utils │ │ └── .ts │ └── vendor ├── webpack │ ├── templates │ ├── app.config.js │ └── main.configjs ├── Makefile └── package.json ``` - `assets`: This folder is used to house all the fonts, images, and SCSS files. The component-specific SCSS files should have the same name as their component file. - `components`: Each app will rely on similar components for functionality and display purposes. If there is a need to reuse code, even if that's in only 2 places, a common component should be extracted and placed in the components folder. - `libs`: Independent applications that may be used in non-React environments outside of Chaise. - `models`: The models or types that are - `providers`: Providers are a way to have a consistent state that can be accessed by any component at any level of the parent/child component hierarchy. Providers make use of React hooks to manage the app state. - `services`: Services are used for common functionality like interacting with the server, configuring the application, managing the user session, and more. These functional services provide a scope that is shared throughout the service that each function can interact with. - `utils`: Utilities are intended to be collections of functions exported individually that are then imported as needed in other places. ## Building and installation This section will focus on more advanced details related to installation. Please refer to the installation guide in the `user-docs` folder for general information. The build process uses Makefile to simplify what needs to be called from the command line to get started. `Make` will manage dependency installation (through `npm`) and the react build process (using `webpack`). #### Make targets The following targers can be used for installing dependencies. Try to avoid directly calling `npm` and use these commands unless you want to add a new module or update the existing ones. - `npm-install-modules`: Installs the dependencies needed for building the app in production mode. - `npm-install-all-modules`: Install all the dependencies including the ones needed during development and testing. Since we had to patch webdriver-manager, this command will also call `patch-package` to apply the patch. The following targets are designed for building chaise apps: - `dist`: This target is designed for deployment environments, where we want to make sure we can install from scratch without any hiccups and errors. That's why we're always doing a clean installation (`npm ci`) as part of this command, which will ensure that the dependencies are installed based on what's encoded in the `package-lock.json` (without fetching new versions from the upstream). While developing features in Chaise, you should only run this command during the first installation or when `package-lock.json` has been modified. - `dist-wo-deps`: Designed for development purposes, will only build and install Chaise. The following are deploy targets: - `deploy`: Deployed the built folders into the given location. - `deploy-w-config`: The same as `deploy`, but will also `rsync` the configuration files. #### NPM This section will go over how we think the NPM modules should be managed. - Ensure the latest stable Node.js and NPM versions are used. - Use `make npm-install-all-modules` to install all the NPM modules regardless of `NODE_ENV` value. - Useful when you first clone the repository, or want to download dependencies from scratch. - Use it yo update the installed dependencies based on the changes in `pacakge-lock.json` file. - his command will also call `patch-package` to apply the patches to dependencies. Refer to [patches folder](../../patches/README.md) for more information. - Use `make dist-wo-deps` while developing so you don't have to install depenendencies for each build. - Avoid using `npm install` (it can have unwanted side effects like updating `package-lock.json`). - `pacakge-lock.json` should not be changed. If you noticed a change in your branch, consult with the main contributors. - Only for main contributors: If we want to upgrade the dependencies or install a new package, we should, - Ensure the used node and npm versions are updated and the latest stable. - Run `npm install --include=dev` to sync `package-lock.json` with `package.json`. - Double-check the changes to `package-lock.json`. - Only for main contributors: to publish a new version of chaise to npm, we should, 1. Update the `version` property in `package.json`. 2. Update the `version` and `packages.version` properties in `package-lock.json`. Or run `npm install --include=dev`. - If you used `npm install` double-check the changes to `package-lock.json`. 3. Push the changes to the main branch. 4. After pushing the changes, `npm-publish.yml` GitHub workflow will detect the version change and properly publish the new version to npm. ## Structure of an App Since Chaise is a collection of multiple single-page apps (`recordset`, `record`, `recordedit`, etc.), the app setup will be very similar. This similar structure allowed us to factor out a lot of that common setup code into different bits described below. ### Main HTML The instantiation and bundle of dependencies should be the same for each app. The build process using webpack will generate the outer HTML based on `webpack/templates/main.html`. Each app attaches to the element with `id` equal to `chaise-app-root` defined in `main.html`. ### App Wrapper Each app in Chaise is instantiated and configured the same way as far as creating the outer HTML and tag, wrapping the app in the proper providers, configuring Chaise and ermrestJS, and fetching the session. To help manage parts of this, we created a component called `AppWrapper` to wrap each app for setup and configuration. ### Context For state sharing between components, Chaise is using the built-in `useContext` hook. Each application has its top-level context, with each component defining its own context as needed (like alerts and errors). ### Error Provider To handle global errors, the app wrapper adds an `ErrorProvider` to handle the error state and an `ErrorBoundary` to catch the errors. Each app only needs to throw errors to let the global handler decide what to do with them. ### Alerts Provider `Alerts` also has its own provider created to have a consistent state at the app level when trying to show alerts from sub-components of the app. The provider here acts like a service that handles the functionality surrounding alerts. This provider also allows for showing alerts in multiple places without having duplicate alerts show in the wrong contexts. ### Authn Provider `Authn` has its own provider that acts as a service to manage the logged in user and keep that user state consistent throughout the duration of using the app. Each app has to interact with the session to best inform the user of what actions they can take related to create, update and delete. ### Chaise Navbar The navbar for each Chaise app is the same style. It is loaded as part of the configuration phase in the app wrapper. All apps in Chaise can decide to show or hide the navbar as part of defining the `AppWrapper` component. ### Buttons vs Links We want to be aware of why we are using `