Commit 7d38a45e authored by Phil Hughes's avatar Phil Hughes Committed by Amy Qualls

Improved merge request extensions docs

Updated the extensions docs to mention all available
API hooks.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/350290
parent b66b1ca9
--- ---
stage: Create stage: Create
group: Source Code group: Code Review
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 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
--- ---
...@@ -11,27 +11,39 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -11,27 +11,39 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Summary ## Summary
Extensions in the merge request widget enable you to add new features Extensions in the merge request widget enable you to add new features
into the widget that match the existing design and interaction as other extensions. into the merge request widget that match the design framework.
With extensions we get a lot of benefits out of the box without much effort required, like:
- A consistent look and feel.
- Tracking when the extension is opened.
- Virtual scrolling for performance.
## Usage ## Usage
To use extensions you need to first create a new extension object to fetch the To use extensions you must first create a new extension object to fetch the
data to render in the extension. See the example file in data to render in the extension. For a working example, refer to the example file in
`app/assets/javascripts/vue_merge_request_widget/extensions/issues.js` for a working example. `app/assets/javascripts/vue_merge_request_widget/extensions/issues.js`.
The basic object structure is as below: The basic object structure:
```javascript ```javascript
export default { export default {
name: '', name: '', // Required: This helps identify the widget
props: [], props: [], // Required: Props passed from the widget state
i18n: { // Required: Object to hold i18n text
label: '', // Required: Used for tooltips and aria-labels
loading: '', // Required: Loading text for when data is loading
},
expandEvent: '', // Optional: RedisHLL event name to track expanding content
enablePolling: false, // Optional: Tells extension to poll for data
computed: { computed: {
summary() {}, summary(data) {}, // Required: Level 1 summary text
statusIcon() {}, statusIcon(data) {}, // Required: Level 1 status icon
tertiaryButtons() {}, // Optional: Level 1 action buttons
}, },
methods: { methods: {
fetchCollapsedData() {}, fetchCollapsedData(props) {}, // Required: Fetches data required for collapsed state
fetchFullData() {}, fetchFullData(props) {}, // Required: Fetches data for the full expanded content
}, },
}; };
``` ```
...@@ -39,10 +51,8 @@ export default { ...@@ -39,10 +51,8 @@ export default {
By following the same data structure, each extension can follow the same registering structure, By following the same data structure, each extension can follow the same registering structure,
but each extension can manage its data sources. but each extension can manage its data sources.
After creating this structure you need to register it. Registering the extension can happen at any After creating this structure, you must register it. You can register the extension at any
point _after_ the widget has been created. point _after_ the widget has been created. To register a extension:
To register a extension the following can be done:
```javascript ```javascript
// Import the register method // Import the register method
...@@ -55,10 +65,75 @@ import issueExtension from '~/vue_merge_request_widget/extensions/issues'; ...@@ -55,10 +65,75 @@ import issueExtension from '~/vue_merge_request_widget/extensions/issues';
registerExtension(issueExtension); registerExtension(issueExtension);
``` ```
## Polling ## Data fetching
Each extension must fetch data. Fetching is handled when registering the extension,
not by the core component itself. This approach allows for various different
data fetching methods to be used, such as GraphQL or REST API calls.
### API calls
For performance reasons, it is best if the collapsed state fetches only the data required to
render the collapsed state. This fetching happens within the `fetchCollapsedData` method.
This method is called with the props as an argument, so you can easily access
any paths set in the state.
To allow the extension to set the data, this method **must** return the data. No
special formatting is required. When the extension receives this data,
it is set to `collapsedData`. You can access `collapsedData` in any computed property or
method.
When the user clicks **Expand**, the `fetchFullData` method is called. This method
also gets called with the props as an argument. This method **must** also return
the full data. However, this data needs to be correctly formatted to match the format
mentioned in the data structure section.
#### Technical debt
For some of the current extensions, there is no split in data fetching. All the data
is fetched through the `fetchCollapsedData` method. While less performant,
it allows for faster iteration.
To handle this the `fetchFullData` returns the data set through
the `fetchCollapsedData` method call. In these cases, the `fetchFullData` must
return a promise:
```javascript
fetchCollapsedData() {
return ['Some data'];
},
fetchFullData() {
return Promise.resolve(this.collapsedData)
},
```
### Data structure
The data returned from `fetchFullData` must match the format below. This format
allows the core component to render the data in a way that matches
the design framework. Any text properties can use the styling placeholders
mentioned below:
```javascript
{
id: data.id, // Required: ID used as a key for each row
header: 'Header' || ['Header', 'sub-header'], // Required: String or array can be used for the header text
text: '', // Required: Main text for the row
subtext: '', // Optional: Smaller sub-text to be displayed below the main text
icon: { // Optional: Icon object
name: EXTENSION_ICONS.success, // Required: The icon name for the row
},
badge: { // Optional: Badge displayed after text
text: '', // Required: Text to be displayed inside badge
variant: '', // Optional: GitLab UI badge variant, defaults to info
},
actions: [], // Optional: Action button for row
}
```
### Polling
To enable polling for an extension, an options flag needs to be present in the extension. To enable polling for an extension, an options flag must be present in the extension:
For example:
```javascript ```javascript
export default { export default {
...@@ -67,12 +142,11 @@ export default { ...@@ -67,12 +142,11 @@ export default {
}; };
``` ```
This flag tells the base component that we should poll the `fetchCollapsedData()` This flag tells the base component we should poll the `fetchCollapsedData()`
defined in the extension. Polling stops if the response has data or if an error is present. defined in the extension. Polling stops if the response has data, or if an error is present.
When writing the logic for `fetchCollapsedData()`, a complete Axios response must be returned When writing the logic for `fetchCollapsedData()`, a complete Axios response must be returned
from the method, due to the polling utility needing data like polling headers. from the method. The polling utility needs data like polling headers to work correctly:
Otherwise, polling does not work correctly.
```javascript ```javascript
export default { export default {
...@@ -134,12 +208,12 @@ export default { ...@@ -134,12 +208,12 @@ export default {
} }
}) })
}, },
// custom method // Custom method
prepareReports() { prepareReports() {
// unpack values from collapsedData // Unpack values from collapsedData
const { new_errors, existing_errors, resolved_errors } = this.collapsedData; const { new_errors, existing_errors, resolved_errors } = this.collapsedData;
// perform data formatting // Perform data formatting
return [...newErrors, ...existingErrors, ...resolvedErrors] return [...newErrors, ...existingErrors, ...resolvedErrors]
} }
...@@ -147,18 +221,18 @@ export default { ...@@ -147,18 +221,18 @@ export default {
}; };
``` ```
## Fetching errors ### Errors
If `fetchCollapsedData()` or `fetchFullData()` methods throw an error: If `fetchCollapsedData()` or `fetchFullData()` methods throw an error:
- The loading state of the extension is updated to `LOADING_STATES.collapsedError` and `LOADING_STATES.expandedError` - The loading state of the extension is updated to `LOADING_STATES.collapsedError`
respectively. and `LOADING_STATES.expandedError` respectively.
- The extensions header displays an error icon and updates the text to be either: - The extensions header displays an error icon and updates the text to be either:
- The text defined in `$options.i18n.error`. - The text defined in `$options.i18n.error`.
- "Failed to load" if `$options.i18n.error` is not defined. - "Failed to load" if `$options.i18n.error` is not defined.
- The error is sent to Sentry to log that it occurred. - The error is sent to Sentry to log that it occurred.
To customise the error text, you need to add it to the `i18n` object in your extension: To customise the error text, add it to the `i18n` object in your extension:
```javascript ```javascript
export default { export default {
...@@ -169,3 +243,77 @@ export default { ...@@ -169,3 +243,77 @@ export default {
}, },
}; };
``` ```
## Icons
Level 1 and all subsequent levels can have their own status icons. To keep with
the design framework, import the `EXTENSION_ICONS` constant
from the `constants.js` file:
```javascript
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants.js';
```
This constant has the below icons available for use. Per the design framework,
only some of these icons should be used on level 1:
- `failed`
- `warning`
- `success`
- `neutral`
- `error`
- `notice`
- `severityCritical`
- `severityHigh`
- `severityMedium`
- `severityLow`
- `severityInfo`
- `severityUnknown`
## Text styling
Any area that has text can be styled with the placeholders below. This
technique follows the same technique as `sprintf`. However, instead of specifying
these through `sprintf`, the extension does this automatically.
Every placeholder contains starting and ending tags. For example, `success` uses
`Hello %{success_start}world%{success_end}`. The extension then
adds the start and end tags with the correct styling classes.
| Placeholder | Style |
|---|---|
| success | `gl-font-weight-bold gl-text-green-500` |
| danger | `gl-font-weight-bold gl-text-red-500` |
| critical | `gl-font-weight-bold gl-text-red-800` |
| same | `gl-font-weight-bold gl-text-gray-700` |
| strong | `gl-font-weight-bold` |
| small | `gl-font-sm` |
## Action buttons
You can add action buttons to all level 1 and 2 in each extension. These buttons
are meant as a way to provide links or actions for each row:
- Action buttons for level 1 can be set through the `tertiaryButtons` computed property.
This property should return an array of objects for each action button.
- Action buttons for level 2 can be set by adding the `actions` key to the level 2 rows object.
The value for this key must also be an array of objects for each action button.
Links must follow this structure:
```javascript
{
text: 'Click me',
href: this.someLinkHref,
target: '_blank', // Optional
}
```
For internal action buttons, follow this structure:
```javascript
{
text: 'Click me',
onClick() {}
}
```
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment