diff options
Diffstat (limited to 'doc/development/fe_guide/widgets.md')
-rw-r--r-- | doc/development/fe_guide/widgets.md | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/doc/development/fe_guide/widgets.md b/doc/development/fe_guide/widgets.md new file mode 100644 index 00000000000..02876afe597 --- /dev/null +++ b/doc/development/fe_guide/widgets.md @@ -0,0 +1,143 @@ +--- +stage: none +group: unassigned +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Widgets + +Frontend widgets are standalone Vue applications or Vue component trees that can be added on a page +to handle a part of the functionality. + +Good examples of widgets are [sidebar assignees](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue) and [sidebar confidentiality](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue). + +When building a widget, we should follow a few principles described below. + +## Vue Apollo is required + +All widgets should use the same stack (Vue + Apollo Client). +To make it happen, we must add Vue Apollo to the application root (if we use a widget +as a component) or provide it directly to a widget. For sidebar widgets, use the +[sidebar Apollo Client and Apollo Provider](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/sidebar/graphql.js): + +```javascript +import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; +import { apolloProvider } from '~/sidebar/graphql'; + +function mountConfidentialComponent() { + new Vue({ + apolloProvider, + components: { + SidebarConfidentialityWidget, + }, + /* ... */ + }); +} +``` + +## Required injections + +All editable sidebar widgets should use [`SidebarEditableItem`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue) to handle collapsed/expanded state. This component requires the `canUpdate` property provided in the application root. + +## No global state mappings + +We aim to make widgets as reusable as possible. That's why we should avoid adding any external state +bindings to widgets or to their child components. This includes Vuex mappings and mediator stores. + +## Widget's responsibility + +A widget is responsible for fetching and updating an entity it's designed for (assignees, iterations, and so on). +This means a widget should **always** fetch data (if it's not in Apollo cache already). +Even if we provide an initial value to the widget, it should perform a GraphQL query in the background +to be stored in Apollo cache. + +Eventually, when we have an Apollo Client cache as a global application state, we won't need to pass +initial data to the sidebar widget. Then it will be capable of retrieving the data from the cache. + +## Using GraphQL queries and mutations + +We need widgets to be flexible to work with different entities (epics, issues, merge requests, and so on). +Because we need different GraphQL queries and mutations for different sidebars, we create +[_mappings_](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/sidebar/constants.js#L9): + +```javascript +export const assigneesQueries = { + [IssuableType.Issue]: { + query: getIssueParticipants, + mutation: updateAssigneesMutation, + }, + [IssuableType.MergeRequest]: { + query: getMergeRequestParticipants, + mutation: updateMergeRequestParticipantsMutation, + }, +}; +``` + +To handle the same logic for query updates, we **alias** query fields. For example: + +- `group` or `project` become `workspace` +- `issue`, `epic`, or `mergeRequest` become `issuable` + +Unfortunately, Apollo assigns aliased fields a typename of `undefined`, so we need to fetch `__typename` explicitly: + +```plaintext +query issueConfidential($fullPath: ID!, $iid: String) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: issue(iid: $iid) { + __typename + id + confidential + } + } +} +``` + +## Communication with other Vue applications + +If we need to communicate the changes of the widget state (for example, after successful mutation) +to the parent application, we should emit an event: + +```javascript +updateAssignees(assigneeUsernames) { + return this.$apollo + .mutate({ + mutation: this.$options.assigneesQueries[this.issuableType].mutation, + variables: {...}, + }) + .then(({ data }) => { + const assignees = data.issueSetAssignees?.issue?.assignees?.nodes || []; + this.$emit('assignees-updated', assignees); + }) +} +``` + +Sometimes, we want to listen to the changes on the different Vue application like `NotesApp`. +In this case, we can use a renderless component that imports a client and listens to a certain query: + +```javascript +import { fetchPolicies } from '~/lib/graphql'; +import { confidentialityQueries } from '~/sidebar/constants'; +import { defaultClient as gqlClient } from '~/sidebar/graphql'; + +created() { + if (this.issuableType !== IssuableType.Issue) { + return; + } + + gqlClient + .watchQuery({ + query: confidentialityQueries[this.issuableType].query, + variables: {...}, + fetchPolicy: fetchPolicies.CACHE_ONLY, + }) + .subscribe((res) => { + this.setConfidentiality(issuable.confidential); + }); +}, +methods: { + ...mapActions(['setConfidentiality']), +}, +``` + +[View an example of such a component.](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/notes/components/sidebar_subscription.vue) |