gitlab-org--gitlab-foss/doc/development/fe_guide/widgets.md

148 lines
5.4 KiB
Markdown

---
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/product/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
[issuable Apollo Client and Apollo Provider](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/graphql_shared/issuable_client.js):
```javascript
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import { apolloProvider } from '~/graphql_shared/issuable_client';
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 '~/graphql_shared/issuable_client';
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)
## Merge request widgets
Refer to the documentation specific to the [merge request widget extension framework](merge_request_widget_extensions.md).