Gitlab plus

Gitlab utils

  1. // ==UserScript==
  2. // @name Gitlab plus
  3. // @namespace https://lukaszmical.pl/
  4. // @version 2025-04-18
  5. // @description Gitlab utils
  6. // @author Łukasz Micał
  7. // @match https://gitlab.com/*
  8. // @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
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=gitlab.com
  10. // ==/UserScript==
  11.  
  12. // Vite helpers
  13. const __defProp = Object.defineProperty;
  14. const __defNormalProp = (obj, key, value) =>
  15. key in obj
  16. ? __defProp(obj, key, {
  17. enumerable: true,
  18. configurable: true,
  19. writable: true,
  20. value,
  21. })
  22. : (obj[key] = value);
  23. const __publicField = (obj, key, value) =>
  24. __defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value);
  25.  
  26. // App code
  27. const { jsx, jsxs, Fragment } = this.jsxRuntime;
  28. const { render } = this.preact;
  29. const { useMemo, useState, useEffect, useRef, useCallback, useLayoutEffect } =
  30. this.preactHooks;
  31.  
  32. // libs/share/src/ui/GlobalStyle.ts
  33. class GlobalStyle {
  34. static addStyle(key, styles) {
  35. const style =
  36. document.getElementById(key) ||
  37. (function () {
  38. const style22 = document.createElement('style');
  39. style22.id = key;
  40. document.head.appendChild(style22);
  41. return style22;
  42. })();
  43. style.textContent = styles;
  44. }
  45. }
  46.  
  47. const style2 =
  48. '.glp-image-preview-modal {\n position: fixed;\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 display: flex;\n justify-content: center;\n align-items: center;\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-close {\n position: absolute;\n z-index: 2;\n top: 5px;\n right: 5px;\n color: black;\n width: 30px;\n height: 30px;\n display: flex;\n justify-content: center;\n align-items: center;\n background: white;\n border-radius: 15px;\n cursor: pointer;\n}\n\n';
  49. const style1 =
  50. '.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';
  51. const style3 =
  52. '.glp-preview-modal {\n position: fixed;\n border-radius: .25rem;\n max-width: 350px;\n width: 350px;\n min-height: 200px;\n visibility: hidden;\n opacity: 0;\n transition: all .2s ease-out;\n transition-property: visibility, opacity, transform;\n\n}\n\n.glp-preview-modal.glp-modal-visible {\n visibility: visible;\n opacity: 1;\n}\n\n.glp-preview-modal ::-webkit-scrollbar {\n width: 5px;\n}\n\n.glp-preview-modal ::-webkit-scrollbar-track {\n background: var(--gl-background-color-overlap);\n}\n\n\n.glp-preview-modal ::-webkit-scrollbar-thumb {\n background: #888;\n border-radius: 5px;\n}\n\n\n.glp-preview-modal ::-webkit-scrollbar-thumb:hover {\n background: #555;\n}\n\n.glp-preview-modal * {\n max-width: 100%;\n}\n';
  53.  
  54. // apps/gitlab-plus/src/styles/index.ts
  55. GlobalStyle.addStyle('glp-style', [style1, style2, style3].join('\n'));
  56.  
  57. // libs/share/src/store/Store.ts
  58. class Store {
  59. constructor(key) {
  60. this.key = key;
  61. }
  62.  
  63. get(defaultValue = void 0) {
  64. try {
  65. const data = localStorage.getItem(this.key);
  66. if (data) {
  67. return this.decode(data);
  68. }
  69. return defaultValue;
  70. } catch (_e) {
  71. return defaultValue;
  72. }
  73. }
  74.  
  75. remove() {
  76. localStorage.removeItem(this.key);
  77. }
  78.  
  79. set(value) {
  80. try {
  81. localStorage.setItem(this.key, this.encode(value));
  82. } catch (_e) {}
  83. }
  84.  
  85. decode(val) {
  86. return JSON.parse(val);
  87. }
  88.  
  89. encode(val) {
  90. return JSON.stringify(val);
  91. }
  92. }
  93.  
  94. // libs/share/src/store/CacheHelper.ts
  95. class CacheHelper {
  96. static clearInvalid(prefix) {
  97. for (const key in localStorage) {
  98. if (!key.startsWith(prefix)) {
  99. continue;
  100. }
  101. const item = new Store(key).get();
  102. if (!this.isCacheEntity(item)) {
  103. continue;
  104. }
  105. if (!this.isValid(item)) {
  106. new Store(key).remove();
  107. }
  108. }
  109. }
  110.  
  111. static expirationDate(minutes) {
  112. if (typeof minutes === 'string') {
  113. return minutes;
  114. }
  115. const time = new Date();
  116. time.setMinutes(time.getMinutes() + minutes);
  117. return time;
  118. }
  119.  
  120. static isCacheEntity(item) {
  121. return !!item && typeof item === 'object' && 'expirationDate' in item;
  122. }
  123.  
  124. static isValid(item) {
  125. if (item) {
  126. return (
  127. item.expirationDate === 'lifetime' ||
  128. new Date(item.expirationDate) > new Date()
  129. );
  130. }
  131. return false;
  132. }
  133. }
  134.  
  135. // libs/share/src/store/Cache.ts
  136. class Cache {
  137. constructor(prefix) {
  138. this.prefix = prefix;
  139. }
  140.  
  141. get(key, defaultValue) {
  142. const data = new Store(this.createKey(key)).get();
  143. if (CacheHelper.isValid(data)) {
  144. return data.value;
  145. }
  146. return defaultValue;
  147. }
  148.  
  149. set(key, value, minutes) {
  150. new Store(this.createKey(key)).set({
  151. expirationDate: CacheHelper.expirationDate(minutes),
  152. value,
  153. });
  154. }
  155.  
  156. createKey(key) {
  157. return `${this.prefix}${key}`;
  158. }
  159. }
  160.  
  161. // apps/gitlab-plus/src/consts/AppConfig.ts
  162. var AppConfig = ((AppConfig2) => {
  163. AppConfig2['CachePrefix'] = 'glp-';
  164. return AppConfig2;
  165. })(AppConfig || {});
  166.  
  167. // apps/gitlab-plus/src/consts/ServiceName.ts
  168. var ServiceName = ((ServiceName2) => {
  169. ServiceName2['ClearCacheService'] = 'ClearCacheService';
  170. ServiceName2['CreateChildIssue'] = 'CreateChildIssue';
  171. ServiceName2['CreateRelatedIssue'] = 'CreateRelatedIssue';
  172. ServiceName2['EpicPreview'] = 'EpicPreview';
  173. ServiceName2['EpicStatus'] = 'EpicStatus';
  174. ServiceName2['ImagePreview'] = 'ImagePreview';
  175. ServiceName2['IssuePreview'] = 'IssuePreview';
  176. ServiceName2['IssueStatus'] = 'IssueStatus';
  177. ServiceName2['MrPreview'] = 'MrPreview';
  178. ServiceName2['RelatedIssueAutocomplete'] = 'RelatedIssueAutocomplete';
  179. ServiceName2['RelatedIssuesLabelStatus'] = 'RelatedIssuesLabelStatus';
  180. ServiceName2['SortIssue'] = 'SortIssue';
  181. ServiceName2['UserSettings'] = 'UserSettings';
  182. return ServiceName2;
  183. })(ServiceName || {});
  184. const servicesConfig = {
  185. ['ClearCacheService']: { label: 'Clear cache', required: true },
  186. ['CreateChildIssue']: {
  187. label: 'Create child issue form on epic page',
  188. },
  189. ['CreateRelatedIssue']: {
  190. label: 'Create related issue form on issue page',
  191. },
  192. ['EpicPreview']: { label: 'Epic preview modal' },
  193. ['EpicStatus']: { label: 'Epic status select' },
  194. ['ImagePreview']: { label: 'Image preview modal' },
  195. ['IssuePreview']: { label: 'Issue preview modal' },
  196. ['IssueStatus']: { label: 'Issue status select' },
  197. ['MrPreview']: { label: 'Merge request preview modal' },
  198. ['RelatedIssueAutocomplete']: {
  199. label: 'Related issue autocomplete in related issues input',
  200. },
  201. ['RelatedIssuesLabelStatus']: {
  202. label: 'Label status in related issues list items (old design)',
  203. },
  204. ['SortIssue']: {
  205. experimental: true,
  206. label: 'Sort issues in board',
  207. },
  208. ['UserSettings']: { label: 'User settings', required: true },
  209. };
  210.  
  211. // apps/gitlab-plus/src/components/user-settings/UserConfig.ts
  212. var UserConfig = ((UserConfig2) => {
  213. UserConfig2['StatusLabelPrefix'] = 'StatusLabelPrefix';
  214. return UserConfig2;
  215. })(UserConfig || {});
  216. const configLabels = {
  217. ['StatusLabelPrefix']: 'Status label prefix',
  218. };
  219. const defaultUserConfig = {
  220. ['StatusLabelPrefix']: 'Status::',
  221. };
  222.  
  223. // apps/gitlab-plus/src/components/user-settings/UserSettingsStore.ts
  224. const _UserSettingsStore = class _UserSettingsStore {
  225. constructor() {
  226. __publicField(this, 'store', new Cache(AppConfig.CachePrefix));
  227. }
  228.  
  229. getConfig(name2) {
  230. return this.getConfigItem(name2);
  231. }
  232.  
  233. isActive(name2) {
  234. if (!(name2 in servicesConfig)) {
  235. return false;
  236. }
  237. if (servicesConfig[name2].required) {
  238. return true;
  239. }
  240. if (servicesConfig[name2].experimental) {
  241. return this.getConfigItem(name2, false);
  242. }
  243. return this.getConfigItem(name2, true);
  244. }
  245.  
  246. setConfig(name2, value) {
  247. this.setConfigItem(name2, value);
  248. }
  249.  
  250. setIsActive(name2, value) {
  251. this.setConfigItem(name2, value);
  252. }
  253.  
  254. getConfigItem(key, defaultValue) {
  255. const items = this.getConfigItems();
  256. if (items[key] === void 0) {
  257. return defaultValue;
  258. }
  259. return items[key];
  260. }
  261.  
  262. getConfigItems() {
  263. return {
  264. ...defaultUserConfig,
  265. ...this.store.get(_UserSettingsStore.ConfigKey, {}),
  266. };
  267. }
  268.  
  269. setConfigItem(key, value) {
  270. const items = this.getConfigItems();
  271. this.store.set(
  272. _UserSettingsStore.ConfigKey,
  273. {
  274. ...items,
  275. [key]: value,
  276. },
  277. 'lifetime'
  278. );
  279. }
  280. };
  281. __publicField(_UserSettingsStore, 'ConfigKey', 'app-config');
  282. const UserSettingsStore = _UserSettingsStore;
  283. const userSettingsStore = new UserSettingsStore();
  284.  
  285. // apps/gitlab-plus/src/services/BaseService.ts
  286. class BaseService {
  287. constructor() {
  288. __publicField(this, 'ready', false);
  289. }
  290.  
  291. setup(callback, linkValidator) {
  292. if (linkValidator && !linkValidator(window.location.href)) {
  293. return;
  294. }
  295. callback();
  296. [1, 3, 5].forEach((time) => {
  297. window.setTimeout(() => {
  298. if (!this.ready) {
  299. callback();
  300. }
  301. }, time * 1e3);
  302. });
  303. }
  304. }
  305.  
  306. // apps/gitlab-plus/src/services/ClearCacheService.ts
  307. class ClearCacheService extends BaseService {
  308. constructor() {
  309. super(...arguments);
  310. __publicField(this, 'name', ServiceName.ClearCacheService);
  311. }
  312.  
  313. init() {
  314. this.invalidateCache();
  315. window.setInterval(this.invalidateCache.bind(this), 60 * 1e3);
  316. }
  317.  
  318. invalidateCache() {
  319. CacheHelper.clearInvalid(AppConfig.CachePrefix);
  320. }
  321. }
  322.  
  323. // apps/gitlab-plus/src/components/common/modal/events.ts
  324. var ModalEvents = ((ModalEvents2) => {
  325. ModalEvents2['showChildIssueModal'] = 'glp-show-create-child-issue-modal';
  326. ModalEvents2['showRelatedIssueModal'] = 'glp-show-create-issue-modal';
  327. ModalEvents2['showUserSettingsModal'] = 'glp-show-user-settings-modal';
  328. return ModalEvents2;
  329. })(ModalEvents || {});
  330.  
  331. // libs/share/src/utils/clsx.ts
  332. function clsx(...args) {
  333. return args
  334. .map((item) => {
  335. if (!item) {
  336. return '';
  337. }
  338. if (typeof item === 'string') {
  339. return item;
  340. }
  341. if (Array.isArray(item)) {
  342. return clsx(...item);
  343. }
  344. if (typeof item === 'object') {
  345. return clsx(
  346. Object.entries(item)
  347. .filter(([_, value]) => value)
  348. .map(([key]) => key)
  349. );
  350. }
  351. return '';
  352. })
  353. .filter(Boolean)
  354. .join(' ');
  355. }
  356.  
  357. // apps/gitlab-plus/src/components/common/GitlabIcon.tsx
  358. const buildId =
  359. '236e3b687d786d9dfe4709143a94d4c53b8d5a1f235775401e5825148297fa84';
  360. const iconUrl = (icon) => {
  361. let _a;
  362. const svgSprite =
  363. ((_a = unsafeWindow.gon) == null ? void 0 : _a.sprite_icons) ||
  364. `/assets/icons-${buildId}.svg`;
  365. return `${svgSprite}#${icon}`;
  366. };
  367.  
  368. function GitlabIcon({ className, icon, size = 12, title }) {
  369. return jsx('svg', {
  370. className: clsx('gl-icon gl-fill-current', `s${size}`, className),
  371. title,
  372. children: jsx('use', { href: iconUrl(icon) }),
  373. });
  374. }
  375.  
  376. // apps/gitlab-plus/src/components/common/base/Row.tsx
  377. function Row({ children, className, gap, items, justify }) {
  378. return jsx('div', {
  379. class: clsx(
  380. 'gl-flex gl-flex-row',
  381. justify && `gl-justify-${justify}`,
  382. items && `gl-items-${items}`,
  383. gap && `gl-gap-${gap}`,
  384. className
  385. ),
  386. children,
  387. });
  388. }
  389.  
  390. // apps/gitlab-plus/src/components/common/GitlabLoader.tsx
  391. function GitlabLoader({ asOverlay, size = 24 }) {
  392. const loader = useMemo(() => {
  393. return jsx('span', {
  394. class: 'gl-spinner-container',
  395. role: 'status',
  396. children: jsx('span', {
  397. class: 'gl-spinner gl-spinner-sm gl-spinner-dark !gl-align-text-bottom',
  398. style: {
  399. width: size,
  400. height: size,
  401. },
  402. }),
  403. });
  404. }, [size]);
  405. if (asOverlay) {
  406. return jsx(Row, {
  407. className: 'gl-h-full gl-w-full gl-absolute gl-bg-overlay',
  408. items: 'center',
  409. justify: 'center',
  410. children: loader,
  411. });
  412. }
  413. return loader;
  414. }
  415.  
  416. // apps/gitlab-plus/src/components/common/GitlabButton.tsx
  417. const buttonVariantClass = {
  418. default: 'btn-default',
  419. info: 'btn-confirm',
  420. tertiary: 'btn-default-tertiary',
  421. };
  422.  
  423. function GitlabButton({
  424. children,
  425. className,
  426. icon,
  427. iconSize = 12,
  428. isLoading,
  429. onClick,
  430. size = 'sm',
  431. title,
  432. variant = 'default',
  433. }) {
  434. const IconComponent = useMemo(() => {
  435. if (isLoading) {
  436. return jsx(GitlabLoader, { size: iconSize });
  437. }
  438. if (icon) {
  439. return jsx(GitlabIcon, { icon, size: iconSize });
  440. }
  441. return null;
  442. }, [icon, isLoading]);
  443. return jsxs('button', {
  444. onClick,
  445. title,
  446. type: 'button',
  447. class: clsx(
  448. `btn btn-${size} gl-button`,
  449. buttonVariantClass[variant],
  450. className
  451. ),
  452. children: [
  453. children && jsx('span', { class: 'gl-button-text', children }),
  454. IconComponent,
  455. ],
  456. });
  457. }
  458.  
  459. // apps/gitlab-plus/src/components/common/CloseButton.tsx
  460. function CloseButton({ onClick, title = 'Close' }) {
  461. return jsx(GitlabButton, {
  462. className: 'btn-icon',
  463. icon: 'close-xs',
  464. iconSize: 16,
  465. onClick,
  466. title,
  467. variant: 'tertiary',
  468. });
  469. }
  470.  
  471. // apps/gitlab-plus/src/components/common/modal/GlpModal.tsx
  472. function GlpModal({ children, isVisible, onClose, title }) {
  473. return jsx('div', {
  474. class: clsx('glp-modal', isVisible && 'glp-modal-visible'),
  475. children: jsxs('div', {
  476. className: clsx(
  477. 'glp-modal-content crud gl-border',
  478. 'gl-rounded-form gl-border-section gl-bg-subtle gl-mt-5'
  479. ),
  480. children: [
  481. jsxs('div', {
  482. className: clsx(
  483. 'crud-header gl-border-b gl-flex gl-flex-wrap',
  484. 'gl-justify-between gl-gap-x-5 gl-gap-y-2 gl-rounded-t-form',
  485. 'gl-border-section gl-bg-section gl-px-5 gl-py-4 gl-relative'
  486. ),
  487. children: [
  488. jsx('h2', {
  489. className: clsx(
  490. 'gl-m-0 gl-inline-flex gl-items-center gl-gap-3',
  491. 'gl-text-form gl-font-bold gl-leading-normal'
  492. ),
  493. children: title,
  494. }),
  495. jsx(CloseButton, { onClick: onClose }),
  496. ],
  497. }),
  498. children,
  499. ],
  500. }),
  501. });
  502. }
  503.  
  504. // apps/gitlab-plus/src/components/common/modal/useGlpModal.ts
  505. function useGlpModal(eventName) {
  506. const [isVisible, setIsVisible] = useState(false);
  507. useEffect(() => {
  508. document.addEventListener(eventName, () => setIsVisible(true));
  509. }, []);
  510. return {
  511. isVisible,
  512. onClose: () => setIsVisible(false),
  513. };
  514. }
  515.  
  516. // apps/gitlab-plus/src/components/common/base/Text.tsx
  517. function Text({ children, className, color, size, variant, weight }) {
  518. return jsx('span', {
  519. class: clsx(
  520. size && `gl-text-${size}`,
  521. weight && `gl-font-${weight}`,
  522. variant && `gl-text-${variant}`,
  523. color && `gl-text-${color}`,
  524. className
  525. ),
  526. children,
  527. });
  528. }
  529.  
  530. // apps/gitlab-plus/src/components/common/form/FormField.tsx
  531. function FormField({ children, error, hint, title }) {
  532. return jsxs('fieldset', {
  533. class: clsx(
  534. 'form-group gl-form-group gl-w-full',
  535. error && 'gl-show-field-errors'
  536. ),
  537. children: [
  538. jsx('legend', {
  539. class: 'bv-no-focus-ring col-form-label pt-0 col-form-label',
  540. children: title,
  541. }),
  542. children,
  543. Boolean(!error && hint) && jsx('small', { children: hint }),
  544. Boolean(error) &&
  545. jsx('small', { class: 'gl-field-error', children: error }),
  546. ],
  547. });
  548. }
  549.  
  550. // apps/gitlab-plus/src/components/common/form/FormRow.tsx
  551. function FormRow({ children }) {
  552. return jsx('div', { class: 'gl-flex gl-gap-x-3', children });
  553. }
  554.  
  555. // libs/share/src/utils/camelizeKeys.ts
  556. function camelizeKeys(data) {
  557. if (!data || ['string', 'number', 'boolean'].includes(typeof data)) {
  558. return data;
  559. }
  560. if (Array.isArray(data)) {
  561. return data.map(camelizeKeys);
  562. }
  563. const camelize = (key) => {
  564. const _key = key.replace(/[-_\s]+(.)?/g, (_, chr) =>
  565. chr ? chr.toUpperCase() : ''
  566. );
  567. return _key.substring(0, 1).toLowerCase() + _key.substring(1);
  568. };
  569. return Object.entries(data).reduce(
  570. (result, [key, value]) => ({
  571. ...result,
  572. [camelize(key)]: camelizeKeys(value),
  573. }),
  574. {}
  575. );
  576. }
  577.  
  578. // apps/gitlab-plus/src/providers/GitlabProvider.ts
  579. class GitlabProvider {
  580. constructor(force = false) {
  581. __publicField(this, 'cache', new Cache(AppConfig.CachePrefix));
  582. __publicField(this, 'graphqlApi', 'https://gitlab.com/api/graphql');
  583. __publicField(this, 'url', 'https://gitlab.com/api/v4/');
  584. this.force = force;
  585. }
  586.  
  587. async cached(key, getValue, minutes) {
  588. const cacheValue = this.cache.get(key);
  589. if (cacheValue && !this.force) {
  590. return cacheValue;
  591. }
  592. const value = await getValue();
  593. this.cache.set(key, value, minutes);
  594. return value;
  595. }
  596.  
  597. csrf() {
  598. const token = document.querySelector('meta[name=csrf-token]');
  599. if (token) {
  600. return token.getAttribute('content');
  601. }
  602. return '';
  603. }
  604.  
  605. async get(path) {
  606. const response = await fetch(`${this.url}${path}`, {
  607. headers: this.headers(),
  608. method: 'GET',
  609. });
  610. const data = await response.json();
  611. return camelizeKeys(data);
  612. }
  613.  
  614. async getCached(key, path, minutes) {
  615. return this.cached(key, () => this.get(path), minutes);
  616. }
  617.  
  618. headers() {
  619. const headers = {
  620. 'content-type': 'application/json',
  621. };
  622. const csrf = this.csrf();
  623. if (csrf) {
  624. headers['X-CSRF-Token'] = csrf;
  625. }
  626. return headers;
  627. }
  628.  
  629. async post(path, body) {
  630. const response = await fetch(`${this.url}${path}`, {
  631. body: JSON.stringify(body),
  632. headers: this.headers(),
  633. method: 'POST',
  634. });
  635. const data = await response.json();
  636. return camelizeKeys(data);
  637. }
  638.  
  639. async query(query, variables) {
  640. const response = await fetch(this.graphqlApi, {
  641. body: JSON.stringify({ query, variables }),
  642. headers: this.headers(),
  643. method: 'POST',
  644. });
  645. return response.json();
  646. }
  647.  
  648. async queryCached(key, query, variables, minutes) {
  649. return this.cached(key, () => this.query(query, variables), minutes);
  650. }
  651. }
  652.  
  653. // apps/gitlab-plus/src/providers/query/user.ts
  654. const userFragment = `
  655. fragment UserFragment on User {
  656. id
  657. avatarUrl
  658. name
  659. username
  660. webUrl
  661. webPath
  662. }
  663. `;
  664. const userQuery = `
  665. query workspaceAutocompleteUsersSearch($search: String!, $fullPath: ID!) {
  666. workspace: project(fullPath: $fullPath) {
  667. id
  668. users: autocompleteUsers(search: $search) {
  669. ...UserFragment
  670. }
  671. }
  672. }
  673.  
  674. ${userFragment}
  675. `;
  676. const currentUserQuery = `
  677. query currentUser {
  678. currentUser {
  679. ...UserFragment
  680. }
  681. }
  682.  
  683. ${userFragment}
  684. `;
  685.  
  686. // apps/gitlab-plus/src/providers/UsersProvider.ts
  687. class UsersProvider extends GitlabProvider {
  688. async getCurrentUser() {
  689. return this.queryCached('gitlab-current-user', currentUserQuery, {}, 60);
  690. }
  691.  
  692. async getUsers(projectId, search = '') {
  693. return this.queryCached(
  694. `users-${projectId}-${search}`,
  695. userQuery,
  696. {
  697. fullPath: projectId,
  698. search,
  699. },
  700. search === '' ? 20 : 0.5
  701. );
  702. }
  703. }
  704.  
  705. // apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteButton.ts
  706. function useAsyncAutocompleteButton(hide) {
  707. const ref = useRef(null);
  708. useEffect(() => {
  709. document.body.addEventListener('click', (e) => {
  710. if (
  711. ref.current &&
  712. e.target !== ref.current &&
  713. !ref.current.contains(e.target)
  714. ) {
  715. hide();
  716. }
  717. });
  718. }, []);
  719. return ref;
  720. }
  721.  
  722. // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteButton.tsx
  723. function AsyncAutocompleteButton({
  724. isOpen,
  725. renderLabel,
  726. reset,
  727. setIsOpen,
  728. size = 'md',
  729. value,
  730. }) {
  731. const ref = useAsyncAutocompleteButton(() => setIsOpen(false));
  732. const icon = useMemo(() => {
  733. if (value.length) {
  734. return 'close-xs';
  735. }
  736. return isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
  737. }, [isOpen, value]);
  738. return jsx('button', {
  739. class: `btn btn-default btn-${size} btn-block gl-button gl-new-dropdown-toggle`,
  740. ref,
  741. type: 'button',
  742. onClick: (e) => {
  743. e.preventDefault();
  744. setIsOpen(true);
  745. },
  746. children: jsxs('span', {
  747. class: 'gl-button-text gl-w-full',
  748. children: [
  749. jsx('span', {
  750. class: 'gl-new-dropdown-button-text',
  751. children: renderLabel(value),
  752. }),
  753. jsx('span', {
  754. onClick: (e) => {
  755. if (value.length) {
  756. e.preventDefault();
  757. reset();
  758. }
  759. },
  760. children: jsx(GitlabIcon, { icon, size: 16 }),
  761. }),
  762. ],
  763. }),
  764. });
  765. }
  766.  
  767. // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteOption.tsx
  768. function AsyncAutocompleteOption({
  769. hideCheckbox = false,
  770. isActive,
  771. onClick,
  772. option,
  773. removeFromRecent,
  774. renderOption,
  775. selected,
  776. }) {
  777. const selectedIds = selected.map((i) => i.id);
  778. const selectedClass = (id) => selectedIds.includes(id);
  779. return jsx('li', {
  780. onClick: () => onClick(option),
  781. class: clsx(
  782. 'gl-new-dropdown-item',
  783. // selectedClass(option.id),
  784. isActive && 'glp-active'
  785. ),
  786. children: jsxs('span', {
  787. class: 'gl-new-dropdown-item-content',
  788. children: [
  789. !hideCheckbox &&
  790. jsx(GitlabIcon, {
  791. className: 'glp-item-check gl-pr-2',
  792. icon: selectedClass(option.id) ? 'mobile-issue-close' : '',
  793. size: 16,
  794. }),
  795. renderOption(option),
  796. removeFromRecent &&
  797. jsx(CloseButton, {
  798. title: 'Remove from recently used',
  799. onClick: (e) => {
  800. e.preventDefault();
  801. e.stopPropagation();
  802. removeFromRecent(option);
  803. },
  804. }),
  805. ],
  806. }),
  807. });
  808. }
  809.  
  810. // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteList.tsx
  811. function AsyncAutocompleteList({
  812. hideCheckbox,
  813. activeIndex,
  814. onClick,
  815. options,
  816. recently,
  817. removeRecently,
  818. renderOption,
  819. value,
  820. }) {
  821. return jsx('div', {
  822. onClick: (e) => e.stopPropagation(),
  823. class:
  824. 'gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay bottom-scrim-visible gl-new-dropdown-contents',
  825. style: {
  826. maxWidth: '800px',
  827. width: '100%',
  828. left: '0',
  829. top: '100%',
  830. },
  831. children: jsx('div', {
  832. class: 'gl-new-dropdown-inner',
  833. children: jsxs('ul', {
  834. class: 'gl-mb-0 gl-pl-0',
  835. children: [
  836. Boolean(recently.length) &&
  837. jsxs(Fragment, {
  838. children: [
  839. jsx('li', {
  840. class:
  841. 'gl-pb-2 gl-pl-4 gl-pt-3 gl-text-sm gl-font-bold gl-text-strong',
  842. children: 'Recently used',
  843. }),
  844. recently.map((item, index) =>
  845. jsx(
  846. AsyncAutocompleteOption,
  847. {
  848. hideCheckbox,
  849. isActive: index === activeIndex,
  850. onClick,
  851. option: item,
  852. removeFromRecent: removeRecently,
  853. renderOption,
  854. selected: value,
  855. },
  856. item.id
  857. )
  858. ),
  859. ],
  860. }),
  861. Boolean(options.length) &&
  862. jsxs(Fragment, {
  863. children: [
  864. jsx('li', {
  865. class:
  866. 'gl-pb-2 gl-pl-4 gl-pt-3 gl-text-sm gl-font-bold gl-text-strong gl-border-t',
  867. }),
  868. options.map((item, index) =>
  869. jsx(
  870. AsyncAutocompleteOption,
  871. {
  872. hideCheckbox,
  873. isActive: recently.length + index === activeIndex,
  874. onClick,
  875. option: item,
  876. renderOption,
  877. selected: value,
  878. },
  879. item.id
  880. )
  881. ),
  882. ],
  883. }),
  884. options.length + recently.length === 0 &&
  885. jsx('li', { class: 'gl-p-4', children: 'No options' }),
  886. ],
  887. }),
  888. }),
  889. });
  890. }
  891.  
  892. // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteSearch.tsx
  893. function AsyncAutocompleteSearch({ navigate, setValue, value }) {
  894. return jsx('div', {
  895. class: 'gl-border-b-1 gl-border-b-solid gl-border-b-dropdown',
  896. children: jsxs('div', {
  897. class: 'gl-listbox-search gl-listbox-topmost',
  898. children: [
  899. jsx(GitlabIcon, {
  900. className: 'gl-search-box-by-type-search-icon',
  901. icon: 'search',
  902. size: 16,
  903. }),
  904. jsx('input', {
  905. class: 'gl-listbox-search-input',
  906. onInput: (e) => setValue(e.target.value),
  907. onKeyDown: (e) => navigate(e.key),
  908. value,
  909. autofocus: true,
  910. }),
  911. Boolean(value) &&
  912. jsx('div', {
  913. class: 'gl-search-box-by-type-right-icons',
  914. style: { top: '0' },
  915. children: jsx(CloseButton, {
  916. onClick: () => setValue(''),
  917. title: 'Clear input',
  918. }),
  919. }),
  920. ],
  921. }),
  922. });
  923. }
  924.  
  925. // apps/gitlab-plus/src/components/common/form/autocomplete/useListNavigate.ts
  926. function useListNavigate(options, recent, onClick, onClose) {
  927. const [activeIndex, setActiveIndex] = useState(-1);
  928. const navigate = (key) => {
  929. if (['ArrowDown', 'ArrowUp'].includes(key)) {
  930. const total = recent.length + options.length;
  931. const diff = key === 'ArrowDown' ? 1 : -1;
  932. setActiveIndex((activeIndex + diff + total) % total);
  933. } else if (key === 'Enter') {
  934. const allItems = [...recent, ...options];
  935. if (-1 < activeIndex && activeIndex < allItems.length) {
  936. onClick(allItems[activeIndex]);
  937. }
  938. } else if (key === 'Escape') {
  939. onClose();
  940. }
  941. };
  942. return {
  943. activeIndex,
  944. navigate,
  945. };
  946. }
  947.  
  948. // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteDropdown.tsx
  949. function AsyncAutocompleteDropdown({
  950. hideCheckbox,
  951. onClick,
  952. onClose,
  953. options,
  954. recently = [],
  955. removeRecently,
  956. renderOption,
  957. searchTerm,
  958. setSearchTerm,
  959. value,
  960. }) {
  961. const { activeIndex, navigate } = useListNavigate(
  962. options,
  963. recently,
  964. onClick,
  965. onClose
  966. );
  967. return jsx('div', {
  968. class: clsx('gl-new-dropdown-panel gl-absolute !gl-block'),
  969. onClick: (e) => e.stopPropagation(),
  970. style: {
  971. maxWidth: '800px',
  972. width: '100%',
  973. left: 'auto',
  974. right: '0',
  975. top: '100%',
  976. },
  977. children: jsxs('div', {
  978. class: 'gl-new-dropdown-inner',
  979. children: [
  980. jsx(AsyncAutocompleteSearch, {
  981. navigate,
  982. setValue: setSearchTerm,
  983. value: searchTerm,
  984. }),
  985. jsx(AsyncAutocompleteList, {
  986. hideCheckbox,
  987. activeIndex,
  988. onClick,
  989. options,
  990. recently,
  991. removeRecently,
  992. renderOption,
  993. value,
  994. }),
  995. ],
  996. }),
  997. });
  998. }
  999.  
  1000. // libs/share/src/utils/useDebounce.ts
  1001. function useDebounce(value, delay = 300) {
  1002. const [debouncedValue, setDebouncedValue] = useState(value);
  1003. useEffect(() => {
  1004. const handler = setTimeout(() => {
  1005. setDebouncedValue(value);
  1006. }, delay);
  1007. return () => {
  1008. clearTimeout(handler);
  1009. };
  1010. }, [value, delay]);
  1011. return debouncedValue;
  1012. }
  1013.  
  1014. // apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteOptions.ts
  1015. function useAsyncAutocompleteOptions(searchTerm, getValues) {
  1016. const [options, setOptions] = useState([]);
  1017. const term = useDebounce(searchTerm);
  1018. const loadOptions = useCallback(
  1019. async (term2) => {
  1020. const items = await getValues(term2);
  1021. setOptions(items);
  1022. },
  1023. [getValues]
  1024. );
  1025. useEffect(() => {
  1026. loadOptions(term);
  1027. }, [term, loadOptions]);
  1028. return options;
  1029. }
  1030.  
  1031. // apps/gitlab-plus/src/providers/RecentlyProvider.ts
  1032. class RecentlyProvider {
  1033. constructor(key) {
  1034. __publicField(this, 'cache', new Cache(AppConfig.CachePrefix));
  1035. __publicField(this, 'key');
  1036. __publicField(this, 'eventName');
  1037. this.key = `recently-${key}`;
  1038. this.eventName = `recently-${key}-change`;
  1039. }
  1040.  
  1041. add(...items) {
  1042. const itemsId = items.map((i) => i.id);
  1043. this.cache.set(
  1044. this.key,
  1045. [...items, ...this.get().filter((el) => !itemsId.includes(el.id))],
  1046. 'lifetime'
  1047. );
  1048. this.triggerChange();
  1049. }
  1050.  
  1051. get() {
  1052. return this.cache.get(this.key) || [];
  1053. }
  1054.  
  1055. onChange(callback) {
  1056. document.addEventListener(this.eventName, callback);
  1057. }
  1058.  
  1059. remove(...items) {
  1060. const itemsId = items.map((i) => i.id);
  1061. this.cache.set(
  1062. this.key,
  1063. this.get().filter((el) => !itemsId.includes(el.id)),
  1064. 'lifetime'
  1065. );
  1066. this.triggerChange();
  1067. }
  1068.  
  1069. triggerChange() {
  1070. document.dispatchEvent(new CustomEvent(this.eventName));
  1071. }
  1072. }
  1073.  
  1074. // apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteRecently.ts
  1075. function useAsyncAutocompleteRecently(name2) {
  1076. const store = useRef(new RecentlyProvider(name2));
  1077. const [recently, setRecently] = useState(store.current.get());
  1078. useEffect(() => {
  1079. store.current.onChange(() => {
  1080. setRecently(store.current.get());
  1081. });
  1082. }, []);
  1083. return {
  1084. add: store.current.add.bind(store.current),
  1085. recently,
  1086. remove: store.current.remove.bind(store.current),
  1087. };
  1088. }
  1089.  
  1090. // apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocomplete.ts
  1091. function useAsyncAutocomplete(
  1092. name2,
  1093. value,
  1094. getValues,
  1095. onChange,
  1096. isMultiselect
  1097. ) {
  1098. const [searchTerm, setSearchTerm] = useState('');
  1099. const [isOpen, setIsOpen] = useState(false);
  1100. const { recently: allRecently, remove: removeRecently } =
  1101. useAsyncAutocompleteRecently(name2);
  1102. const options = useAsyncAutocompleteOptions(searchTerm, getValues);
  1103. const onClick = (item) => {
  1104. if (isMultiselect) {
  1105. if (value.find((i) => i.id === item.id)) {
  1106. onChange(value.filter((i) => i.id !== item.id));
  1107. } else {
  1108. onChange([...value, item]);
  1109. }
  1110. } else {
  1111. onChange([item]);
  1112. setIsOpen(false);
  1113. }
  1114. };
  1115. const recently = useMemo(() => {
  1116. const optionsIds = options.map((i) => i.id);
  1117. return searchTerm.length
  1118. ? allRecently.filter((i) => optionsIds.includes(i.id))
  1119. : allRecently;
  1120. }, [options, allRecently]);
  1121. return {
  1122. isOpen,
  1123. onClick,
  1124. options: useMemo(() => {
  1125. const recentlyIds = recently.map((i) => i.id);
  1126. return options.filter((i) => !recentlyIds.includes(i.id));
  1127. }, [options, recently]),
  1128. recently,
  1129. removeRecently,
  1130. searchTerm,
  1131. setIsOpen,
  1132. setSearchTerm,
  1133. };
  1134. }
  1135.  
  1136. // apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocomplete.tsx
  1137. function AsyncAutocomplete({
  1138. hideCheckbox = false,
  1139. buttonSize,
  1140. getValues,
  1141. isDisabled,
  1142. isMultiselect = false,
  1143. name: name2,
  1144. onChange,
  1145. renderLabel,
  1146. renderOption,
  1147. value,
  1148. }) {
  1149. const {
  1150. isOpen,
  1151. onClick,
  1152. options,
  1153. recently,
  1154. removeRecently,
  1155. searchTerm,
  1156. setIsOpen,
  1157. setSearchTerm,
  1158. } = useAsyncAutocomplete(name2, value, getValues, onChange, isMultiselect);
  1159. return jsxs('div', {
  1160. class: clsx(
  1161. 'gl-relative gl-w-full gl-new-dropdown !gl-block',
  1162. isDisabled && 'gl-pointer-events-none gl-opacity-5'
  1163. ),
  1164. children: [
  1165. jsx(AsyncAutocompleteButton, {
  1166. isOpen,
  1167. renderLabel,
  1168. reset: () => onChange([]),
  1169. setIsOpen,
  1170. size: buttonSize,
  1171. value,
  1172. }),
  1173. isOpen &&
  1174. jsx(AsyncAutocompleteDropdown, {
  1175. hideCheckbox,
  1176. onClick,
  1177. onClose: () => setIsOpen(false),
  1178. options,
  1179. recently,
  1180. removeRecently,
  1181. renderOption,
  1182. searchTerm,
  1183. setSearchTerm,
  1184. value,
  1185. }),
  1186. ],
  1187. });
  1188. }
  1189.  
  1190. // apps/gitlab-plus/src/components/common/GitlabUser.tsx
  1191. function GitlabUser({ showUsername, size = 24, smallText, user, withLink }) {
  1192. const label = useMemo(() => {
  1193. return jsxs(Fragment, {
  1194. children: [
  1195. jsx('span', {
  1196. class: clsx('gl-mr-2 gl-block', smallText && '!gl-text-sm'),
  1197. children: user.name,
  1198. }),
  1199. showUsername &&
  1200. jsx('span', {
  1201. class: 'gl-block gl-text-secondary !gl-text-sm',
  1202. children: user.username,
  1203. }),
  1204. ],
  1205. });
  1206. }, [smallText, showUsername, user]);
  1207. const iconClsx = [
  1208. `gl-avatar gl-avatar-s${size}`,
  1209. smallText ? 'gl-mr-1' : 'gl-mr-3',
  1210. ];
  1211. return jsxs('div', {
  1212. class: 'gl-flex gl-items-center',
  1213. children: [
  1214. user.avatarUrl
  1215. ? jsx('img', {
  1216. alt: `${user.name}'s avatar`,
  1217. class: clsx(...iconClsx, `gl-avatar-circle`),
  1218. src: user.avatarUrl,
  1219. })
  1220. : jsx('div', {
  1221. class: clsx(
  1222. ...iconClsx,
  1223. `gl-avatar-identicon gl-avatar-identicon-bg1`
  1224. ),
  1225. children: user.name[0].toUpperCase(),
  1226. }),
  1227. withLink
  1228. ? jsx('a', { href: user.webUrl, children: label })
  1229. : jsx('div', { children: label }),
  1230. ],
  1231. });
  1232. }
  1233.  
  1234. // apps/gitlab-plus/src/components/create-issue/fields/AssigneesField.tsx
  1235. function AssigneesField({ projectPath, setValue, value }) {
  1236. const getUsers = useCallback(
  1237. async (search) => {
  1238. if (!projectPath) {
  1239. return [];
  1240. }
  1241. const response = await new UsersProvider().getUsers(projectPath, search);
  1242. return response.data.workspace.users;
  1243. },
  1244. [projectPath]
  1245. );
  1246. const renderLabel = useCallback((items) => {
  1247. const label = items.map((i) => i.name).join(', ');
  1248. return jsx('div', {
  1249. title: label,
  1250. children: items.length ? label : 'Select assignee',
  1251. });
  1252. }, []);
  1253. const renderOption = useCallback((item) => {
  1254. return jsx('span', {
  1255. class: 'gl-new-dropdown-item-text-wrapper',
  1256. children: jsx(GitlabUser, { user: item, showUsername: true }),
  1257. });
  1258. }, []);
  1259. return jsx(AsyncAutocomplete, {
  1260. getValues: getUsers,
  1261. isDisabled: !projectPath,
  1262. name: 'assignees',
  1263. onChange: setValue,
  1264. renderLabel,
  1265. renderOption,
  1266. value,
  1267. isMultiselect: true,
  1268. });
  1269. }
  1270.  
  1271. // apps/gitlab-plus/src/components/create-issue/fields/ButtonField.tsx
  1272. function ButtonField({ create, isLoading, reset }) {
  1273. return jsxs(Fragment, {
  1274. children: [
  1275. jsxs('button', {
  1276. class: 'btn btn-confirm btn-sm gl-button gl-gap-2',
  1277. disabled: isLoading,
  1278. onClick: create,
  1279. type: 'button',
  1280. children: [
  1281. jsx('span', { class: 'gl-button-text', children: 'Add' }),
  1282. isLoading
  1283. ? jsx(GitlabLoader, { size: 12 })
  1284. : jsx(GitlabIcon, { icon: 'plus', size: 12 }),
  1285. ],
  1286. }),
  1287. jsx('button', {
  1288. class: 'btn btn-sm gl-button',
  1289. onClick: reset,
  1290. type: 'button',
  1291. children: jsx('span', { class: 'gl-button-text', children: 'Reset' }),
  1292. }),
  1293. ],
  1294. });
  1295. }
  1296.  
  1297. // apps/gitlab-plus/src/providers/query/iteration.ts
  1298. const iterationFragment = `fragment IterationFragment on Iteration {
  1299. id
  1300. title
  1301. startDate
  1302. dueDate
  1303. webUrl
  1304. state
  1305. iterationCadence {
  1306. id
  1307. title
  1308. }
  1309. }`;
  1310. const iterationQuery = `query issueIterationsAliased($fullPath: ID!, $title: String, $state: IterationState) {
  1311. workspace: group(fullPath: $fullPath) {
  1312. id
  1313. attributes: iterations(
  1314. search: $title
  1315. in: [TITLE, CADENCE_TITLE]
  1316. state: $state
  1317. ) {
  1318. nodes {
  1319. ...IterationFragment
  1320. }
  1321. }
  1322. }
  1323. }
  1324. ${iterationFragment}
  1325. `;
  1326.  
  1327. // apps/gitlab-plus/src/providers/IterationsProvider.ts
  1328. class IterationsProvider extends GitlabProvider {
  1329. async getIterations(projectId, title = '') {
  1330. return this.queryCached(
  1331. `iterations-${projectId}-search-${title}`,
  1332. iterationQuery,
  1333. {
  1334. fullPath: projectId,
  1335. state: 'opened',
  1336. title,
  1337. },
  1338. title !== '' ? 0.5 : 20
  1339. );
  1340. }
  1341. }
  1342.  
  1343. // apps/gitlab-plus/src/components/create-issue/fields/IterationField.tsx
  1344. function iterationName(iteration) {
  1345. const start = new Date(iteration.startDate).toLocaleDateString();
  1346. const end = new Date(iteration.dueDate).toLocaleDateString();
  1347. return `${iteration.iterationCadence.title}: ${start} - ${end}`;
  1348. }
  1349.  
  1350. function IterationField({ link, setValue, value }) {
  1351. const getUsers = useCallback(
  1352. async (search) => {
  1353. const response = await new IterationsProvider().getIterations(
  1354. link.workspacePath,
  1355. search
  1356. );
  1357. return response.data.workspace.attributes.nodes
  1358. .map((iteration) => ({
  1359. ...iteration,
  1360. name: iterationName(iteration),
  1361. }))
  1362. .toSorted((a, b) => a.name.localeCompare(b.name));
  1363. },
  1364. [link]
  1365. );
  1366. const renderLabel = useCallback(([item]) => {
  1367. return item ? item.name : 'Select iteration';
  1368. }, []);
  1369. const renderOption = useCallback((item) => {
  1370. return jsx('span', {
  1371. class: 'gl-new-dropdown-item-text-wrapper',
  1372. children: jsx('span', {
  1373. class: 'gl-flex gl-w-full gl-items-center',
  1374. children: jsx('span', {
  1375. class: 'gl-mr-2 gl-block',
  1376. children: item.name,
  1377. }),
  1378. }),
  1379. });
  1380. }, []);
  1381. return jsx(AsyncAutocomplete, {
  1382. getValues: getUsers,
  1383. name: 'iterations',
  1384. onChange: setValue,
  1385. renderLabel,
  1386. renderOption,
  1387. value,
  1388. });
  1389. }
  1390.  
  1391. // apps/gitlab-plus/src/providers/query/label.ts
  1392. const labelFragment = `
  1393. fragment LabelFragment on Label {
  1394. id
  1395. title
  1396. description
  1397. color
  1398. textColor
  1399. }
  1400. `;
  1401. const projectLabelsQuery = `query projectLabels($fullPath: ID!, $searchTerm: String) {
  1402. workspace: project(fullPath: $fullPath) {
  1403. id
  1404. labels(
  1405. searchTerm: $searchTerm
  1406. includeAncestorGroups: true
  1407. ) {
  1408. nodes {
  1409. ...LabelFragment
  1410. }
  1411. }
  1412. }
  1413. }
  1414. ${labelFragment}
  1415. `;
  1416. const workspaceLabelsQuery = `query groupLabels($fullPath: ID!, $searchTerm: String) {
  1417. workspace: group(fullPath: $fullPath) {
  1418. id
  1419. labels(
  1420. searchTerm: $searchTerm
  1421. onlyGroupLabels: true
  1422. includeAncestorGroups: true
  1423. ) {
  1424. nodes {
  1425. ...LabelFragment
  1426. }
  1427. }
  1428. }
  1429. }
  1430.  
  1431. ${labelFragment}
  1432. `;
  1433.  
  1434. // apps/gitlab-plus/src/providers/LabelsProvider.ts
  1435. class LabelsProvider extends GitlabProvider {
  1436. async getProjectLabels(projectPath, search = '') {
  1437. return this.queryCached(
  1438. `project-${projectPath}-labels-${search}`,
  1439. projectLabelsQuery,
  1440. {
  1441. fullPath: projectPath,
  1442. searchTerm: search,
  1443. },
  1444. search === '' ? 20 : 2
  1445. );
  1446. }
  1447.  
  1448. async getWorkspaceLabels(workspacePath, search = '') {
  1449. return this.queryCached(
  1450. `workspace-${workspacePath}-labels-${search}`,
  1451. workspaceLabelsQuery,
  1452. {
  1453. fullPath: workspacePath,
  1454. searchTerm: search,
  1455. },
  1456. search === '' ? 20 : 2
  1457. );
  1458. }
  1459. }
  1460.  
  1461. // apps/gitlab-plus/src/components/common/GitlabLabel.tsx
  1462. function GitlabLabel({ label, onRemove }) {
  1463. const [scope, text] = label.title.split('::');
  1464. const props = useMemo(() => {
  1465. const className = [
  1466. 'gl-label',
  1467. 'hide-collapsed',
  1468. label.textColor === '#FFFFFF'
  1469. ? 'gl-label-text-light'
  1470. : 'gl-label-text-dark',
  1471. ];
  1472. if (label.title.includes('::')) {
  1473. className.push('gl-label-scoped');
  1474. }
  1475. return {
  1476. class: clsx(className),
  1477. style: {
  1478. '--label-background-color': label.color,
  1479. '--label-inset-border': `inset 0 0 0 2px ${label.color}`,
  1480. },
  1481. };
  1482. }, [label]);
  1483. return jsxs('span', {
  1484. class: props.class,
  1485. style: props.style,
  1486. children: [
  1487. jsxs('span', {
  1488. class: 'gl-link gl-label-link gl-label-link-underline',
  1489. children: [
  1490. jsx('span', { class: 'gl-label-text', children: scope }),
  1491. text &&
  1492. jsx('span', { class: 'gl-label-text-scoped', children: text }),
  1493. ],
  1494. }),
  1495. onRemove &&
  1496. jsx('button', {
  1497. onClick: onRemove,
  1498. type: 'button',
  1499. class:
  1500. 'btn gl-label-close !gl-p-0 btn-reset btn-sm gl-button btn-reset-tertiary',
  1501. children: jsx('span', {
  1502. class: 'gl-button-text',
  1503. children: jsx(GitlabIcon, { icon: 'close-xs' }),
  1504. }),
  1505. }),
  1506. ],
  1507. });
  1508. }
  1509.  
  1510. // apps/gitlab-plus/src/components/create-issue/fields/LabelsField.tsx
  1511. function LabelField({ copyLabels, projectPath, setValue, value }) {
  1512. const getLabels = useCallback(
  1513. async (search) => {
  1514. if (!projectPath) {
  1515. return [];
  1516. }
  1517. const response = await new LabelsProvider().getProjectLabels(
  1518. projectPath,
  1519. search
  1520. );
  1521. return response.data.workspace.labels.nodes;
  1522. },
  1523. [projectPath]
  1524. );
  1525. const renderLabel = useCallback((items) => {
  1526. return items.length
  1527. ? items.map((i) => i.title).join(', ')
  1528. : 'Select labels';
  1529. }, []);
  1530. const renderOption = useCallback((item) => {
  1531. return jsxs('div', {
  1532. class: 'gl-flex gl-flex-1 gl-break-anywhere gl-pb-3 gl-pl-4 gl-pt-3',
  1533. children: [
  1534. jsx('span', {
  1535. class: 'dropdown-label-box gl-top-0 gl-mr-3 gl-shrink-0',
  1536. style: { backgroundColor: item.color },
  1537. }),
  1538. jsx('span', { children: item.title }),
  1539. ],
  1540. });
  1541. }, []);
  1542. return jsxs(Fragment, {
  1543. children: [
  1544. jsx('div', {
  1545. class: 'gl-mt-1 gl-pb-2 gl-flex gl-flex-wrap gl-gap-2',
  1546. children: value.map((label) =>
  1547. jsx(
  1548. GitlabLabel,
  1549. {
  1550. label,
  1551. onRemove: () =>
  1552. setValue(value.filter((item) => label.id !== item.id)),
  1553. },
  1554. label.id
  1555. )
  1556. ),
  1557. }),
  1558. jsxs('div', {
  1559. className: 'gl-flex gl-gap-1 gl-relative gl-pr-7',
  1560. children: [
  1561. jsx(AsyncAutocomplete, {
  1562. getValues: getLabels,
  1563. isDisabled: !projectPath,
  1564. name: 'labels',
  1565. onChange: setValue,
  1566. renderLabel,
  1567. renderOption,
  1568. value,
  1569. isMultiselect: true,
  1570. }),
  1571. jsx('div', {
  1572. className: 'gl-flex gl-absolute gl-h-full gl-right-0',
  1573. children: jsx(GitlabButton, {
  1574. icon: 'labels',
  1575. onClick: copyLabels,
  1576. title: 'Copy labels from parent',
  1577. }),
  1578. }),
  1579. ],
  1580. }),
  1581. ],
  1582. });
  1583. }
  1584.  
  1585. // apps/gitlab-plus/src/providers/query/milestone.ts
  1586. const milestoneFragment = `
  1587. fragment MilestoneFragment on Milestone {
  1588. id
  1589. iid
  1590. title
  1591. webUrl: webPath
  1592. dueDate
  1593. expired
  1594. state
  1595. }
  1596.  
  1597. `;
  1598. const milestoneQuery = `query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
  1599. workspace: project(fullPath: $fullPath) {
  1600. id
  1601. attributes: milestones(
  1602. searchTitle: $title
  1603. state: $state
  1604. sort: EXPIRED_LAST_DUE_DATE_ASC
  1605. first: 40
  1606. includeAncestors: true
  1607. ) {
  1608. nodes {
  1609. ...MilestoneFragment
  1610. }
  1611. }
  1612. }
  1613. }
  1614.  
  1615. ${milestoneFragment}
  1616. `;
  1617.  
  1618. // apps/gitlab-plus/src/providers/MilestonesProvider.ts
  1619. class MilestonesProvider extends GitlabProvider {
  1620. async getMilestones(projectId, title = '') {
  1621. return this.queryCached(
  1622. `milestones-${projectId}-${title}`,
  1623. milestoneQuery,
  1624. {
  1625. fullPath: projectId,
  1626. state: 'active',
  1627. title,
  1628. },
  1629. title === '' ? 20 : 0.5
  1630. );
  1631. }
  1632. }
  1633.  
  1634. // apps/gitlab-plus/src/components/create-issue/fields/MilestoneField.tsx
  1635. function MilestoneField({ projectPath, setValue, value }) {
  1636. const getMilestones = useCallback(
  1637. async (search) => {
  1638. if (!projectPath) {
  1639. return [];
  1640. }
  1641. const response = await new MilestonesProvider().getMilestones(
  1642. projectPath,
  1643. search
  1644. );
  1645. return response.data.workspace.attributes.nodes;
  1646. },
  1647. [projectPath]
  1648. );
  1649. const renderLabel = useCallback(([item]) => {
  1650. return item ? item.title : 'Select milestone';
  1651. }, []);
  1652. const renderOption = useCallback((item) => {
  1653. return jsx('span', {
  1654. class: 'gl-new-dropdown-item-text-wrapper',
  1655. children: jsx('span', {
  1656. class: 'gl-flex gl-w-full gl-items-center',
  1657. children: jsx('span', {
  1658. class: 'gl-mr-2 gl-block',
  1659. children: item.title,
  1660. }),
  1661. }),
  1662. });
  1663. }, []);
  1664. return jsx(AsyncAutocomplete, {
  1665. getValues: getMilestones,
  1666. isDisabled: !projectPath,
  1667. name: 'milestones',
  1668. onChange: setValue,
  1669. renderLabel,
  1670. renderOption,
  1671. value,
  1672. });
  1673. }
  1674.  
  1675. // apps/gitlab-plus/src/providers/query/project.ts
  1676. const projectsQuery = `query boardsGetGroupProjects($fullPath: ID!, $search: String, $after: String) {
  1677. workspace: group(fullPath: $fullPath) {
  1678. id
  1679. projects(search: $search, after: $after, first: 100, includeSubgroups: true) {
  1680. nodes {
  1681. id
  1682. name
  1683. avatarUrl
  1684. fullPath
  1685. nameWithNamespace
  1686. archived
  1687. }
  1688. }
  1689. }
  1690. }
  1691. `;
  1692.  
  1693. // apps/gitlab-plus/src/providers/ProjectsProvider.ts
  1694. class ProjectsProvider extends GitlabProvider {
  1695. async getProjects(workspacePath, search = '') {
  1696. return this.queryCached(
  1697. `projects-${workspacePath}-${search}`,
  1698. projectsQuery,
  1699. {
  1700. fullPath: workspacePath,
  1701. search,
  1702. },
  1703. search === '' ? 20 : 0.5
  1704. );
  1705. }
  1706. }
  1707.  
  1708. // apps/gitlab-plus/src/components/common/GitlabProject.tsx
  1709. function GitlabProject({ project, size = 32 }) {
  1710. return jsxs('span', {
  1711. class: 'gl-flex gl-w-full gl-items-center',
  1712. children: [
  1713. project.avatarUrl
  1714. ? jsx('img', {
  1715. alt: project.name,
  1716. class: `gl-mr-3 gl-avatar gl-avatar-s${size}`,
  1717. src: project.avatarUrl,
  1718. })
  1719. : jsx('div', {
  1720. class: `gl-mr-3 gl-avatar gl-avatar-identicon gl-avatar-s${size} gl-avatar-identicon-bg1`,
  1721. children: project.name[0].toUpperCase(),
  1722. }),
  1723. jsxs('span', {
  1724. children: [
  1725. jsx('span', { class: 'gl-mr-2 gl-block', children: project.name }),
  1726. jsx('span', {
  1727. class: 'gl-block gl-text-secondary !gl-text-sm',
  1728. children: project.nameWithNamespace,
  1729. }),
  1730. ],
  1731. }),
  1732. ],
  1733. });
  1734. }
  1735.  
  1736. // apps/gitlab-plus/src/components/create-issue/fields/ProjectField.tsx
  1737. function ProjectField({ link, setValue, value }) {
  1738. const getProjects = useCallback(
  1739. async (search) => {
  1740. const response = await new ProjectsProvider().getProjects(
  1741. link.workspacePath,
  1742. search
  1743. );
  1744. return response.data.workspace.projects.nodes;
  1745. },
  1746. [link]
  1747. );
  1748. const renderLabel = useCallback(([item]) => {
  1749. return item ? item.nameWithNamespace : 'Select project';
  1750. }, []);
  1751. const renderOption = useCallback((item) => {
  1752. return jsx('span', {
  1753. class: 'gl-new-dropdown-item-text-wrapper',
  1754. children: jsx(GitlabProject, { project: item }),
  1755. });
  1756. }, []);
  1757. return jsx(AsyncAutocomplete, {
  1758. getValues: getProjects,
  1759. name: 'projects',
  1760. onChange: setValue,
  1761. renderLabel,
  1762. renderOption,
  1763. value,
  1764. });
  1765. }
  1766.  
  1767. // apps/gitlab-plus/src/types/Issue.ts
  1768. const issueRelation = ['blocks', 'is_blocked_by', 'relates_to'];
  1769.  
  1770. // apps/gitlab-plus/src/components/create-issue/fields/RelationField.tsx
  1771. const labels = (relation) => {
  1772. switch (relation) {
  1773. case 'blocks':
  1774. return 'blocks current issue';
  1775. case 'is_blocked_by':
  1776. return 'is blocked by current issue';
  1777. case 'relates_to':
  1778. return 'relates to current issue';
  1779. default:
  1780. return 'is not related to current issue';
  1781. }
  1782. };
  1783.  
  1784. function RelationField({ setValue, value }) {
  1785. return jsx('div', {
  1786. class: 'linked-issue-type-radio',
  1787. children: [...issueRelation, null].map((relation) =>
  1788. jsxs(
  1789. 'div',
  1790. {
  1791. class: 'gl-form-radio custom-control custom-radio',
  1792. children: [
  1793. jsx('input', {
  1794. id: `create-related-issue-relation-${relation}`,
  1795. checked: value === relation,
  1796. class: 'custom-control-input',
  1797. name: 'linked-issue-type-radio',
  1798. onChange: () => setValue(relation),
  1799. type: 'radio',
  1800. value: relation ?? '',
  1801. }),
  1802. jsx('label', {
  1803. class: 'custom-control-label',
  1804. for: `create-related-issue-relation-${relation}`,
  1805. children: labels(relation),
  1806. }),
  1807. ],
  1808. },
  1809. relation
  1810. )
  1811. ),
  1812. });
  1813. }
  1814.  
  1815. // apps/gitlab-plus/src/components/create-issue/fields/TitleField.tsx
  1816. function TitleField({ error, onChange, value }) {
  1817. return jsx('input', {
  1818. onInput: (e) => onChange(e.target.value),
  1819. placeholder: 'Add a title',
  1820. value,
  1821. class: clsx(
  1822. 'gl-form-input form-control',
  1823. error && 'gl-field-error-outline'
  1824. ),
  1825. });
  1826. }
  1827.  
  1828. // apps/gitlab-plus/src/types/Epic.ts
  1829. var WidgetType = ((WidgetType2) => {
  1830. WidgetType2['hierarchy'] = 'HIERARCHY';
  1831. WidgetType2['label'] = 'LABELS';
  1832. return WidgetType2;
  1833. })(WidgetType || {});
  1834.  
  1835. // apps/gitlab-plus/src/helpers/WidgetHelper.ts
  1836. class WidgetHelper {
  1837. static getWidget(widgets, type) {
  1838. return widgets == null
  1839. ? void 0
  1840. : widgets.find((widget) => widget.type === type);
  1841. }
  1842. }
  1843.  
  1844. // apps/gitlab-plus/src/helpers/LabelHelper.ts
  1845. class LabelHelper {
  1846. static getLabelsFromWidgets(widgets) {
  1847. let _a;
  1848. return (
  1849. ((_a = LabelHelper.getLabelWidget(widgets)) == null
  1850. ? void 0
  1851. : _a.labels.nodes) || []
  1852. );
  1853. }
  1854.  
  1855. static getLabelWidget(widgets) {
  1856. return WidgetHelper.getWidget(widgets, WidgetType.label);
  1857. }
  1858.  
  1859. static getStatusLabel(labels2) {
  1860. return labels2 == null
  1861. ? void 0
  1862. : labels2.find((label) =>
  1863. label.title.startsWith(LabelHelper.getStatusPrefix())
  1864. );
  1865. }
  1866.  
  1867. static getStatusLabelFromWidgets(widgets) {
  1868. return LabelHelper.getStatusLabel(
  1869. LabelHelper.getLabelsFromWidgets(widgets)
  1870. );
  1871. }
  1872.  
  1873. static getStatusPrefix() {
  1874. return userSettingsStore.getConfig(UserConfig.StatusLabelPrefix);
  1875. }
  1876. }
  1877.  
  1878. // apps/gitlab-plus/src/helpers/LinkParser.ts
  1879. class LinkParser {
  1880. static epicPattern(strict = false) {
  1881. return LinkParser.patternGroup('epic', 'epics', strict);
  1882. }
  1883.  
  1884. static isEpicLink(link) {
  1885. return link.epic !== void 0;
  1886. }
  1887.  
  1888. static isIssueLink(link) {
  1889. return link.issue !== void 0;
  1890. }
  1891.  
  1892. static isMrLink(link) {
  1893. return link.mr !== void 0;
  1894. }
  1895.  
  1896. static issuePattern(strict = false) {
  1897. return LinkParser.patternProject('issue', 'issues', strict);
  1898. }
  1899.  
  1900. static mrPattern(strict = false) {
  1901. return LinkParser.patternProject('mr', 'merge_requests', strict);
  1902. }
  1903.  
  1904. static parseEpicLink(link, strict = false) {
  1905. if (LinkParser.validateEpicLink(link, strict)) {
  1906. return LinkParser.parseGitlabLink(link, LinkParser.epicPattern(strict));
  1907. }
  1908. return void 0;
  1909. }
  1910.  
  1911. static parseGitlabLink(link, pattern) {
  1912. const result = new URL(link).pathname.match(pattern);
  1913. if (result && result.groups) {
  1914. return result.groups;
  1915. }
  1916. return void 0;
  1917. }
  1918.  
  1919. static parseIssueLink(link, strict = false) {
  1920. if (LinkParser.validateIssueLink(link)) {
  1921. return LinkParser.parseGitlabLink(link, LinkParser.issuePattern(strict));
  1922. }
  1923. return void 0;
  1924. }
  1925.  
  1926. static parseMrLink(link, strict = false) {
  1927. if (LinkParser.validateMrLink(link)) {
  1928. return LinkParser.parseGitlabLink(link, LinkParser.mrPattern(strict));
  1929. }
  1930. return void 0;
  1931. }
  1932.  
  1933. static patternGroup(name2, entity, strict) {
  1934. const end = !strict ? '([?#]{1}.*)?' : '';
  1935. return new RegExp(
  1936. `\\/groups\\/(?<workspacePath>.+)\\/-\\/${entity}\\/(?<${name2}>\\d+)\\/?${end}$`
  1937. );
  1938. }
  1939.  
  1940. static patternProject(name2, entity, strict) {
  1941. const end = !strict ? '([?#]{1}.*)?' : '';
  1942. return new RegExp(
  1943. `\\/(?<projectPath>(?<workspacePath>.+)\\/[^/]+)\\/-\\/${entity}\\/(?<${name2}>\\d+)\\/?${end}$`
  1944. );
  1945. }
  1946.  
  1947. static validateEpicLink(link, strict = false) {
  1948. return LinkParser.validateGitlabLink(link, LinkParser.epicPattern(strict));
  1949. }
  1950.  
  1951. static validateGitlabLink(link, pattern) {
  1952. return Boolean(typeof link === 'string' && pattern.test(link));
  1953. }
  1954.  
  1955. static validateIssueLink(link, strict = false) {
  1956. return LinkParser.validateGitlabLink(link, LinkParser.issuePattern(strict));
  1957. }
  1958.  
  1959. static validateMrLink(link, strict = false) {
  1960. return LinkParser.validateGitlabLink(link, LinkParser.mrPattern(strict));
  1961. }
  1962. }
  1963.  
  1964. // apps/gitlab-plus/src/providers/query/widget.ts
  1965. const labelsWidgetFragment = `
  1966. fragment LabelsWidgetFragment on WorkItemWidgetLabels {
  1967. labels {
  1968. nodes {
  1969. ...LabelFragment
  1970. }
  1971. }
  1972. }
  1973. `;
  1974. const hierarchyWidgetFragment = `
  1975. fragment HierarchyWidgetFragment on WorkItemWidgetHierarchy {
  1976. hasChildren
  1977. children(first: 100) {
  1978. count
  1979. nodes {
  1980. id
  1981. iid
  1982. title
  1983. state
  1984. webUrl
  1985. widgets {
  1986. type
  1987. ...LabelsWidgetFragment
  1988. }
  1989. }
  1990. }
  1991. }
  1992. `;
  1993.  
  1994. // apps/gitlab-plus/src/providers/query/epic.ts
  1995. const epicQuery = `query namespaceWorkItem($fullPath: ID!, $iid: String!) {
  1996. workspace: namespace(fullPath: $fullPath) {
  1997. id
  1998. workItem(iid: $iid) {
  1999. ...WorkItem
  2000. }
  2001. }
  2002. }
  2003.  
  2004. fragment WorkItem on WorkItem {
  2005. id
  2006. iid
  2007. archived
  2008. title
  2009. state
  2010. description
  2011. createdAt
  2012. closedAt
  2013. webUrl
  2014. project {
  2015. id
  2016. }
  2017. namespace {
  2018. id
  2019. fullPath
  2020. name
  2021. fullName
  2022. }
  2023. author {
  2024. ...UserFragment
  2025. }
  2026. widgets {
  2027. type
  2028. ...LabelsWidgetFragment
  2029. ...HierarchyWidgetFragment
  2030. }
  2031. }
  2032.  
  2033. ${labelsWidgetFragment}
  2034. ${hierarchyWidgetFragment}
  2035. ${labelFragment}
  2036. ${userFragment}
  2037. `;
  2038. const epicSetLabelsMutation = `
  2039. mutation workItemUpdate($input: WorkItemUpdateInput!) {
  2040. workItemUpdate(input: $input) {
  2041. errors
  2042. }
  2043. }
  2044. `;
  2045.  
  2046. // apps/gitlab-plus/src/providers/EpicProvider.ts
  2047. class EpicProvider extends GitlabProvider {
  2048. async getEpic(workspacePath, epicId) {
  2049. return this.queryCached(
  2050. `epic-${workspacePath}-${epicId}`,
  2051. epicQuery,
  2052. {
  2053. iid: epicId,
  2054. cursor: '',
  2055. fullPath: workspacePath,
  2056. pageSize: 50,
  2057. },
  2058. 2
  2059. );
  2060. }
  2061.  
  2062. async updateEpicLabels(id, addLabelIds, removeLabelIds) {
  2063. return await this.query(epicSetLabelsMutation, {
  2064. input: {
  2065. id,
  2066. labelsWidget: {
  2067. addLabelIds,
  2068. removeLabelIds,
  2069. },
  2070. },
  2071. });
  2072. }
  2073. }
  2074.  
  2075. // apps/gitlab-plus/src/providers/query/issue.ts
  2076. const issueQuery = `query issueEE($projectPath: ID!, $iid: String!) {
  2077. project(fullPath: $projectPath) {
  2078. id
  2079. issue(iid: $iid) {
  2080. id
  2081. iid
  2082. title
  2083. description
  2084. createdAt
  2085. state
  2086. dueDate
  2087. projectId
  2088. webUrl
  2089. weight
  2090. type
  2091. milestone {
  2092. id
  2093. title
  2094. startDate
  2095. dueDate
  2096. }
  2097. epic {
  2098. id
  2099. iid
  2100. title
  2101. webUrl
  2102. labels {
  2103. nodes {
  2104. ...LabelFragment
  2105. }
  2106. }
  2107. }
  2108. iteration {
  2109. ...IterationFragment
  2110. }
  2111. labels {
  2112. nodes {
  2113. ...LabelFragment
  2114. }
  2115. }
  2116. relatedMergeRequests {
  2117. nodes {
  2118. iid
  2119. title
  2120. state
  2121. webUrl
  2122. author {
  2123. ...UserFragment
  2124. }
  2125. }
  2126. }
  2127. assignees {
  2128. nodes {
  2129. ...UserFragment
  2130. }
  2131. }
  2132. author {
  2133. ...UserFragment
  2134. }
  2135. linkedWorkItems {
  2136. nodes {
  2137. linkType
  2138. workItemState
  2139. workItem {
  2140. id
  2141. iid
  2142. webUrl
  2143. title
  2144. widgets {
  2145. type
  2146. ...LabelsWidgetFragment
  2147. }
  2148. }
  2149. }
  2150. }
  2151. }
  2152. }
  2153. }
  2154.  
  2155. ${labelFragment}
  2156. ${labelsWidgetFragment}
  2157. ${userFragment}
  2158. ${iterationFragment}
  2159. `;
  2160. const issueWithRelatedIssuesLabelsQuery = `query issueEE($projectPath: ID!, $iid: String!) {
  2161. project(fullPath: $projectPath) {
  2162. issue(iid: $iid) {
  2163. linkedWorkItems {
  2164. nodes {
  2165. workItem {
  2166. id
  2167. iid
  2168. widgets {
  2169. type
  2170. ...LabelsWidgetFragment
  2171. }
  2172. }
  2173. }
  2174. }
  2175. }
  2176. }
  2177. }
  2178.  
  2179. ${labelsWidgetFragment}
  2180. ${labelFragment}
  2181. `;
  2182. 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) {
  2183. workspace: group(fullPath: $fullPath) {
  2184. id
  2185. workItems(
  2186. search: $searchTerm
  2187. types: $types
  2188. in: $in
  2189. includeAncestors: $includeAncestors
  2190. includeDescendants: $includeDescendants
  2191. ) @include(if: $searchByText) {
  2192. nodes {
  2193. ...WorkItemSearchFragment
  2194. }
  2195. }
  2196. workItemsByIid: workItems(
  2197. iid: $iid
  2198. types: $types
  2199. includeAncestors: $includeAncestors
  2200. includeDescendants: $includeDescendants
  2201. ) @include(if: $searchByIid) {
  2202. nodes {
  2203. ...WorkItemSearchFragment
  2204. }
  2205. }
  2206. workItemsEmpty: workItems(
  2207. types: $types
  2208. includeAncestors: $includeAncestors
  2209. includeDescendants: $includeDescendants
  2210. ) @include(if: $searchEmpty) {
  2211. nodes {
  2212. ...WorkItemSearchFragment
  2213. }
  2214. }
  2215. }
  2216. }
  2217.  
  2218. fragment WorkItemSearchFragment on WorkItem {
  2219. id
  2220. iid
  2221. title
  2222. project {
  2223. fullPath
  2224. }
  2225. }
  2226. `;
  2227. const issueMutation = `
  2228. mutation CreateIssue($input: CreateIssueInput!) {
  2229. createIssue(input: $input) {
  2230. issue {
  2231. ...CreatedIssue
  2232. }
  2233. errors
  2234. }
  2235. }
  2236.  
  2237. fragment CreatedIssue on Issue {
  2238. id
  2239. iid
  2240. projectId
  2241. }
  2242. `;
  2243. const issueSetEpicMutation = `
  2244. mutation projectIssueUpdateParent($input: WorkItemUpdateInput!) {
  2245. workItemUpdate(input: $input) {
  2246. errors
  2247. }
  2248. }
  2249. `;
  2250. const issueSetLabelsMutation = `
  2251. mutation issueSetLabels($input: UpdateIssueInput!) {
  2252. updateIssue(input: $input) {
  2253. errors
  2254. }
  2255. }
  2256. `;
  2257. const issueSetAssigneesMutation = `
  2258. mutation issueSetAssignees($input: IssueSetAssigneesInput!) {
  2259. issueSetAssignees(input: $input) {
  2260. errors
  2261. }
  2262. }
  2263. `;
  2264.  
  2265. // apps/gitlab-plus/src/providers/IssueProvider.ts
  2266. class IssueProvider extends GitlabProvider {
  2267. async createIssue(input) {
  2268. return await this.query(issueMutation, { input });
  2269. }
  2270.  
  2271. async createIssueRelation(input) {
  2272. const path = [
  2273. 'projects/:PROJECT_ID',
  2274. '/issues/:ISSUE_ID/links',
  2275. '?target_project_id=:TARGET_PROJECT_ID',
  2276. '&target_issue_iid=:TARGET_ISSUE_IID',
  2277. '&link_type=:LINK_TYPE',
  2278. ]
  2279. .join('')
  2280. .replace(':PROJECT_ID', `${input.projectId}`)
  2281. .replace(':ISSUE_ID', `${input.issueId}`)
  2282. .replace(':TARGET_PROJECT_ID', input.targetProjectId)
  2283. .replace(':TARGET_ISSUE_IID', input.targetIssueIid)
  2284. .replace(':LINK_TYPE', input.linkType);
  2285. return await this.post(path, {});
  2286. }
  2287.  
  2288. async getIssue(projectPath, iid) {
  2289. return this.queryCached(
  2290. `issue-${projectPath}-${iid}`,
  2291. issueQuery,
  2292. {
  2293. iid,
  2294. projectPath,
  2295. },
  2296. 2
  2297. );
  2298. }
  2299.  
  2300. async getIssues(projectPath, search) {
  2301. const searchById = !!search.match(/^\d+$/);
  2302. return await this.query(issuesQuery, {
  2303. iid: searchById ? search : null,
  2304. searchByIid: searchById,
  2305. fullPath: projectPath,
  2306. in: 'TITLE',
  2307. includeAncestors: true,
  2308. includeDescendants: true,
  2309. searchByText: Boolean(search),
  2310. searchEmpty: !search,
  2311. searchTerm: search,
  2312. types: ['ISSUE'],
  2313. });
  2314. }
  2315.  
  2316. async getIssueWithRelatedIssuesLabels(projectPath, iid) {
  2317. return this.queryCached(
  2318. `issue-related-issues-${projectPath}-${iid}`,
  2319. issueWithRelatedIssuesLabelsQuery,
  2320. {
  2321. iid,
  2322. projectPath,
  2323. },
  2324. 2
  2325. );
  2326. }
  2327.  
  2328. async issueSetAssignees(input) {
  2329. return await this.query(issueSetAssigneesMutation, { input });
  2330. }
  2331.  
  2332. async issueSetEpic(issueId, epicId) {
  2333. return await this.query(issueSetEpicMutation, {
  2334. input: {
  2335. hierarchyWidget: {
  2336. parentId: epicId,
  2337. },
  2338. id: issueId,
  2339. },
  2340. });
  2341. }
  2342.  
  2343. async issueSetLabels(input) {
  2344. return await this.query(issueSetLabelsMutation, { input });
  2345. }
  2346. }
  2347.  
  2348. // apps/gitlab-plus/src/components/create-issue/useCreateIssueForm.ts
  2349. const initialState = () => ({
  2350. assignees: [],
  2351. iteration: null,
  2352. labels: [],
  2353. milestone: null,
  2354. project: null,
  2355. relation: null,
  2356. title: '',
  2357. });
  2358. const initialError = () => ({
  2359. assignees: void 0,
  2360. iteration: void 0,
  2361. labels: void 0,
  2362. milestone: void 0,
  2363. project: void 0,
  2364. relation: void 0,
  2365. title: void 0,
  2366. });
  2367.  
  2368. function useCreateIssueForm({ isVisible, link, onClose }) {
  2369. let _a;
  2370. const [values, setValues] = useState(initialState());
  2371. const [errors, setErrors] = useState(initialError());
  2372. const [parentIssue, setParentIssue] = useState(null);
  2373. const [parentEpic, setParentEpic] = useState(null);
  2374. const [isLoading, setIsLoading] = useState(false);
  2375. const [message, setMessage] = useState('');
  2376. const [error, setError] = useState('');
  2377. const reset = (resetParent = false) => {
  2378. setIsLoading(false);
  2379. setValues(initialState());
  2380. setErrors(initialError());
  2381. setMessage('');
  2382. setError('');
  2383. if (resetParent) {
  2384. setParentIssue(null);
  2385. setParentEpic(null);
  2386. }
  2387. };
  2388. const createPayload = () => {
  2389. const data = {
  2390. projectPath: values.project.fullPath,
  2391. title: values.title,
  2392. };
  2393. if (values.milestone) {
  2394. data['milestoneId'] = values.milestone.id;
  2395. }
  2396. if (values.iteration) {
  2397. data['iterationId'] = values.iteration.id;
  2398. data['iterationCadenceId'] = values.iteration.iterationCadence.id;
  2399. }
  2400. if (values.assignees) {
  2401. data['assigneeIds'] = values.assignees.map((a) => a.id);
  2402. }
  2403. data['labelIds'] = values.labels.map((label) => label.id);
  2404. return data;
  2405. };
  2406. const persistRecently = () => {
  2407. Object.entries({
  2408. assignees: values.assignees,
  2409. iterations: values.iteration ? [values.iteration] : [],
  2410. labels: values.labels,
  2411. milestones: values.milestone ? [values.milestone] : [],
  2412. projects: values.project ? [values.project] : [],
  2413. }).map(([key, values2]) => {
  2414. new RecentlyProvider(key).add(...values2);
  2415. });
  2416. };
  2417. const validate = () => {
  2418. let isValid = true;
  2419. const errors2 = {};
  2420. if (values.title.length < 1) {
  2421. errors2.title = 'Title is required';
  2422. isValid = false;
  2423. } else if (values.title.length > 255) {
  2424. errors2.title = 'Title is too long';
  2425. isValid = false;
  2426. }
  2427. if (!values.project) {
  2428. errors2.project = 'Project must be selected';
  2429. isValid = false;
  2430. }
  2431. setErrors((prev) => ({ ...prev, ...errors2 }));
  2432. return isValid;
  2433. };
  2434. const createIssue = async (payload) => {
  2435. return await new IssueProvider().createIssue(payload);
  2436. };
  2437. const createRelation = async (issue, targetIssue, relation) => {
  2438. await new IssueProvider().createIssueRelation({
  2439. targetIssueIid: targetIssue.iid,
  2440. issueId: issue.iid,
  2441. linkType: relation,
  2442. projectId: issue.projectId,
  2443. targetProjectId: targetIssue.projectId,
  2444. });
  2445. };
  2446. const setIssueEpic = async (issue, epic) => {
  2447. await new IssueProvider().issueSetEpic(issue.id, epic.id);
  2448. };
  2449. const submit = async () => {
  2450. if (!validate()) {
  2451. return;
  2452. }
  2453. setIsLoading(true);
  2454. try {
  2455. setMessage('Creating issue...');
  2456. const payload = createPayload();
  2457. const response = await createIssue(payload);
  2458. persistRecently();
  2459. if (values.relation && parentIssue) {
  2460. setMessage('Creating relation to parent issue...');
  2461. await createRelation(
  2462. response.data.createIssue.issue,
  2463. parentIssue,
  2464. values.relation
  2465. );
  2466. }
  2467. if (parentEpic) {
  2468. setMessage('Linking to epic...');
  2469. await setIssueEpic(response.data.createIssue.issue, parentEpic);
  2470. }
  2471. setMessage('Issue was created');
  2472. window.setTimeout(() => onClose(), 2e3);
  2473. } catch (e) {
  2474. setMessage('');
  2475. setError(e.message);
  2476. }
  2477. setIsLoading(false);
  2478. };
  2479. const fetchParent = async () => {
  2480. if (LinkParser.isIssueLink(link)) {
  2481. const issue = await new IssueProvider().getIssue(
  2482. link.projectPath,
  2483. link.issue
  2484. );
  2485. setParentIssue(issue.data.project.issue);
  2486. }
  2487. if (LinkParser.isEpicLink(link)) {
  2488. const epic = await new EpicProvider().getEpic(
  2489. link.workspacePath,
  2490. link.epic
  2491. );
  2492. setParentEpic(epic.data.workspace.workItem);
  2493. }
  2494. };
  2495. const getParentTitle = () => {
  2496. return (
  2497. (parentIssue == null ? void 0 : parentIssue.title) ||
  2498. (parentEpic == null ? void 0 : parentEpic.title) ||
  2499. ''
  2500. );
  2501. };
  2502. const getParentLabels = () => {
  2503. if (parentEpic) {
  2504. return LabelHelper.getLabelsFromWidgets(parentEpic.widgets);
  2505. }
  2506. if (parentIssue) {
  2507. return parentIssue.labels.nodes;
  2508. }
  2509. return [];
  2510. };
  2511. const fillForm = () => {
  2512. const assignees = new RecentlyProvider('assignees').get();
  2513. const iterations = new RecentlyProvider('iterations').get();
  2514. const milestones = new RecentlyProvider('milestones').get();
  2515. const projects = new RecentlyProvider('projects').get();
  2516. setValues({
  2517. ...values,
  2518. assignees: assignees.length ? [assignees[0]] : values.assignees,
  2519. iteration: iterations.length ? iterations[0] : values.iteration,
  2520. labels: getParentLabels(),
  2521. milestone: milestones.length ? milestones[0] : values.milestone,
  2522. project: projects.length ? projects[0] : values.project,
  2523. title: getParentTitle(),
  2524. });
  2525. };
  2526. useEffect(() => {
  2527. if (isVisible) {
  2528. fetchParent();
  2529. } else {
  2530. reset(true);
  2531. }
  2532. }, [isVisible]);
  2533. return {
  2534. actions: {
  2535. fillForm,
  2536. onSubmit: (e) => {
  2537. e.preventDefault();
  2538. submit();
  2539. },
  2540. reset: () => reset(false),
  2541. submit,
  2542. },
  2543. error,
  2544. form: {
  2545. assignees: {
  2546. errors: errors.assignees,
  2547. onChange: (assignees) => setValues({ ...values, assignees }),
  2548. value: values.assignees,
  2549. },
  2550. iteration: {
  2551. errors: errors.iteration,
  2552. onChange: ([iteration]) =>
  2553. setValues({ ...values, iteration: iteration ?? null }),
  2554. value: values.iteration ? [values.iteration] : [],
  2555. },
  2556. labels: {
  2557. copy: () => setValues({ ...values, labels: getParentLabels() }),
  2558. errors: errors.labels,
  2559. onChange: (labels2) => setValues({ ...values, labels: labels2 }),
  2560. value: values.labels,
  2561. },
  2562. milestone: {
  2563. errors: errors.milestone,
  2564. onChange: ([milestone]) =>
  2565. setValues({ ...values, milestone: milestone ?? null }),
  2566. value: values.milestone ? [values.milestone] : [],
  2567. },
  2568. project: {
  2569. errors: errors.project,
  2570. onChange: ([project]) =>
  2571. setValues({ ...values, project: project ?? null }),
  2572. value: values.project ? [values.project] : [],
  2573. },
  2574. relation: {
  2575. errors: errors.relation,
  2576. onChange: (relation) => setValues({ ...values, relation }),
  2577. value: values.relation,
  2578. },
  2579. title: {
  2580. copy: () => setValues({ ...values, title: getParentTitle() }),
  2581. errors: errors.title,
  2582. onChange: (title) => setValues({ ...values, title }),
  2583. value: values.title,
  2584. },
  2585. },
  2586. isLoading,
  2587. message,
  2588. parentEpic,
  2589. parentIssue,
  2590. projectPath: (_a = values.project) == null ? void 0 : _a.fullPath,
  2591. };
  2592. }
  2593.  
  2594. // apps/gitlab-plus/src/components/create-issue/CreateIssueForm.tsx
  2595. function CreateIssueForm({ isVisible, link, onClose }) {
  2596. const {
  2597. actions,
  2598. error,
  2599. form,
  2600. isLoading,
  2601. message,
  2602. parentEpic,
  2603. parentIssue,
  2604. projectPath,
  2605. } = useCreateIssueForm({ isVisible, link, onClose });
  2606. return jsxs('form', {
  2607. class: 'crud-body add-tree-form gl-mx-5 gl-my-4 gl-rounded-b-form',
  2608. onSubmit: actions.onSubmit,
  2609. children: [
  2610. jsx(FormField, {
  2611. error: form.title.errors,
  2612. hint: 'Maximum of 255 characters',
  2613. title: 'Title',
  2614. children: jsxs('div', {
  2615. className: 'gl-flex gl-gap-1',
  2616. children: [
  2617. jsx(TitleField, {
  2618. error: form.title.errors,
  2619. onChange: form.title.onChange,
  2620. value: form.title.value,
  2621. }),
  2622. jsx(GitlabButton, {
  2623. icon: 'title',
  2624. onClick: form.title.copy,
  2625. title: 'Copy from parent title',
  2626. }),
  2627. jsx(GitlabButton, {
  2628. icon: 'insert',
  2629. onClick: actions.fillForm,
  2630. title: 'Fill form with parent and last used data',
  2631. }),
  2632. ],
  2633. }),
  2634. }),
  2635. jsxs(FormRow, {
  2636. children: [
  2637. jsx(FormField, {
  2638. error: form.project.errors,
  2639. title: 'Project',
  2640. children: jsx(ProjectField, {
  2641. link,
  2642. setValue: form.project.onChange,
  2643. value: form.project.value,
  2644. }),
  2645. }),
  2646. jsx(FormField, {
  2647. error: form.assignees.errors,
  2648. title: 'Assignees',
  2649. children: jsx(AssigneesField, {
  2650. projectPath,
  2651. setValue: form.assignees.onChange,
  2652. value: form.assignees.value,
  2653. }),
  2654. }),
  2655. ],
  2656. }),
  2657. jsxs(FormRow, {
  2658. children: [
  2659. jsx(FormField, {
  2660. error: form.iteration.errors,
  2661. title: 'Iteration',
  2662. children: jsx(IterationField, {
  2663. link,
  2664. setValue: form.iteration.onChange,
  2665. value: form.iteration.value,
  2666. }),
  2667. }),
  2668. jsx(FormField, {
  2669. error: form.milestone.errors,
  2670. title: 'Milestone',
  2671. children: jsx(MilestoneField, {
  2672. projectPath,
  2673. setValue: form.milestone.onChange,
  2674. value: form.milestone.value,
  2675. }),
  2676. }),
  2677. ],
  2678. }),
  2679. jsx(FormField, {
  2680. error: form.labels.errors,
  2681. title: 'Labels',
  2682. children: jsx(LabelField, {
  2683. copyLabels: form.labels.copy,
  2684. projectPath,
  2685. setValue: form.labels.onChange,
  2686. value: form.labels.value,
  2687. }),
  2688. }),
  2689. parentIssue &&
  2690. jsxs(FormField, {
  2691. error: form.relation.errors,
  2692. title: 'New issue',
  2693. children: [
  2694. jsx(RelationField, {
  2695. setValue: form.relation.onChange,
  2696. value: form.relation.value,
  2697. }),
  2698. jsxs(Text, {
  2699. size: 'sm',
  2700. variant: 'secondary',
  2701. children: [
  2702. 'Parent issue: #',
  2703. parentIssue.iid,
  2704. ' ',
  2705. parentIssue.title,
  2706. ],
  2707. }),
  2708. ],
  2709. }),
  2710. parentEpic &&
  2711. jsx(FormField, {
  2712. title: '',
  2713. children: jsxs(Text, {
  2714. size: 'sm',
  2715. variant: 'secondary',
  2716. children: ['Parent epic: &', parentEpic.iid, ' ', parentEpic.title],
  2717. }),
  2718. }),
  2719. jsx(FormField, {
  2720. error,
  2721. hint: message,
  2722. title: '',
  2723. children: jsx(FormRow, {
  2724. children: jsx(ButtonField, {
  2725. create: actions.submit,
  2726. isLoading,
  2727. reset: actions.reset,
  2728. }),
  2729. }),
  2730. }),
  2731. ],
  2732. });
  2733. }
  2734.  
  2735. // apps/gitlab-plus/src/components/create-issue/CreateChildIssueModal.tsx
  2736. function CreateChildIssueModal({ link }) {
  2737. const { isVisible, onClose } = useGlpModal(ModalEvents.showChildIssueModal);
  2738. return jsx(GlpModal, {
  2739. isVisible,
  2740. onClose,
  2741. title: 'Create child issue',
  2742. children: jsx(CreateIssueForm, { isVisible, link, onClose }),
  2743. });
  2744. }
  2745.  
  2746. // apps/gitlab-plus/src/components/create-issue/CreateIssueButton.tsx
  2747. function CreateIssueButton({ eventName, label }) {
  2748. const onClick = () => document.dispatchEvent(new CustomEvent(eventName));
  2749. return jsx(GitlabButton, { onClick, children: label });
  2750. }
  2751.  
  2752. // apps/gitlab-plus/src/helpers/GitlabHtmlElements.ts
  2753. class GitlabHtmlElements {
  2754. static crudActionElement(...ids) {
  2755. const selector = ids
  2756. .map((s) => `${s} [data-testid="crud-actions"]`)
  2757. .join(', ');
  2758. return document.querySelector(selector);
  2759. }
  2760. }
  2761.  
  2762. // apps/gitlab-plus/src/helpers/RendererHelper.ts
  2763. class RendererHelper {
  2764. static mountPoint(mountPoint) {
  2765. return mountPoint instanceof HTMLElement
  2766. ? mountPoint
  2767. : document.querySelector(mountPoint);
  2768. }
  2769.  
  2770. static pageLink(linkParser) {
  2771. return linkParser(window.location.href);
  2772. }
  2773.  
  2774. static render(id, mountPoint, renderer, mode = 'append') {
  2775. const node = RendererHelper.mountPoint(mountPoint);
  2776. if (!node) {
  2777. return false;
  2778. }
  2779. return RendererHelper.renderInNode(
  2780. RendererHelper.root(id, node, mode),
  2781. renderer
  2782. );
  2783. }
  2784.  
  2785. static renderInBody(id, renderer) {
  2786. return RendererHelper.renderInNode(
  2787. RendererHelper.root(id, document.body),
  2788. renderer
  2789. );
  2790. }
  2791.  
  2792. static renderInNode(node, renderer) {
  2793. render(renderer instanceof Function ? renderer() : renderer, node);
  2794. return true;
  2795. }
  2796.  
  2797. static renderWithLink(id, mountPoint, linkParser, renderer, mode = 'append') {
  2798. const link = RendererHelper.pageLink(linkParser);
  2799. if (!link) {
  2800. return false;
  2801. }
  2802. return RendererHelper.render(id, mountPoint, () => renderer(link), mode);
  2803. }
  2804.  
  2805. static root(className, node, mode = 'append') {
  2806. const root = document.createElement('div');
  2807. root.className = className;
  2808. if (node) {
  2809. node[mode](root);
  2810. }
  2811. return root;
  2812. }
  2813. }
  2814.  
  2815. // apps/gitlab-plus/src/services/CreateChildIssue.tsx
  2816. class CreateChildIssue extends BaseService {
  2817. constructor() {
  2818. super(...arguments);
  2819. __publicField(this, 'name', ServiceName.CreateChildIssue);
  2820. }
  2821.  
  2822. init() {
  2823. this.setup(this.mount.bind(this), LinkParser.validateEpicLink);
  2824. }
  2825.  
  2826. mount() {
  2827. const link = RendererHelper.pageLink(LinkParser.parseEpicLink);
  2828. const parent = GitlabHtmlElements.crudActionElement('#childitems');
  2829. if (!link || !parent) {
  2830. return;
  2831. }
  2832. this.ready = true;
  2833. RendererHelper.render(
  2834. 'glp-child-issue-button',
  2835. parent,
  2836. jsx(CreateIssueButton, {
  2837. eventName: ModalEvents.showChildIssueModal,
  2838. label: 'Create child issue',
  2839. }),
  2840. 'prepend'
  2841. );
  2842. RendererHelper.renderInBody(
  2843. 'glp-child-issue-modal',
  2844. jsx(CreateChildIssueModal, { link })
  2845. );
  2846. }
  2847. }
  2848.  
  2849. // apps/gitlab-plus/src/components/create-issue/CreateRelatedIssueModal.tsx
  2850. function CreateRelatedIssueModal({ link }) {
  2851. const { isVisible, onClose } = useGlpModal(ModalEvents.showRelatedIssueModal);
  2852. return jsx(GlpModal, {
  2853. isVisible,
  2854. onClose,
  2855. title: 'Create related issue',
  2856. children: jsx(CreateIssueForm, { isVisible, link, onClose }),
  2857. });
  2858. }
  2859.  
  2860. // apps/gitlab-plus/src/services/CreateRelatedIssue.tsx
  2861. class CreateRelatedIssue extends BaseService {
  2862. constructor() {
  2863. super(...arguments);
  2864. __publicField(this, 'name', ServiceName.CreateRelatedIssue);
  2865. }
  2866.  
  2867. init() {
  2868. this.setup(this.mount.bind(this), LinkParser.validateIssueLink);
  2869. }
  2870.  
  2871. mount() {
  2872. const link = LinkParser.parseIssueLink(window.location.href);
  2873. const parent = GitlabHtmlElements.crudActionElement(
  2874. '#related-issues',
  2875. '#linkeditems'
  2876. );
  2877. if (!link || !parent) {
  2878. return;
  2879. }
  2880. this.ready = true;
  2881. RendererHelper.render(
  2882. 'glp-related-issue-button',
  2883. parent,
  2884. jsx(CreateIssueButton, {
  2885. eventName: ModalEvents.showRelatedIssueModal,
  2886. label: 'Create related issue',
  2887. }),
  2888. 'prepend'
  2889. );
  2890. RendererHelper.renderInBody(
  2891. 'glp-related-issue-modal',
  2892. jsx(CreateRelatedIssueModal, { link })
  2893. );
  2894. }
  2895. }
  2896.  
  2897. // libs/share/src/ui/Events.ts
  2898. class Events {
  2899. static intendHover(validate, mouseover, mouseleave, timeout = 500) {
  2900. let hover = false;
  2901. let id = 0;
  2902. const onHover = (event) => {
  2903. if (!event.target || !validate(event.target)) {
  2904. return;
  2905. }
  2906. const element = event.target;
  2907. hover = true;
  2908. element.addEventListener(
  2909. 'mouseleave',
  2910. (ev) => {
  2911. mouseleave == null ? void 0 : mouseleave.call(element, ev);
  2912. clearTimeout(id);
  2913. hover = false;
  2914. },
  2915. { once: true }
  2916. );
  2917. clearTimeout(id);
  2918. id = window.setTimeout(() => {
  2919. if (hover) {
  2920. mouseover.call(element, event);
  2921. }
  2922. }, timeout);
  2923. };
  2924. document.body.addEventListener('mouseover', onHover);
  2925. }
  2926. }
  2927.  
  2928. // apps/gitlab-plus/src/components/common/modal/useOnLinkHover.ts
  2929. const modalZIndex = 5e3;
  2930.  
  2931. function useOnLinkHover(parser, validator) {
  2932. const [hoverPosition, setHoverPosition] = useState({ x: 0, y: 0 });
  2933. const [hoverLink, setHoverLink] = useState();
  2934. const [zIndex, setZIndex] = useState(modalZIndex);
  2935. const hoverLinkRef = useRef(false);
  2936. const onHover = (event) => {
  2937. const anchor = event.target;
  2938. const link = parser(anchor.href);
  2939. if (!link) {
  2940. return;
  2941. }
  2942. anchor.title = '';
  2943. setHoverLink(link);
  2944. setZIndex(
  2945. anchor.dataset.zIndex ? Number(anchor.dataset.zIndex) : modalZIndex
  2946. );
  2947. setHoverPosition({
  2948. x: event.clientX + 15,
  2949. y: event.clientY,
  2950. });
  2951. };
  2952. useEffect(() => {
  2953. Events.intendHover(
  2954. (element) => validator(element.href, true),
  2955. onHover,
  2956. () => {
  2957. setTimeout(() => {
  2958. if (!hoverLinkRef.current) {
  2959. setHoverLink(void 0);
  2960. }
  2961. }, 50);
  2962. }
  2963. );
  2964. }, []);
  2965. return {
  2966. hoverLink,
  2967. hoverPosition,
  2968. onLinkEnter: () => (hoverLinkRef.current = true),
  2969. onLinkLeave: () => {
  2970. hoverLinkRef.current = false;
  2971. setHoverLink(void 0);
  2972. },
  2973. zIndex,
  2974. };
  2975. }
  2976.  
  2977. // apps/gitlab-plus/src/components/common/modal/usePreviewModal.ts
  2978. function usePreviewModal(link, fetch2, reset, isLoading) {
  2979. const [isVisible, setIsVisible] = useState(false);
  2980. const [offset, setOffset] = useState({ x: 0, y: 0 });
  2981. const ref = useRef(null);
  2982. useEffect(() => {
  2983. if (!isLoading) {
  2984. setTimeout(() => {
  2985. const rect = ref.current.getBoundingClientRect();
  2986. const dY = rect.height + rect.top - window.innerHeight;
  2987. const dX = rect.width + rect.left - window.innerWidth;
  2988. setOffset({
  2989. x: dX > 0 ? dX + 15 : 0,
  2990. y: dY > 0 ? dY + 15 : 0,
  2991. });
  2992. }, 300);
  2993. }
  2994. }, [isLoading]);
  2995. useEffect(() => {
  2996. if (!isVisible) {
  2997. setOffset({ x: 0, y: 0 });
  2998. }
  2999. }, [isVisible]);
  3000. useEffect(() => {
  3001. if (link) {
  3002. fetch2(link);
  3003. setIsVisible(true);
  3004. } else {
  3005. setIsVisible(false);
  3006. reset();
  3007. }
  3008. }, [link]);
  3009. return {
  3010. isVisible,
  3011. offset,
  3012. ref,
  3013. };
  3014. }
  3015.  
  3016. // apps/gitlab-plus/src/components/common/modal/PreviewModal.tsx
  3017. function PreviewModal({
  3018. validator,
  3019. children,
  3020. fetch: fetch2,
  3021. isError,
  3022. isLoading = false,
  3023. isRefreshing = false,
  3024. parser,
  3025. reset,
  3026. }) {
  3027. const { hoverLink, hoverPosition, onLinkEnter, onLinkLeave, zIndex } =
  3028. useOnLinkHover(parser, validator);
  3029. const { isVisible, offset, ref } = usePreviewModal(
  3030. hoverLink,
  3031. fetch2,
  3032. reset,
  3033. isLoading
  3034. );
  3035. const content = useMemo(() => {
  3036. if (isLoading || !isVisible) {
  3037. return jsx(GitlabLoader, { size: '3em', asOverlay: true });
  3038. }
  3039. if (isError) {
  3040. return jsx(Row, {
  3041. className: 'gl-flex-1',
  3042. items: 'center',
  3043. justify: 'center',
  3044. children: 'Error',
  3045. });
  3046. }
  3047. return jsxs('div', {
  3048. className: 'gl-flex gl-w-full gl-flex-col',
  3049. children: [
  3050. children,
  3051. isRefreshing && jsx(GitlabLoader, { size: '3em', asOverlay: true }),
  3052. ],
  3053. });
  3054. }, [isLoading, isRefreshing, isError, isVisible, children]);
  3055. return jsx('div', {
  3056. onMouseEnter: onLinkEnter,
  3057. onMouseLeave: onLinkLeave,
  3058. ref,
  3059. className: clsx(
  3060. 'popover gl-popover glp-preview-modal',
  3061. isVisible && 'glp-modal-visible'
  3062. ),
  3063. style: {
  3064. left: hoverPosition.x,
  3065. top: hoverPosition.y,
  3066. transform: `translate(-${offset.x}px, -${offset.y}px )`,
  3067. zIndex,
  3068. },
  3069. children: content,
  3070. });
  3071. }
  3072.  
  3073. // apps/gitlab-plus/src/components/common/base/Link.tsx
  3074. function Link({ blockHover, children, className, href, inline, title }) {
  3075. const [zIndex, setZIndex] = useState(modalZIndex + 1);
  3076. const ref = useRef(null);
  3077. const onHover = (e) => {
  3078. e.stopPropagation();
  3079. e.preventDefault();
  3080. return false;
  3081. };
  3082. useLayoutEffect(() => {
  3083. let _a;
  3084. const modal =
  3085. (_a = ref.current) == null ? void 0 : _a.closest('.glp-preview-modal');
  3086. setZIndex(
  3087. (modal == null ? void 0 : modal.style.zIndex)
  3088. ? Number(modal.style.zIndex) + 1
  3089. : modalZIndex + 1
  3090. );
  3091. }, []);
  3092. return jsx('a', {
  3093. 'data-z-index': zIndex,
  3094. href,
  3095. onMouseOver: blockHover ? onHover : void 0,
  3096. ref,
  3097. target: '_blank',
  3098. title,
  3099. class: clsx(
  3100. inline ? 'gl-inline' : 'gl-flex',
  3101. 'gl-link sortable-link gl-items-center',
  3102. className
  3103. ),
  3104. style: {
  3105. overflow: 'hidden',
  3106. textOverflow: 'ellipsis',
  3107. },
  3108. children,
  3109. });
  3110. }
  3111.  
  3112. // apps/gitlab-plus/src/components/common/block/InfoBlock.tsx
  3113. function InfoBlock({
  3114. children,
  3115. className,
  3116. contentMaxHeight,
  3117. icon,
  3118. link,
  3119. rightTitle,
  3120. title,
  3121. titleClassName,
  3122. }) {
  3123. const style = useMemo(() => {
  3124. if (contentMaxHeight) {
  3125. return {
  3126. maxHeight: contentMaxHeight,
  3127. overflowY: 'auto',
  3128. };
  3129. }
  3130. return {};
  3131. }, [contentMaxHeight]);
  3132. return jsxs('div', {
  3133. class: 'gl-relative gl-w-full gl-py-2 gl-border-b gl-border-b-solid',
  3134. style: { borderColor: 'var(--gl-border-color-subtle)' },
  3135. children: [
  3136. jsxs(Row, {
  3137. className: 'gl-px-3',
  3138. items: 'center',
  3139. justify: 'between',
  3140. children: [
  3141. jsxs(Row, {
  3142. gap: 2,
  3143. items: 'center',
  3144. children: [
  3145. icon && jsx(GitlabIcon, { icon, size: 16 }),
  3146. jsxs('h5', {
  3147. className: clsx('gl-my-0', titleClassName),
  3148. children: [
  3149. title,
  3150. link &&
  3151. jsx(Link, {
  3152. className: 'gl-ml-3',
  3153. href: link,
  3154. blockHover: true,
  3155. inline: true,
  3156. children: jsx(GitlabIcon, { icon: 'symlink', size: 16 }),
  3157. }),
  3158. ],
  3159. }),
  3160. ],
  3161. }),
  3162. rightTitle,
  3163. ],
  3164. }),
  3165. jsx('div', {
  3166. style,
  3167. children: jsx('div', { class: clsx('gl-px-3', className), children }),
  3168. }),
  3169. ],
  3170. });
  3171. }
  3172.  
  3173. // apps/gitlab-plus/src/components/common/block/HeadingBlock.tsx
  3174. function HeadingBlock({
  3175. author,
  3176. badge,
  3177. createdAt,
  3178. entityId,
  3179. icon,
  3180. link,
  3181. onRefresh,
  3182. title,
  3183. }) {
  3184. return jsxs(InfoBlock, {
  3185. link,
  3186. title,
  3187. titleClassName: 'gl-pr-2',
  3188. rightTitle:
  3189. onRefresh &&
  3190. jsx('div', {
  3191. onClick: onRefresh,
  3192. className: 'gl-absolute gl-right-0 gl-top-1 gl-p-2 gl-cursor-pointer',
  3193. children: jsx(GitlabIcon, { icon: 'repeat' }),
  3194. }),
  3195. children: [
  3196. jsxs(Row, {
  3197. className: 'gl-mt-2',
  3198. gap: 2,
  3199. items: 'center',
  3200. children: [
  3201. jsxs(Row, {
  3202. gap: 2,
  3203. items: 'center',
  3204. children: [
  3205. jsx(GitlabIcon, { icon, size: 16 }),
  3206. jsx(Text, {
  3207. size: 'sm',
  3208. variant: 'secondary',
  3209. weight: 'bold',
  3210. children: entityId,
  3211. }),
  3212. ],
  3213. }),
  3214. badge,
  3215. ],
  3216. }),
  3217. jsxs(Row, {
  3218. className: 'gl-mt-1',
  3219. gap: 2,
  3220. items: 'center',
  3221. children: [
  3222. jsx(Text, {
  3223. size: 'sm',
  3224. variant: 'secondary',
  3225. children: 'Created at',
  3226. }),
  3227. jsx(Text, {
  3228. size: 'sm',
  3229. weight: 'bold',
  3230. children: new Date(createdAt).toLocaleDateString(),
  3231. }),
  3232. jsx(Text, { size: 'sm', variant: 'secondary', children: 'by' }),
  3233. jsx(GitlabUser, {
  3234. size: 16,
  3235. user: author,
  3236. smallText: true,
  3237. withLink: true,
  3238. }),
  3239. ],
  3240. }),
  3241. ],
  3242. });
  3243. }
  3244.  
  3245. // apps/gitlab-plus/src/components/common/GitlabBadge.tsx
  3246. function GitlabBadge({ icon, label, title, variant }) {
  3247. return jsxs('span', {
  3248. className: `gl-badge badge badge-pill badge-${variant}`,
  3249. title,
  3250. children: [
  3251. icon && jsx(GitlabIcon, { icon }),
  3252. label && jsx('span', { className: 'gl-badge-content', children: label }),
  3253. ],
  3254. });
  3255. }
  3256.  
  3257. // apps/gitlab-plus/src/components/common/IssueStatus.tsx
  3258. function IssueStatus$1({ isOpen }) {
  3259. return jsx(GitlabBadge, {
  3260. icon: isOpen ? 'issue-open-m' : 'issue-close',
  3261. label: isOpen ? 'Open' : 'Closed',
  3262. variant: isOpen ? 'success' : 'info',
  3263. });
  3264. }
  3265.  
  3266. // apps/gitlab-plus/src/components/epic-preview/blocks/EpicHeading.tsx
  3267. function EpicHeader({ epic, onRefresh }) {
  3268. return jsx(HeadingBlock, {
  3269. author: epic.author,
  3270. badge: jsx(IssueStatus$1, { isOpen: epic.state === 'OPEN' }),
  3271. createdAt: epic.createdAt,
  3272. entityId: `&${epic.iid}`,
  3273. icon: 'epic',
  3274. link: epic.webUrl,
  3275. onRefresh,
  3276. title: epic.title,
  3277. });
  3278. }
  3279.  
  3280. // apps/gitlab-plus/src/components/common/block/useChangeStatusSelect.ts
  3281. const name = 'status-labels';
  3282.  
  3283. function useChangeStatusSelect({
  3284. getStatusLabels,
  3285. onStausLabelUpdate,
  3286. statusLabel,
  3287. }) {
  3288. const [isLoading, setIsLoading] = useState(false);
  3289. const [statusLabels, setStatusLabels] = useState([]);
  3290. const filterValues = useCallback(
  3291. async (search) => {
  3292. return statusLabels
  3293. .filter((option) => option.title.includes(search))
  3294. .filter(
  3295. (label) =>
  3296. label.id !== (statusLabel == null ? void 0 : statusLabel.id)
  3297. );
  3298. },
  3299. [statusLabels]
  3300. );
  3301. const onSelectStatus = useCallback(
  3302. async (label) => {
  3303. setIsLoading(true);
  3304. await onStausLabelUpdate(label);
  3305. new RecentlyProvider(name).add(label);
  3306. setIsLoading(false);
  3307. },
  3308. [onStausLabelUpdate]
  3309. );
  3310. useEffect(() => {
  3311. getStatusLabels().then((labels2) => setStatusLabels(labels2));
  3312. }, []);
  3313. return {
  3314. filterValues,
  3315. isLoading,
  3316. name,
  3317. onSelectStatus,
  3318. };
  3319. }
  3320.  
  3321. // apps/gitlab-plus/src/components/common/block/ChangeStatusSelect.tsx
  3322. function ChangeStatusSelect({
  3323. width = 130,
  3324. getStatusLabels,
  3325. label = 'Change status',
  3326. onStausLabelUpdate,
  3327. statusLabel,
  3328. }) {
  3329. const {
  3330. filterValues,
  3331. isLoading,
  3332. name: name2,
  3333. onSelectStatus,
  3334. } = useChangeStatusSelect({
  3335. getStatusLabels,
  3336. onStausLabelUpdate,
  3337. statusLabel,
  3338. });
  3339. if (isLoading) {
  3340. return jsx(GitlabLoader, {});
  3341. }
  3342. const renderOption = useCallback((item) => {
  3343. return jsxs('div', {
  3344. class: 'gl-flex gl-flex-1 gl-break-anywhere gl-pb-3 gl-pl-4 gl-pt-3',
  3345. children: [
  3346. jsx('span', {
  3347. class: 'dropdown-label-box gl-top-0 gl-mr-3 gl-shrink-0',
  3348. style: { backgroundColor: item.color },
  3349. }),
  3350. jsx('span', { children: item.title }),
  3351. ],
  3352. });
  3353. }, []);
  3354. return jsx('div', {
  3355. className: 'gl-py-2',
  3356. style: { width },
  3357. title: `Current ${statusLabel == null ? void 0 : statusLabel.title}`,
  3358. children: jsx(AsyncAutocomplete, {
  3359. hideCheckbox: true,
  3360. buttonSize: 'sm',
  3361. getValues: filterValues,
  3362. name: name2,
  3363. onChange: ([label2]) => label2 && onSelectStatus(label2),
  3364. renderLabel: () => label,
  3365. renderOption,
  3366. value: [],
  3367. }),
  3368. });
  3369. }
  3370.  
  3371. // apps/gitlab-plus/src/components/common/block/LabelsBlock.tsx
  3372. function LabelsBlock({ labels: labels2, updateStatus }) {
  3373. if (!labels2.length && !updateStatus) {
  3374. return null;
  3375. }
  3376. return jsx(InfoBlock, {
  3377. className: 'issuable-show-labels',
  3378. icon: 'labels',
  3379. title: 'Labels',
  3380. rightTitle:
  3381. updateStatus &&
  3382. jsx(ChangeStatusSelect, {
  3383. getStatusLabels: updateStatus.getStatusLabels,
  3384. onStausLabelUpdate: updateStatus.onStausLabelUpdate,
  3385. statusLabel: updateStatus.statusLabel,
  3386. }),
  3387. children: labels2.map((label) => jsx(GitlabLabel, { label }, label.id)),
  3388. });
  3389. }
  3390.  
  3391. // apps/gitlab-plus/src/components/common/hooks/useEpicLabels.ts
  3392. function useEpicLabels({ epic, link, refetch }) {
  3393. const { labels: labels2, statusLabel } = useMemo(() => {
  3394. const labels22 = LabelHelper.getLabelsFromWidgets(
  3395. epic == null ? void 0 : epic.widgets
  3396. );
  3397. return {
  3398. labels: labels22,
  3399. statusLabel: LabelHelper.getStatusLabel(labels22),
  3400. };
  3401. }, [epic]);
  3402. const onStatusChange = useCallback(
  3403. async (label) => {
  3404. if (!epic) {
  3405. return;
  3406. }
  3407. await new EpicProvider().updateEpicLabels(
  3408. epic.id,
  3409. [label.id],
  3410. statusLabel ? [statusLabel.id] : []
  3411. );
  3412. if (refetch) {
  3413. await refetch();
  3414. }
  3415. },
  3416. [labels2, statusLabel]
  3417. );
  3418. const fetchStatusLabels = useCallback(async () => {
  3419. const response = await new LabelsProvider().getWorkspaceLabels(
  3420. link.workspacePath,
  3421. LabelHelper.getStatusPrefix()
  3422. );
  3423. return response.data.workspace.labels.nodes;
  3424. }, []);
  3425. return {
  3426. labels: labels2,
  3427. updateStatus: {
  3428. getStatusLabels: fetchStatusLabels,
  3429. onStausLabelUpdate: onStatusChange,
  3430. statusLabel,
  3431. },
  3432. };
  3433. }
  3434.  
  3435. // apps/gitlab-plus/src/components/epic-preview/blocks/EpicLabels.tsx
  3436. function EpicLabels({ epic, link, refresh }) {
  3437. const { labels: labels2, updateStatus } = useEpicLabels({
  3438. epic,
  3439. link,
  3440. refetch: refresh,
  3441. });
  3442. if (!labels2.length) {
  3443. return null;
  3444. }
  3445. return jsx(LabelsBlock, { labels: labels2, updateStatus });
  3446. }
  3447.  
  3448. // apps/gitlab-plus/src/components/common/block/ListBlock.tsx
  3449. function ListBlock({ itemId, items, maxHeight = 100, renderItem, ...props }) {
  3450. if (!items.length) {
  3451. return null;
  3452. }
  3453. return jsx(InfoBlock, {
  3454. contentMaxHeight: maxHeight,
  3455. ...props,
  3456. children: items.map((item) =>
  3457. jsx(Fragment, { children: renderItem(item) }, itemId(item))
  3458. ),
  3459. });
  3460. }
  3461.  
  3462. // apps/gitlab-plus/src/components/common/StatusIndicator.tsx
  3463. function StatusIndicator({ label }) {
  3464. if (!label) {
  3465. return null;
  3466. }
  3467. return jsx('div', {
  3468. title: label.title,
  3469. style: {
  3470. minWidth: 10,
  3471. width: 10,
  3472. backgroundColor: label.color,
  3473. borderRadius: 10,
  3474. height: 10,
  3475. marginRight: 2,
  3476. },
  3477. });
  3478. }
  3479.  
  3480. // apps/gitlab-plus/src/components/epic-preview/blocks/EpicRelatedIssues.tsx
  3481. function EpicRelatedIssues({ epic }) {
  3482. const issues = useMemo(() => {
  3483. let _a;
  3484. const hierarchyWidget = WidgetHelper.getWidget(
  3485. epic.widgets,
  3486. WidgetType.hierarchy
  3487. );
  3488. return (
  3489. ((_a = hierarchyWidget == null ? void 0 : hierarchyWidget.children) ==
  3490. null
  3491. ? void 0
  3492. : _a.nodes) || []
  3493. );
  3494. }, [epic]);
  3495. return jsx(ListBlock, {
  3496. icon: 'issue-type-issue',
  3497. itemId: (i) => i.iid,
  3498. items: issues,
  3499. title: `Child issues (${issues.length})`,
  3500. renderItem: (issue) =>
  3501. jsxs(Link, {
  3502. href: issue.webUrl,
  3503. title: issue.title,
  3504. children: [
  3505. jsx(StatusIndicator, {
  3506. label: LabelHelper.getStatusLabelFromWidgets(issue.widgets),
  3507. }),
  3508. '#',
  3509. issue.iid,
  3510. ' ',
  3511. issue.title,
  3512. ],
  3513. }),
  3514. });
  3515. }
  3516.  
  3517. // apps/gitlab-plus/src/components/common/modal/useFetchEntity.ts
  3518. function useFetchEntity(fetcher) {
  3519. const [entityData, setEntityData] = useState(null);
  3520. const [isLoading, setIsLoading] = useState(false);
  3521. const [isRefreshing, setIsRefreshing] = useState(false);
  3522. const fetch2 = async (link, force = false) => {
  3523. if (force) {
  3524. setIsRefreshing(true);
  3525. } else {
  3526. setIsLoading(true);
  3527. }
  3528. const entity = await fetcher(link, force);
  3529. setEntityData({ entity, link });
  3530. setIsRefreshing(false);
  3531. setIsLoading(false);
  3532. };
  3533. const reset = () => {
  3534. setEntityData(null);
  3535. setIsRefreshing(false);
  3536. setIsLoading(false);
  3537. };
  3538. return {
  3539. entityData,
  3540. fetch: fetch2,
  3541. isLoading,
  3542. isRefreshing,
  3543. onRefresh: async () => {
  3544. if (entityData) {
  3545. await fetch2(entityData.link, true);
  3546. }
  3547. },
  3548. reset,
  3549. };
  3550. }
  3551.  
  3552. // apps/gitlab-plus/src/components/epic-preview/useFetchEpic.ts
  3553. function useFetchEpic() {
  3554. return useFetchEntity(async (link, force = false) => {
  3555. const response = await new EpicProvider(force).getEpic(
  3556. link.workspacePath,
  3557. link.epic
  3558. );
  3559. return response.data.workspace.workItem;
  3560. });
  3561. }
  3562.  
  3563. // apps/gitlab-plus/src/components/epic-preview/EpicPreviewModal.tsx
  3564. function EpicPreviewModal() {
  3565. const {
  3566. entityData,
  3567. fetch: fetch2,
  3568. isLoading,
  3569. isRefreshing,
  3570. reset,
  3571. } = useFetchEpic();
  3572. return jsx(PreviewModal, {
  3573. validator: LinkParser.validateEpicLink,
  3574. fetch: fetch2,
  3575. isError: !entityData,
  3576. isLoading,
  3577. isRefreshing,
  3578. parser: LinkParser.parseEpicLink,
  3579. reset,
  3580. children:
  3581. entityData &&
  3582. jsxs(Fragment, {
  3583. children: [
  3584. jsx(EpicHeader, {
  3585. epic: entityData.entity,
  3586. onRefresh: () => fetch2(entityData.link, true),
  3587. }),
  3588. jsx(EpicLabels, {
  3589. epic: entityData.entity,
  3590. link: entityData.link,
  3591. refresh: () => fetch2(entityData.link, true),
  3592. }),
  3593. jsx(EpicRelatedIssues, { epic: entityData.entity }),
  3594. ],
  3595. }),
  3596. });
  3597. }
  3598.  
  3599. // apps/gitlab-plus/src/services/EpicPreview.tsx
  3600. class EpicPreview extends BaseService {
  3601. constructor() {
  3602. super(...arguments);
  3603. __publicField(this, 'name', ServiceName.EpicPreview);
  3604. }
  3605.  
  3606. init() {
  3607. RendererHelper.renderInBody(
  3608. 'glp-epic-preview-root',
  3609. jsx(EpicPreviewModal, {})
  3610. );
  3611. }
  3612. }
  3613.  
  3614. // apps/gitlab-plus/src/components/epic-status-select/useEpicStatusSelect.ts
  3615. function useEpicStatusSelect({ link }) {
  3616. const { entityData, fetch: fetch2, onRefresh } = useFetchEpic();
  3617. const { updateStatus } = useEpicLabels({
  3618. epic: entityData == null ? void 0 : entityData.entity,
  3619. link,
  3620. refetch: onRefresh,
  3621. });
  3622. useEffect(() => {
  3623. fetch2(link);
  3624. }, []);
  3625. return updateStatus;
  3626. }
  3627.  
  3628. // apps/gitlab-plus/src/components/epic-status-select/EpicStatusSelect.tsx
  3629. function EpicStatusSelect({ link }) {
  3630. const updateStatus = useEpicStatusSelect({ link });
  3631. return jsx(ChangeStatusSelect, {
  3632. ...updateStatus,
  3633. width: 75,
  3634. label: 'Status',
  3635. });
  3636. }
  3637.  
  3638. // libs/share/src/ui/Observer.ts
  3639. class Observer {
  3640. start(element, callback, options) {
  3641. this.stop();
  3642. this.observer = new MutationObserver(callback);
  3643. this.observer.observe(
  3644. element,
  3645. options || {
  3646. attributeOldValue: true,
  3647. attributes: true,
  3648. characterData: true,
  3649. characterDataOldValue: true,
  3650. childList: true,
  3651. subtree: true,
  3652. }
  3653. );
  3654. }
  3655.  
  3656. stop() {
  3657. if (this.observer) {
  3658. this.observer.disconnect();
  3659. }
  3660. }
  3661. }
  3662.  
  3663. // apps/gitlab-plus/src/services/DrawerWorkItemStatus.tsx
  3664. class DrawerWorkItemStatus {
  3665. constructor(tag, parser, renderer) {
  3666. __publicField(
  3667. this,
  3668. 'btnQuery',
  3669. 'aside [data-testid="work-item-labels"] [data-testid="edit-button"]'
  3670. );
  3671. this.tag = tag;
  3672. this.parser = parser;
  3673. this.renderer = renderer;
  3674. new Observer().start(document.body, this.onBodyChange.bind(this), {
  3675. childList: true,
  3676. subtree: true,
  3677. });
  3678. }
  3679.  
  3680. initDrawerStatusSelect(aside, attempt = 0) {
  3681. let _a;
  3682. if (attempt > 10) {
  3683. return;
  3684. }
  3685. const link = this.parser(
  3686. ((_a = aside.querySelector(
  3687. '[data-testid="work-item-drawer-ref-link"]'
  3688. )) == null
  3689. ? void 0
  3690. : _a.href) || ''
  3691. );
  3692. if (!link) {
  3693. return;
  3694. }
  3695. const editButton = aside.querySelector(this.btnQuery);
  3696. if (!editButton) {
  3697. window.setTimeout(
  3698. () => this.initDrawerStatusSelect(aside, attempt + 1),
  3699. 500
  3700. );
  3701. return;
  3702. }
  3703. RendererHelper.render(
  3704. `glp-${this.tag}-status-select`,
  3705. editButton,
  3706. this.renderer(link),
  3707. 'after'
  3708. );
  3709. }
  3710.  
  3711. onBodyChange(mutations) {
  3712. const addedNodes = mutations.flatMap((mutation) => [
  3713. ...mutation.addedNodes,
  3714. ]);
  3715. const aside = addedNodes.find((node) => node.tagName === 'ASIDE');
  3716. if (aside && aside.dataset.testid === 'work-item-drawer') {
  3717. this.initDrawerStatusSelect(aside);
  3718. }
  3719. }
  3720. }
  3721.  
  3722. // apps/gitlab-plus/src/services/EpicStatus.tsx
  3723. class EpicStatus extends BaseService {
  3724. constructor() {
  3725. super(...arguments);
  3726. __publicField(this, 'name', ServiceName.EpicStatus);
  3727. }
  3728.  
  3729. init() {
  3730. console.log(
  3731. LinkParser.validateEpicLink(window.location.href),
  3732. LinkParser.parseEpicLink(window.location.href)
  3733. );
  3734. this.setup(
  3735. this.initEpicStatusSelect.bind(this),
  3736. LinkParser.validateEpicLink
  3737. );
  3738. new DrawerWorkItemStatus('epic', LinkParser.parseEpicLink, (link) =>
  3739. jsx(EpicStatusSelect, { link })
  3740. );
  3741. }
  3742.  
  3743. initEpicStatusSelect() {
  3744. this.ready = RendererHelper.renderWithLink(
  3745. 'glp-epic-status-select',
  3746. '[data-testid="work-item-labels"] [data-testid="edit-button"]',
  3747. LinkParser.parseEpicLink,
  3748. (link) => jsx(EpicStatusSelect, { link }),
  3749. 'after'
  3750. );
  3751. }
  3752. }
  3753.  
  3754. // apps/gitlab-plus/src/components/image-preview/useImagePreviewModal.ts
  3755. function useImagePreviewModal() {
  3756. const [zoom, setZoom] = useState('contains');
  3757. const [src, setSrc] = useState('');
  3758. const validate = (element) => {
  3759. return (
  3760. element.classList.contains('no-attachment-icon') &&
  3761. /\.(png|jpg|jpeg|heic)$/.test(element.href.toLowerCase())
  3762. );
  3763. };
  3764. const getAnchor = (element) => {
  3765. if (!element) {
  3766. return void 0;
  3767. }
  3768. if (element instanceof HTMLAnchorElement) {
  3769. return validate(element) ? element : void 0;
  3770. }
  3771. if (
  3772. element instanceof HTMLImageElement &&
  3773. element.parentElement instanceof HTMLAnchorElement
  3774. ) {
  3775. return validate(element.parentElement) ? element.parentElement : void 0;
  3776. }
  3777. return void 0;
  3778. };
  3779. useEffect(() => {
  3780. document.body.addEventListener('click', (ev) => {
  3781. const anchor = getAnchor(ev.target);
  3782. if (anchor) {
  3783. setSrc(anchor.href);
  3784. ev.preventDefault();
  3785. ev.stopPropagation();
  3786. return false;
  3787. }
  3788. });
  3789. }, []);
  3790. const style = useMemo(() => {
  3791. if (zoom === 'auto') {
  3792. return {
  3793. cursor: 'zoom-out',
  3794. display: 'block',
  3795. margin: '0 auto',
  3796. padding: 15,
  3797. };
  3798. }
  3799. return {
  3800. maxWidth: '95vw',
  3801. cursor: 'zoom-in',
  3802. display: 'block',
  3803. margin: '0 auto',
  3804. maxHeight: '95vh',
  3805. };
  3806. }, [zoom]);
  3807. return {
  3808. onClose: () => {
  3809. setSrc('');
  3810. setZoom('contains');
  3811. },
  3812. onZoom: () => setZoom(zoom === 'auto' ? 'contains' : 'auto'),
  3813. src,
  3814. style,
  3815. };
  3816. }
  3817.  
  3818. // apps/gitlab-plus/src/components/image-preview/ImagePreviewModal.tsx
  3819. function ImagePreviewModal() {
  3820. const { onClose, onZoom, src, style } = useImagePreviewModal();
  3821. return jsxs('div', {
  3822. className: clsx(
  3823. 'glp-image-preview-modal',
  3824. Boolean(src) && 'glp-modal-visible'
  3825. ),
  3826. children: [
  3827. jsx('div', {
  3828. className:
  3829. 'gl-flex gl-items-center gl-overflow-auto gl-h-full gl-w-full',
  3830. children: jsx('img', {
  3831. alt: 'Image preview',
  3832. onClick: onZoom,
  3833. src,
  3834. style,
  3835. }),
  3836. }),
  3837. jsx('div', {
  3838. className: 'glp-modal-close',
  3839. onClick: onClose,
  3840. children: jsx(GitlabIcon, { icon: 'close-xs', size: 24 }),
  3841. }),
  3842. ],
  3843. });
  3844. }
  3845.  
  3846. // apps/gitlab-plus/src/services/ImagePreview.tsx
  3847. class ImagePreview extends BaseService {
  3848. constructor() {
  3849. super(...arguments);
  3850. __publicField(this, 'name', ServiceName.ImagePreview);
  3851. }
  3852.  
  3853. init() {
  3854. RendererHelper.renderInBody(
  3855. 'glp-image-preview-root',
  3856. jsx(ImagePreviewModal, {})
  3857. );
  3858. }
  3859. }
  3860.  
  3861. // apps/gitlab-plus/src/components/common/block/UsersBlock.tsx
  3862. function UsersBlock({
  3863. assign,
  3864. icon,
  3865. label,
  3866. pluralIcon,
  3867. pluralLabel,
  3868. users = [],
  3869. }) {
  3870. if (!users.length && !assign) {
  3871. return null;
  3872. }
  3873. if (!users.length && assign) {
  3874. return jsx(InfoBlock, {
  3875. icon: icon || 'user',
  3876. title: `${label}:`,
  3877. rightTitle: assign.isLoading
  3878. ? jsx(GitlabLoader, {})
  3879. : jsx(GitlabButton, {
  3880. onClick: assign.onUpdate,
  3881. children: 'Assign yourself',
  3882. }),
  3883. });
  3884. }
  3885. if (users.length === 1) {
  3886. return jsx(InfoBlock, {
  3887. icon: icon || 'user',
  3888. rightTitle: jsx(GitlabUser, { user: users[0], withLink: true }),
  3889. title: `${label}:`,
  3890. });
  3891. }
  3892. return jsx(ListBlock, {
  3893. className: 'gl-flex gl-flex-col gl-gap-3',
  3894. icon: pluralIcon || icon || 'users',
  3895. itemId: (u) => u.id,
  3896. items: users,
  3897. renderItem: (user) => jsx(GitlabUser, { user, withLink: true }),
  3898. title: pluralLabel || `${label}s`,
  3899. });
  3900. }
  3901.  
  3902. // apps/gitlab-plus/src/components/issue-preview/blocks/useIssueAssignees.ts
  3903. function useIssueAssignees({ issue, link, refetch }) {
  3904. const [isLoading, setIsLoading] = useState(false);
  3905. const onUpdate = useCallback(async () => {
  3906. setIsLoading(true);
  3907. const user = await new UsersProvider().getCurrentUser();
  3908. await new IssueProvider().issueSetAssignees({
  3909. iid: issue.iid,
  3910. assigneeUsernames: [user.data.currentUser.username],
  3911. projectPath: link.projectPath,
  3912. });
  3913. setIsLoading(false);
  3914. refetch == null ? void 0 : refetch();
  3915. }, []);
  3916. return {
  3917. isLoading,
  3918. onUpdate,
  3919. };
  3920. }
  3921.  
  3922. // apps/gitlab-plus/src/components/issue-preview/blocks/IssueAssignees.tsx
  3923. function IssueAssignees({ issue, link, refetch }) {
  3924. const { isLoading, onUpdate } = useIssueAssignees({ issue, link, refetch });
  3925. return jsx(UsersBlock, {
  3926. assign: { isLoading, onUpdate },
  3927. icon: 'assignee',
  3928. label: 'Assignee',
  3929. users: issue.assignees.nodes,
  3930. });
  3931. }
  3932.  
  3933. // apps/gitlab-plus/src/components/issue-preview/blocks/IssueEpic.tsx
  3934. function IssueEpic({ issue }) {
  3935. let _a;
  3936. if (!issue.epic) {
  3937. return null;
  3938. }
  3939. return jsx(InfoBlock, {
  3940. icon: 'epic',
  3941. title: 'Epic',
  3942. children: jsxs(Link, {
  3943. href: issue.epic.webUrl,
  3944. title: issue.epic.title,
  3945. children: [
  3946. jsx(StatusIndicator, {
  3947. label: LabelHelper.getStatusLabel(
  3948. (_a = issue.epic.labels) == null ? void 0 : _a.nodes
  3949. ),
  3950. }),
  3951. issue.epic.title,
  3952. ],
  3953. }),
  3954. });
  3955. }
  3956.  
  3957. // apps/gitlab-plus/src/components/issue-preview/blocks/IssueHeading.tsx
  3958. function IssueHeader({ issue, onRefresh }) {
  3959. return jsx(HeadingBlock, {
  3960. author: issue.author,
  3961. badge: jsx(IssueStatus$1, { isOpen: issue.state === 'opened' }),
  3962. createdAt: issue.createdAt,
  3963. entityId: `#${issue.iid}`,
  3964. icon: 'issue-type-issue',
  3965. link: issue.webUrl,
  3966. onRefresh,
  3967. title: issue.title,
  3968. });
  3969. }
  3970.  
  3971. // apps/gitlab-plus/src/components/issue-preview/blocks/IssueIteration.tsx
  3972. function IssueIteration({ issue }) {
  3973. const label = useMemo(() => {
  3974. let _a;
  3975. const date = (date2) => {
  3976. return new Intl.DateTimeFormat('en-US', {
  3977. day: 'numeric',
  3978. month: 'short',
  3979. }).format(new Date(date2));
  3980. };
  3981. if (!issue.iteration) {
  3982. return '';
  3983. }
  3984. return [
  3985. (_a = issue.iteration.iterationCadence) == null ? void 0 : _a.title,
  3986. ': ',
  3987. date(issue.iteration.startDate),
  3988. ' - ',
  3989. date(issue.iteration.dueDate),
  3990. ].join('');
  3991. }, [issue]);
  3992. if (!issue.iteration) {
  3993. return null;
  3994. }
  3995. return jsx(InfoBlock, {
  3996. icon: 'iteration',
  3997. rightTitle: label,
  3998. title: 'Iteration',
  3999. });
  4000. }
  4001.  
  4002. // apps/gitlab-plus/src/components/common/hooks/useIssueLabels.ts
  4003. function useIssueLabels({ issue, link, refetch }) {
  4004. const { labels: labels2, statusLabel } = useMemo(() => {
  4005. const labels22 = (issue == null ? void 0 : issue.labels.nodes) || [];
  4006. return {
  4007. labels: labels22,
  4008. statusLabel: LabelHelper.getStatusLabel(labels22),
  4009. };
  4010. }, [issue]);
  4011. const onStatusChange = useCallback(
  4012. async (label) => {
  4013. const updatedLabels = [
  4014. ...labels2.filter(
  4015. (label2) =>
  4016. label2.id !== (statusLabel == null ? void 0 : statusLabel.id)
  4017. ),
  4018. label,
  4019. ];
  4020. if (!issue) {
  4021. return;
  4022. }
  4023. await new IssueProvider().issueSetLabels({
  4024. iid: issue.iid,
  4025. labelIds: updatedLabels.map((l) => l.id),
  4026. projectPath: link.projectPath,
  4027. });
  4028. if (refetch) {
  4029. await refetch();
  4030. }
  4031. },
  4032. [issue, labels2, statusLabel]
  4033. );
  4034. const fetchStatusLabels = useCallback(async () => {
  4035. const response = await new LabelsProvider().getProjectLabels(
  4036. link.projectPath,
  4037. LabelHelper.getStatusPrefix()
  4038. );
  4039. return response.data.workspace.labels.nodes;
  4040. }, []);
  4041. return {
  4042. labels: labels2,
  4043. updateStatus: {
  4044. getStatusLabels: fetchStatusLabels,
  4045. onStausLabelUpdate: onStatusChange,
  4046. statusLabel: LabelHelper.getStatusLabel(labels2),
  4047. },
  4048. };
  4049. }
  4050.  
  4051. // apps/gitlab-plus/src/components/issue-preview/blocks/IssueLabels.tsx
  4052. function IssueLabels({ issue, link, refetch }) {
  4053. const { labels: labels2, updateStatus } = useIssueLabels({
  4054. issue,
  4055. link,
  4056. refetch,
  4057. });
  4058. if (!labels2.length && !updateStatus) {
  4059. return null;
  4060. }
  4061. return jsx(LabelsBlock, { labels: labels2, updateStatus });
  4062. }
  4063.  
  4064. // apps/gitlab-plus/src/components/common/MrStatus.tsx
  4065. const iconMap = {
  4066. closed: 'merge-request-close',
  4067. locked: 'search',
  4068. merged: 'merge',
  4069. opened: 'merge-request',
  4070. };
  4071. const classMap = {
  4072. closed: 'danger',
  4073. locked: 'warning',
  4074. merged: 'info',
  4075. opened: 'success',
  4076. };
  4077. const labelMap = {
  4078. closed: 'Closed',
  4079. locked: 'Locked',
  4080. merged: 'Merged',
  4081. opened: 'Opened',
  4082. };
  4083.  
  4084. function MrStatus({ state, withIcon, withLabel }) {
  4085. return jsx(GitlabBadge, {
  4086. icon: withIcon ? iconMap[state] : void 0,
  4087. label: withLabel ? labelMap[state] : void 0,
  4088. variant: classMap[state],
  4089. });
  4090. }
  4091.  
  4092. // apps/gitlab-plus/src/components/common/GitlabMergeRequest.tsx
  4093. function GitlabMergeRequest({ mr }) {
  4094. return jsxs('div', {
  4095. style: { marginTop: 10 },
  4096. children: [
  4097. jsxs(Row, {
  4098. gap: 2,
  4099. children: [
  4100. jsx(MrStatus, { state: mr.state, withIcon: true, withLabel: true }),
  4101. jsxs(Text, { variant: 'secondary', children: ['!', mr.iid] }),
  4102. jsx(GitlabUser, { size: 16, user: mr.author, withLink: true }),
  4103. ],
  4104. }),
  4105. jsx(Link, { href: mr.webUrl, title: mr.title, children: mr.title }),
  4106. ],
  4107. });
  4108. }
  4109.  
  4110. // apps/gitlab-plus/src/components/issue-preview/blocks/IssueMergeRequests.tsx
  4111. function IssueMergeRequests({ issue }) {
  4112. return jsx(ListBlock, {
  4113. icon: 'merge-request',
  4114. itemId: (mr) => mr.iid,
  4115. items: issue.relatedMergeRequests.nodes,
  4116. renderItem: (mr) => jsx(GitlabMergeRequest, { mr }),
  4117. title: 'Merge requests',
  4118. });
  4119. }
  4120.  
  4121. // apps/gitlab-plus/src/components/issue-preview/blocks/IssueMilestone.tsx
  4122. function IssueMilestone({ issue }) {
  4123. if (!issue.milestone) {
  4124. return null;
  4125. }
  4126. return jsx(InfoBlock, {
  4127. icon: 'milestone',
  4128. rightTitle: issue.milestone.title,
  4129. title: 'Milestone',
  4130. });
  4131. }
  4132.  
  4133. // apps/gitlab-plus/src/components/issue-preview/blocks/IssueRelatedIssue.tsx
  4134. const relationMap = {
  4135. blocks: 'Blocks',
  4136. is_blocked_by: 'Is blocked by',
  4137. relates_to: 'Related to',
  4138. };
  4139.  
  4140. function IssueRelatedIssue({ issue }) {
  4141. const groups = useMemo(() => {
  4142. return Object.entries(
  4143. issue.linkedWorkItems.nodes.reduce(
  4144. (acc, issue2) => ({
  4145. ...acc,
  4146. [issue2.linkType]: [...acc[issue2.linkType], issue2],
  4147. }),
  4148. {
  4149. blocks: [],
  4150. is_blocked_by: [],
  4151. relates_to: [],
  4152. }
  4153. )
  4154. ).filter(([_, issues]) => issues.length);
  4155. }, [issue]);
  4156. if (!issue.linkedWorkItems.nodes.length) {
  4157. return null;
  4158. }
  4159. return groups.map(([key, issues]) =>
  4160. jsx(
  4161. ListBlock,
  4162. {
  4163. itemId: (i) => i.workItem.iid,
  4164. items: issues,
  4165. title: `${relationMap[key]} (${issues.length}):`,
  4166. renderItem: (issue2) =>
  4167. jsxs(Link, {
  4168. href: issue2.workItem.webUrl,
  4169. blockHover: true,
  4170. children: [
  4171. jsx(StatusIndicator, {
  4172. label: LabelHelper.getStatusLabelFromWidgets(
  4173. issue2.workItem.widgets
  4174. ),
  4175. }),
  4176. '#',
  4177. issue2.workItem.iid,
  4178. ' ',
  4179. issue2.workItem.title,
  4180. ],
  4181. }),
  4182. },
  4183. key
  4184. )
  4185. );
  4186. }
  4187.  
  4188. // apps/gitlab-plus/src/components/issue-preview/useFetchIssue.ts
  4189. function useFetchIssue() {
  4190. return useFetchEntity(async (link, force = false) => {
  4191. const response = await new IssueProvider(force).getIssue(
  4192. link.projectPath,
  4193. link.issue
  4194. );
  4195. return response.data.project.issue;
  4196. });
  4197. }
  4198.  
  4199. // apps/gitlab-plus/src/components/issue-preview/IssuePreviewModal.tsx
  4200. function IssuePreviewModal() {
  4201. const {
  4202. entityData,
  4203. fetch: fetch2,
  4204. isLoading,
  4205. isRefreshing,
  4206. onRefresh,
  4207. reset,
  4208. } = useFetchIssue();
  4209. return jsx(PreviewModal, {
  4210. validator: LinkParser.validateIssueLink,
  4211. fetch: fetch2,
  4212. isError: !entityData,
  4213. isLoading,
  4214. isRefreshing,
  4215. parser: LinkParser.parseIssueLink,
  4216. reset,
  4217. children:
  4218. entityData &&
  4219. jsxs(Fragment, {
  4220. children: [
  4221. jsx(IssueHeader, { issue: entityData.entity, onRefresh }),
  4222. jsx(IssueAssignees, {
  4223. issue: entityData.entity,
  4224. link: entityData.link,
  4225. refetch: onRefresh,
  4226. }),
  4227. jsx(IssueLabels, {
  4228. issue: entityData.entity,
  4229. link: entityData.link,
  4230. refetch: onRefresh,
  4231. }),
  4232. jsx(IssueEpic, { issue: entityData.entity }),
  4233. jsx(IssueMilestone, { issue: entityData.entity }),
  4234. jsx(IssueIteration, { issue: entityData.entity }),
  4235. jsx(IssueMergeRequests, { issue: entityData.entity }),
  4236. jsx(IssueRelatedIssue, { issue: entityData.entity }),
  4237. ],
  4238. }),
  4239. });
  4240. }
  4241.  
  4242. // apps/gitlab-plus/src/services/IssuePreview.tsx
  4243. class IssuePreview extends BaseService {
  4244. constructor() {
  4245. super(...arguments);
  4246. __publicField(this, 'name', ServiceName.IssuePreview);
  4247. }
  4248.  
  4249. init() {
  4250. RendererHelper.renderInBody(
  4251. 'glp-issue-preview-root',
  4252. jsx(IssuePreviewModal, {})
  4253. );
  4254. }
  4255. }
  4256.  
  4257. // apps/gitlab-plus/src/components/issue-status-select/useIssueStatusSelect.ts
  4258. function useIssueStatusSelect({ link }) {
  4259. const { entityData, fetch: fetch2, onRefresh } = useFetchIssue();
  4260. const { updateStatus } = useIssueLabels({
  4261. issue: entityData == null ? void 0 : entityData.entity,
  4262. link,
  4263. refetch: onRefresh,
  4264. });
  4265. useEffect(() => {
  4266. fetch2(link);
  4267. }, []);
  4268. return updateStatus;
  4269. }
  4270.  
  4271. // apps/gitlab-plus/src/components/issue-status-select/IssueStatusSelect.tsx
  4272. function IssueStatusSelect({ link }) {
  4273. const updateStatus = useIssueStatusSelect({ link });
  4274. return jsx(ChangeStatusSelect, {
  4275. ...updateStatus,
  4276. width: 75,
  4277. label: 'Status',
  4278. });
  4279. }
  4280.  
  4281. // apps/gitlab-plus/src/services/IssueStatus.tsx
  4282. class IssueStatus extends BaseService {
  4283. constructor() {
  4284. super(...arguments);
  4285. __publicField(this, 'name', ServiceName.IssueStatus);
  4286. }
  4287.  
  4288. init() {
  4289. this.setup(
  4290. this.initIssuesStausSelect.bind(this),
  4291. LinkParser.validateIssueLink
  4292. );
  4293. new DrawerWorkItemStatus('issue', LinkParser.parseIssueLink, (link) =>
  4294. jsx(IssueStatusSelect, { link })
  4295. );
  4296. }
  4297.  
  4298. async initIssuesStausSelect() {
  4299. this.ready = RendererHelper.renderWithLink(
  4300. 'glp-issue-status-select',
  4301. '[data-testid="work-item-labels"] [data-testid="edit-button"]',
  4302. LinkParser.parseIssueLink,
  4303. (link) => jsx(IssueStatusSelect, { link }),
  4304. 'after'
  4305. );
  4306. }
  4307. }
  4308.  
  4309. // apps/gitlab-plus/src/components/mr-preview/blocks/MrApprovedBy.tsx
  4310. function MrApprovedBy({ mr }) {
  4311. return jsx(UsersBlock, {
  4312. label: 'Approved by',
  4313. pluralLabel: 'Approved by',
  4314. users: mr.approvedBy.nodes,
  4315. });
  4316. }
  4317.  
  4318. // apps/gitlab-plus/src/components/mr-preview/blocks/MrAssignee.tsx
  4319. function MrAssignee({ mr }) {
  4320. return jsx(UsersBlock, {
  4321. icon: 'assignee',
  4322. label: 'Assignee',
  4323. users: mr.assignees.nodes,
  4324. });
  4325. }
  4326.  
  4327. // apps/gitlab-plus/src/components/mr-preview/blocks/MrBranch.tsx
  4328. function MrBranch({ mr }) {
  4329. return jsx(InfoBlock, {
  4330. icon: 'branch',
  4331. title: 'Merge',
  4332. children: jsxs('span', {
  4333. children: [
  4334. jsx(Text, { children: mr.sourceBranch }),
  4335. jsx(Text, {
  4336. className: 'gl-mx-2',
  4337. variant: 'secondary',
  4338. children: 'in to',
  4339. }),
  4340. jsx(Text, { children: mr.targetBranch }),
  4341. ],
  4342. }),
  4343. });
  4344. }
  4345.  
  4346. // apps/gitlab-plus/src/components/mr-preview/blocks/MrDiff.tsx
  4347. function MrDiff({ mr }) {
  4348. const label = useMemo(() => {
  4349. if (mr.diffStatsSummary.fileCount === 1) {
  4350. return '1 file';
  4351. }
  4352. return `${mr.diffStatsSummary.fileCount} files`;
  4353. }, [mr.diffStatsSummary.fileCount]);
  4354. return jsx(InfoBlock, {
  4355. icon: 'commit',
  4356. title: `Commit: ${mr.commitCount}`,
  4357. rightTitle: jsxs(Row, {
  4358. gap: 2,
  4359. items: 'center',
  4360. children: [
  4361. jsx(GitlabIcon, { icon: 'doc-code', size: 16 }),
  4362. jsx(Text, { size: 'subtle', weight: 'bold', children: label }),
  4363. jsxs(Text, {
  4364. color: 'success',
  4365. weight: 'bold',
  4366. children: ['+', mr.diffStatsSummary.additions],
  4367. }),
  4368. jsxs(Text, {
  4369. color: 'danger',
  4370. weight: 'bold',
  4371. children: ['-', mr.diffStatsSummary.deletions],
  4372. }),
  4373. ],
  4374. }),
  4375. });
  4376. }
  4377.  
  4378. // apps/gitlab-plus/src/components/mr-preview/blocks/MrDiscussion.tsx
  4379. function MrDiscussion({ mr }) {
  4380. const [resolved, total] = [
  4381. mr.resolvedDiscussionsCount,
  4382. mr.resolvableDiscussionsCount,
  4383. ];
  4384. if (!total) {
  4385. return null;
  4386. }
  4387. const { label, title } = useMemo(() => {
  4388. const plural = total !== 1 ? 's' : '';
  4389. return {
  4390. label: `${resolved} of ${total}`,
  4391. title: `${resolved} of ${total} thread${plural} resolved`,
  4392. };
  4393. }, [mr]);
  4394. return jsx(InfoBlock, {
  4395. icon: 'comments',
  4396. title: 'Discussion',
  4397. rightTitle: jsx(GitlabBadge, {
  4398. icon: 'comments',
  4399. label,
  4400. title,
  4401. variant: resolved === total ? 'success' : 'muted',
  4402. }),
  4403. });
  4404. }
  4405.  
  4406. // libs/share/src/utils/textWithChild.ts
  4407. function textWithChild(text, pattern, replacer) {
  4408. const matches = text.match(RegExp(pattern, 'g'));
  4409. const parts = text.split(RegExp(pattern, 'g'));
  4410. if (!(matches == null ? void 0 : matches.length)) {
  4411. return text;
  4412. }
  4413. return parts.reduce((items, text2, index) => {
  4414. const textToReplace = index < matches.length ? matches[index] : void 0;
  4415. return [
  4416. ...items,
  4417. text2,
  4418. ...(textToReplace ? [replacer(textToReplace)] : []),
  4419. ];
  4420. }, []);
  4421. }
  4422.  
  4423. // apps/gitlab-plus/src/components/mr-preview/blocks/MrHeading.tsx
  4424. function MrHeader({ mr, onRefresh }) {
  4425. const title = useMemo(() => {
  4426. const issueLink = (id) =>
  4427. `${mr.project.webUrl}/-/issues/${id.replace(/\D+/g, '')}`;
  4428. return textWithChild(mr.title, /#\d+/, (id) =>
  4429. jsx(Link, { href: issueLink(id), inline: true, children: id })
  4430. );
  4431. }, [mr]);
  4432. return jsx(HeadingBlock, {
  4433. author: mr.author,
  4434. createdAt: mr.createdAt,
  4435. entityId: `!${mr.iid}`,
  4436. icon: 'merge-request',
  4437. link: mr.webUrl,
  4438. onRefresh,
  4439. title,
  4440. badge: jsxs(Row, {
  4441. className: 'gl-gap-2',
  4442. items: 'center',
  4443. children: [
  4444. jsx(MrStatus, { state: mr.state, withIcon: true, withLabel: true }),
  4445. Boolean(mr.approvedBy.nodes.length) &&
  4446. jsx(GitlabBadge, {
  4447. icon: 'check-circle',
  4448. label: 'Approved',
  4449. variant: 'success',
  4450. }),
  4451. mr.conflicts &&
  4452. jsx(GitlabIcon, {
  4453. icon: 'warning-solid',
  4454. size: 16,
  4455. title: 'Merge request can not be merged',
  4456. }),
  4457. ],
  4458. }),
  4459. });
  4460. }
  4461.  
  4462. // apps/gitlab-plus/src/components/mr-preview/blocks/MrLabels.tsx
  4463. function MrLabels({ mr }) {
  4464. if (!mr.labels.nodes.length) {
  4465. return null;
  4466. }
  4467. return jsx(InfoBlock, {
  4468. className: 'issuable-show-labels',
  4469. title: 'Labels',
  4470. children: mr.labels.nodes.map((label) =>
  4471. jsx(GitlabLabel, { label }, label.id)
  4472. ),
  4473. });
  4474. }
  4475.  
  4476. // apps/gitlab-plus/src/providers/query/mr.ts
  4477. const mrQuery = `query MergeRequestQuery($fullPath: ID!, $iid: String!) {
  4478. workspace: project(fullPath: $fullPath) {
  4479. mergeRequest(iid: $iid) {
  4480. id
  4481. iid
  4482. assignees {
  4483. nodes {
  4484. ...UserFragment
  4485. }
  4486. }
  4487. approvedBy {
  4488. nodes {
  4489. ...UserFragment
  4490. }
  4491. }
  4492. author {
  4493. ...UserFragment
  4494. }
  4495. project {
  4496. webUrl
  4497. path
  4498. fullPath
  4499. }
  4500. commitCount
  4501. conflicts
  4502. createdAt
  4503. title
  4504. titleHtml
  4505. diffStatsSummary {
  4506. additions
  4507. changes
  4508. deletions
  4509. fileCount
  4510. }
  4511. draft
  4512. labels {
  4513. nodes {
  4514. ...LabelFragment
  4515. }
  4516. }
  4517. mergeable
  4518. resolvedDiscussionsCount
  4519. resolvableDiscussionsCount
  4520. reviewers {
  4521. nodes {
  4522. ...UserFragment
  4523. }
  4524. }
  4525. shouldBeRebased
  4526. sourceBranch
  4527. targetBranch
  4528. state
  4529. webUrl
  4530. }
  4531. }
  4532. }
  4533.  
  4534. ${userFragment}
  4535. ${labelFragment}
  4536. `;
  4537.  
  4538. // apps/gitlab-plus/src/providers/MrProvider.ts
  4539. class MrProvider extends GitlabProvider {
  4540. async getMr(projectPath, mrId) {
  4541. return this.queryCached(
  4542. `mr-${projectPath}-${mrId}`,
  4543. mrQuery,
  4544. {
  4545. iid: mrId,
  4546. fullPath: projectPath,
  4547. },
  4548. 2
  4549. );
  4550. }
  4551. }
  4552.  
  4553. // apps/gitlab-plus/src/components/mr-preview/useFetchMr.ts
  4554. function useFetchMr() {
  4555. return useFetchEntity(async (link, force = false) => {
  4556. const response = await new MrProvider(force).getMr(
  4557. link.projectPath,
  4558. link.mr
  4559. );
  4560. return response.data.workspace.mergeRequest;
  4561. });
  4562. }
  4563.  
  4564. // apps/gitlab-plus/src/components/mr-preview/MrPreviewModal.tsx
  4565. function MrPreviewModal() {
  4566. const {
  4567. entityData,
  4568. fetch: fetch2,
  4569. isLoading,
  4570. isRefreshing,
  4571. reset,
  4572. } = useFetchMr();
  4573. return jsx(PreviewModal, {
  4574. validator: LinkParser.validateMrLink,
  4575. fetch: fetch2,
  4576. isError: !entityData,
  4577. isLoading,
  4578. isRefreshing,
  4579. parser: LinkParser.parseMrLink,
  4580. reset,
  4581. children:
  4582. entityData &&
  4583. jsxs(Fragment, {
  4584. children: [
  4585. jsx(MrHeader, {
  4586. mr: entityData.entity,
  4587. onRefresh: () => fetch2(entityData.link, true),
  4588. }),
  4589. jsx(MrBranch, { mr: entityData.entity }),
  4590. jsx(MrAssignee, { mr: entityData.entity }),
  4591. jsx(MrApprovedBy, { mr: entityData.entity }),
  4592. jsx(MrLabels, { mr: entityData.entity }),
  4593. jsx(MrDiff, { mr: entityData.entity }),
  4594. jsx(MrDiscussion, { mr: entityData.entity }),
  4595. ],
  4596. }),
  4597. });
  4598. }
  4599.  
  4600. // apps/gitlab-plus/src/services/MrPreview.tsx
  4601. class MrPreview extends BaseService {
  4602. constructor() {
  4603. super(...arguments);
  4604. __publicField(this, 'name', ServiceName.MrPreview);
  4605. }
  4606.  
  4607. init() {
  4608. RendererHelper.renderInBody('glp-mr-preview-root', jsx(MrPreviewModal, {}));
  4609. }
  4610. }
  4611.  
  4612. // apps/gitlab-plus/src/components/related-issue-autocomplete/useRelatedIssuesAutocompleteModal.ts
  4613. function useRelatedIssuesAutocompleteModal(link, input) {
  4614. const [searchTerm, setSearchTerm] = useState('');
  4615. const [isVisible, setIsVisible] = useState(false);
  4616. const searchIssues = useCallback(async (term) => {
  4617. const response = await new IssueProvider().getIssues(
  4618. link.workspacePath,
  4619. term
  4620. );
  4621. return [
  4622. response.data.workspace.workItems,
  4623. response.data.workspace.workItemsByIid,
  4624. response.data.workspace.workItemsEmpty,
  4625. ].flatMap((item) => (item == null ? void 0 : item.nodes) || []);
  4626. }, []);
  4627. const options = useAsyncAutocompleteOptions(searchTerm, searchIssues);
  4628. const onSelect = (item) => {
  4629. input.value = `${item.project.fullPath}#${item.iid} `;
  4630. input.dispatchEvent(new Event('input'));
  4631. input.dispatchEvent(new Event('change'));
  4632. };
  4633. useEffect(() => {
  4634. document.body.addEventListener('click', (e) => {
  4635. if (e.target !== input && !input.contains(e.target)) {
  4636. setIsVisible(false);
  4637. }
  4638. });
  4639. input.addEventListener('click', () => setIsVisible(true));
  4640. }, []);
  4641. return {
  4642. isVisible,
  4643. onClose: () => setIsVisible(false),
  4644. onSelect,
  4645. options,
  4646. searchTerm,
  4647. setSearchTerm,
  4648. };
  4649. }
  4650.  
  4651. // apps/gitlab-plus/src/components/related-issue-autocomplete/RelatedIssuesAutocompleteModal.tsx
  4652. function RelatedIssuesAutocompleteModal({ input, link }) {
  4653. const { isVisible, onClose, onSelect, options, searchTerm, setSearchTerm } =
  4654. useRelatedIssuesAutocompleteModal(link, input);
  4655. if (!isVisible) {
  4656. return null;
  4657. }
  4658. return jsx('div', {
  4659. class: 'gl-relative gl-w-full gl-new-dropdown !gl-block',
  4660. children: jsx(AsyncAutocompleteDropdown, {
  4661. hideCheckbox: true,
  4662. onClick: onSelect,
  4663. onClose,
  4664. options,
  4665. searchTerm,
  4666. setSearchTerm,
  4667. value: [],
  4668. renderOption: (item) =>
  4669. jsxs('div', {
  4670. class: 'gl-flex gl-gap-x-2 gl-py-2',
  4671. children: [
  4672. jsx(GitlabIcon, { icon: 'issue-type-issue', size: 16 }),
  4673. jsx('small', { children: item.iid }),
  4674. jsx('span', {
  4675. class: 'gl-flex gl-flex-wrap',
  4676. children: item.title,
  4677. }),
  4678. ],
  4679. }),
  4680. }),
  4681. });
  4682. }
  4683.  
  4684. // apps/gitlab-plus/src/services/RelatedIssueAutocomplete.tsx
  4685. class RelatedIssueAutocomplete extends BaseService {
  4686. constructor() {
  4687. super(...arguments);
  4688. __publicField(this, 'name', ServiceName.RelatedIssueAutocomplete);
  4689. __publicField(this, 'readyClass', 'glp-input-ready');
  4690. }
  4691.  
  4692. init() {
  4693. this.setup(this.initObserver.bind(this), LinkParser.validateIssueLink);
  4694. }
  4695.  
  4696. initAutocomplete(section) {
  4697. const input = section.querySelector('#add-related-issues-form-input');
  4698. const link = LinkParser.parseIssueLink(window.location.href);
  4699. if (!input || this.isMounted(input) || !link) {
  4700. return;
  4701. }
  4702. const container = input.closest('.add-issuable-form-input-wrapper');
  4703. if (!container || document.querySelector('.related-issues-autocomplete')) {
  4704. return;
  4705. }
  4706. RendererHelper.render(
  4707. 'related-issues-autocomplete',
  4708. container,
  4709. jsx(RelatedIssuesAutocompleteModal, { input, link })
  4710. );
  4711. }
  4712.  
  4713. initObserver() {
  4714. const section = document.querySelector('#related-issues');
  4715. if (this.ready || !section) {
  4716. return;
  4717. }
  4718. this.ready = true;
  4719. const observer = new MutationObserver((mutations) => {
  4720. mutations.forEach((mutation) => {
  4721. if (mutation.type === 'childList') {
  4722. this.initAutocomplete(section);
  4723. }
  4724. });
  4725. });
  4726. observer.observe(section, {
  4727. childList: true,
  4728. });
  4729. }
  4730.  
  4731. isMounted(input) {
  4732. return input.classList.contains(this.readyClass);
  4733. }
  4734. }
  4735.  
  4736. // apps/gitlab-plus/src/services/RelatedIssuesLabelStatus.tsx
  4737. class RelatedIssuesLabelStatus extends BaseService {
  4738. constructor() {
  4739. super(...arguments);
  4740. __publicField(this, 'name', ServiceName.RelatedIssuesLabelStatus);
  4741. }
  4742.  
  4743. init() {
  4744. this.setup(this.initIssuesList.bind(this), LinkParser.validateIssueLink);
  4745. }
  4746.  
  4747. initIssuesList() {
  4748. const lists = document.querySelectorAll(
  4749. '#related-issues .related-items-list'
  4750. );
  4751. const link = LinkParser.parseIssueLink(window.location.href);
  4752. if (!lists.length || !link) {
  4753. return;
  4754. }
  4755. this.ready = true;
  4756. const items = [...lists].flatMap((list) => [
  4757. ...list.querySelectorAll('li'),
  4758. ]);
  4759. this.updateIssuesItem(link, items);
  4760. }
  4761.  
  4762. async updateIssuesItem(link, items) {
  4763. const response = await new IssueProvider().getIssueWithRelatedIssuesLabels(
  4764. link.projectPath,
  4765. link.issue
  4766. );
  4767. const issueStatusMap =
  4768. response.data.project.issue.linkedWorkItems.nodes.reduce((acc, value) => {
  4769. return {
  4770. ...acc,
  4771. [value.workItem.id.replace(/\D/g, '')]:
  4772. LabelHelper.getStatusLabelFromWidgets(value.workItem.widgets),
  4773. };
  4774. }, {});
  4775. items.forEach((item) => {
  4776. if (!item.dataset.key || !issueStatusMap[item.dataset.key]) {
  4777. return;
  4778. }
  4779. const statusLabel = issueStatusMap[item.dataset.key];
  4780. const infoArea = item.querySelector('.item-attributes-area');
  4781. if (infoArea && statusLabel) {
  4782. RendererHelper.render(
  4783. 'glp-status-label',
  4784. infoArea,
  4785. jsx(GitlabLabel, { label: statusLabel }),
  4786. 'prepend'
  4787. );
  4788. }
  4789. });
  4790. }
  4791. }
  4792.  
  4793. // libs/share/src/ui/Component.ts
  4794. class Component {
  4795. constructor(tag, props = {}) {
  4796. this.element = Dom.create({ tag, ...props });
  4797. }
  4798.  
  4799. addClassName(...className) {
  4800. this.element.classList.add(...className);
  4801. }
  4802.  
  4803. event(event, callback) {
  4804. this.element.addEventListener(event, callback);
  4805. }
  4806.  
  4807. getElement() {
  4808. return this.element;
  4809. }
  4810.  
  4811. mount(parent) {
  4812. parent.appendChild(this.element);
  4813. }
  4814. }
  4815.  
  4816. // libs/share/src/ui/SvgComponent.ts
  4817. class SvgComponent {
  4818. constructor(tag, props = {}) {
  4819. this.element = Dom.createSvg({ tag, ...props });
  4820. }
  4821.  
  4822. addClassName(...className) {
  4823. this.element.classList.add(...className);
  4824. }
  4825.  
  4826. event(event, callback) {
  4827. this.element.addEventListener(event, callback);
  4828. }
  4829.  
  4830. getElement() {
  4831. return this.element;
  4832. }
  4833.  
  4834. mount(parent) {
  4835. parent.appendChild(this.element);
  4836. }
  4837. }
  4838.  
  4839. // libs/share/src/ui/Dom.ts
  4840. class Dom {
  4841. static appendChildren(element, children, isSvgMode = false) {
  4842. if (children) {
  4843. element.append(
  4844. ...Dom.array(children).map((item) => {
  4845. if (typeof item === 'string') {
  4846. return document.createTextNode(item);
  4847. }
  4848. if (item instanceof HTMLElement || item instanceof SVGElement) {
  4849. return item;
  4850. }
  4851. if (item instanceof Component || item instanceof SvgComponent) {
  4852. return item.getElement();
  4853. }
  4854. const isSvg =
  4855. 'svg' === item.tag
  4856. ? true
  4857. : 'foreignObject' === item.tag
  4858. ? false
  4859. : isSvgMode;
  4860. if (isSvg) {
  4861. return Dom.createSvg(item);
  4862. }
  4863. return Dom.create(item);
  4864. })
  4865. );
  4866. }
  4867. }
  4868.  
  4869. static applyAttrs(element, attrs) {
  4870. if (attrs) {
  4871. Object.entries(attrs).forEach(([key, value]) => {
  4872. if (value === void 0 || value === false) {
  4873. element.removeAttribute(key);
  4874. } else {
  4875. element.setAttribute(key, `${value}`);
  4876. }
  4877. });
  4878. }
  4879. }
  4880.  
  4881. static applyClass(element, classes) {
  4882. if (classes) {
  4883. element.classList.add(...classes.split(' ').filter(Boolean));
  4884. }
  4885. }
  4886.  
  4887. static applyEvents(element, events) {
  4888. if (events) {
  4889. Object.entries(events).forEach(([name2, callback]) => {
  4890. element.addEventListener(name2, callback);
  4891. });
  4892. }
  4893. }
  4894.  
  4895. static applyStyles(element, styles) {
  4896. if (styles) {
  4897. Object.entries(styles).forEach(([key, value]) => {
  4898. const name2 = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
  4899. element.style.setProperty(name2, value);
  4900. });
  4901. }
  4902. }
  4903.  
  4904. static array(element) {
  4905. return Array.isArray(element) ? element : [element];
  4906. }
  4907.  
  4908. static create(data) {
  4909. const element = document.createElement(data.tag);
  4910. Dom.appendChildren(element, data.children);
  4911. Dom.applyClass(element, data.classes);
  4912. Dom.applyAttrs(element, data.attrs);
  4913. Dom.applyEvents(element, data.events);
  4914. Dom.applyStyles(element, data.styles);
  4915. return element;
  4916. }
  4917.  
  4918. static createSvg(data) {
  4919. const element = document.createElementNS(
  4920. 'http://www.w3.org/2000/svg',
  4921. data.tag
  4922. );
  4923. Dom.appendChildren(element, data.children, true);
  4924. Dom.applyClass(element, data.classes);
  4925. Dom.applyAttrs(element, data.attrs);
  4926. Dom.applyEvents(element, data.events);
  4927. Dom.applyStyles(element, data.styles);
  4928. return element;
  4929. }
  4930.  
  4931. static element(tag, classes, children) {
  4932. return Dom.create({ tag, children, classes });
  4933. }
  4934.  
  4935. static elementSvg(tag, classes, children) {
  4936. return Dom.createSvg({ tag, children, classes });
  4937. }
  4938. }
  4939.  
  4940. // apps/gitlab-plus/src/services/SortIssue.ts
  4941. const sortWeight = {
  4942. ['issue']: 4,
  4943. ['label']: 0,
  4944. ['ownIssue']: 10,
  4945. ['ownUserStory']: 8,
  4946. ['unknown']: 2,
  4947. ['userStory']: 6,
  4948. };
  4949.  
  4950. class SortIssue extends BaseService {
  4951. constructor() {
  4952. super(...arguments);
  4953. __publicField(this, 'name', ServiceName.SortIssue);
  4954. __publicField(this, 'userName', '');
  4955. }
  4956.  
  4957. init() {
  4958. this.start();
  4959. }
  4960.  
  4961. childType(child) {
  4962. if (child instanceof HTMLDivElement) {
  4963. return 'label';
  4964. }
  4965. const title = child.querySelector('[data-testid="board-card-title-link"]');
  4966. if (!title) {
  4967. return 'unknown';
  4968. }
  4969. const isOwn = [...child.querySelectorAll('.gl-avatar-link img')].some(
  4970. (img) => img.alt.includes(this.userName)
  4971. );
  4972. const isUserStory = [...child.querySelectorAll('.gl-label')].some((span) =>
  4973. span.innerText.includes('User Story')
  4974. );
  4975. if (isUserStory && isOwn) {
  4976. return 'ownUserStory';
  4977. }
  4978. if (isOwn) {
  4979. return 'ownIssue';
  4980. }
  4981. if (isUserStory) {
  4982. return 'userStory';
  4983. }
  4984. return 'issue';
  4985. }
  4986.  
  4987. initBoard(board) {
  4988. Dom.applyClass(board, 'glp-ready');
  4989. const observer = new Observer();
  4990. observer.start(board, () => this.sortBoard(board), {
  4991. childList: true,
  4992. });
  4993. }
  4994.  
  4995. run() {
  4996. [...document.querySelectorAll('.board-list:not(.glp-ready)')].forEach(
  4997. (board) => this.initBoard(board)
  4998. );
  4999. }
  5000.  
  5001. shouldSort(items) {
  5002. return items.some((item) => {
  5003. return ['ownIssue', 'ownUserStory'].includes(item.type);
  5004. });
  5005. }
  5006.  
  5007. sortBoard(board) {
  5008. Dom.applyStyles(board, {
  5009. display: 'flex',
  5010. flexDirection: 'column',
  5011. });
  5012. const children = [...board.children].map((element) => ({
  5013. element,
  5014. type: this.childType(element),
  5015. }));
  5016. if (!this.shouldSort(children)) {
  5017. return;
  5018. }
  5019. this.sortChildren(children).forEach(({ element }, index) => {
  5020. const order =
  5021. index !== children.length - 1 ? index + 1 : children.length + 100;
  5022. element.style.order = `${order}`;
  5023. });
  5024. }
  5025.  
  5026. sortChildren(items) {
  5027. return items.toSorted((a, b) => {
  5028. return Math.sign(sortWeight[b.type] - sortWeight[a.type]);
  5029. });
  5030. }
  5031.  
  5032. async start() {
  5033. const response = await new UsersProvider().getCurrentUser();
  5034. this.userName = response.data.currentUser.username;
  5035. const observer = new Observer();
  5036. const board = document.querySelector('.boards-list');
  5037. if (board) {
  5038. observer.start(board, () => this.run());
  5039. }
  5040. }
  5041. }
  5042.  
  5043. // apps/gitlab-plus/src/components/user-settings/UserSettingsButton.tsx
  5044. function UserSettingsButton() {
  5045. return jsx('span', {
  5046. className: 'gl-new-dropdown-item-content',
  5047. onClick: () =>
  5048. document.dispatchEvent(
  5049. new CustomEvent(ModalEvents.showUserSettingsModal)
  5050. ),
  5051. children: jsxs('span', {
  5052. className: 'gl-new-dropdown-item-text-wrapper',
  5053. children: [
  5054. jsx('span', { style: { color: '#e24329' }, children: 'Gitlab Plus' }),
  5055. ' settings',
  5056. ],
  5057. }),
  5058. });
  5059. }
  5060.  
  5061. // apps/gitlab-plus/src/components/common/base/Column.tsx
  5062. function Column({ children, className, gap, items, justify }) {
  5063. return jsx('div', {
  5064. class: clsx(
  5065. 'gl-flex gl-flex-col',
  5066. justify && `gl-justify-${justify}`,
  5067. items && `gl-items-${items}`,
  5068. gap && `gl-gap-${gap}`,
  5069. className
  5070. ),
  5071. children,
  5072. });
  5073. }
  5074.  
  5075. // apps/gitlab-plus/src/components/common/GitlabSwitch.tsx
  5076. function GitlabSwitch({ checked, disabled, onChange }) {
  5077. return jsx('button', {
  5078. 'aria-checked': checked,
  5079. 'aria-disabled': disabled,
  5080. disabled,
  5081. onClick: () => onChange(!checked),
  5082. role: 'switch',
  5083. type: 'button',
  5084. className: clsx(
  5085. 'gl-toggle gl-shrink-0',
  5086. checked && 'is-checked',
  5087. disabled && 'is-disabled'
  5088. ),
  5089. children: jsx('span', {
  5090. className: 'toggle-icon',
  5091. children: jsx(GitlabIcon, { icon: checked ? 'check-xs' : 'close-xs' }),
  5092. }),
  5093. });
  5094. }
  5095.  
  5096. // apps/gitlab-plus/src/components/user-settings/UserConfigForm.tsx
  5097. function UserConfigForm({ setValue, value }) {
  5098. const [isEditable, setIsEditable] = useState(false);
  5099. const [inputValue, setInputValue] = useState(value);
  5100. return jsx(Row, {
  5101. gap: 2,
  5102. items: 'center',
  5103. justify: 'end',
  5104. children: isEditable
  5105. ? jsxs(Fragment, {
  5106. children: [
  5107. jsx('input', {
  5108. className: clsx('gl-form-input form-control gl-form-input-sm'),
  5109. onInput: (e) => setInputValue(e.target.value),
  5110. value: inputValue,
  5111. }),
  5112. jsx(GitlabButton, {
  5113. className: 'btn-icon',
  5114. icon: 'check-sm',
  5115. iconSize: 16,
  5116. onClick: () => {
  5117. setIsEditable(false);
  5118. setValue(inputValue);
  5119. },
  5120. }),
  5121. jsx(GitlabButton, {
  5122. className: 'btn-icon',
  5123. icon: 'close',
  5124. iconSize: 16,
  5125. onClick: () => {
  5126. setIsEditable(false);
  5127. setInputValue(value);
  5128. },
  5129. }),
  5130. ],
  5131. })
  5132. : jsxs(Fragment, {
  5133. children: [
  5134. jsx(Text, { weight: 'bold', children: value }),
  5135. jsx(GitlabButton, {
  5136. className: 'btn-icon',
  5137. icon: 'pencil',
  5138. iconSize: 16,
  5139. onClick: () => setIsEditable(true),
  5140. }),
  5141. ],
  5142. }),
  5143. });
  5144. }
  5145.  
  5146. // apps/gitlab-plus/src/components/user-settings/useUserSettingsModal.tsx
  5147. function useUserSettingsModal() {
  5148. const [refreshFlag, setRefreshFlag] = useState(false);
  5149. const services = useMemo(() => {
  5150. return Object.entries(servicesConfig)
  5151. .map(([name2, config]) => ({
  5152. isActive: Boolean(userSettingsStore.isActive(name2)),
  5153. isExperimental: config.experimental,
  5154. isRequired: config.required,
  5155. label: config.label,
  5156. name: name2,
  5157. }))
  5158. .sort((a, b) => {
  5159. if (a.isRequired || b.isRequired) {
  5160. return a.isRequired ? 1 : -1;
  5161. }
  5162. if (a.isExperimental || b.isExperimental) {
  5163. return a.isExperimental ? 1 : -1;
  5164. }
  5165. return a.name.localeCompare(b.name);
  5166. });
  5167. }, [refreshFlag]);
  5168. const configs = useMemo(() => {
  5169. return Object.values(UserConfig).map((name2) => ({
  5170. label: configLabels[name2],
  5171. name: name2,
  5172. value: userSettingsStore.getConfig(name2),
  5173. }));
  5174. }, [refreshFlag]);
  5175. return {
  5176. configs,
  5177. services,
  5178. setConfig: (name2, value) => {
  5179. userSettingsStore.setConfig(name2, value);
  5180. setRefreshFlag((flag) => !flag);
  5181. },
  5182. setServiceState: (name2, value) => {
  5183. userSettingsStore.setIsActive(name2, value);
  5184. setRefreshFlag((flag) => !flag);
  5185. },
  5186. };
  5187. }
  5188.  
  5189. // apps/gitlab-plus/src/components/user-settings/UserSettingsModal.tsx
  5190. function UserSettingModal() {
  5191. const { isVisible, onClose } = useGlpModal(ModalEvents.showUserSettingsModal);
  5192. const { configs, services, setConfig, setServiceState } =
  5193. useUserSettingsModal();
  5194. return jsx(GlpModal, {
  5195. isVisible,
  5196. onClose,
  5197. title: jsxs(Fragment, {
  5198. children: [
  5199. jsx('span', { style: { color: '#e24329' }, children: 'Gitlab Plus' }),
  5200. ' settings',
  5201. ],
  5202. }),
  5203. children: jsxs(Column, {
  5204. className: 'gl-p-4',
  5205. gap: 2,
  5206. children: [
  5207. configs.map((config) =>
  5208. jsxs(
  5209. Row,
  5210. {
  5211. gap: 2,
  5212. items: 'center',
  5213. justify: 'between',
  5214. children: [
  5215. jsx(Text, { children: config.label }),
  5216. jsx(UserConfigForm, {
  5217. setValue: (value) => setConfig(config.name, value),
  5218. value: config.value,
  5219. }),
  5220. ],
  5221. },
  5222. config.name
  5223. )
  5224. ),
  5225. jsx('hr', { class: 'gl-my-2' }),
  5226. services.map((service) =>
  5227. jsxs(Row, {
  5228. gap: 2,
  5229. items: 'center',
  5230. children: [
  5231. jsx(GitlabSwitch, {
  5232. checked: service.isActive,
  5233. disabled: service.isRequired,
  5234. onChange: (value) => setServiceState(service.name, value),
  5235. }),
  5236. jsx(Text, {
  5237. variant: service.isRequired ? 'secondary' : void 0,
  5238. children: service.label,
  5239. }),
  5240. service.isExperimental &&
  5241. jsx(GitlabBadge, { label: 'Experimental', variant: 'warning' }),
  5242. service.isRequired &&
  5243. jsx(GitlabBadge, { label: 'Required', variant: 'muted' }),
  5244. ],
  5245. })
  5246. ),
  5247. ],
  5248. }),
  5249. });
  5250. }
  5251.  
  5252. // apps/gitlab-plus/src/services/UserSettings.tsx
  5253. class UserSettings extends BaseService {
  5254. constructor() {
  5255. super(...arguments);
  5256. __publicField(this, 'name', ServiceName.UserSettings);
  5257. }
  5258.  
  5259. init() {
  5260. this.setup(this.initUserSettings.bind(this));
  5261. }
  5262.  
  5263. getMenuItem() {
  5264. const userMenu = document.querySelector('[data-testid="preferences-item"]');
  5265. if (!userMenu || !userMenu.parentElement) {
  5266. return void 0;
  5267. }
  5268. const li = document.createElement('li');
  5269. li.className = 'gl-new-dropdown-item';
  5270. userMenu.parentElement.append(li);
  5271. return li;
  5272. }
  5273.  
  5274. initUserSettings() {
  5275. const userMenu = this.getMenuItem();
  5276. if (!userMenu) {
  5277. return;
  5278. }
  5279. this.ready = true;
  5280. RendererHelper.renderInNode(userMenu, jsx(UserSettingsButton, {}));
  5281. RendererHelper.renderInBody(
  5282. 'glp-user-settings-root',
  5283. jsx(UserSettingModal, {})
  5284. );
  5285. }
  5286. }
  5287.  
  5288. // apps/gitlab-plus/src/main.ts
  5289. [
  5290. ClearCacheService,
  5291. ImagePreview,
  5292. MrPreview,
  5293. EpicPreview,
  5294. IssuePreview,
  5295. CreateRelatedIssue,
  5296. CreateChildIssue,
  5297. RelatedIssueAutocomplete,
  5298. RelatedIssuesLabelStatus,
  5299. SortIssue,
  5300. UserSettings,
  5301. IssueStatus,
  5302. EpicStatus,
  5303. ].forEach((Service) => {
  5304. const service = new Service();
  5305. if (userSettingsStore.isActive(service.name)) {
  5306. service.init();
  5307. }
  5308. });