- // ==UserScript==
- // @name Gitlab plus
- // @namespace https://lukaszmical.pl/
- // @version 2025-02-21
- // @description Gitlab utils
- // @author Łukasz Micał
- // @match https://gitlab.com/*
- // @require https://cdn.jsdelivr.net/combine/npm/preact@10.25.4/dist/preact.min.umd.min.js,npm/preact@10.25.4/hooks/dist/hooks.umd.min.js,npm/preact@10.25.4/jsx-runtime/dist/jsxRuntime.umd.min.js
- // @icon https://www.google.com/s2/favicons?sz=64&domain=gitlab.com
- // ==/UserScript==
-
- // Vite helpers
- const __defProp = Object.defineProperty;
- const __defNormalProp = (obj, key, value) =>
- key in obj
- ? __defProp(obj, key, {
- enumerable: true,
- configurable: true,
- writable: true,
- value,
- })
- : (obj[key] = value);
- const __publicField = (obj, key, value) =>
- __defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value);
-
- // App code
- const { jsx, jsxs, Fragment } = this.jsxRuntime;
- const { render } = this.preact;
- const { useMemo, useState, useEffect, useRef, useCallback, useLayoutEffect } =
- this.preactHooks;
-
- // libs/share/src/ui/GlobalStyle.ts
- class GlobalStyle {
- static addStyle(key, styles) {
- const style =
- document.getElementById(key) ||
- (function () {
- const style22 = document.createElement('style');
- style22.id = key;
- document.head.appendChild(style22);
- return style22;
- })();
- style.textContent = styles;
- }
- }
-
- const style1 =
- '.glp-modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 99999;\n display: none;\n background: rgba(0, 0, 0, 0.6);\n justify-content: center;\n align-items: center;\n}\n\n.glp-modal.glp-modal-visible {\n display: flex;\n}\n\n.glp-modal .glp-modal-content {\n width: 700px;\n max-width: 95vw;\n}\n\n.gl-new-dropdown-item.glp-active .gl-new-dropdown-item-content {\n box-shadow: inset 0 0 0 2px var(--gl-focus-ring-outer-color), inset 0 0 0 3px var(--gl-focus-ring-inner-color), inset 0 0 0 1px var(--gl-focus-ring-inner-color);\n background-color: var(--gl-dropdown-option-background-color-unselected-hover);\n outline: none;\n}\n\n';
- const style2 =
- '.glp-image-preview-modal {\n position: fixed;\n display: flex;\n justify-content: center;\n align-items: center;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.6);\n visibility: hidden;\n opacity: 0;\n pointer-events: none;\n z-index: 99999;\n}\n\n.glp-image-preview-modal.glp-modal-visible {\n visibility: visible;\n opacity: 1;\n pointer-events: auto;\n}\n\n.glp-image-preview-modal .glp-modal-img {\n max-width: 95%;\n max-height: 95%;\n}\n\n.glp-image-preview-modal .glp-modal-close {\n position: absolute;\n z-index: 2;\n top: 20px;\n right: 20px;\n color: black;\n width: 40px;\n height: 40px;\n display: flex;\n justify-content: center;\n align-items: center;\n background: white;\n border-radius: 20px;\n cursor: pointer;\n}\n\n';
- const style3 =
- '.glp-preview-modal {\n position: fixed;\n display: flex;\n background-color: var(--gl-background-color-default, var(--gl-color-neutral-0, #fff));\n border: 1px solid var(--gl-border-color-default);\n border-radius: .25rem;\n width: 350px;\n min-height: 200px;\n z-index: 99999;\n visibility: hidden;\n top: 0;\n left: 0;\n opacity: 0;\n transition: all .2s ease-out;\n transition-property: visibility, opacity, transform;\n}\n\n.glp-preview-modal.glp-modal-visible {\n visibility: visible;\n opacity: 1;\n}\n \n.glp-preview-modal .glp-block {\n padding: .4rem .5em;\n border-bottom-style: solid;\n border-bottom-color: var(--gl-border-color-subtle, var(--gl-color-neutral-50, #ececef));\n border-bottom-width: 1px;\n width: 100%;\n}\n\n\n.glp-preview-modal * {\n max-width: 100%;\n}\n';
-
- // apps/gitlab-plus/src/styles/index.ts
- GlobalStyle.addStyle('glp-style', [style1, style2, style3].join('\n'));
-
- // libs/share/src/store/Store.ts
- class Store {
- constructor(key) {
- this.key = key;
- }
-
- decode(val) {
- return JSON.parse(val);
- }
-
- encode(val) {
- return JSON.stringify(val);
- }
-
- get(defaultValue = void 0) {
- try {
- const data = localStorage.getItem(this.key);
- if (data) {
- return this.decode(data);
- }
- return defaultValue;
- } catch (e) {
- return defaultValue;
- }
- }
-
- remove() {
- localStorage.removeItem(this.key);
- }
-
- set(value) {
- try {
- localStorage.setItem(this.key, this.encode(value));
- } catch (e) {}
- }
- }
-
- // apps/gitlab-plus/src/services/ServiceName.ts
- var ServiceName = ((ServiceName2) => {
- ServiceName2['ClearCacheService'] = 'ClearCacheService';
- ServiceName2['CreateChildIssue'] = 'CreateChildIssue';
- ServiceName2['CreateRelatedIssue'] = 'CreateRelatedIssue';
- ServiceName2['EpicPreview'] = 'EpicPreview';
- ServiceName2['ImagePreview'] = 'ImagePreview';
- ServiceName2['IssuePreview'] = 'IssuePreview';
- ServiceName2['MrPreview'] = 'MrPreview';
- ServiceName2['RelatedIssueAutocomplete'] = 'RelatedIssueAutocomplete';
- ServiceName2['RelatedIssuesLabelStatus'] = 'RelatedIssuesLabelStatus';
- ServiceName2['SortIssue'] = 'SortIssue';
- ServiceName2['UserSettings'] = 'UserSettings';
- return ServiceName2;
- })(ServiceName || {});
- const servicesConfig = {
- ['ClearCacheService']: { label: 'Clear cache', required: true },
- ['CreateChildIssue']: {
- label: 'Create child issue form on epic page',
- },
- ['CreateRelatedIssue']: {
- label: 'Create related issue form on issue page',
- },
- ['EpicPreview']: { label: 'Epic preview modal' },
- ['ImagePreview']: { label: 'Image preview modal' },
- ['IssuePreview']: { label: 'Issue preview modal' },
- ['MrPreview']: { label: 'Merge request preview modal' },
- ['RelatedIssueAutocomplete']: {
- label: 'Related issue autocomplete in related issues input',
- },
- ['RelatedIssuesLabelStatus']: {
- label: 'Label status in related issues list items',
- },
- ['SortIssue']: {
- experimental: true,
- label: 'Sort issues in board',
- },
- ['UserSettings']: { label: 'User settings', required: true },
- };
-
- // apps/gitlab-plus/src/components/user-settings/UserSettingsStore.ts
- class UserSettingsStore {
- constructor() {
- __publicField(this, 'settings', {});
- __publicField(this, 'store', new Store('gitlab-plus-settings'));
- this.load();
- }
-
- isActive(name2) {
- if (!(name2 in servicesConfig)) {
- return false;
- }
- if (servicesConfig[name2].required) {
- return true;
- }
- if (servicesConfig[name2].experimental) {
- return this.getItem(name2, false);
- }
- return this.getItem(name2, true);
- }
-
- setIsActive(name2, value) {
- this.setItem(name2, value);
- }
-
- getItem(key, defaultValue) {
- if (this.settings[key] === void 0) {
- return defaultValue;
- }
- return this.settings[key];
- }
-
- load() {
- this.settings = this.store.get() || {};
- }
-
- persist() {
- this.store.set(this.settings);
- }
-
- setItem(key, value) {
- this.settings[key] = value;
- this.persist();
- }
- }
-
- const userSettingsStore = new UserSettingsStore();
-
- // libs/share/src/store/Cache.ts
- class Cache {
- constructor(prefix) {
- this.prefix = prefix;
- }
-
- clearInvalid() {
- for (const key in localStorage) {
- if (key.startsWith(this.prefix) && !this.isValid(this.getItem(key))) {
- localStorage.removeItem(key);
- }
- }
- }
-
- expirationDate(minutes) {
- if (typeof minutes === 'string') {
- return minutes;
- }
- const time = new Date();
- time.setMinutes(time.getMinutes() + minutes);
- return time;
- }
-
- get(key) {
- try {
- const data = this.getItem(this.key(key));
- if (this.isValid(data)) {
- return data.value;
- }
- } catch (e) {
- return void 0;
- }
- return void 0;
- }
-
- key(key) {
- return `${this.prefix}${key}`;
- }
-
- set(key, value, minutes) {
- localStorage.setItem(
- this.key(key),
- JSON.stringify({
- expirationDate: this.expirationDate(minutes),
- value,
- })
- );
- }
-
- getItem(key) {
- try {
- return JSON.parse(localStorage.getItem(key) || '');
- } catch (e) {
- return void 0;
- }
- }
-
- isValid(item) {
- if (item) {
- return (
- item.expirationDate === 'lifetime' ||
- new Date(item.expirationDate) > new Date()
- );
- }
- return false;
- }
- }
-
- // apps/gitlab-plus/src/services/BaseService.ts
- class BaseService {
- root(className, parent, usePrepend = false) {
- const root = document.createElement('div');
- root.classList.add(className);
- if (parent) {
- parent[usePrepend ? 'prepend' : 'append'](root);
- }
- return root;
- }
-
- rootBody(className) {
- return this.root(className, document.body);
- }
- }
-
- // apps/gitlab-plus/src/services/ClearCacheService.ts
- class ClearCacheService extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.ClearCacheService);
- __publicField(this, 'cache', new Cache('glp-'));
- }
-
- init() {
- this.cache.clearInvalid();
- window.setInterval(this.cache.clearInvalid.bind(this.cache), 60 * 1e3);
- }
- }
-
- // libs/share/src/utils/clsx.ts
- function clsx(...args) {
- return args
- .map((item) => {
- if (!item) {
- return '';
- }
- if (typeof item === 'string') {
- return item;
- }
- if (Array.isArray(item)) {
- return clsx(...item);
- }
- if (typeof item === 'object') {
- return clsx(
- Object.entries(item)
- .filter(([_, value]) => value)
- .map(([key]) => key)
- );
- }
- return '';
- })
- .filter(Boolean)
- .join(' ');
- }
-
- // apps/gitlab-plus/src/components/common/GitlabIcon.tsx
- const buildId =
- '236e3b687d786d9dfe4709143a94d4c53b8d5a1f235775401e5825148297fa84';
- const iconUrl = (icon) => {
- let _a;
- const svgSprite =
- ((_a = unsafeWindow.gon) == null ? void 0 : _a.sprite_icons) ||
- `/assets/icons-${buildId}.svg`;
- return `${svgSprite}#${icon}`;
- };
-
- function GitlabIcon({ className, icon, size = 12, title }) {
- return jsx('svg', {
- className: clsx('gl-icon gl-fill-current', `s${size}`, className),
- title,
- children: jsx('use', { href: iconUrl(icon) }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/GitlabLoader.tsx
- function GitlabLoader({ size = 24 }) {
- return jsx('span', {
- class: 'gl-spinner-container',
- role: 'status',
- children: jsx('span', {
- class: 'gl-spinner gl-spinner-sm gl-spinner-dark !gl-align-text-bottom',
- style: {
- width: size,
- height: size,
- },
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/GitlabButton.tsx
- const buttonVariantClass = {
- default: 'btn-default',
- info: 'btn-confirm',
- tertiary: 'btn-default-tertiary',
- };
-
- function GitlabButton({
- children,
- className,
- icon,
- iconSize = 12,
- isLoading,
- onClick,
- size = 'sm',
- title,
- variant = 'default',
- }) {
- const IconComponent = useMemo(() => {
- if (isLoading) {
- return jsx(GitlabLoader, { size: iconSize });
- }
- if (icon) {
- return jsx(GitlabIcon, { icon, size: iconSize });
- }
- return null;
- }, [icon, isLoading]);
- return jsxs('button', {
- onClick,
- title,
- type: 'button',
- class: clsx(
- `btn btn-${size} gl-button`,
- buttonVariantClass[variant],
- className
- ),
- children: [
- children && jsx('span', { class: 'gl-button-text', children }),
- IconComponent,
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/common/CloseButton.tsx
- function CloseButton({ onClick, title = 'Close' }) {
- return jsx(GitlabButton, {
- className: 'btn-icon',
- icon: 'close-xs',
- iconSize: 16,
- onClick,
- title,
- variant: 'tertiary',
- });
- }
-
- // apps/gitlab-plus/src/components/common/modal/GlpModal.tsx
- function GlpModal({ children, isVisible, onClose, title }) {
- return jsx('div', {
- class: clsx('glp-modal', isVisible && 'glp-modal-visible'),
- children: jsxs('div', {
- className: clsx(
- 'glp-modal-content crud gl-border',
- 'gl-rounded-form gl-border-section gl-bg-subtle gl-mt-5'
- ),
- children: [
- jsxs('div', {
- className: clsx(
- 'crud-header gl-border-b gl-flex gl-flex-wrap',
- 'gl-justify-between gl-gap-x-5 gl-gap-y-2 gl-rounded-t-form',
- 'gl-border-section gl-bg-section gl-px-5 gl-py-4 gl-relative'
- ),
- children: [
- jsx('h2', {
- className: clsx(
- 'gl-m-0 gl-inline-flex gl-items-center gl-gap-3',
- 'gl-text-form gl-font-bold gl-leading-normal'
- ),
- children: title,
- }),
- jsx(CloseButton, { onClick: onClose }),
- ],
- }),
- children,
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/modal/useGlpModal.ts
- function useGlpModal(eventName) {
- const [isVisible, setIsVisible] = useState(false);
- useEffect(() => {
- document.addEventListener(eventName, () => setIsVisible(true));
- }, []);
- return {
- isVisible,
- onClose: () => setIsVisible(false),
- };
- }
-
- // apps/gitlab-plus/src/components/common/base/Text.tsx
- function Text({ children, className, color, size, variant, weight }) {
- return jsx('span', {
- class: clsx(
- size && `gl-text-${size}`,
- weight && `gl-font-${weight}`,
- variant && `gl-text-${variant}`,
- color && `gl-text-${color}`,
- className
- ),
- children,
- });
- }
-
- // apps/gitlab-plus/src/components/common/form/FormField.tsx
- function FormField({ children, error, hint, title }) {
- return jsxs('fieldset', {
- class: clsx(
- 'form-group gl-form-group gl-w-full',
- error && 'gl-show-field-errors'
- ),
- children: [
- jsx('legend', {
- class: 'bv-no-focus-ring col-form-label pt-0 col-form-label',
- children: title,
- }),
- children,
- Boolean(!error && hint) && jsx('small', { children: hint }),
- Boolean(error) &&
- jsx('small', {
- class: 'gl-field-error',
- children: error,
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/common/form/FormRow.tsx
- function FormRow({ children }) {
- return jsx('div', { class: 'gl-flex gl-gap-x-3', children });
- }
-
- // libs/share/src/utils/camelizeKeys.ts
- function camelizeKeys(data) {
- if (!data || ['string', 'number', 'boolean'].includes(typeof data)) {
- return data;
- }
- if (Array.isArray(data)) {
- return data.map(camelizeKeys);
- }
- const camelize = (key) => {
- const _key = key.replace(/[-_\s]+(.)?/g, (_, chr) =>
- chr ? chr.toUpperCase() : ''
- );
- return _key.substring(0, 1).toLowerCase() + _key.substring(1);
- };
- return Object.entries(data).reduce(
- (result, [key, value]) => ({
- ...result,
- [camelize(key)]: camelizeKeys(value),
- }),
- {}
- );
- }
-
- // apps/gitlab-plus/src/providers/GitlabProvider.ts
- class GitlabProvider {
- constructor(force = false) {
- __publicField(this, 'cache', new Cache('glp-'));
- __publicField(this, 'graphqlApi', 'https://gitlab.com/api/graphql');
- __publicField(this, 'url', 'https://gitlab.com/api/v4/');
- this.force = force;
- }
-
- async cached(key, getValue, minutes) {
- const cacheValue = this.cache.get(key);
- if (cacheValue && !this.force) {
- return cacheValue;
- }
- const value = await getValue();
- this.cache.set(key, value, minutes);
- return value;
- }
-
- csrf() {
- const token = document.querySelector('meta[name=csrf-token]');
- if (token) {
- return token.getAttribute('content');
- }
- return '';
- }
-
- async get(path) {
- const response = await fetch(`${this.url}${path}`, {
- headers: this.headers(),
- method: 'GET',
- });
- const data = await response.json();
- return camelizeKeys(data);
- }
-
- async getCached(key, path, minutes) {
- return this.cached(key, () => this.get(path), minutes);
- }
-
- headers() {
- const headers = {
- 'content-type': 'application/json',
- };
- const csrf = this.csrf();
- if (csrf) {
- headers['X-CSRF-Token'] = csrf;
- }
- return headers;
- }
-
- async post(path, body) {
- const response = await fetch(`${this.url}${path}`, {
- body: JSON.stringify(body),
- headers: this.headers(),
- method: 'POST',
- });
- const data = await response.json();
- return camelizeKeys(data);
- }
-
- async query(query, variables) {
- const response = await fetch(this.graphqlApi, {
- body: JSON.stringify({ query, variables }),
- headers: this.headers(),
- method: 'POST',
- });
- return response.json();
- }
-
- async queryCached(key, query, variables, minutes) {
- return this.cached(key, () => this.query(query, variables), minutes);
- }
- }
-
- // apps/gitlab-plus/src/providers/query/user.ts
- const userFragment = `
- fragment User on User {
- id
- avatarUrl
- name
- username
- webUrl
- webPath
- __typename
- }
- `;
- const userQuery = `
- query workspaceAutocompleteUsersSearch($search: String!, $fullPath: ID!, $isProject: Boolean = true) {
- groupWorkspace: group(fullPath: $fullPath) @skip(if: $isProject) {
- id
- users: autocompleteUsers(search: $search) {
- ...User
- ...UserAvailability
- __typename
- }
- __typename
- }
- workspace: project(fullPath: $fullPath) {
- id
- users: autocompleteUsers(search: $search) {
- ...User
- ...UserAvailability
- __typename
- }
- __typename
- }
- }
-
- ${userFragment}
- fragment UserAvailability on User {
- status {
- availability
- __typename
- }
- __typename
- }
- `;
-
- // apps/gitlab-plus/src/providers/UsersProvider.ts
- class UsersProvider extends GitlabProvider {
- async getUsers(projectId, search = '') {
- return this.queryCached(
- `users-${projectId}-${search}`,
- userQuery,
- {
- fullPath: projectId,
- search,
- },
- search === '' ? 20 : 0.5
- );
- }
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteButton.ts
- function useAsyncAutocompleteButton(hide) {
- const ref = useRef(null);
- useEffect(() => {
- document.body.addEventListener('click', (e) => {
- if (
- ref.current &&
- e.target !== ref.current &&
- !ref.current.contains(e.target)
- ) {
- hide();
- }
- });
- }, []);
- return ref;
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteButton.tsx
- function AsyncAutocompleteButton({
- isOpen,
- renderLabel,
- reset,
- setIsOpen,
- size = 'md',
- value,
- }) {
- const ref = useAsyncAutocompleteButton(() => setIsOpen(false));
- const icon = useMemo(() => {
- if (value.length) {
- return 'close-xs';
- }
- return isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
- }, [isOpen, value]);
- return jsx('button', {
- class: `btn btn-default btn-${size} btn-block gl-button gl-new-dropdown-toggle`,
- ref,
- type: 'button',
- onClick: (e) => {
- e.preventDefault();
- setIsOpen(true);
- },
- children: jsxs('span', {
- class: 'gl-button-text gl-w-full',
- children: [
- jsx('span', {
- class: 'gl-new-dropdown-button-text',
- children: renderLabel(value),
- }),
- jsx('span', {
- onClick: (e) => {
- if (value.length) {
- e.preventDefault();
- reset();
- }
- },
- children: jsx(GitlabIcon, { icon, size: 16 }),
- }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteOption.tsx
- function AsyncAutocompleteOption({
- hideCheckbox = false,
- isActive,
- onClick,
- option,
- removeFromRecent,
- renderOption,
- selected,
- }) {
- const selectedIds = selected.map((i) => i.id);
- const selectedClass = (id) => selectedIds.includes(id);
- return jsx('li', {
- onClick: () => onClick(option),
- class: clsx(
- 'gl-new-dropdown-item', // selectedClass(option.id),
- isActive && 'glp-active'
- ),
- children: jsxs('span', {
- class: 'gl-new-dropdown-item-content',
- children: [
- !hideCheckbox &&
- jsx(GitlabIcon, {
- className: 'glp-item-check gl-pr-2',
- icon: selectedClass(option.id) ? 'mobile-issue-close' : '',
- size: 16,
- }),
- renderOption(option),
- removeFromRecent &&
- jsx(CloseButton, {
- title: 'Remove from recently used',
- onClick: (e) => {
- e.preventDefault();
- e.stopPropagation();
- removeFromRecent(option);
- },
- }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteList.tsx
- function AsyncAutocompleteList({
- hideCheckbox,
- activeIndex,
- onClick,
- options,
- recently,
- removeRecently,
- renderOption,
- value,
- }) {
- return jsx('div', {
- onClick: (e) => e.stopPropagation(),
- class:
- 'gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay bottom-scrim-visible gl-new-dropdown-contents',
- style: {
- maxWidth: '800px',
- width: '100%',
- left: '0',
- top: '100%',
- },
- children: jsx('div', {
- class: 'gl-new-dropdown-inner',
- children: jsxs('ul', {
- class: 'gl-mb-0 gl-pl-0',
- children: [
- Boolean(recently.length) &&
- jsxs(Fragment, {
- children: [
- jsx('li', {
- class:
- 'gl-pb-2 gl-pl-4 gl-pt-3 gl-text-sm gl-font-bold gl-text-strong',
- children: 'Recently used',
- }),
- recently.map((item, index) =>
- jsx(
- AsyncAutocompleteOption,
- {
- hideCheckbox,
- isActive: index === activeIndex,
- onClick,
- option: item,
- removeFromRecent: removeRecently,
- renderOption,
- selected: value,
- },
- item.id
- )
- ),
- ],
- }),
- Boolean(options.length) &&
- jsxs(Fragment, {
- children: [
- jsx('li', {
- class:
- 'gl-pb-2 gl-pl-4 gl-pt-3 gl-text-sm gl-font-bold gl-text-strong gl-border-t',
- }),
- options.map((item, index) =>
- jsx(
- AsyncAutocompleteOption,
- {
- hideCheckbox,
- isActive: recently.length + index === activeIndex,
- onClick,
- option: item,
- renderOption,
- selected: value,
- },
- item.id
- )
- ),
- ],
- }),
- options.length + recently.length === 0 &&
- jsx('li', { class: 'gl-p-4', children: 'No options' }),
- ],
- }),
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteSearch.tsx
- function AsyncAutocompleteSearch({ navigate, setValue, value }) {
- return jsx('div', {
- class: 'gl-border-b-1 gl-border-b-solid gl-border-b-dropdown',
- children: jsxs('div', {
- class: 'gl-listbox-search gl-listbox-topmost',
- children: [
- jsx(GitlabIcon, {
- className: 'gl-search-box-by-type-search-icon',
- icon: 'search',
- size: 16,
- }),
- jsx('input', {
- class: 'gl-listbox-search-input',
- onInput: (e) => setValue(e.target.value),
- onKeyDown: (e) => navigate(e.key),
- value,
- autofocus: true,
- }),
- Boolean(value) &&
- jsx('div', {
- class: 'gl-search-box-by-type-right-icons',
- style: { top: '0' },
- children: jsx(CloseButton, {
- onClick: () => setValue(''),
- title: 'Clear input',
- }),
- }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/useListNavigate.ts
- function useListNavigate(options, recent, onClick, onClose) {
- const [activeIndex, setActiveIndex] = useState(-1);
- const navigate = (key) => {
- if (['ArrowDown', 'ArrowUp'].includes(key)) {
- const total = recent.length + options.length;
- const diff = key === 'ArrowDown' ? 1 : -1;
- setActiveIndex((activeIndex + diff + total) % total);
- } else if (key === 'Enter') {
- const allItems = [...recent, ...options];
- if (-1 < activeIndex && activeIndex < allItems.length) {
- onClick(allItems[activeIndex]);
- }
- } else if (key === 'Escape') {
- onClose();
- }
- };
- return {
- activeIndex,
- navigate,
- };
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteDropdown.tsx
- function AsyncAutocompleteDropdown({
- hideCheckbox,
- onClick,
- onClose,
- options,
- recently = [],
- removeRecently,
- renderOption,
- searchTerm,
- setSearchTerm,
- value,
- }) {
- const { activeIndex, navigate } = useListNavigate(
- options,
- recently,
- onClick,
- onClose
- );
- return jsx('div', {
- class: clsx('gl-new-dropdown-panel gl-absolute !gl-block'),
- onClick: (e) => e.stopPropagation(),
- style: {
- maxWidth: '800px',
- width: '100%',
- left: 'auto',
- right: '0',
- top: '100%',
- },
- children: jsxs('div', {
- class: 'gl-new-dropdown-inner',
- children: [
- jsx(AsyncAutocompleteSearch, {
- navigate,
- setValue: setSearchTerm,
- value: searchTerm,
- }),
- jsx(AsyncAutocompleteList, {
- hideCheckbox,
- activeIndex,
- onClick,
- options,
- recently,
- removeRecently,
- renderOption,
- value,
- }),
- ],
- }),
- });
- }
-
- // libs/share/src/utils/useDebounce.ts
- function useDebounce(value, delay = 300) {
- const [debouncedValue, setDebouncedValue] = useState(value);
- useEffect(() => {
- const handler = setTimeout(() => {
- setDebouncedValue(value);
- }, delay);
- return () => {
- clearTimeout(handler);
- };
- }, [value, delay]);
- return debouncedValue;
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteOptions.ts
- function useAsyncAutocompleteOptions(searchTerm, getValues) {
- const [options, setOptions] = useState([]);
- const term = useDebounce(searchTerm);
- const loadOptions = useCallback(
- async (term2) => {
- const items = await getValues(term2);
- setOptions(items);
- },
- [getValues]
- );
- useEffect(() => {
- loadOptions(term);
- }, [term, loadOptions]);
- return options;
- }
-
- // apps/gitlab-plus/src/providers/RecentlyProvider.ts
- class RecentlyProvider {
- constructor(key) {
- __publicField(this, 'cache', new Cache('glp-'));
- __publicField(this, 'key');
- __publicField(this, 'eventName');
- this.key = `recently-${key}`;
- this.eventName = `recently-${key}-change`;
- }
-
- add(...items) {
- const itemsId = items.map((i) => i.id);
- this.cache.set(
- this.key,
- [...items, ...this.get().filter((el) => !itemsId.includes(el.id))],
- 'lifetime'
- );
- this.triggerChange();
- }
-
- get() {
- return this.cache.get(this.key) || [];
- }
-
- onChange(callback) {
- document.addEventListener(this.eventName, callback);
- }
-
- remove(...items) {
- const itemsId = items.map((i) => i.id);
- this.cache.set(
- this.key,
- this.get().filter((el) => !itemsId.includes(el.id)),
- 'lifetime'
- );
- this.triggerChange();
- }
-
- triggerChange() {
- document.dispatchEvent(new CustomEvent(this.eventName));
- }
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteRecently.ts
- function useAsyncAutocompleteRecently(name2) {
- const store = useRef(new RecentlyProvider(name2));
- const [recently, setRecently] = useState(store.current.get());
- useEffect(() => {
- store.current.onChange(() => {
- setRecently(store.current.get());
- });
- }, []);
- return {
- add: store.current.add.bind(store.current),
- recently,
- remove: store.current.remove.bind(store.current),
- };
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocomplete.ts
- function useAsyncAutocomplete(
- name2,
- value,
- getValues,
- onChange,
- isMultiselect
- ) {
- const [searchTerm, setSearchTerm] = useState('');
- const [isOpen, setIsOpen] = useState(false);
- const { recently: allRecently, remove: removeRecently } =
- useAsyncAutocompleteRecently(name2);
- const options = useAsyncAutocompleteOptions(searchTerm, getValues);
- const onClick = (item) => {
- if (isMultiselect) {
- if (value.find((i) => i.id === item.id)) {
- onChange(value.filter((i) => i.id !== item.id));
- } else {
- onChange([...value, item]);
- }
- } else {
- onChange([item]);
- setIsOpen(false);
- }
- };
- const recently = useMemo(() => {
- const optionsIds = options.map((i) => i.id);
- return searchTerm.length
- ? allRecently.filter((i) => optionsIds.includes(i.id))
- : allRecently;
- }, [options, allRecently]);
- return {
- isOpen,
- onClick,
- options: useMemo(() => {
- const recentlyIds = recently.map((i) => i.id);
- return options.filter((i) => !recentlyIds.includes(i.id));
- }, [options, recently]),
- recently,
- removeRecently,
- searchTerm,
- setIsOpen,
- setSearchTerm,
- };
- }
-
- // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocomplete.tsx
- function AsyncAutocomplete({
- hideCheckbox = false,
- buttonSize,
- getValues,
- isDisabled,
- isMultiselect = false,
- name: name2,
- onChange,
- renderLabel,
- renderOption,
- value,
- }) {
- const {
- isOpen,
- onClick,
- options,
- recently,
- removeRecently,
- searchTerm,
- setIsOpen,
- setSearchTerm,
- } = useAsyncAutocomplete(name2, value, getValues, onChange, isMultiselect);
- return jsxs('div', {
- class: clsx(
- 'gl-relative gl-w-full gl-new-dropdown !gl-block',
- isDisabled && 'gl-pointer-events-none gl-opacity-5'
- ),
- children: [
- jsx(AsyncAutocompleteButton, {
- isOpen,
- renderLabel,
- reset: () => onChange([]),
- setIsOpen,
- size: buttonSize,
- value,
- }),
- isOpen &&
- jsx(AsyncAutocompleteDropdown, {
- hideCheckbox,
- onClick,
- onClose: () => setIsOpen(false),
- options,
- recently,
- removeRecently,
- renderOption,
- searchTerm,
- setSearchTerm,
- value,
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/common/GitlabUser.tsx
- function GitlabUser({ showUsername, size = 24, smallText, user, withLink }) {
- const label = useMemo(() => {
- return jsxs(Fragment, {
- children: [
- jsx('span', {
- class: clsx('gl-mr-2 gl-block', smallText && '!gl-text-sm'),
- children: user.name,
- }),
- showUsername &&
- jsx('span', {
- class: 'gl-block gl-text-secondary !gl-text-sm',
- children: user.username,
- }),
- ],
- });
- }, [smallText, showUsername, user]);
- const iconClsx = [
- `gl-avatar gl-avatar-s${size}`,
- smallText ? 'gl-mr-1' : 'gl-mr-3',
- ];
- return jsxs('div', {
- class: 'gl-flex gl-items-center',
- children: [
- user.avatarUrl
- ? jsx('img', {
- alt: `${user.name}'s avatar`,
- class: clsx(...iconClsx, `gl-avatar-circle`),
- src: user.avatarUrl,
- })
- : jsx('div', {
- class: clsx(
- ...iconClsx,
- `gl-avatar-identicon gl-avatar-identicon-bg1`
- ),
- children: user.name[0].toUpperCase(),
- }),
- withLink
- ? jsx('a', { href: user.webUrl, children: label })
- : jsx('div', { children: label }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/create-issue/fields/AssigneesField.tsx
- function AssigneesField({ projectPath, setValue, value }) {
- const getUsers = useCallback(
- async (search) => {
- if (!projectPath) {
- return [];
- }
- const response = await new UsersProvider().getUsers(projectPath, search);
- return response.data.workspace.users;
- },
- [projectPath]
- );
- const renderLabel = useCallback((items) => {
- const label = items.map((i) => i.name).join(', ');
- return jsx('div', {
- title: label,
- children: items.length ? label : 'Select assignee',
- });
- }, []);
- const renderOption = useCallback((item) => {
- return jsx('span', {
- class: 'gl-new-dropdown-item-text-wrapper',
- children: jsx(GitlabUser, { user: item, showUsername: true }),
- });
- }, []);
- return jsx(AsyncAutocomplete, {
- getValues: getUsers,
- isDisabled: !projectPath,
- name: 'assignees',
- onChange: setValue,
- renderLabel,
- renderOption,
- value,
- isMultiselect: true,
- });
- }
-
- // apps/gitlab-plus/src/components/create-issue/fields/ButtonField.tsx
- function ButtonField({ create, isLoading, reset }) {
- return jsxs(Fragment, {
- children: [
- jsxs('button', {
- class: 'btn btn-confirm btn-sm gl-button gl-gap-2',
- disabled: isLoading,
- onClick: create,
- type: 'button',
- children: [
- jsx('span', {
- class: 'gl-button-text',
- children: 'Add',
- }),
- isLoading
- ? jsx(GitlabLoader, { size: 12 })
- : jsx(GitlabIcon, { icon: 'plus', size: 12 }),
- ],
- }),
- jsx('button', {
- class: 'btn btn-sm gl-button',
- onClick: reset,
- type: 'button',
- children: jsx('span', { class: 'gl-button-text', children: 'Reset' }),
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/providers/query/iteration.ts
- const iterationFragment = `fragment IterationFragment on Iteration {
- id
- title
- startDate
- dueDate
- webUrl
- iterationCadence {
- id
- title
- __typename
- }
- __typename
- }`;
- const iterationQuery = `query issueIterationsAliased($fullPath: ID!, $title: String, $state: IterationState) {
- workspace: group(fullPath: $fullPath) {
- id
- attributes: iterations(
- search: $title
- in: [TITLE, CADENCE_TITLE]
- state: $state
- ) {
- nodes {
- ...IterationFragment
- state
- __typename
- }
- __typename
- }
- __typename
- }
- }
- ${iterationFragment}
- `;
-
- // apps/gitlab-plus/src/providers/IterationsProvider.ts
- class IterationsProvider extends GitlabProvider {
- async getIterations(projectId, title = '') {
- return this.queryCached(
- `iterations-${projectId}-search-${title}`,
- iterationQuery,
- {
- fullPath: projectId,
- state: 'opened',
- title,
- },
- title !== '' ? 0.5 : 20
- );
- }
- }
-
- // apps/gitlab-plus/src/components/create-issue/fields/IterationField.tsx
- function iterationName(iteration) {
- const start = new Date(iteration.startDate).toLocaleDateString();
- const end = new Date(iteration.dueDate).toLocaleDateString();
- return `${iteration.iterationCadence.title}: ${start} - ${end}`;
- }
-
- function IterationField({ link, setValue, value }) {
- const getUsers = useCallback(
- async (search) => {
- const response = await new IterationsProvider().getIterations(
- link.workspacePath,
- search
- );
- return response.data.workspace.attributes.nodes
- .map((iteration) => ({
- ...iteration,
- name: iterationName(iteration),
- }))
- .toSorted((a, b) => a.name.localeCompare(b.name));
- },
- [link]
- );
- const renderLabel = useCallback(([item]) => {
- return item ? item.name : 'Select iteration';
- }, []);
- const renderOption = useCallback((item) => {
- return jsx('span', {
- class: 'gl-new-dropdown-item-text-wrapper',
- children: jsx('span', {
- class: 'gl-flex gl-w-full gl-items-center',
- children: jsx('span', {
- class: 'gl-mr-2 gl-block',
- children: item.name,
- }),
- }),
- });
- }, []);
- return jsx(AsyncAutocomplete, {
- getValues: getUsers,
- name: 'iterations',
- onChange: setValue,
- renderLabel,
- renderOption,
- value,
- });
- }
-
- // apps/gitlab-plus/src/providers/query/label.ts
- const labelFragment = `
- fragment Label on Label {
- id
- title
- description
- color
- textColor
- __typename
- }
- `;
- const projectLabelsQuery = `query projectLabels($fullPath: ID!, $searchTerm: String) {
- workspace: project(fullPath: $fullPath) {
- id
- labels(
- searchTerm: $searchTerm
- includeAncestorGroups: true
- ) {
- nodes {
- ...Label
- __typename
- }
- __typename
- }
- __typename
- }
- }
- ${labelFragment}
- `;
- const workspaceLabelsQuery = `query groupLabels($fullPath: ID!, $searchTerm: String) {
- workspace: group(fullPath: $fullPath) {
- id
- labels(
- searchTerm: $searchTerm
- onlyGroupLabels: true
- includeAncestorGroups: true
- ) {
- nodes {
- ...Label
- __typename
- }
- __typename
- }
- __typename
- }
- }
-
- ${labelFragment}
- `;
-
- // apps/gitlab-plus/src/providers/LabelsProvider.ts
- class LabelsProvider extends GitlabProvider {
- async getProjectLabels(projectPath, search = '') {
- return this.queryCached(
- `project-${projectPath}-labels-${search}`,
- projectLabelsQuery,
- {
- fullPath: projectPath,
- searchTerm: search,
- },
- search === '' ? 20 : 0.5
- );
- }
-
- async getWorkspaceLabels(workspacePath, search = '') {
- return this.queryCached(
- `workspace-${workspacePath}-labels-${search}`,
- workspaceLabelsQuery,
- {
- fullPath: workspacePath,
- searchTerm: search,
- },
- search === '' ? 20 : 0.5
- );
- }
- }
-
- // apps/gitlab-plus/src/components/common/GitlabLabel.tsx
- function GitlabLabel({ label, onRemove }) {
- const [scope, text] = label.title.split('::');
- const props = useMemo(() => {
- const className = [
- 'gl-label',
- 'hide-collapsed',
- label.textColor === '#FFFFFF'
- ? 'gl-label-text-light'
- : 'gl-label-text-dark',
- ];
- if (label.title.includes('::')) {
- className.push('gl-label-scoped');
- }
- return {
- class: clsx(className),
- style: {
- '--label-background-color': label.color,
- '--label-inset-border': `inset 0 0 0 2px ${label.color}`,
- },
- };
- }, [label]);
- return jsxs('span', {
- class: props.class,
- style: props.style,
- children: [
- jsxs('span', {
- class: 'gl-link gl-label-link gl-label-link-underline',
- children: [
- jsx('span', {
- class: 'gl-label-text',
- children: scope,
- }),
- text &&
- jsx('span', { class: 'gl-label-text-scoped', children: text }),
- ],
- }),
- onRemove &&
- jsx('button', {
- onClick: onRemove,
- type: 'button',
- class:
- 'btn gl-label-close !gl-p-0 btn-reset btn-sm gl-button btn-reset-tertiary',
- children: jsx('span', {
- class: 'gl-button-text',
- children: jsx(GitlabIcon, { icon: 'close-xs' }),
- }),
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/create-issue/fields/LabelsField.tsx
- function LabelField({ copyLabels, projectPath, setValue, value }) {
- const getLabels = useCallback(
- async (search) => {
- if (!projectPath) {
- return [];
- }
- const response = await new LabelsProvider().getProjectLabels(
- projectPath,
- search
- );
- return response.data.workspace.labels.nodes;
- },
- [projectPath]
- );
- const renderLabel = useCallback((items) => {
- return items.length
- ? items.map((i) => i.title).join(', ')
- : 'Select labels';
- }, []);
- const renderOption = useCallback((item) => {
- return jsxs('div', {
- class: 'gl-flex gl-flex-1 gl-break-anywhere gl-pb-3 gl-pl-4 gl-pt-3',
- children: [
- jsx('span', {
- class: 'dropdown-label-box gl-top-0 gl-mr-3 gl-shrink-0',
- style: { backgroundColor: item.color },
- }),
- jsx('span', { children: item.title }),
- ],
- });
- }, []);
- return jsxs(Fragment, {
- children: [
- jsx('div', {
- class: 'gl-mt-1 gl-pb-2 gl-flex gl-flex-wrap gl-gap-2',
- children: value.map((label) =>
- jsx(
- GitlabLabel,
- {
- label,
- onRemove: () =>
- setValue(value.filter((item) => label.id !== item.id)),
- },
- label.id
- )
- ),
- }),
- jsxs('div', {
- className: 'gl-flex gl-gap-1 gl-relative gl-pr-7',
- children: [
- jsx(AsyncAutocomplete, {
- getValues: getLabels,
- isDisabled: !projectPath,
- name: 'labels',
- onChange: setValue,
- renderLabel,
- renderOption,
- value,
- isMultiselect: true,
- }),
- jsx('div', {
- className: 'gl-flex gl-absolute gl-h-full gl-right-0',
- children: jsx(GitlabButton, {
- icon: 'labels',
- onClick: copyLabels,
- title: 'Copy labels from parent',
- }),
- }),
- ],
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/providers/query/milestone.ts
- const milestoneQuery = `query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
- workspace: project(fullPath: $fullPath) {
- id
- attributes: milestones(
- searchTitle: $title
- state: $state
- sort: EXPIRED_LAST_DUE_DATE_ASC
- first: 20
- includeAncestors: true
- ) {
- nodes {
- ...MilestoneFragment
- state
- __typename
- }
- __typename
- }
- __typename
- }
- }
-
- fragment MilestoneFragment on Milestone {
- id
- iid
- title
- webUrl: webPath
- dueDate
- expired
- __typename
- }
-
- `;
-
- // apps/gitlab-plus/src/providers/MilestonesProvider.ts
- class MilestonesProvider extends GitlabProvider {
- async getMilestones(projectId, title = '') {
- return this.queryCached(
- `milestones-${projectId}-${title}`,
- milestoneQuery,
- {
- fullPath: projectId,
- state: 'active',
- title,
- },
- title === '' ? 20 : 0.5
- );
- }
- }
-
- // apps/gitlab-plus/src/components/create-issue/fields/MilestoneField.tsx
- function MilestoneField({ projectPath, setValue, value }) {
- const getMilestones = useCallback(
- async (search) => {
- if (!projectPath) {
- return [];
- }
- const response = await new MilestonesProvider().getMilestones(
- projectPath,
- search
- );
- return response.data.workspace.attributes.nodes;
- },
- [projectPath]
- );
- const renderLabel = useCallback(([item]) => {
- return item ? item.title : 'Select milestone';
- }, []);
- const renderOption = useCallback((item) => {
- return jsx('span', {
- class: 'gl-new-dropdown-item-text-wrapper',
- children: jsx('span', {
- class: 'gl-flex gl-w-full gl-items-center',
- children: jsx('span', {
- class: 'gl-mr-2 gl-block',
- children: item.title,
- }),
- }),
- });
- }, []);
- return jsx(AsyncAutocomplete, {
- getValues: getMilestones,
- isDisabled: !projectPath,
- name: 'milestones',
- onChange: setValue,
- renderLabel,
- renderOption,
- value,
- });
- }
-
- // apps/gitlab-plus/src/providers/query/project.ts
- const projectsQuery = `query boardsGetGroupProjects($fullPath: ID!, $search: String, $after: String) {
- group(fullPath: $fullPath) {
- id
- projects(search: $search, after: $after, first: 100, includeSubgroups: true) {
- nodes {
- id
- name
- avatarUrl
- fullPath
- nameWithNamespace
- archived
- __typename
- }
- pageInfo {
- ...PageInfo
- __typename
- }
- __typename
- }
- __typename
- }
- }
-
- fragment PageInfo on PageInfo {
- hasNextPage
- hasPreviousPage
- startCursor
- endCursor
- __typename
- }
-
- `;
-
- // apps/gitlab-plus/src/providers/ProjectsProvider.ts
- class ProjectsProvider extends GitlabProvider {
- async getProjects(workspacePath, search = '') {
- return this.queryCached(
- `projects-${workspacePath}-${search}`,
- projectsQuery,
- {
- fullPath: workspacePath,
- search,
- },
- search === '' ? 20 : 0.5
- );
- }
- }
-
- // apps/gitlab-plus/src/components/common/GitlabProject.tsx
- function GitlabProject({ project, size = 32 }) {
- return jsxs('span', {
- class: 'gl-flex gl-w-full gl-items-center',
- children: [
- project.avatarUrl
- ? jsx('img', {
- alt: project.name,
- class: `gl-mr-3 gl-avatar gl-avatar-s${size}`,
- src: project.avatarUrl,
- })
- : jsx('div', {
- class: `gl-mr-3 gl-avatar gl-avatar-identicon gl-avatar-s${size} gl-avatar-identicon-bg1`,
- children: project.name[0].toUpperCase(),
- }),
- jsxs('span', {
- children: [
- jsx('span', { class: 'gl-mr-2 gl-block', children: project.name }),
- jsx('span', {
- class: 'gl-block gl-text-secondary !gl-text-sm',
- children: project.nameWithNamespace,
- }),
- ],
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/create-issue/fields/ProjectField.tsx
- function ProjectField({ link, setValue, value }) {
- const getProjects = useCallback(
- async (search) => {
- const response = await new ProjectsProvider().getProjects(
- link.workspacePath,
- search
- );
- return response.data.group.projects.nodes;
- },
- [link]
- );
- const renderLabel = useCallback(([item]) => {
- return item ? item.nameWithNamespace : 'Select project';
- }, []);
- const renderOption = useCallback((item) => {
- return jsx('span', {
- class: 'gl-new-dropdown-item-text-wrapper',
- children: jsx(GitlabProject, { project: item }),
- });
- }, []);
- return jsx(AsyncAutocomplete, {
- getValues: getProjects,
- name: 'projects',
- onChange: setValue,
- renderLabel,
- renderOption,
- value,
- });
- }
-
- // apps/gitlab-plus/src/types/Issue.ts
- const issueRelation = ['blocks', 'is_blocked_by', 'relates_to'];
-
- // apps/gitlab-plus/src/components/create-issue/fields/RelationField.tsx
- const labels = (relation) => {
- switch (relation) {
- case 'blocks':
- return 'blocks current issue';
- case 'is_blocked_by':
- return 'is blocked by current issue';
- case 'relates_to':
- return 'relates to current issue';
- default:
- return 'is not related to current issue';
- }
- };
-
- function RelationField({ setValue, value }) {
- return jsx('div', {
- class: 'linked-issue-type-radio',
- children: [...issueRelation, null].map((relation) =>
- jsxs(
- 'div',
- {
- class: 'gl-form-radio custom-control custom-radio',
- children: [
- jsx('input', {
- id: `create-related-issue-relation-${relation}`,
- checked: value === relation,
- class: 'custom-control-input',
- name: 'linked-issue-type-radio',
- onChange: () => setValue(relation),
- type: 'radio',
- value: relation ?? '',
- }),
- jsx('label', {
- class: 'custom-control-label',
- for: `create-related-issue-relation-${relation}`,
- children: labels(relation),
- }),
- ],
- },
- relation
- )
- ),
- });
- }
-
- // apps/gitlab-plus/src/components/create-issue/fields/TitleField.tsx
- function TitleField({ error, onChange, value }) {
- return jsx('input', {
- onInput: (e) => onChange(e.target.value),
- placeholder: 'Add a title',
- value,
- class: clsx(
- 'gl-form-input form-control',
- error && 'gl-field-error-outline'
- ),
- });
- }
-
- // apps/gitlab-plus/src/helpers/LinkParser.ts
- class LinkParser {
- static isEpicLink(link) {
- return link.epic !== void 0;
- }
-
- static isIssueLink(link) {
- return link.issue !== void 0;
- }
-
- static isMrLink(link) {
- return link.mr !== void 0;
- }
-
- static parseEpicLink(link) {
- if (LinkParser.validateEpicLink(link)) {
- return LinkParser.parseGitlabLink(
- link,
- /\/groups\/(?<workspacePath>.+)\/-\/epics\/(?<epic>\d+)/
- );
- }
- return void 0;
- }
-
- static parseGitlabLink(link, pattern) {
- const url = new URL(link);
- const result = url.pathname.match(pattern);
- if (result && result.groups) {
- return result.groups;
- }
- return void 0;
- }
-
- static parseIssueLink(link) {
- if (LinkParser.validateIssueLink(link)) {
- return LinkParser.parseGitlabLink(
- link,
- /\/(?<projectPath>(?<workspacePath>.+)\/[^/]+)\/-\/issues\/(?<issue>\d+)/
- );
- }
- return void 0;
- }
-
- static parseMrLink(link) {
- if (LinkParser.validateMrLink(link)) {
- return LinkParser.parseGitlabLink(
- link,
- /\/(?<projectPath>(?<workspacePath>.+)\/[^/]+)\/-\/merge_requests\/(?<mr>\d+)\/?$/
- );
- }
- return void 0;
- }
-
- static validateEpicLink(link) {
- return LinkParser.validateGitlabLink(link, 'epics');
- }
-
- static validateGitlabLink(link, type) {
- return Boolean(typeof link === 'string' && link.includes(`/-/${type}/`));
- }
-
- static validateIssueLink(link) {
- return LinkParser.validateGitlabLink(link, 'issues');
- }
-
- static validateMrLink(link) {
- return LinkParser.validateGitlabLink(link, 'merge_requests');
- }
- }
-
- // apps/gitlab-plus/src/helpers/Widget.ts
- class WidgetHelper {
- static epicLabels(epic) {
- const labelWidgets = epic.widgets.find((w) => w.type === 'LABELS');
- if (labelWidgets) {
- return labelWidgets.labels.nodes;
- }
- return [];
- }
- }
-
- // apps/gitlab-plus/src/providers/query/epic.ts
- const epicQuery = `query namespaceWorkItem($fullPath: ID!, $iid: String!) {
- workspace: namespace(fullPath: $fullPath) {
- id
- workItem(iid: $iid) {
- ...WorkItem
- __typename
- }
- __typename
- }
- }
-
- fragment WorkItem on WorkItem {
- id
- iid
- archived
- title
- state
- description
- confidential
- createdAt
- closedAt
- webUrl
- reference(full: true)
- createNoteEmail
- project {
- id
- __typename
- }
- namespace {
- id
- fullPath
- name
- fullName
- __typename
- }
- author {
- ...Author
- __typename
- }
-
- workItemType {
- id
- name
- iconName
- __typename
- }
- userPermissions {
- deleteWorkItem
- updateWorkItem
- adminParentLink
- setWorkItemMetadata
- createNote
- adminWorkItemLink
- markNoteAsInternal
- reportSpam
- __typename
- }
- widgets {
- ...WorkItemWidgets
- __typename
- }
- __typename
- }
-
- fragment WorkItemWidgets on WorkItemWidget {
- type
- ... on WorkItemWidgetHierarchy {
- hasChildren
- children(first: 100) {
- count
- nodes {
- id
- iid
- title
- state
- webUrl
- }
- }
- }
- ... on WorkItemWidgetAssignees {
- assignees {
- nodes {
- ...User
- }
- }
- }
- ... on WorkItemWidgetLabels {
- labels {
- nodes {
- ...Label
- }
- }
- }
- ... on WorkItemWidgetStartAndDueDate {
- dueDate
- startDate
- rollUp
- isFixed
- __typename
- }
- ... on WorkItemWidgetProgress {
- progress
- updatedAt
- __typename
- }
- ... on WorkItemWidgetIteration {
- iteration {
- id
- title
- startDate
- dueDate
- webUrl
- iterationCadence {
- id
- title
- }
- __typename
- }
- __typename
- }
- ... on WorkItemWidgetMilestone {
- milestone {
- ...MilestoneFragment
- __typename
- }
- __typename
- }
- ... on WorkItemWidgetNotes {
- discussionLocked
- __typename
- }
- ... on WorkItemWidgetHealthStatus {
- healthStatus
- rolledUpHealthStatus {
- count
- healthStatus
- __typename
- }
- __typename
- }
- ... on WorkItemWidgetNotifications {
- subscribed
- __typename
- }
- ... on WorkItemWidgetCurrentUserTodos {
- currentUserTodos(state: pending) {
- nodes {
- id
- __typename
- }
- __typename
- }
- __typename
- }
- ... on WorkItemWidgetColor {
- color
- textColor
- __typename
- }
- ... on WorkItemWidgetLinkedItems {
- linkedItems {
- nodes {
- linkId
- linkType
- __typename
- }
- __typename
- }
- __typename
- }
- ... on WorkItemWidgetCrmContacts {
- contacts {
- nodes {
- id
- email
- firstName
- lastName
- phone
- description
- organization {
- id
- name
- description
- defaultRate
- __typename
- }
- __typename
- }
- __typename
- }
- __typename
- }
- __typename
- }
-
- fragment User on User {
- id
- avatarUrl
- name
- username
- webUrl
- webPath
- __typename
- }
-
- fragment MilestoneFragment on Milestone {
- expired
- id
- title
- state
- startDate
- dueDate
- webPath
- __typename
- }
-
- fragment Author on User {
- id
- avatarUrl
- name
- username
- webUrl
- webPath
- __typename
- }
-
- ${labelFragment}
- `;
- const epicSetLabelsMutation = `
- mutation workItemUpdate($input: WorkItemUpdateInput!) {
- workItemUpdate(input: $input) {
- workItem {
- __typename
- }
- errors
- }
- }
- `;
-
- // apps/gitlab-plus/src/providers/EpicProvider.ts
- class EpicProvider extends GitlabProvider {
- async getEpic(workspacePath, epicId) {
- return this.queryCached(
- `epic-${workspacePath}-${epicId}`,
- epicQuery,
- {
- iid: epicId,
- cursor: '',
- fullPath: workspacePath,
- pageSize: 50,
- },
- 2
- );
- }
-
- async updateEpicLabels(id, addLabelIds, removeLabelIds) {
- return await this.query(epicSetLabelsMutation, {
- input: {
- id,
- labelsWidget: {
- addLabelIds,
- removeLabelIds,
- },
- },
- });
- }
- }
-
- // apps/gitlab-plus/src/providers/query/issue.ts
- const issueQuery = `query issueEE($projectPath: ID!, $iid: String!) {
- project(fullPath: $projectPath) {
- id
- issue(iid: $iid) {
- id
- iid
- title
- description
- createdAt
- state
- confidential
- dueDate
- projectId
- milestone {
- id
- title
- startDate
- dueDate
- __typename
- }
- epic {
- id
- iid
- title
- webUrl
- }
- iteration {
- id
- title
- startDate
- dueDate
- iterationCadence {
- id
- title
- __typename
- }
- __typename
- }
- labels {
- nodes {
- ...Label
- }
- }
- relatedMergeRequests {
- nodes {
- iid
- title
- state
- webUrl
- author {
- ...User
- }
- }
- }
- assignees {
- nodes {
- ...User
- }
- }
- author {
- ...User
- }
- weight
- type
- linkedWorkItems {
- nodes {
- linkType
- workItemState
- workItem {
- id
- iid
- webUrl
- title
- }
- }
- }
- __typename
- }
- __typename
- }
- }
-
- ${labelFragment}
- ${userFragment}
- `;
- const issueWithRelatedIssuesLabelsQuery = `query issueEE($projectPath: ID!, $iid: String!) {
- project(fullPath: $projectPath) {
- issue(iid: $iid) {
- linkedWorkItems {
- nodes {
- workItem {
- id
- iid
- widgets {
- type
- ...LabelsWidget
- }
- }
- }
- }
- }
- }
- }
-
- fragment LabelsWidget on WorkItemWidgetLabels {
- labels {
- nodes {
- ...Label
- }
- }
- }
-
- ${labelFragment}
- `;
- const issuesQuery = `query groupWorkItems($searchTerm: String, $fullPath: ID!, $types: [IssueType!], $in: [IssuableSearchableField!], $includeAncestors: Boolean = false, $includeDescendants: Boolean = false, $iid: String = null, $searchByIid: Boolean = false, $searchByText: Boolean = true, $searchEmpty: Boolean = true) {
- workspace: group(fullPath: $fullPath) {
- id
- workItems(
- search: $searchTerm
- types: $types
- in: $in
- includeAncestors: $includeAncestors
- includeDescendants: $includeDescendants
- ) @include(if: $searchByText) {
- nodes {
- id
- iid
- title
- confidential
- project {
- fullPath
- }
- __typename
- }
- __typename
- }
- workItemsByIid: workItems(
- iid: $iid
- types: $types
- includeAncestors: $includeAncestors
- includeDescendants: $includeDescendants
- ) @include(if: $searchByIid) {
- nodes {
- id
- iid
- title
- confidential
- project {
- fullPath
- }
- __typename
- }
- __typename
- }
- workItemsEmpty: workItems(
- types: $types
- includeAncestors: $includeAncestors
- includeDescendants: $includeDescendants
- ) @include(if: $searchEmpty) {
- nodes {
- id
- iid
- title
- confidential
- project {
- fullPath
- }
- __typename
- }
- __typename
- }
- __typename
- }
- }
- `;
- const issueMutation = `
- mutation CreateIssue($input: CreateIssueInput!) {
- createIssuable: createIssue(input: $input) {
- issuable: issue {
- ...Issue
- __typename
- }
- errors
- __typename
- }
- }
-
- fragment Issue on Issue {
- ...IssueNode
- id
- weight
- blocked
- blockedByCount
- epic {
- id
- __typename
- }
- iteration {
- id
- title
- startDate
- dueDate
- iterationCadence {
- id
- title
- __typename
- }
- __typename
- }
- healthStatus
- __typename
- }
-
- fragment IssueNode on Issue {
- id
- iid
- title
- referencePath: reference(full: true)
- closedAt
- dueDate
- timeEstimate
- totalTimeSpent
- humanTimeEstimate
- humanTotalTimeSpent
- emailsDisabled
- confidential
- hidden
- webUrl
- relativePosition
- projectId
- type
- severity
- milestone {
- ...MilestoneFragment
- __typename
- }
- assignees {
- nodes {
- ...User
- __typename
- }
- __typename
- }
- labels {
- nodes {
- id
- title
- color
- description
- __typename
- }
- __typename
- }
- __typename
- }
-
- fragment MilestoneFragment on Milestone {
- expired
- id
- state
- title
- __typename
- }
-
- fragment User on User {
- id
- avatarUrl
- name
- username
- webUrl
- webPath
- __typename
- }
- `;
- const issueSetEpicMutation = `
- mutation projectIssueUpdateParent($input: WorkItemUpdateInput!) {
- issuableSetAttribute: workItemUpdate(input: $input) {
- workItem {
- id
- widgets {
- ... on WorkItemWidgetHierarchy {
- type
- parent {
- id
- title
- webUrl
- }
- }
- }
- }
- errors
- }
- }
- `;
- const issueSetLabelsMutation = `
- mutation issueSetLabels($input: UpdateIssueInput!) {
- updateIssuableLabels: updateIssue(input: $input) {
- issuable: issue {
- __typename
- }
- errors
- __typename
- }
- }
- `;
-
- // apps/gitlab-plus/src/providers/IssueProvider.ts
- class IssueProvider extends GitlabProvider {
- async createIssue(input) {
- return await this.query(issueMutation, { input });
- }
-
- async createIssueRelation(input) {
- const path = [
- 'projects/:PROJECT_ID',
- '/issues/:ISSUE_ID/links',
- '?target_project_id=:TARGET_PROJECT_ID',
- '&target_issue_iid=:TARGET_ISSUE_IID',
- '&link_type=:LINK_TYPE',
- ]
- .join('')
- .replace(':PROJECT_ID', `${input.projectId}`)
- .replace(':ISSUE_ID', `${input.issueId}`)
- .replace(':TARGET_PROJECT_ID', input.targetProjectId)
- .replace(':TARGET_ISSUE_IID', input.targetIssueIid)
- .replace(':LINK_TYPE', input.linkType);
- return await this.post(path, {});
- }
-
- async getIssue(projectPath, iid) {
- return this.queryCached(
- `issue-${projectPath}-${iid}`,
- issueQuery,
- {
- iid,
- projectPath,
- },
- 2
- );
- }
-
- async getIssues(projectPath, search) {
- const searchById = !!search.match(/^\d+$/);
- return await this.query(issuesQuery, {
- iid: searchById ? search : null,
- searchByIid: searchById,
- fullPath: projectPath,
- in: 'TITLE',
- includeAncestors: true,
- includeDescendants: true,
- searchByText: Boolean(search),
- searchEmpty: !search,
- searchTerm: search,
- types: ['ISSUE'],
- });
- }
-
- async getIssueWithRelatedIssuesLabels(projectPath, iid) {
- return this.queryCached(
- `issue-related-issues-${projectPath}-${iid}`,
- issueWithRelatedIssuesLabelsQuery,
- {
- iid,
- projectPath,
- },
- 0.02
- );
- }
-
- async issueSetEpic(issueId, epicId) {
- return await this.query(issueSetEpicMutation, {
- input: {
- hierarchyWidget: {
- parentId: epicId,
- },
- id: issueId,
- },
- });
- }
-
- async issueSetLabels(input) {
- return await this.query(issueSetLabelsMutation, {
- input,
- });
- }
- }
-
- // apps/gitlab-plus/src/components/create-issue/useCreateIssueForm.ts
- const initialState = () => ({
- assignees: [],
- iteration: null,
- labels: [],
- milestone: null,
- project: null,
- relation: null,
- title: '',
- });
- const initialError = () => ({
- assignees: void 0,
- iteration: void 0,
- labels: void 0,
- milestone: void 0,
- project: void 0,
- relation: void 0,
- title: void 0,
- });
-
- function useCreateIssueForm({ isVisible, link, onClose }) {
- let _a;
- const [values, setValues] = useState(initialState());
- const [errors, setErrors] = useState(initialError());
- const [parentIssue, setParentIssue] = useState(null);
- const [parentEpic, setParentEpic] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [message, setMessage] = useState('');
- const [error, setError] = useState('');
- const reset = () => {
- setIsLoading(false);
- setValues(initialState());
- setErrors(initialError());
- setMessage('');
- setError('');
- setParentIssue(null);
- setParentEpic(null);
- };
- const createPayload = () => {
- const data = {
- projectPath: values.project.fullPath,
- title: values.title,
- };
- if (values.milestone) {
- data['milestoneId'] = values.milestone.id;
- }
- if (values.iteration) {
- data['iterationId'] = values.iteration.id;
- data['iterationCadenceId'] = values.iteration.iterationCadence.id;
- }
- if (values.assignees) {
- data['assigneeIds'] = values.assignees.map((a) => a.id);
- }
- data['labelIds'] = values.labels.map((label) => label.id);
- return data;
- };
- const persistRecently = () => {
- Object.entries({
- assignees: values.assignees,
- iterations: values.iteration ? [values.iteration] : [],
- labels: values.labels,
- milestones: values.milestone ? [values.milestone] : [],
- projects: values.project ? [values.project] : [],
- }).map(([key, values2]) => {
- new RecentlyProvider(key).add(...values2);
- });
- };
- const validate = () => {
- let isValid = true;
- const errors2 = {};
- if (values.title.length < 1) {
- errors2.title = 'Title is required';
- isValid = false;
- } else if (values.title.length > 255) {
- errors2.title = 'Title is too long';
- isValid = false;
- }
- if (!values.project) {
- errors2.project = 'Project must be selected';
- isValid = false;
- }
- setErrors((prev) => ({ ...prev, ...errors2 }));
- return isValid;
- };
- const createIssue = async (payload) => {
- return await new IssueProvider().createIssue(payload);
- };
- const createRelation = async (issue, targetIssue, relation) => {
- await new IssueProvider().createIssueRelation({
- targetIssueIid: targetIssue.iid,
- issueId: issue.iid,
- linkType: relation,
- projectId: issue.projectId,
- targetProjectId: targetIssue.projectId,
- });
- };
- const setIssueEpic = async (issue, epic) => {
- await new IssueProvider().issueSetEpic(issue.id, epic.id);
- };
- const submit = async () => {
- if (!validate()) {
- return;
- }
- setIsLoading(true);
- try {
- setMessage('Creating issue...');
- const payload = createPayload();
- const response = await createIssue(payload);
- persistRecently();
- if (values.relation && parentIssue) {
- setMessage('Creating relation to parent issue...');
- await createRelation(
- response.data.createIssuable.issuable,
- parentIssue,
- values.relation
- );
- }
- if (parentEpic) {
- setMessage('Linking to epic...');
- await setIssueEpic(response.data.createIssuable.issuable, parentEpic);
- }
- setMessage('Issue was created');
- window.setTimeout(() => onClose(), 2e3);
- } catch (e) {
- setMessage('');
- setError(e.message);
- }
- setIsLoading(false);
- };
- const fetchParent = async () => {
- if (LinkParser.isIssueLink(link)) {
- const issue = await new IssueProvider().getIssue(
- link.projectPath,
- link.issue
- );
- setParentIssue(issue.data.project.issue);
- }
- if (LinkParser.isEpicLink(link)) {
- const epic = await new EpicProvider().getEpic(
- link.workspacePath,
- link.epic
- );
- setParentEpic(epic.data.workspace.workItem);
- }
- };
- useEffect(() => {
- if (isVisible) {
- fetchParent();
- } else {
- reset();
- }
- }, [isVisible]);
- return {
- actions: {
- reset,
- submit,
- },
- error,
- form: {
- assignees: {
- errors: errors.assignees,
- onChange: (assignees) => setValues({ ...values, assignees }),
- value: values.assignees,
- },
- iteration: {
- errors: errors.iteration,
- onChange: ([iteration]) =>
- setValues({ ...values, iteration: iteration ?? null }),
- value: values.iteration ? [values.iteration] : [],
- },
- labels: {
- copy: () => {
- if (parentEpic) {
- setValues({
- ...values,
- labels: WidgetHelper.epicLabels(parentEpic),
- });
- }
- if (parentIssue) {
- setValues({ ...values, labels: parentIssue.labels.nodes });
- }
- },
- errors: errors.labels,
- onChange: (labels2) => setValues({ ...values, labels: labels2 }),
- value: values.labels,
- },
- milestone: {
- errors: errors.milestone,
- onChange: ([milestone]) =>
- setValues({ ...values, milestone: milestone ?? null }),
- value: values.milestone ? [values.milestone] : [],
- },
- project: {
- errors: errors.project,
- onChange: ([project]) =>
- setValues({ ...values, project: project ?? null }),
- value: values.project ? [values.project] : [],
- },
- relation: {
- errors: errors.relation,
- onChange: (relation) => setValues({ ...values, relation }),
- value: values.relation,
- },
- title: {
- copy: () => {
- const parentTitle =
- (parentIssue == null ? void 0 : parentIssue.title) ||
- (parentEpic == null ? void 0 : parentEpic.title);
- if (parentTitle) {
- setValues({
- ...values,
- title: parentTitle,
- });
- }
- },
- errors: errors.title,
- onChange: (title) => setValues({ ...values, title }),
- value: values.title,
- },
- },
- isLoading,
- message,
- parentEpic,
- parentIssue,
- projectPath: (_a = values.project) == null ? void 0 : _a.fullPath,
- };
- }
-
- // apps/gitlab-plus/src/components/create-issue/CreateIssueForm.tsx
- function CreateIssueForm({ isVisible, link, onClose }) {
- const {
- actions,
- error,
- form,
- isLoading,
- message,
- parentEpic,
- parentIssue,
- projectPath,
- } = useCreateIssueForm({ isVisible, link, onClose });
- return jsxs('form', {
- class: 'crud-body add-tree-form gl-mx-5 gl-my-4 gl-rounded-b-form',
- children: [
- jsx(FormField, {
- error: form.title.errors,
- hint: 'Maximum of 255 characters',
- title: 'Title',
- children: jsxs('div', {
- className: 'gl-flex gl-gap-1',
- children: [
- jsx(TitleField, {
- error: form.title.errors,
- onChange: form.title.onChange,
- value: form.title.value,
- }),
- jsx(GitlabButton, {
- icon: 'title',
- onClick: form.title.copy,
- title: 'Copy from parent title',
- }),
- ],
- }),
- }),
- jsxs(FormRow, {
- children: [
- jsx(FormField, {
- error: form.project.errors,
- title: 'Project',
- children: jsx(ProjectField, {
- link,
- setValue: form.project.onChange,
- value: form.project.value,
- }),
- }),
- jsx(FormField, {
- error: form.assignees.errors,
- title: 'Assignees',
- children: jsx(AssigneesField, {
- projectPath,
- setValue: form.assignees.onChange,
- value: form.assignees.value,
- }),
- }),
- ],
- }),
- jsxs(FormRow, {
- children: [
- jsx(FormField, {
- error: form.iteration.errors,
- title: 'Iteration',
- children: jsx(IterationField, {
- link,
- setValue: form.iteration.onChange,
- value: form.iteration.value,
- }),
- }),
- jsx(FormField, {
- error: form.milestone.errors,
- title: 'Milestone',
- children: jsx(MilestoneField, {
- projectPath,
- setValue: form.milestone.onChange,
- value: form.milestone.value,
- }),
- }),
- ],
- }),
- jsx(FormField, {
- error: form.labels.errors,
- title: 'Labels',
- children: jsx(LabelField, {
- copyLabels: form.labels.copy,
- projectPath,
- setValue: form.labels.onChange,
- value: form.labels.value,
- }),
- }),
- parentIssue &&
- jsxs(FormField, {
- error: form.relation.errors,
- title: 'New issue',
- children: [
- jsx(RelationField, {
- setValue: form.relation.onChange,
- value: form.relation.value,
- }),
- jsxs(Text, {
- size: 'sm',
- variant: 'secondary',
- children: [
- 'Parent issue: #',
- parentIssue.iid,
- ' ',
- parentIssue.title,
- ],
- }),
- ],
- }),
- parentEpic &&
- jsx(FormField, {
- title: '',
- children: jsxs(Text, {
- size: 'sm',
- variant: 'secondary',
- children: ['Parent epic: &', parentEpic.iid, ' ', parentEpic.title],
- }),
- }),
- jsx(FormField, {
- error,
- hint: message,
- title: '',
- children: jsx(FormRow, {
- children: jsx(ButtonField, {
- create: actions.submit,
- isLoading,
- reset: actions.reset,
- }),
- }),
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/create-issue/events.ts
- const showRelatedIssueModal = 'glp-show-create-issue-modal';
- const showChildIssueModal = 'glp-show-create-child-issue-modal';
- const ShowRelatedIssueModalEvent = new CustomEvent(showRelatedIssueModal);
- const ShowChildIssueModalEvent = new CustomEvent(showChildIssueModal);
-
- // apps/gitlab-plus/src/components/create-issue/CreateChildIssueModal.tsx
- function CreateChildIssueModal({ link }) {
- const { isVisible, onClose } = useGlpModal(showChildIssueModal);
- return jsx(GlpModal, {
- isVisible,
- onClose,
- title: 'Create child issue',
- children: jsx(CreateIssueForm, { isVisible, link, onClose }),
- });
- }
-
- // apps/gitlab-plus/src/services/CreateChildIssue.tsx
- class CreateChildIssue extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.CreateChildIssue);
- __publicField(this, 'isMounted', false);
- }
-
- init() {
- this.mount();
- setTimeout(this.mount.bind(this), 1e3);
- setTimeout(this.mount.bind(this), 3e3);
- }
-
- mount() {
- if (this.isMounted) {
- return;
- }
- const link = LinkParser.parseEpicLink(window.location.href);
- const parent = document.querySelector(
- '#childitems [data-testid="crud-actions"]'
- );
- if (!link || !parent) {
- return;
- }
- this.isMounted = true;
- render(
- jsx(GitlabButton, {
- onClick: () => document.dispatchEvent(ShowChildIssueModalEvent),
- children: 'Create child item',
- }),
- this.root('glp-child-issue-button', parent, true)
- );
- render(
- jsx(CreateChildIssueModal, { link }),
- this.rootBody('glp-child-issue-modal')
- );
- }
- }
-
- // apps/gitlab-plus/src/components/create-issue/CreateRelatedIssueModal.tsx
- function CreateRelatedIssueModal({ link }) {
- const { isVisible, onClose } = useGlpModal(showRelatedIssueModal);
- return jsx(GlpModal, {
- isVisible,
- onClose,
- title: 'Create related issue',
- children: jsx(CreateIssueForm, { isVisible, link, onClose }),
- });
- }
-
- // apps/gitlab-plus/src/services/CreateRelatedIssue.tsx
- class CreateRelatedIssue extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.CreateRelatedIssue);
- __publicField(this, 'isMounted', false);
- }
-
- init() {
- this.mount();
- setTimeout(this.mount.bind(this), 1e3);
- setTimeout(this.mount.bind(this), 3e3);
- }
-
- mount() {
- if (this.isMounted) {
- return;
- }
- const link = LinkParser.parseIssueLink(window.location.href);
- const parent = document.querySelector(
- '#related-issues [data-testid="crud-actions"]'
- );
- if (!link || !parent) {
- return;
- }
- this.isMounted = true;
- render(
- jsx(GitlabButton, {
- onClick: () => document.dispatchEvent(ShowRelatedIssueModalEvent),
- children: 'Create related issue',
- }),
- this.root('glp-related-issue-button', parent)
- );
- render(
- jsx(CreateRelatedIssueModal, { link }),
- this.rootBody('glp-related-issue-modal')
- );
- }
- }
-
- // apps/gitlab-plus/src/components/common/base/Row.tsx
- function Row({ children, className, gap, items, justify }) {
- return jsx('div', {
- class: clsx(
- 'gl-flex gl-flex-row',
- justify && `gl-justify-${justify}`,
- items && `gl-items-${items}`,
- gap && `gl-gap-${gap}`,
- className
- ),
- children,
- });
- }
-
- // libs/share/src/ui/Events.ts
- class Events {
- static intendHover(validate, mouseover, mouseleave, timeout = 500) {
- let hover = false;
- let id = 0;
- const onHover = (event) => {
- if (!event.target || !validate(event.target)) {
- return;
- }
- const element = event.target;
- hover = true;
- element.addEventListener(
- 'mouseleave',
- (ev) => {
- mouseleave == null ? void 0 : mouseleave.call(element, ev);
- clearTimeout(id);
- hover = false;
- },
- { once: true }
- );
- clearTimeout(id);
- id = window.setTimeout(() => {
- if (hover) {
- mouseover.call(element, event);
- }
- }, timeout);
- };
- document.body.addEventListener('mouseover', onHover);
- }
- }
-
- // apps/gitlab-plus/src/components/common/useOnLinkHover.ts
- const modalZIndex = 1e3;
-
- function useOnLinkHover(parser, validator) {
- const [hoverPosition, setHoverPosition] = useState({ x: 0, y: 0 });
- const [hoverLink, setHoverLink] = useState();
- const [zIndex, setZIndex] = useState(modalZIndex);
- const hoverLinkRef = useRef(false);
- const onHover = (event) => {
- const anchor = event.target;
- const link = parser(anchor.href);
- if (!link) {
- return;
- }
- anchor.title = '';
- setHoverLink(link);
- setZIndex(
- anchor.dataset.zIndex ? Number(anchor.dataset.zIndex) : modalZIndex
- );
- setHoverPosition({
- x: event.clientX + 15,
- y: event.clientY,
- });
- };
- useEffect(() => {
- Events.intendHover(
- (element) => validator(element.href),
- onHover,
- () => {
- setTimeout(() => {
- if (!hoverLinkRef.current) {
- setHoverLink(void 0);
- }
- }, 50);
- }
- );
- }, []);
- return {
- hoverLink,
- hoverPosition,
- onLinkEnter: () => (hoverLinkRef.current = true),
- onLinkLeave: () => {
- hoverLinkRef.current = false;
- setHoverLink(void 0);
- },
- zIndex,
- };
- }
-
- // apps/gitlab-plus/src/components/common/usePreviewModal.ts
- function usePreviewModal(link, fetch2, reset, isLoading) {
- const [isVisible, setIsVisible] = useState(false);
- const [offset, setOffset] = useState({ x: 0, y: 0 });
- const ref = useRef(null);
- useEffect(() => {
- if (!isLoading) {
- setTimeout(() => {
- const rect = ref.current.getBoundingClientRect();
- const dY = rect.height + rect.top - window.innerHeight;
- const dX = rect.width + rect.left - window.innerWidth;
- setOffset({
- x: dX > 0 ? dX + 15 : 0,
- y: dY > 0 ? dY + 15 : 0,
- });
- }, 300);
- }
- }, [isLoading]);
- useEffect(() => {
- if (!isVisible) {
- setOffset({ x: 0, y: 0 });
- }
- }, [isVisible]);
- useEffect(() => {
- if (link) {
- fetch2(link);
- setIsVisible(true);
- } else {
- setIsVisible(false);
- reset();
- }
- }, [link]);
- return {
- isVisible,
- offset,
- ref,
- };
- }
-
- // apps/gitlab-plus/src/components/common/PreviewModal.tsx
- function PreviewModal({
- validator,
- children,
- fetch: fetch2,
- isError,
- isLoading = false,
- isRefreshing = false,
- parser,
- reset,
- }) {
- const { hoverLink, hoverPosition, onLinkEnter, onLinkLeave, zIndex } =
- useOnLinkHover(parser, validator);
- const { isVisible, offset, ref } = usePreviewModal(
- hoverLink,
- fetch2,
- reset,
- isLoading
- );
- const content = useMemo(() => {
- if (isLoading || !isVisible) {
- return jsx(Row, {
- className: 'gl-flex-1',
- items: 'center',
- justify: 'center',
- children: jsx(GitlabLoader, { size: '3em' }),
- });
- }
- if (isError) {
- return jsx(Row, {
- className: 'gl-flex-1',
- items: 'center',
- justify: 'center',
- children: 'Error',
- });
- }
- return jsxs('div', {
- className: 'gl-flex gl-w-full gl-flex-col',
- children: [
- children,
- isRefreshing &&
- jsx(Row, {
- className: 'gl-h-full gl-w-full gl-absolute gl-bg-overlay',
- items: 'center',
- justify: 'center',
- children: jsx(GitlabLoader, { size: '3em' }),
- }),
- ],
- });
- }, [isLoading, isRefreshing, isError, isVisible, children]);
- return jsx('div', {
- className: clsx('glp-preview-modal', isVisible && 'glp-modal-visible'),
- onMouseEnter: onLinkEnter,
- onMouseLeave: onLinkLeave,
- ref,
- style: {
- left: hoverPosition.x,
- top: hoverPosition.y,
- transform: `translate(-${offset.x}px, -${offset.y}px )`,
- zIndex,
- },
- children: content,
- });
- }
-
- // apps/gitlab-plus/src/components/common/block/HeadingBlock.tsx
- function HeadingBlock({
- author,
- badge,
- createdAt,
- entityId,
- icon,
- onRefresh,
- title,
- }) {
- return jsxs('div', {
- className: 'glp-block gl-relative',
- children: [
- jsxs(Row, {
- className: '',
- items: 'center',
- justify: 'between',
- children: [
- jsx('span', {
- className: clsx(
- 'gl-font-bold gl-leading-20 gl-text-gray-900',
- onRefresh && 'gl-pr-5'
- ),
- children: title,
- }),
- onRefresh &&
- jsx('div', {
- onClick: onRefresh,
- className:
- 'gl-absolute gl-right-0 gl-top-0 gl-p-2 gl-cursor-pointer',
- children: jsx(GitlabIcon, { icon: 'repeat' }),
- }),
- ],
- }),
- jsxs(Row, {
- className: 'gl-mt-2',
- gap: 2,
- items: 'center',
- children: [
- jsxs(Row, {
- gap: 2,
- items: 'center',
- children: [
- jsx(GitlabIcon, { icon, size: 16 }),
- jsx(Text, {
- size: 'sm',
- variant: 'secondary',
- weight: 'bold',
- children: entityId,
- }),
- ],
- }),
- badge,
- ],
- }),
- jsxs(Row, {
- className: 'gl-mt-1',
- gap: 2,
- items: 'center',
- children: [
- jsx(Text, {
- size: 'sm',
- variant: 'secondary',
- children: 'Created at',
- }),
- jsx(Text, {
- size: 'sm',
- weight: 'bold',
- children: new Date(createdAt).toLocaleDateString(),
- }),
- jsx(Text, { size: 'sm', variant: 'secondary', children: 'by' }),
- jsx(GitlabUser, {
- size: 16,
- user: author,
- smallText: true,
- withLink: true,
- }),
- ],
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/common/GitlabBadge.tsx
- function GitlabBadge({ icon, label, title, variant }) {
- return jsxs('span', {
- className: `gl-badge badge badge-pill badge-${variant}`,
- title,
- children: [
- icon && jsx(GitlabIcon, { icon }),
- label &&
- jsx('span', {
- className: 'gl-badge-content',
- children: label,
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/common/IssueStatus.tsx
- function IssueStatus({ isOpen }) {
- return jsx(GitlabBadge, {
- icon: isOpen ? 'issue-open-m' : 'issue-close',
- label: isOpen ? 'Open' : 'Closed',
- variant: isOpen ? 'success' : 'info',
- });
- }
-
- // apps/gitlab-plus/src/components/epic-preview/blocks/EpicHeading.tsx
- function EpicHeader({ epic, onRefresh }) {
- return jsx(HeadingBlock, {
- author: epic.author,
- badge: jsx(IssueStatus, { isOpen: epic.state === 'OPEN' }),
- createdAt: epic.createdAt,
- entityId: `&${epic.iid}`,
- icon: 'epic',
- onRefresh,
- title: epic.title,
- });
- }
-
- // apps/gitlab-plus/src/components/common/block/InfoBlock.tsx
- function InfoBlock({ children, className, icon, rightTitle, title }) {
- return jsxs('div', {
- class: 'glp-block gl-relative',
- children: [
- jsxs(Row, {
- items: 'center',
- justify: 'between',
- children: [
- jsxs(Row, {
- gap: 2,
- items: 'center',
- children: [
- icon && jsx(GitlabIcon, { icon, size: 16 }),
- jsx('span', {
- className: 'gl-font-bold gl-leading-20 gl-text-gray-900',
- dangerouslySetInnerHTML: { __html: title },
- }),
- ],
- }),
- rightTitle,
- ],
- }),
- jsx('div', { class: className, children }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/common/block/LabelsBlockChangeStatus.tsx
- function LabelsBlockChangeStatus({
- isLoading,
- name: name2,
- onChange,
- options,
- }) {
- if (isLoading) {
- return jsx(GitlabLoader, {});
- }
- const getValues = useCallback(
- async (search) => {
- return options.filter((option) => option.title.includes(search));
- },
- [options]
- );
- const renderOption = useCallback((item) => {
- return jsxs('div', {
- class: 'gl-flex gl-flex-1 gl-break-anywhere gl-pb-3 gl-pl-4 gl-pt-3',
- children: [
- jsx('span', {
- class: 'dropdown-label-box gl-top-0 gl-mr-3 gl-shrink-0',
- style: { backgroundColor: item.color },
- }),
- jsx('span', { children: item.title }),
- ],
- });
- }, []);
- return jsx('div', {
- className: 'gl-py-2',
- style: { width: 130 },
- children: jsx(AsyncAutocomplete, {
- hideCheckbox: true,
- buttonSize: 'sm',
- getValues,
- name: name2,
- onChange: ([label]) => label && onChange(label),
- renderLabel: () => 'Change status',
- renderOption,
- value: [],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/block/useLabelBlock.ts
- const name = 'status-labels';
-
- function useLabelBlock(statusUpdate) {
- const [isLoading, setIsLoading] = useState(false);
- const onSelectStatus = useCallback(async (label) => {
- setIsLoading(true);
- if (statusUpdate) {
- await statusUpdate.update(label);
- new RecentlyProvider(name).add(label);
- }
- setIsLoading(false);
- }, []);
- return {
- isLoading,
- name,
- onSelectStatus,
- showChangeStatusComponent: Boolean(statusUpdate),
- statusLabels: (statusUpdate == null ? void 0 : statusUpdate.labels) || [],
- };
- }
-
- // apps/gitlab-plus/src/components/common/block/LabelsBlock.tsx
- function LabelsBlock({ labels: labels2, updateStatus }) {
- const {
- isLoading,
- name: name2,
- onSelectStatus,
- showChangeStatusComponent,
- statusLabels,
- } = useLabelBlock(updateStatus);
- if (!labels2.length && !updateStatus) {
- return null;
- }
- return jsx(InfoBlock, {
- className: 'issuable-show-labels',
- icon: 'labels',
- title: 'Labels',
- rightTitle:
- showChangeStatusComponent &&
- jsx(LabelsBlockChangeStatus, {
- isLoading,
- name: name2,
- onChange: onSelectStatus,
- options: statusLabels,
- }),
- children: labels2.map((label) => jsx(GitlabLabel, { label }, label.id)),
- });
- }
-
- // apps/gitlab-plus/src/components/epic-preview/blocks/useEpicLabels.ts
- function useEpicLabels(epic, refetch) {
- const [statusLabels, setStatusLabels] = useState([]);
- const labels2 = useMemo(() => {
- const labelWidget = epic.widgets.find((widget) => widget.type === 'LABELS');
- if (labelWidget) {
- return labelWidget.labels.nodes;
- }
- return [];
- }, [epic]);
- const onStatusChange = useCallback(
- async (label) => {
- const oldStatus = labels2.filter((l) => l.title.includes('Status::'));
- await new EpicProvider().updateEpicLabels(
- epic.id,
- [label.id],
- oldStatus.map((l) => l.id)
- );
- if (refetch) {
- await refetch();
- }
- },
- [labels2]
- );
- const fetchLabels = useCallback(async (workspacePath) => {
- const response = await new LabelsProvider().getWorkspaceLabels(
- workspacePath,
- 'Status::'
- );
- setStatusLabels(response.data.workspace.labels.nodes);
- }, []);
- useEffect(() => {
- fetchLabels(epic.namespace.fullPath);
- }, []);
- return {
- labels: labels2,
- updateStatus: {
- labels: statusLabels,
- update: onStatusChange,
- },
- };
- }
-
- // apps/gitlab-plus/src/components/epic-preview/blocks/EpicLabels.tsx
- function EpicLabels({ epic, refresh }) {
- const { labels: labels2, updateStatus } = useEpicLabels(epic, refresh);
- if (!labels2.length) {
- return null;
- }
- return jsx(LabelsBlock, { labels: labels2, updateStatus });
- }
-
- // apps/gitlab-plus/src/components/common/base/Link.tsx
- function Link({ blockHover, children, className, href, inline, title }) {
- const [zIndex, setZIndex] = useState(modalZIndex + 1);
- const ref = useRef(null);
- const onHover = (e) => {
- e.stopPropagation();
- e.preventDefault();
- return false;
- };
- useLayoutEffect(() => {
- let _a;
- const modal =
- (_a = ref.current) == null ? void 0 : _a.closest('.glp-preview-modal');
- setZIndex(
- (modal == null ? void 0 : modal.style.zIndex)
- ? Number(modal.style.zIndex) + 1
- : modalZIndex + 1
- );
- }, []);
- return jsx('a', {
- 'data-z-index': zIndex,
- href,
- onMouseOver: blockHover ? onHover : void 0,
- ref,
- target: '_blank',
- title,
- class: clsx(
- inline ? 'gl-inline' : 'gl-block',
- 'gl-link sortable-link',
- className
- ),
- style: {
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- },
- children,
- });
- }
-
- // apps/gitlab-plus/src/components/epic-preview/blocks/EpicRelatedIssues.tsx
- function EpicRelatedIssues({ epic }) {
- const issues = useMemo(() => {
- const hierarchyWidget = epic.widgets.find(
- (widget) => widget.type === 'HIERARCHY'
- );
- if (!hierarchyWidget) {
- return [];
- }
- return hierarchyWidget.children.nodes;
- }, [epic]);
- if (!issues.length) {
- return null;
- }
- return jsx(InfoBlock, {
- icon: 'issue-type-issue',
- title: `Child issues (${issues.length})`,
- children: issues.map((issue) =>
- jsxs(
- Link,
- {
- href: issue.webUrl,
- title: issue.title,
- children: ['#', issue.iid, ' ', issue.title],
- },
- issue.iid
- )
- ),
- });
- }
-
- // apps/gitlab-plus/src/components/common/useFetchEntity.ts
- function useFetchEntity(fetcher) {
- const [entityData, setEntityData] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [isRefreshing, setIsRefreshing] = useState(false);
- const fetch2 = async (link, force = false) => {
- if (force) {
- setIsRefreshing(true);
- } else {
- setIsLoading(true);
- }
- const entity = await fetcher(link, force);
- setEntityData({ entity, link });
- setIsRefreshing(false);
- setIsLoading(false);
- };
- const reset = () => {
- setEntityData(null);
- setIsRefreshing(false);
- setIsLoading(false);
- };
- return {
- entityData,
- fetch: fetch2,
- isLoading,
- isRefreshing,
- reset,
- };
- }
-
- // apps/gitlab-plus/src/components/epic-preview/useFetchEpic.ts
- function useFetchEpic() {
- return useFetchEntity(async (link, force = false) => {
- const response = await new EpicProvider(force).getEpic(
- link.workspacePath,
- link.epic
- );
- return response.data.workspace.workItem;
- });
- }
-
- // apps/gitlab-plus/src/components/epic-preview/EpicPreviewModal.tsx
- function EpicPreviewModal() {
- const {
- entityData,
- fetch: fetch2,
- isLoading,
- isRefreshing,
- reset,
- } = useFetchEpic();
- return jsx(PreviewModal, {
- validator: LinkParser.validateEpicLink,
- fetch: fetch2,
- isError: !entityData,
- isLoading,
- isRefreshing,
- parser: LinkParser.parseEpicLink,
- reset,
- children:
- entityData &&
- jsxs(Fragment, {
- children: [
- jsx(EpicHeader, {
- epic: entityData.entity,
- onRefresh: () => fetch2(entityData.link, true),
- }),
- jsx(EpicLabels, {
- epic: entityData.entity,
- refresh: () => fetch2(entityData.link, true),
- }),
- jsx(EpicRelatedIssues, { epic: entityData.entity }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/services/EpicPreview.tsx
- class EpicPreview extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.EpicPreview);
- }
-
- init() {
- render(jsx(EpicPreviewModal, {}), this.rootBody('glp-epic-preview-root'));
- }
- }
-
- // apps/gitlab-plus/src/components/image-preview/useImagePreviewModal.ts
- function useImagePreviewModal() {
- const [src, setSrc] = useState('');
- const validate = (element) => {
- return (
- element.classList.contains('no-attachment-icon') &&
- /\.(png|jpg|jpeg|heic)$/.test(element.href.toLowerCase())
- );
- };
- const getAnchor = (element) => {
- if (!element) {
- return void 0;
- }
- if (element instanceof HTMLAnchorElement) {
- return validate(element) ? element : void 0;
- }
- if (
- element instanceof HTMLImageElement &&
- element.parentElement instanceof HTMLAnchorElement
- ) {
- return validate(element.parentElement) ? element.parentElement : void 0;
- }
- return void 0;
- };
- useEffect(() => {
- document.body.addEventListener('click', (ev) => {
- const anchor = getAnchor(ev.target);
- if (anchor) {
- setSrc(anchor.href);
- ev.preventDefault();
- ev.stopPropagation();
- return false;
- }
- });
- }, []);
- return {
- onClose: () => setSrc(''),
- src,
- };
- }
-
- // apps/gitlab-plus/src/components/image-preview/ImagePreviewModal.tsx
- function ImagePreviewModal() {
- const { onClose, src } = useImagePreviewModal();
- return jsxs('div', {
- className: clsx(
- 'glp-image-preview-modal',
- Boolean(src) && 'glp-modal-visible'
- ),
- children: [
- jsx('img', { alt: 'Image preview', className: 'glp-modal-img', src }),
- jsx('div', {
- className: 'glp-modal-close',
- onClick: onClose,
- children: jsx(GitlabIcon, { icon: 'close-xs', size: 24 }),
- }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/services/ImagePreview.tsx
- class ImagePreview extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.ImagePreview);
- }
-
- init() {
- render(jsx(ImagePreviewModal, {}), this.rootBody('glp-image-preview-root'));
- }
- }
-
- // apps/gitlab-plus/src/components/common/block/UsersBlock.tsx
- function UsersBlock({ assignees, icon, label, pluralIcon, pluralLabel }) {
- if (!assignees || !assignees.length) {
- return null;
- }
- if (assignees.length === 1) {
- return jsx(InfoBlock, {
- className: 'gl-flex gl-flex-col gl-gap-3',
- icon: icon || 'user',
- rightTitle: jsx(GitlabUser, { user: assignees[0], withLink: true }),
- title: `${label}:`,
- });
- }
- return jsx(InfoBlock, {
- className: 'gl-flex gl-flex-col gl-gap-3',
- icon: pluralIcon || icon || 'users',
- title: pluralLabel || `${label}s`,
- children: assignees.map((assignee) =>
- jsx(GitlabUser, { user: assignee, withLink: true }, assignee.id)
- ),
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/IssueAssignee.tsx
- function IssueAssignee({ issue }) {
- return jsx(UsersBlock, {
- assignees: issue.assignees.nodes,
- icon: 'assignee',
- label: 'Assignee',
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/IssueEpic.tsx
- function IssueEpic({ issue }) {
- if (!issue.epic) {
- return null;
- }
- return jsx(InfoBlock, {
- icon: 'epic',
- title: 'Epic',
- children: jsx(Link, {
- href: issue.epic.webUrl,
- title: issue.epic.title,
- children: issue.epic.title,
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/IssueHeading.tsx
- function IssueHeader({ issue, onRefresh }) {
- return jsx(HeadingBlock, {
- author: issue.author,
- badge: jsx(IssueStatus, { isOpen: issue.state === 'opened' }),
- createdAt: issue.createdAt,
- entityId: `#${issue.iid}`,
- icon: 'issue-type-issue',
- onRefresh,
- title: issue.title,
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/IssueIteration.tsx
- function IssueIteration({ issue }) {
- const label = useMemo(() => {
- let _a;
- const date = (date2) => {
- return new Intl.DateTimeFormat('en-US', {
- day: 'numeric',
- month: 'short',
- }).format(new Date(date2));
- };
- if (!issue.iteration) {
- return '';
- }
- return [
- (_a = issue.iteration.iterationCadence) == null ? void 0 : _a.title,
- ': ',
- date(issue.iteration.startDate),
- ' - ',
- date(issue.iteration.dueDate),
- ].join('');
- }, [issue]);
- if (!issue.iteration) {
- return null;
- }
- return jsx(InfoBlock, {
- icon: 'iteration',
- rightTitle: label,
- title: 'Iteration',
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/useIssueLabels.ts
- function useIssueLabels(issue, link, refetch) {
- const [statusLabels, setStatusLabels] = useState([]);
- const onStatusChange = useCallback(
- async (label) => {
- const statusLabel = issue.labels.nodes.find((l) =>
- l.title.includes('Status::')
- );
- const labels2 = statusLabel
- ? issue.labels.nodes.map((l) => (l.id === statusLabel.id ? label : l))
- : [...issue.labels.nodes, label];
- await new IssueProvider().issueSetLabels({
- iid: issue.iid,
- labelIds: labels2.map((l) => l.id),
- projectPath: link.projectPath,
- });
- if (refetch) {
- await refetch();
- }
- },
- [issue]
- );
- const fetchLabels = useCallback(async (projectPath) => {
- const response = await new LabelsProvider().getProjectLabels(
- projectPath,
- 'Status::'
- );
- setStatusLabels(response.data.workspace.labels.nodes);
- }, []);
- useEffect(() => {
- fetchLabels(link.projectPath);
- }, []);
- return {
- labels: issue.labels.nodes,
- updateStatus: {
- labels: statusLabels,
- update: onStatusChange,
- },
- };
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/IssueLabels.tsx
- function IssueLabels({ issue, link, refetch }) {
- const { labels: labels2, updateStatus } = useIssueLabels(
- issue,
- link,
- refetch
- );
- if (!labels2.length) {
- return null;
- }
- return jsx(LabelsBlock, { labels: labels2, updateStatus });
- }
-
- // apps/gitlab-plus/src/components/common/MrStatus.tsx
- const iconMap = {
- closed: 'merge-request-close',
- locked: 'search',
- merged: 'merge',
- opened: 'merge-request',
- };
- const classMap = {
- closed: 'danger',
- locked: 'warning',
- merged: 'info',
- opened: 'success',
- };
- const labelMap = {
- closed: 'Closed',
- locked: 'Locked',
- merged: 'Merged',
- opened: 'Opened',
- };
-
- function MrStatus({ state, withIcon, withLabel }) {
- return jsx(GitlabBadge, {
- icon: withIcon ? iconMap[state] : void 0,
- label: withLabel ? labelMap[state] : void 0,
- variant: classMap[state],
- });
- }
-
- // apps/gitlab-plus/src/components/common/GitlabMergeRequest.tsx
- function GitlabMergeRequest({ mr }) {
- return jsxs('div', {
- style: { marginTop: 10 },
- children: [
- jsxs(Row, {
- gap: 2,
- children: [
- jsx(MrStatus, { state: mr.state, withIcon: true, withLabel: true }),
- jsxs(Text, {
- variant: 'secondary',
- children: ['!', mr.iid],
- }),
- jsx(GitlabUser, { size: 16, user: mr.author, withLink: true }),
- ],
- }),
- jsx(Link, { href: mr.webUrl, title: mr.title, children: mr.title }),
- ],
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/IssueMergeRequests.tsx
- function IssueMergeRequests({ issue }) {
- if (!issue.relatedMergeRequests.nodes.length) {
- return null;
- }
- return jsx(InfoBlock, {
- icon: 'merge-request',
- title: 'Merge requests',
- children: issue.relatedMergeRequests.nodes.map((mr) =>
- jsx(GitlabMergeRequest, { mr }, mr.iid)
- ),
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/IssueMilestone.tsx
- function IssueMilestone({ issue }) {
- if (!issue.milestone) {
- return null;
- }
- return jsx(InfoBlock, {
- icon: 'milestone',
- rightTitle: issue.milestone.title,
- title: 'Milestone',
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/blocks/IssueRelatedIssue.tsx
- const relationMap = {
- blocks: 'Blocks:',
- is_blocked_by: 'Is blocked by:',
- relates_to: 'Related to:',
- };
-
- function IssueRelatedIssue({ issue }) {
- const groups = useMemo(() => {
- const initValue = {
- blocks: [],
- is_blocked_by: [],
- relates_to: [],
- };
- return Object.entries(
- issue.linkedWorkItems.nodes.reduce(
- (acc, issue2) => ({
- ...acc,
- [issue2.linkType]: [...acc[issue2.linkType], issue2],
- }),
- initValue
- )
- ).filter(([_, issues]) => issues.length);
- }, [issue]);
- if (!issue.linkedWorkItems.nodes.length) {
- return null;
- }
- return jsx(InfoBlock, {
- title: '',
- children: groups.map(([key, issues]) =>
- jsxs(
- 'div',
- {
- style: { marginTop: 10 },
- children: [
- jsx('div', {
- class: 'item-title gl-flex gl-min-w-0 gl-gap-3',
- children: jsx('span', { children: relationMap[key] }),
- }),
- issues.map((issue2) =>
- jsxs(
- Link,
- {
- href: issue2.workItem.webUrl,
- blockHover: true,
- children: [
- '#',
- issue2.workItem.iid,
- ' ',
- issue2.workItem.title,
- ],
- },
- issue2.workItem.iid
- )
- ),
- ],
- },
- key
- )
- ),
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/useFetchIssue.ts
- function useFetchIssue() {
- return useFetchEntity(async (link, force = false) => {
- const response = await new IssueProvider(force).getIssue(
- link.projectPath,
- link.issue
- );
- console.log(response);
- return response.data.project.issue;
- });
- }
-
- // apps/gitlab-plus/src/components/issue-preview/IssuePreviewModal.tsx
- function IssuePreviewModal() {
- const {
- entityData,
- fetch: fetch2,
- isLoading,
- isRefreshing,
- reset,
- } = useFetchIssue();
- return jsx(PreviewModal, {
- validator: LinkParser.validateIssueLink,
- fetch: fetch2,
- isError: !entityData,
- isLoading,
- isRefreshing,
- parser: LinkParser.parseIssueLink,
- reset,
- children:
- entityData &&
- jsxs(Fragment, {
- children: [
- jsx(IssueHeader, {
- issue: entityData.entity,
- onRefresh: () => fetch2(entityData.link, true),
- }),
- jsx(IssueAssignee, { issue: entityData.entity }),
- jsx(IssueLabels, {
- issue: entityData.entity,
- link: entityData.link,
- refetch: () => fetch2(entityData.link, true),
- }),
- jsx(IssueEpic, { issue: entityData.entity }),
- jsx(IssueMilestone, { issue: entityData.entity }),
- jsx(IssueIteration, { issue: entityData.entity }),
- jsx(IssueMergeRequests, { issue: entityData.entity }),
- jsx(IssueRelatedIssue, { issue: entityData.entity }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/services/IssuePreview.tsx
- class IssuePreview extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.IssuePreview);
- }
-
- init() {
- render(jsx(IssuePreviewModal, {}), this.rootBody('glp-issue-preview-root'));
- }
- }
-
- // apps/gitlab-plus/src/components/mr-preview/blocks/MrApprovedBy.tsx
- function MrApprovedBy({ mr }) {
- return jsx(UsersBlock, {
- assignees: mr.approvedBy.nodes,
- label: 'Approved by',
- pluralLabel: 'Approved by',
- });
- }
-
- // apps/gitlab-plus/src/components/mr-preview/blocks/MrAssignee.tsx
- function MrAssignee({ mr }) {
- return jsx(UsersBlock, {
- assignees: mr.assignees.nodes,
- icon: 'assignee',
- label: 'Assignee',
- });
- }
-
- // apps/gitlab-plus/src/components/mr-preview/blocks/MrBranch.tsx
- function MrBranch({ mr }) {
- return jsx(InfoBlock, {
- icon: 'branch',
- title: 'Merge',
- children: jsxs('span', {
- children: [
- jsx(Text, { children: mr.sourceBranch }),
- jsx(Text, {
- className: 'gl-mx-2',
- variant: 'secondary',
- children: 'in to',
- }),
- jsx(Text, { children: mr.targetBranch }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/mr-preview/blocks/MrDiff.tsx
- function MrDiff({ mr }) {
- const label = useMemo(() => {
- if (mr.diffStatsSummary.fileCount === 1) {
- return '1 file';
- }
- return `${mr.diffStatsSummary.fileCount} files`;
- }, [mr.diffStatsSummary.fileCount]);
- return jsx(InfoBlock, {
- icon: 'commit',
- title: `Commit: ${mr.commitCount}`,
- rightTitle: jsxs(Row, {
- gap: 2,
- items: 'center',
- children: [
- jsx(GitlabIcon, { icon: 'doc-code', size: 16 }),
- jsx(Text, {
- size: 'subtle',
- weight: 'bold',
- children: label,
- }),
- jsxs(Text, {
- color: 'success',
- weight: 'bold',
- children: ['+', mr.diffStatsSummary.additions],
- }),
- jsxs(Text, {
- color: 'danger',
- weight: 'bold',
- children: ['-', mr.diffStatsSummary.deletions],
- }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/mr-preview/blocks/MrDiscussion.tsx
- function MrDiscussion({ mr }) {
- const [resolved, total] = [
- mr.resolvedDiscussionsCount,
- mr.resolvableDiscussionsCount,
- ];
- if (!total) {
- return null;
- }
- const { label, title } = useMemo(() => {
- const plural = total !== 1 ? 's' : '';
- return {
- label: `${resolved} of ${total}`,
- title: `${resolved} of ${total} thread${plural} resolved`,
- };
- }, [mr]);
- return jsx(InfoBlock, {
- title: 'Discussion',
- rightTitle: jsx(GitlabBadge, {
- icon: 'comments',
- label,
- title,
- variant: resolved === total ? 'success' : 'muted',
- }),
- });
- }
-
- // libs/share/src/utils/textWithChild.ts
- function textWithChild(text, pattern, replacer) {
- const matches = text.match(RegExp(pattern, 'g'));
- const parts = text.split(RegExp(pattern, 'g'));
- if (!(matches == null ? void 0 : matches.length)) {
- return text;
- }
- return parts.reduce((items, text2, index) => {
- const textToReplace = index < matches.length ? matches[index] : void 0;
- return [
- ...items,
- text2,
- ...(textToReplace ? [replacer(textToReplace)] : []),
- ];
- }, []);
- }
-
- // apps/gitlab-plus/src/components/mr-preview/blocks/MrHeading.tsx
- function MrHeader({ mr, onRefresh }) {
- const title = useMemo(() => {
- const issueLink = (id) =>
- `${mr.project.webUrl}/-/issues/${id.replace(/\D+/g, '')}`;
- return textWithChild(mr.title, /#\d+/, (id) =>
- jsx(Link, { href: issueLink(id), inline: true, children: id })
- );
- }, [mr]);
- return jsx(HeadingBlock, {
- author: mr.author,
- createdAt: mr.createdAt,
- entityId: `!${mr.iid}`,
- icon: 'merge-request',
- onRefresh,
- title,
- badge: jsxs(Row, {
- className: 'gl-gap-2',
- items: 'center',
- children: [
- jsx(MrStatus, {
- state: mr.state,
- withIcon: true,
- withLabel: true,
- }),
- Boolean(mr.approvedBy.nodes.length) &&
- jsx(GitlabBadge, {
- icon: 'check-circle',
- label: 'Approved',
- variant: 'success',
- }),
- mr.conflicts &&
- jsx(GitlabIcon, {
- icon: 'warning-solid',
- size: 16,
- title: 'Merge request can not be merged',
- }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/mr-preview/blocks/MrLabels.tsx
- function MrLabels({ mr }) {
- if (!mr.labels.nodes.length) {
- return null;
- }
- return jsx(InfoBlock, {
- className: 'issuable-show-labels',
- title: 'Labels',
- children: mr.labels.nodes.map((label) =>
- jsx(GitlabLabel, { label }, label.id)
- ),
- });
- }
-
- // apps/gitlab-plus/src/providers/query/mr.ts
- const mrQuery = `query MergeRequestQuery($fullPath: ID!, $iid: String!) {
- workspace: project(fullPath: $fullPath) {
- mergeRequest(iid: $iid) {
- id
- iid
- assignees {
- nodes {
- ...User
- }
- }
- approvedBy {
- nodes {
- ...User
- }
- }
- author {
- ...User
- }
- project {
- webUrl
- path
- fullPath
- }
- commitCount
- conflicts
- createdAt
- title
- titleHtml
- diffStatsSummary {
- additions
- changes
- deletions
- fileCount
- }
- draft
- labels {
- nodes {
- ...Label
- }
- }
- mergeable
- resolvedDiscussionsCount
- resolvableDiscussionsCount
- reviewers {
- nodes {
- ...User
- }
- }
- shouldBeRebased
- sourceBranch
- targetBranch
- state
- webUrl
- }
- }
- }
-
- ${userFragment}
- ${labelFragment}
- `;
-
- // apps/gitlab-plus/src/providers/MrProvider.ts
- class MrProvider extends GitlabProvider {
- async getMr(projectPath, mrId) {
- return this.queryCached(
- `mr-${projectPath}-${mrId}`,
- mrQuery,
- {
- iid: mrId,
- fullPath: projectPath,
- },
- 2
- );
- }
- }
-
- // apps/gitlab-plus/src/components/mr-preview/useFetchMr.ts
- function useFetchMr() {
- return useFetchEntity(async (link, force = false) => {
- const response = await new MrProvider(force).getMr(
- link.projectPath,
- link.mr
- );
- return response.data.workspace.mergeRequest;
- });
- }
-
- // apps/gitlab-plus/src/components/mr-preview/MrPreviewModal.tsx
- function MrPreviewModal() {
- const {
- entityData,
- fetch: fetch2,
- isLoading,
- isRefreshing,
- reset,
- } = useFetchMr();
- return jsx(PreviewModal, {
- validator: LinkParser.validateMrLink,
- fetch: fetch2,
- isError: !entityData,
- isLoading,
- isRefreshing,
- parser: LinkParser.parseMrLink,
- reset,
- children:
- entityData &&
- jsxs(Fragment, {
- children: [
- jsx(MrHeader, {
- mr: entityData.entity,
- onRefresh: () => fetch2(entityData.link, true),
- }),
- jsx(MrBranch, { mr: entityData.entity }),
- jsx(MrAssignee, { mr: entityData.entity }),
- jsx(MrApprovedBy, { mr: entityData.entity }),
- jsx(MrLabels, { mr: entityData.entity }),
- jsx(MrDiff, { mr: entityData.entity }),
- jsx(MrDiscussion, { mr: entityData.entity }),
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/services/MrPreview.tsx
- class MrPreview extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.MrPreview);
- }
-
- init() {
- render(jsx(MrPreviewModal, {}), this.rootBody('glp-mr-preview-root'));
- }
- }
-
- // apps/gitlab-plus/src/components/related-issue-autocomplete/useRelatedIssuesAutocompleteModal.ts
- function useRelatedIssuesAutocompleteModal(link, input) {
- const [searchTerm, setSearchTerm] = useState('');
- const [isVisible, setIsVisible] = useState(false);
- const searchIssues = useCallback(async (term) => {
- const response = await new IssueProvider().getIssues(
- link.workspacePath,
- term
- );
- return [
- response.data.workspace.workItems,
- response.data.workspace.workItemsByIid,
- response.data.workspace.workItemsEmpty,
- ].flatMap((item) => (item == null ? void 0 : item.nodes) || []);
- }, []);
- const options = useAsyncAutocompleteOptions(searchTerm, searchIssues);
- const onSelect = (item) => {
- input.value = `${item.project.fullPath}#${item.iid} `;
- input.dispatchEvent(new Event('input'));
- input.dispatchEvent(new Event('change'));
- };
- useEffect(() => {
- document.body.addEventListener('click', (e) => {
- if (e.target !== input && !input.contains(e.target)) {
- setIsVisible(false);
- }
- });
- input.addEventListener('click', () => setIsVisible(true));
- }, []);
- return {
- isVisible,
- onClose: () => setIsVisible(false),
- onSelect,
- options,
- searchTerm,
- setSearchTerm,
- };
- }
-
- // apps/gitlab-plus/src/components/related-issue-autocomplete/RelatedIssuesAutocompleteModal.tsx
- function RelatedIssuesAutocompleteModal({ input, link }) {
- const { isVisible, onClose, onSelect, options, searchTerm, setSearchTerm } =
- useRelatedIssuesAutocompleteModal(link, input);
- if (!isVisible) {
- return null;
- }
- return jsx('div', {
- class: 'gl-relative gl-w-full gl-new-dropdown !gl-block',
- children: jsx(AsyncAutocompleteDropdown, {
- hideCheckbox: true,
- onClick: onSelect,
- onClose,
- options,
- searchTerm,
- setSearchTerm,
- value: [],
- renderOption: (item) =>
- jsxs('div', {
- class: 'gl-flex gl-gap-x-2 gl-py-2',
- children: [
- jsx(GitlabIcon, {
- icon: 'issue-type-issue',
- size: 16,
- }),
- jsx('small', { children: item.iid }),
- jsx('span', {
- class: 'gl-flex gl-flex-wrap',
- children: item.title,
- }),
- ],
- }),
- }),
- });
- }
-
- // apps/gitlab-plus/src/services/RelatedIssueAutocomplete.tsx
- class RelatedIssueAutocomplete extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.RelatedIssueAutocomplete);
- __publicField(this, 'ready', false);
- __publicField(this, 'readyClass', 'glp-input-ready');
- }
-
- init() {
- this.initObserver();
- window.setTimeout(this.initObserver.bind(this), 1e3);
- window.setTimeout(this.initObserver.bind(this), 3e3);
- window.setTimeout(this.initObserver.bind(this), 5e3);
- }
-
- initAutocomplete(section) {
- const input = section.querySelector('#add-related-issues-form-input');
- const link = LinkParser.parseIssueLink(window.location.href);
- if (!input || this.isMounted(input) || !link) {
- return;
- }
- const container = input.closest('.add-issuable-form-input-wrapper');
- if (!container || document.querySelector('.related-issues-autocomplete')) {
- return;
- }
- const root = this.root('related-issues-autocomplete', container);
- render(jsx(RelatedIssuesAutocompleteModal, { input, link }), root);
- }
-
- initObserver() {
- const section = document.querySelector('#related-issues');
- if (this.ready || !section) {
- return;
- }
- this.ready = true;
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.type === 'childList') {
- this.initAutocomplete(section);
- }
- });
- });
- observer.observe(section, {
- childList: true,
- });
- }
-
- isMounted(input) {
- return input.classList.contains(this.readyClass);
- }
- }
-
- // apps/gitlab-plus/src/services/RelatedIssuesLabelStatus.tsx
- class RelatedIssuesLabelStatus extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.RelatedIssuesLabelStatus);
- __publicField(this, 'ready', false);
- }
-
- init() {
- this.initIssuesList();
- window.setTimeout(this.initIssuesList.bind(this), 1e3);
- window.setTimeout(this.initIssuesList.bind(this), 3e3);
- window.setTimeout(this.initIssuesList.bind(this), 5e3);
- }
-
- initIssuesList() {
- if (this.ready) {
- return;
- }
- const lists = document.querySelectorAll(
- '#related-issues .related-items-list'
- );
- const link = LinkParser.parseIssueLink(window.location.href);
- if (!lists.length || !link) {
- return;
- }
- this.ready = true;
- const items = [...lists].flatMap((list) => [
- ...list.querySelectorAll('li'),
- ]);
- this.updateIssuesItem(link, items);
- }
-
- async updateIssuesItem(link, items) {
- const response = await new IssueProvider().getIssueWithRelatedIssuesLabels(
- link.projectPath,
- link.issue
- );
- const getStatusLabel = (item) => {
- const labelsWidget = item.workItem.widgets.find(
- (w) => w.type === 'LABELS'
- );
- return labelsWidget == null
- ? void 0
- : labelsWidget.labels.nodes.find(
- (l) =>
- l.title.toLowerCase().startsWith('status::') ||
- l.title.toLowerCase().startsWith('workflow::')
- );
- };
- const issueStatusMap =
- response.data.project.issue.linkedWorkItems.nodes.reduce((acc, value) => {
- return {
- ...acc,
- [value.workItem.id.replace(/\D/g, '')]: getStatusLabel(value),
- };
- }, {});
- items.forEach((item) => {
- if (!item.dataset.key || !issueStatusMap[item.dataset.key]) {
- return;
- }
- const statusLabel = issueStatusMap[item.dataset.key];
- const infoArea = item.querySelector('.item-attributes-area');
- if (infoArea && statusLabel) {
- render(
- jsx(GitlabLabel, { label: statusLabel }),
- this.root('glp-status-label', infoArea, true)
- );
- }
- });
- }
- }
-
- // libs/share/src/ui/Component.ts
- class Component {
- constructor(tag, props = {}) {
- this.element = Dom.create({ tag, ...props });
- }
-
- addClassName(...className) {
- this.element.classList.add(...className);
- }
-
- event(event, callback) {
- this.element.addEventListener(event, callback);
- }
-
- getElement() {
- return this.element;
- }
-
- mount(parent) {
- parent.appendChild(this.element);
- }
- }
-
- // libs/share/src/ui/SvgComponent.ts
- class SvgComponent {
- constructor(tag, props = {}) {
- this.element = Dom.createSvg({ tag, ...props });
- }
-
- addClassName(...className) {
- this.element.classList.add(...className);
- }
-
- event(event, callback) {
- this.element.addEventListener(event, callback);
- }
-
- getElement() {
- return this.element;
- }
-
- mount(parent) {
- parent.appendChild(this.element);
- }
- }
-
- // libs/share/src/ui/Dom.ts
- class Dom {
- static appendChildren(element, children, isSvgMode = false) {
- if (children) {
- element.append(
- ...Dom.array(children).map((item) => {
- if (typeof item === 'string') {
- return document.createTextNode(item);
- }
- if (item instanceof HTMLElement || item instanceof SVGElement) {
- return item;
- }
- if (item instanceof Component || item instanceof SvgComponent) {
- return item.getElement();
- }
- const isSvg =
- 'svg' === item.tag
- ? true
- : 'foreignObject' === item.tag
- ? false
- : isSvgMode;
- if (isSvg) {
- return Dom.createSvg(item);
- }
- return Dom.create(item);
- })
- );
- }
- }
-
- static applyAttrs(element, attrs) {
- if (attrs) {
- Object.entries(attrs).forEach(([key, value]) => {
- if (value === void 0 || value === false) {
- element.removeAttribute(key);
- } else {
- element.setAttribute(key, `${value}`);
- }
- });
- }
- }
-
- static applyClass(element, classes) {
- if (classes) {
- element.classList.add(...classes.split(' ').filter(Boolean));
- }
- }
-
- static applyEvents(element, events) {
- if (events) {
- Object.entries(events).forEach(([name2, callback]) => {
- element.addEventListener(name2, callback);
- });
- }
- }
-
- static applyStyles(element, styles) {
- if (styles) {
- Object.entries(styles).forEach(([key, value]) => {
- const name2 = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
- element.style.setProperty(name2, value);
- });
- }
- }
-
- static array(element) {
- return Array.isArray(element) ? element : [element];
- }
-
- static create(data) {
- const element = document.createElement(data.tag);
- Dom.appendChildren(element, data.children);
- Dom.applyClass(element, data.classes);
- Dom.applyAttrs(element, data.attrs);
- Dom.applyEvents(element, data.events);
- Dom.applyStyles(element, data.styles);
- return element;
- }
-
- static createSvg(data) {
- const element = document.createElementNS(
- 'http://www.w3.org/2000/svg',
- data.tag
- );
- Dom.appendChildren(element, data.children, true);
- Dom.applyClass(element, data.classes);
- Dom.applyAttrs(element, data.attrs);
- Dom.applyEvents(element, data.events);
- Dom.applyStyles(element, data.styles);
- return element;
- }
-
- static element(tag, classes, children) {
- return Dom.create({ tag, children, classes });
- }
-
- static elementSvg(tag, classes, children) {
- return Dom.createSvg({ tag, children, classes });
- }
- }
-
- // libs/share/src/ui/Observer.ts
- class Observer {
- start(element, callback, options) {
- this.stop();
- this.observer = new MutationObserver(callback);
- this.observer.observe(
- element,
- options || {
- attributeOldValue: true,
- attributes: true,
- characterData: true,
- characterDataOldValue: true,
- childList: true,
- subtree: true,
- }
- );
- }
-
- stop() {
- if (this.observer) {
- this.observer.disconnect();
- }
- }
- }
-
- // apps/gitlab-plus/src/services/SortIssue.ts
- const sortWeight = {
- ['issue']: 4,
- ['label']: 0,
- ['ownIssue']: 10,
- ['ownUserStory']: 8,
- ['unknown']: 2,
- ['userStory']: 6,
- };
-
- class SortIssue extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.SortIssue);
- }
-
- init() {
- const observer = new Observer();
- const userName = this.userName();
- const board = document.querySelector('.boards-list');
- if (!userName || !board) {
- return;
- }
- observer.start(board, () => this.run(userName));
- }
-
- childType(child, userName) {
- if (child instanceof HTMLDivElement) {
- return 'label';
- }
- const title = child.querySelector('[data-testid="board-card-title-link"]');
- if (!title) {
- return 'unknown';
- }
- const isOwn = [...child.querySelectorAll('.gl-avatar-link img')].some(
- (img) => img.alt.includes(userName)
- );
- const isUserStory = [...child.querySelectorAll('.gl-label')].some((span) =>
- span.innerText.includes('User Story')
- );
- if (isUserStory && isOwn) {
- return 'ownUserStory';
- }
- if (isOwn) {
- return 'ownIssue';
- }
- if (isUserStory) {
- return 'userStory';
- }
- return 'issue';
- }
-
- initBoard(board, userName) {
- Dom.applyClass(board, 'glp-ready');
- const observer = new Observer();
- observer.start(board, () => this.sortBoard(board, userName), {
- childList: true,
- });
- }
-
- run(userName) {
- [...document.querySelectorAll('.board-list:not(.glp-ready)')].forEach(
- (board) => this.initBoard(board, userName)
- );
- }
-
- shouldSort(items) {
- return items.some((item) => {
- return ['ownIssue', 'ownUserStory'].includes(item.type);
- });
- }
-
- sortBoard(board, userName) {
- Dom.applyStyles(board, {
- display: 'flex',
- flexDirection: 'column',
- });
- const children = [...board.children].map((element) => ({
- element,
- type: this.childType(element, userName),
- }));
- if (!this.shouldSort(children)) {
- return;
- }
- this.sortChildren(children).forEach(({ element }, index) => {
- const order =
- index !== children.length - 1 ? index + 1 : children.length + 100;
- element.style.order = `${order}`;
- });
- }
-
- sortChildren(items) {
- return items.toSorted((a, b) => {
- return Math.sign(sortWeight[b.type] - sortWeight[a.type]);
- });
- }
-
- userName() {
- const element = document.querySelector(
- '.user-bar-dropdown-toggle .gl-button-text .gl-sr-only'
- );
- const testText = ' user’s menu';
- if (element && element.innerText.includes(testText)) {
- return element.innerText.replace(testText, '');
- }
- return void 0;
- }
- }
-
- // apps/gitlab-plus/src/components/user-settings/events.ts
- const showUserSettingsModal = 'glp-show-user-settings-modal';
- const ShowUserSettingsModalEvent = new CustomEvent(showUserSettingsModal);
-
- // apps/gitlab-plus/src/components/user-settings/UserSettingsButton.tsx
- function UserSettingsButton() {
- return jsx('span', {
- className: 'gl-new-dropdown-item-content',
- onClick: () => document.dispatchEvent(ShowUserSettingsModalEvent),
- children: jsxs('span', {
- className: 'gl-new-dropdown-item-text-wrapper',
- children: [
- jsx('span', { style: { color: '#e24329' }, children: 'Gitlab Plus' }),
- ' settings',
- ],
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/common/base/Column.tsx
- function Column({ children, className, gap, items, justify }) {
- return jsx('div', {
- class: clsx(
- 'gl-flex gl-flex-col',
- justify && `gl-justify-${justify}`,
- items && `gl-items-${items}`,
- gap && `gl-gap-${gap}`,
- className
- ),
- children,
- });
- }
-
- // apps/gitlab-plus/src/components/common/GitlabSwitch.tsx
- function GitlabSwitch({ checked, disabled, onChange }) {
- return jsx('button', {
- 'aria-checked': checked,
- 'aria-disabled': disabled,
- disabled,
- onClick: () => onChange(!checked),
- role: 'switch',
- type: 'button',
- className: clsx(
- 'gl-toggle gl-shrink-0',
- checked && 'is-checked',
- disabled && 'is-disabled'
- ),
- children: jsx('span', {
- className: 'toggle-icon',
- children: jsx(GitlabIcon, { icon: checked ? 'check-xs' : 'close-xs' }),
- }),
- });
- }
-
- // apps/gitlab-plus/src/components/user-settings/useUserSettingsModal.tsx
- function useUserSettingsModal() {
- const [refreshFlag, setRefreshFlag] = useState(false);
- const services = useMemo(() => {
- return Object.entries(servicesConfig)
- .map(([name2, config]) => ({
- isActive: Boolean(userSettingsStore.isActive(name2)),
- isExperimental: config.experimental,
- isRequired: config.required,
- label: config.label,
- name: name2,
- }))
- .sort((a, b) => {
- if (a.isRequired || b.isRequired) {
- return a.isRequired ? 1 : -1;
- }
- if (a.isExperimental || b.isExperimental) {
- return a.isExperimental ? 1 : -1;
- }
- return a.name.localeCompare(b.name);
- });
- }, [refreshFlag]);
- return {
- services,
- setServiceState: (name2, value) => {
- userSettingsStore.setIsActive(name2, value);
- setRefreshFlag((flag) => !flag);
- },
- };
- }
-
- // apps/gitlab-plus/src/components/user-settings/UserSettingsModal.tsx
- function UserSettingModal() {
- const { isVisible, onClose } = useGlpModal(showUserSettingsModal);
- const { services, setServiceState } = useUserSettingsModal();
- return jsx(GlpModal, {
- isVisible,
- onClose,
- title: jsxs(Fragment, {
- children: [
- jsx('span', { style: { color: '#e24329' }, children: 'Gitlab Plus' }),
- ' settings',
- ],
- }),
- children: jsx(Column, {
- className: 'gl-p-4',
- gap: 2,
- children: services.map((service) =>
- jsxs(Row, {
- gap: 2,
- items: 'center',
- children: [
- jsx(GitlabSwitch, {
- checked: service.isActive,
- disabled: service.isRequired,
- onChange: (value) => setServiceState(service.name, value),
- }),
- jsx(Text, {
- variant: service.isRequired ? 'secondary' : void 0,
- children: service.label,
- }),
- service.isExperimental &&
- jsx(GitlabBadge, {
- label: 'Experimental',
- variant: 'warning',
- }),
- service.isRequired &&
- jsx(GitlabBadge, { label: 'Required', variant: 'muted' }),
- ],
- })
- ),
- }),
- });
- }
-
- // apps/gitlab-plus/src/services/UserSettings.tsx
- class UserSettings extends BaseService {
- constructor() {
- super(...arguments);
- __publicField(this, 'name', ServiceName.UserSettings);
- __publicField(this, 'ready', false);
- }
-
- init() {
- this.initUserSettings();
- window.setTimeout(this.initUserSettings.bind(this), 1e3);
- window.setTimeout(this.initUserSettings.bind(this), 3e3);
- window.setTimeout(this.initUserSettings.bind(this), 5e3);
- }
-
- getMenuItem() {
- const userMenu = document.querySelector('[data-testid="preferences-item"]');
- if (!userMenu || !userMenu.parentElement) {
- return void 0;
- }
- const li = document.createElement('li');
- li.className = 'gl-new-dropdown-item';
- userMenu.parentElement.append(li);
- return li;
- }
-
- initUserSettings() {
- if (this.ready) {
- return;
- }
- const userMenu = this.getMenuItem();
- if (!userMenu) {
- return;
- }
- this.ready = true;
- render(jsx(UserSettingsButton, {}), userMenu);
- render(jsx(UserSettingModal, {}), this.rootBody('glp-user-settings-root'));
- }
- }
-
- // apps/gitlab-plus/src/main.ts
- [
- ClearCacheService,
- ImagePreview,
- MrPreview,
- EpicPreview,
- IssuePreview,
- CreateRelatedIssue,
- CreateChildIssue,
- RelatedIssueAutocomplete,
- RelatedIssuesLabelStatus,
- SortIssue,
- UserSettings,
- ].forEach((Service) => {
- const service = new Service();
- if (userSettingsStore.isActive(service.name)) {
- service.init();
- }
- });