diff options
Diffstat (limited to 'doc/development/fe_guide')
-rw-r--r-- | doc/development/fe_guide/frontend_faq.md | 30 | ||||
-rw-r--r-- | doc/development/fe_guide/graphql.md | 37 | ||||
-rw-r--r-- | doc/development/fe_guide/icons.md | 83 | ||||
-rw-r--r-- | doc/development/fe_guide/img/webpack_report_v12_8.png | bin | 0 -> 47453 bytes | |||
-rw-r--r-- | doc/development/fe_guide/index.md | 2 | ||||
-rw-r--r-- | doc/development/fe_guide/style/javascript.md | 2 | ||||
-rw-r--r-- | doc/development/fe_guide/style/scss.md | 12 | ||||
-rw-r--r-- | doc/development/fe_guide/tooling.md | 3 | ||||
-rw-r--r-- | doc/development/fe_guide/vue.md | 8 | ||||
-rw-r--r-- | doc/development/fe_guide/vue3_migration.md | 3 | ||||
-rw-r--r-- | doc/development/fe_guide/vuex.md | 142 |
11 files changed, 204 insertions, 118 deletions
diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md index 3c0845a9aaa..71436a7c7fb 100644 --- a/doc/development/fe_guide/frontend_faq.md +++ b/doc/development/fe_guide/frontend_faq.md @@ -163,3 +163,33 @@ To return to the normal development mode: 1. Run `yarn clean` to remove the production assets and free some space (optional). 1. Start webpack again: `gdk start webpack`. 1. Restart GDK: `gdk-restart rails-web`. + +### 8. Babel polyfills + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/28837) in GitLab 12.8. + +GitLab has enabled the Babel `preset-env` option +[`useBuiltIns: 'usage'`](https://babeljs.io/docs/en/babel-preset-env#usebuiltins-usage), +which adds the appropriate `core-js` polyfills once for each JavaScript feature +we're using that our target browsers don't support. You don't need to add `core-js` +polyfills manually. + +NOTE: **Note:** +GitLab still manually adds non-`core-js` polyfills for extending browser features +(such as GitLab's SVG polyfill) that allow us reference SVGs by using `<use xlink:href>`. +These polyfills should be added to `app/assets/javascripts/commons/polyfills.js`. + +To see what polyfills are being used: + +1. Navigate to your merge request. +1. In the secondary menu below the title of the merge request, click **Pipelines**, then + click the pipeline you want to view, to display the jobs in that pipeline. +1. Click the [`compile-production-assets`](https://gitlab.com/gitlab-org/gitlab/-/jobs/641770154) job. +1. In the right-hand sidebar, scroll to **Job Artifacts**, and click **Browse**. +1. Click the **webpack-report** folder to open it, and click **index.html**. +1. In the upper left corner of the page, click the right arrow **{angle-right}** + to display the explorer. +1. In the **Search modules** field, enter `gitlab/node_modules/core-js` to see + which polyfills are being loaded and where: + + ![Image of webpack report](img/webpack_report_v12_8.png) diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index 3d74ec94ae4..f5e16d377f1 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -75,7 +75,7 @@ their execution by clicking **Execute query** button on the top left: ## Apollo Client To save duplicated clients getting created in different apps, we have a -[default client](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/javascripts/lib/graphql.js) that should be used. This setups the +[default client](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/javascripts/lib/graphql.js) that should be used. This sets up the Apollo client with the correct URL and also sets the CSRF headers. Default client accepts two parameters: `resolvers` and `config`. @@ -85,6 +85,7 @@ Default client accepts two parameters: `resolvers` and `config`. - `cacheConfig` field accepts an optional object of settings to [customize Apollo cache](https://www.apollographql.com/docs/react/caching/cache-configuration/#configuring-the-cache) - `baseUrl` allows us to pass a URL for GraphQL endpoint different from our main endpoint (i.e.`${gon.relative_url_root}/api/graphql`) - `assumeImmutableResults` (set to `false` by default) - this setting, when set to `true`, will assume that every single operation on updating Apollo Cache is immutable. It also sets `freezeResults` to `true`, so any attempt on mutating Apollo Cache will throw a console warning in development environment. Please ensure you're following the immutability pattern on cache update operations before setting this option to `true`. + - `fetchPolicy` determines how you want your component to interact with the Apollo cache. Defaults to "cache-first". ## GraphQL Queries @@ -167,9 +168,7 @@ import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; Vue.use(VueApollo); -const defaultClient = createDefaultClient({ - resolvers: {} -}); +const defaultClient = createDefaultClient(); defaultClient.cache.writeData({ data: { @@ -257,10 +256,7 @@ We need to pass resolvers object to our existing Apollo Client: import createDefaultClient from '~/lib/graphql'; import resolvers from './graphql/resolvers'; -const defaultClient = createDefaultClient( - {}, - resolvers, -); +const defaultClient = createDefaultClient(resolvers); ``` Now every single time on attempt to fetch a version, our client will fetch `id` and `sha` from the remote API endpoint and will assign our hardcoded values to `author` and `createdAt` version properties. With this data, frontend developers are able to work on UI part without being blocked by backend. When actual response is added to the API, a custom local resolver can be removed fast and the only change to query/fragment is `@client` directive removal. @@ -470,6 +466,28 @@ fetchNextPage() { Please note we don't have to save `pageInfo` one more time; `fetchMore` triggers a query `result` hook as well. +### Managing performance + +The Apollo client will batch queries by default. This means that if you have 3 queries defined, +Apollo will group them into one request, send the single request off to the server and only +respond once all 3 queries have completed. + +If you need to have queries sent as individual requests, additional context can be provided +to tell Apollo to do this. + +```javascript +export default { + apollo: { + user: { + query: QUERY_IMPORT, + context: { + isSingleRequest: true, + } + } + }, +}; +``` + ### Testing #### Mocking response as component data @@ -501,6 +519,7 @@ If we need to test how our component renders when results from the GraphQL API a designs: { loading, }, + }, }; wrapper = shallowMount(Index, { @@ -595,7 +614,7 @@ These errors are located at the "top level" of a GraphQL response. These are non #### Handling top-level errors -Apollo is aware of top-level errors, so we are able to leverage Apollo's various error-handling mechanisms to handle these errors (e.g. handling Promise rejections after invoking the [`mutate`](https://www.apollographql.com/docs/react/api/apollo-client/#ApolloClient.mutate) method, or handling the `error` event emitted from the [`ApolloMutation`](https://apollo.vuejs.org/api/apollo-mutation.html#events) component). +Apollo is aware of top-level errors, so we are able to leverage Apollo's various error-handling mechanisms to handle these errors (e.g. handling Promise rejections after invoking the [`mutate`](https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.mutate) method, or handling the `error` event emitted from the [`ApolloMutation`](https://apollo.vuejs.org/api/apollo-mutation.html#events) component). Because these errors are not intended for users, error messages for top-level errors should be defined client-side. diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md index 7078b5e9b2f..b539293e9cf 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -76,6 +76,89 @@ export default { Please use the following function inside JS to render an icon: `gl.utils.spriteIcon(iconName)` +## Loading icon + +### Usage in HAML/Rails + +DANGER: **Danger:** +Do not use the `spinner` or `icon('spinner spin')` rails helpers to insert +loading icons. These helpers rely on the Font Awesome icon library which is +deprecated. + +To insert a loading spinner in HAML or Rails use the `loading_icon` helper: + +```haml += loading_icon +``` + +You can include one or more of the following properties with the `loading_icon` helper, as demonstrated +by the examples that follow: + +- `container` (optional): wraps the loading icon in a container, which centers the loading icon using the `text-center` CSS property. +- `color` (optional): either `orange` (default), `light`, or `dark`. +- `size` (optional): either `sm` (default), `md`, `lg`, or `xl`. +- `css_class` (optional): defaults to an empty string, but can be useful for utility classes to fine-tune alignment or spacing. + +**Example 1:** + +The following HAML expression generates a loading icon’s markup and +centers the icon by wrapping it in a `gl-spinner-container` element. + +```haml += loading_icon(container: true) +``` + +**Output from example 1:** + +```html +<div class="gl-spinner-container"> + <span class="gl-spinner gl-spinner-orange gl-spinner-sm" aria-label="Loading"></span> +</div> +``` + +**Example 2:** + +The following HAML expression generates a loading icon’s markup +with a custom size. It also appends a margin utility class. + +```haml += loading_icon(size: 'lg', css_class: 'gl-mr-2') +``` + +**Output from example 2:** + +```html +<span class="gl-spinner gl-spinner-orange gl-spinner-lg gl-mr-2" aria-label="Loading"></span> +``` + +### Usage in Vue + +The [GitLab UI](https://gitlab-org.gitlab.io/gitlab-ui/) components library provides a +`GlLoadingIcon` component. See the component’s +[storybook](https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-loading-icon--default) +for more information about its usage. + +**Example:** + +The following code snippet demonstrates how to use `GlLoadingIcon` in +a Vue component. + +```html +<script> +import { GlLoadingIcon } from "@gitlab/ui"; + +export default { + components: { + GlLoadingIcon, + }, +}; +<script> + +<template> + <gl-loading-icon inline /> +</template> +``` + ## SVG Illustrations Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers. diff --git a/doc/development/fe_guide/img/webpack_report_v12_8.png b/doc/development/fe_guide/img/webpack_report_v12_8.png Binary files differnew file mode 100644 index 00000000000..99c6f7d0eec --- /dev/null +++ b/doc/development/fe_guide/img/webpack_report_v12_8.png diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 3a2b3cac9bf..ef23b6c4ed2 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -5,7 +5,7 @@ across GitLab's frontend team. ## Overview -GitLab is built on top of [Ruby on Rails](https://rubyonrails.org) using [Haml](http://haml.info/) and also a JavaScript based Frontend with [Vue.js](https://vuejs.org). +GitLab is built on top of [Ruby on Rails](https://rubyonrails.org) using [Haml](https://haml.info/) and also a JavaScript based Frontend with [Vue.js](https://vuejs.org). Be wary of [the limitations that come with using Hamlit](https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations). We also use [SCSS](https://sass-lang.com) and plain JavaScript with modern ECMAScript standards supported through [Babel](https://babeljs.io/) and ES module support through [webpack](https://webpack.js.org/). diff --git a/doc/development/fe_guide/style/javascript.md b/doc/development/fe_guide/style/javascript.md index b69a6f1941c..2a06a473878 100644 --- a/doc/development/fe_guide/style/javascript.md +++ b/doc/development/fe_guide/style/javascript.md @@ -10,7 +10,7 @@ linter to manage most of our JavaScript style guidelines. In addition to the style guidelines set by Airbnb, we also have a few specific rules listed below. -> **Tip:** +TIP: **Tip:** You can run eslint locally by running `yarn eslint` ## Avoid forEach diff --git a/doc/development/fe_guide/style/scss.md b/doc/development/fe_guide/style/scss.md index 336c9b8ca35..dba39eeb98c 100644 --- a/doc/development/fe_guide/style/scss.md +++ b/doc/development/fe_guide/style/scss.md @@ -23,6 +23,14 @@ Classes in [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/a Avoid [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/). +NOTE: **Note:** +While migrating [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/) +to the [GitLab UI](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/doc/css.md#utilities) +utility classes, note both the classes for margin and padding differ. The size scale used at +GitLab differs from the scale used in the Bootstrap library. For a Bootstrap padding or margin +utility, you may need to double the size of the applied utility to achieve the same visual +result (such as `ml-1` becoming `gl-ml-2`). + #### Where should I put new utility classes? If a class you need has not been added to GitLab UI, you get to add it! Follow the naming patterns documented in the [utility files](https://gitlab.com/gitlab-org/gitlab-ui/-/tree/master/src/scss/utility-mixins) and refer to [GitLab UI's CSS documentation](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/doc/contributing/adding_css.md#adding-utility-mixins) for more details, especially about adding responsive and stateful rules. @@ -45,8 +53,8 @@ Examples of component classes that were created using "utility-first" include: Inspiration: -- <https://tailwindcss.com/docs/utility-first/> -- <https://tailwindcss.com/docs/extracting-components/> +- <https://tailwindcss.com/docs/utility-first> +- <https://tailwindcss.com/docs/extracting-components> ### Naming diff --git a/doc/development/fe_guide/tooling.md b/doc/development/fe_guide/tooling.md index 28deb7d95f9..5685ac5abcd 100644 --- a/doc/development/fe_guide/tooling.md +++ b/doc/development/fe_guide/tooling.md @@ -40,7 +40,8 @@ yarn eslint-fix _If manual changes are required, a list of changes will be sent to the console._ -**Caution:** Limit use to global rule updates. Otherwise, the changes can lead to huge Merge Requests. +CAUTION: **Caution:** +Limit use to global rule updates. Otherwise, the changes can lead to huge Merge Requests. ### Disabling ESLint in new files diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 2a0556c6cda..58a8332589d 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -64,11 +64,11 @@ which will make the tests easier. See the following example: ```javascript // haml -.js-vue-app{ data: { endpoint: 'foo' }} +#js-vue-app{ data: { endpoint: 'foo' }} // index.js document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '.js-vue-app', + el: '#js-vue-app', data() { const dataset = this.$options.el.dataset; return { @@ -85,6 +85,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ })); ``` +> When adding an `id` attribute to mount a Vue application, please make sure this `id` is unique across the codebase + #### Accessing the `gl` object When we need to query the `gl` object for data that won't change during the application's life cycle, we should do it in the same place where we query the DOM. @@ -283,7 +285,7 @@ describe('~/todos/app.vue', () => { ### Test the component's output The main return value of a Vue component is the rendered output. In order to test the component we -need to test the rendered output. [Vue](https://vuejs.org/v2/guide/unit-testing.html) guide's to unit test show us exactly that: +need to test the rendered output. Visit the [Vue testing guide](https://vuejs.org/v2/guide/testing.html#Unit-Testing). ### Events diff --git a/doc/development/fe_guide/vue3_migration.md b/doc/development/fe_guide/vue3_migration.md index 7ab48db7f76..afdb2690c02 100644 --- a/doc/development/fe_guide/vue3_migration.md +++ b/doc/development/fe_guide/vue3_migration.md @@ -76,6 +76,9 @@ const FunctionalComp = (props, slots) => { } ``` +NOTE: **Note:** +It is not recommended to replace stateful components with functional components unless you absolutely need a performance improvement right now. In Vue 3, performance gains for functional components will be negligible. + ## Old slots syntax with `slot` attribute **Why?** diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index 02387c15951..9573dd36e63 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -40,24 +40,25 @@ The following example shows an application that lists and adds users to the stat This is the entry point for our store. You can use the following as a guide: ```javascript -import Vue from 'vue'; import Vuex from 'vuex'; import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; import state from './state'; -Vue.use(Vuex); - -export const createStore = () => new Vuex.Store({ - actions, - getters, - mutations, - state, -}); -export default createStore(); +export const createStore = () => + new Vuex.Store({ + actions, + getters, + mutations, + state, + }); ``` +_Note:_ Until this +[RFC](https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20) is implemented, +the above will need to disable the `import/prefer-default-export` ESLint rule. + ### `state.js` The first thing you should do before writing any code is to design the state. @@ -137,44 +138,12 @@ import { mapActions } from 'vuex'; ### `mutations.js` The mutations specify how the application state changes in response to actions sent to the store. -The only way to change state in a Vuex store should be by committing a mutation. - -**It's a good idea to think of the state before writing any code.** - -Remember that actions only describe that something happened, they don't describe how the application state changes. +The only way to change state in a Vuex store is by committing a mutation. -**Never commit a mutation directly from a component** - -Instead, you should create an action that will commit a mutation. - -```javascript - import * as types from './mutation_types'; +Most mutations are committed from an action using `commit`. If you don't have any +asynchronous operations, you can call mutations from a component using the `mapMutations` helper. - export default { - [types.REQUEST_USERS](state) { - state.isLoading = true; - }, - [types.RECEIVE_USERS_SUCCESS](state, data) { - // Do any needed data transformation to the received payload here - state.users = data; - state.isLoading = false; - }, - [types.RECEIVE_USERS_ERROR](state, error) { - state.isLoading = false; - }, - [types.REQUEST_ADD_USER](state, user) { - state.isAddingUser = true; - }, - [types.RECEIVE_ADD_USER_SUCCESS](state, user) { - state.isAddingUser = false; - state.users.push(user); - }, - [types.REQUEST_ADD_USER_ERROR](state, error) { - state.isAddingUser = false; - state.errorAddingUser = error; - }, - }; -``` +See the Vuex docs for examples of [committing mutations from components](https://vuex.vuejs.org/guide/mutations.html#committing-mutations-in-components). #### Naming Pattern: `REQUEST` and `RECEIVE` namespaces @@ -316,9 +285,12 @@ function when mounting your Vue component: // in the Vue app's initialization script (e.g. mount_show.js) import Vue from 'vue'; -import createStore from './stores'; +import Vuex from 'vuex'; +import { createStore } from './stores'; import AwesomeVueApp from './components/awesome_vue_app.vue' +Vue.use(Vuex); + export default () => { const el = document.getElementById('js-awesome-vue-app'); @@ -398,10 +370,8 @@ discussion](https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/56#note_3025148 ```javascript <script> import { mapActions, mapState, mapGetters } from 'vuex'; -import store from './store'; export default { - store, computed: { ...mapGetters([ 'getUsersWithPets' @@ -417,12 +387,10 @@ export default { 'fetchUsers', 'addUser', ]), - onClickAddUser(data) { this.addUser(data); } }, - created() { this.fetchUsers() } @@ -448,29 +416,6 @@ export default { </template> ``` -### Vuex Gotchas - -1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency throughout the application. From Vuex docs: - - > Why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action. - - ```javascript - // component.vue - - // bad - created() { - this.$store.commit('mutation'); - } - - // good - created() { - this.$store.dispatch('action'); - } - ``` - -1. Use mutation types instead of hardcoding strings. It will be less error prone. -1. The State will be accessible in all components descending from the use where the store is instantiated. - ### Testing Vuex #### Testing Vuex concerns @@ -485,55 +430,50 @@ In order to write unit tests for those components, we need to include the store ```javascript //component_spec.js import Vue from 'vue'; +import Vuex from 'vuex'; +import { mount, createLocalVue } from '@vue/test-utils'; import { createStore } from './store'; -import component from './component.vue' +import Component from './component.vue' + +const localVue = createLocalVue(); +localVue.use(Vuex); describe('component', () => { let store; - let vm; - let Component; + let wrapper; + + const createComponent = () => { + store = createStore(); + + wrapper = mount(Component, { + localVue, + store, + }); + }; beforeEach(() => { - Component = Vue.extend(issueActions); + createComponent(); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); - it('should show a user', () => { + it('should show a user', async () => { const user = { name: 'Foo', age: '30', }; - store = createStore(); - // populate the store - store.dispatch('addUser', user); + await store.dispatch('addUser', user); - vm = new Component({ - store, - propsData: props, - }).$mount(); + expect(wrapper.text()).toContain(user.name); }); }); ``` -#### Testing Vuex actions and getters - -Because we're currently using [`babel-plugin-rewire`](https://github.com/speedskater/babel-plugin-rewire), you may encounter the following error when testing your Vuex actions and getters: -`[vuex] actions should be function or object with "handler" function` - -To prevent this error from happening, you need to export an empty function as `default`: - -```javascript -// getters.js or actions.js - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; -``` - ### Two way data binding When storing form data in Vuex, it is sometimes necessary to update the value stored. The store should never be mutated directly, and an action should be used instead. |