Gitlab plus

Gitlab utils

目前为 2024-12-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         Gitlab plus
// @namespace    https://lukaszmical.pl/
// @version      2024-12-30
// @description  Gitlab utils
// @author       Łukasz Micał
// @match        https://gitlab.com/*
// @require      https://cdn.jsdelivr.net/combine/npm/[email protected]/dist/preact.min.umd.min.js,npm/[email protected]/hooks/dist/hooks.umd.min.js,npm/[email protected]/jsx-runtime/dist/jsxRuntime.umd.min.js
// @icon         https://www.google.com/s2/favicons?sz=64&gitlab.com
// ==/UserScript==

// Vite helpers
const __defProp = Object.defineProperty;
const __defNormalProp = (obj, key, value) =>
  key in obj
    ? __defProp(obj, key, {
        enumerable: true,
        configurable: true,
        writable: true,
        value,
      })
    : (obj[key] = value);
const __publicField = (obj, key, value) =>
  __defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value);

// App code
const { jsx, jsxs, Fragment } = this.jsxRuntime;
const { render } = this.preact;
const { useMemo, useState, useRef, useEffect, useCallback } = this.preactHooks;

// libs/share/src/ui/GlobalStyle.ts
class GlobalStyle {
  static addStyle(key, styles) {
    const style =
      document.getElementById(key) ||
      (function () {
        const style22 = document.createElement('style');
        style22.id = key;
        document.head.appendChild(style22);
        return style22;
      })();
    style.textContent = styles;
  }
}

const style1 =
  '.glp-create-related-issue-layer {\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-create-related-issue-layer.glp-modal-visible {\n  display: flex;\n}\n\n.glp-create-related-issue-layer .glp-create-related-issue-modal {\n  width: 700px;\n  max-width: 95vw;\n}\n\n.gl-new-dropdown-item .glp-item-check {\n  opacity: 0;\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.gl-new-dropdown-item.glp-selected .glp-item-check {\n  opacity: 1;\n}\n\n';
const style2 =
  '.glp-image-preview-modal {\n  position: fixed;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background: rgba(0, 0, 0, 0.6);\n  visibility: hidden;\n  opacity: 0;\n  pointer-events: none;\n  z-index: 99999;\n}\n\n.glp-image-preview-modal.glp-modal-visible {\n  visibility: visible;\n  opacity: 1;\n  pointer-events: auto;\n}\n\n.glp-image-preview-modal .glp-modal-img {\n  max-width: 95%;\n  max-height: 95%;\n}\n\n.glp-image-preview-modal .glp-modal-close {\n  position: absolute;\n  z-index: 2;\n  top: 20px;\n  right: 20px;\n  color: black;\n  width: 40px;\n  height: 40px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background: white;\n  border-radius: 20px;\n  cursor: pointer;\n}\n\n';
const style3 =
  '.glp-issue-preview-modal {\n  position: fixed;\n  display: flex;\n  padding: 0 15px;\n  background-color: var(--gl-background-color-default, var(--gl-color-neutral-0, #fff));\n  border: 1px solid var(--gl-border-color-default);\n  border-radius: .25rem;\n  width: 300px;\n  min-height: 300px;\n  z-index: 99999;\n  visibility: hidden;\n  top: 0;\n  left: 0;\n  opacity: 0;\n  transition: all .2s ease-out;\n  transition-property: visibility, opacity, transform;\n}\n\n.glp-issue-preview-modal.glp-modal-visible {\n  visibility: visible;\n  opacity: 1;\n}\n\n.glp-issue-preview-modal .glp-issue-modal-inner {\n  display: flex;\n  flex-direction: column;\n  max-width: 100%;\n}\n\n.glp-issue-preview-modal .glp-block {\n  padding: .5rem 0 .5rem;\n  border-bottom-style: solid;\n  border-bottom-color: var(--gl-border-color-subtle, var(--gl-color-neutral-50, #ececef));\n  border-bottom-width: 1px;\n  width: 100%;\n}\n\n.glp-issue-preview-modal .assignee-grid {\n  margin-top: 4px;\n  gap: 4px\n}\n\n\n.glp-issue-preview-modal * {\n  max-width: 100%;\n}\n';

// apps/gitlab-plus/src/styles/index.ts
GlobalStyle.addStyle('glp-style', [style1, style2, style3].join('\n'));

// libs/share/src/utils/clsx.ts
function clsx(...args) {
  return args
    .map((item) => {
      if (!item) {
        return '';
      }
      if (typeof item === 'string') {
        return item;
      }
      if (Array.isArray(item)) {
        return clsx(...item);
      }
      if (typeof item === 'object') {
        return clsx(
          Object.entries(item)
            .filter(([_, value]) => value)
            .map(([key]) => key)
        );
      }
      return '';
    })
    .filter(Boolean)
    .join(' ');
}

// apps/gitlab-plus/src/components/common/GitlabLoader.tsx
function GitlabLoader({ size = 24 }) {
  return jsx('span', {
    role: 'status',
    class: 'gl-spinner-container',
    children: jsx('span', {
      style: {
        width: size,
        height: size,
      },
      class: 'gl-spinner gl-spinner-sm gl-spinner-dark !gl-align-text-bottom',
    }),
  });
}

// apps/gitlab-plus/src/components/common/GitlabUser.tsx
function GitlabUser({ showUsername, size = 24, user, withLink }) {
  const label = useMemo(() => {
    return jsxs(Fragment, {
      children: [
        jsx('span', { class: 'gl-mr-2 gl-block', children: user.name }),
        showUsername &&
          jsx('span', {
            class: 'gl-block gl-text-secondary !gl-text-sm',
            children: user.username,
          }),
      ],
    });
  }, [withLink, showUsername, user]);
  return jsxs('div', {
    class: 'gl-flex gl-w-full gl-items-center',
    children: [
      user.avatarUrl
        ? jsx('img', {
            alt: `${user.name}'s avatar`,
            class: `gl-mr-3 gl-avatar gl-avatar-circle gl-avatar-s${size}`,
            src: user.avatarUrl,
          })
        : jsx('div', {
            class: `gl-mr-3 gl-avatar gl-avatar-identicon gl-avatar-s${size} gl-avatar-identicon-bg1`,
            children: user.name[0].toUpperCase(),
          }),
      withLink
        ? jsx('a', { href: user.webUrl, children: label })
        : jsx('div', { children: label }),
    ],
  });
}

// apps/gitlab-plus/src/components/issue-preview/blocks/IssueBlock.tsx
function IssueBlock({ children, className, tile }) {
  return jsxs('div', {
    class: 'glp-block',
    children: [
      jsx('div', {
        class:
          'gl-flex gl-items-center gl-font-bold gl-leading-20 gl-text-gray-900',
        children: tile,
      }),
      jsx('div', { class: className, children }),
    ],
  });
}

// apps/gitlab-plus/src/components/issue-preview/blocks/IssueAssignee.tsx
function IssueAssignee({ issue }) {
  if (!issue.assignees.nodes.length) {
    return null;
  }
  return jsx(IssueBlock, {
    className: 'gl-flex gl-flex-col gl-gap-3',
    tile: 'Assignee',
    children: issue.assignees.nodes.map((assignee) =>
      jsx(GitlabUser, { withLink: true, user: assignee }, assignee.id)
    ),
  });
}

// apps/gitlab-plus/src/components/common/GitlabIcon.tsx
const buildId =
  '236e3b687d786d9dfe4709143a94d4c53b8d5a1f235775401e5825148297fa84';
const iconUrl = (icon) => {
  let _a;
  const svgSprite =
    ((_a = unsafeWindow.gon) == null ? void 0 : _a.sprite_icons) ||
    `/assets/icons-${buildId}.svg`;
  return `${svgSprite}#${icon}`;
};

function GitlabIcon({ className, icon, size = 12 }) {
  return jsx('svg', {
    className: clsx('gl-icon gl-fill-current', `s${size}`, className),
    children: jsx('use', { href: iconUrl(icon) }),
  });
}

// apps/gitlab-plus/src/components/common/IssueStatus.tsx
function IssueStatus({ isOpen }) {
  return jsxs('span', {
    class: clsx(
      'gl-badge badge badge-pill',
      isOpen ? 'badge-success' : 'badge-info'
    ),
    children: [
      jsx(GitlabIcon, {
        icon: isOpen ? 'issue-open-m' : 'issue-close',
        size: 16,
      }),
      jsx('span', {
        class: 'gl-badge-content',
        children: isOpen ? 'Open' : 'Closed',
      }),
    ],
  });
}

// apps/gitlab-plus/src/components/issue-preview/blocks/IssueHeading.tsx
function IssueHeader({ issue }) {
  return jsx(IssueBlock, {
    tile: issue.title,
    children: jsx('div', {
      children: jsxs('div', {
        class: 'gl-flex',
        children: [
          jsx(GitlabIcon, {
            icon: 'issue-type-issue',
            className: 'gl-mr-2',
            size: 16,
          }),
          jsxs('span', {
            class: 'gl-text-sm gl-text-secondary gl-mr-4',
            children: ['#', issue.iid],
          }),
          jsx(IssueStatus, { isOpen: issue.state === 'opened' }),
        ],
      }),
    }),
  });
}

// apps/gitlab-plus/src/components/issue-preview/blocks/IssueIteration.tsx
function IssueIteration({ issue }) {
  const label = useMemo(() => {
    let _a;
    const date = (date2) => {
      return new Intl.DateTimeFormat('en-US', {
        day: 'numeric',
        month: 'short',
      }).format(new Date(date2));
    };
    if (!issue.iteration) {
      return '';
    }
    return [
      (_a = issue.iteration.iterationCadence) == null ? void 0 : _a.title,
      ': ',
      date(issue.iteration.startDate),
      ' - ',
      date(issue.iteration.dueDate),
    ].join('');
  }, [issue]);
  if (!issue.iteration) {
    return null;
  }
  return jsxs(IssueBlock, {
    tile: 'Iteration',
    children: [
      jsx(GitlabIcon, { icon: 'iteration', className: 'gl-mr-2', size: 16 }),
      jsx('span', { children: label }),
    ],
  });
}

// apps/gitlab-plus/src/components/common/GitlabLabel.tsx
function GitlabLabel({ label, onRemove }) {
  const [scope, text] = label.title.split('::');
  const props = useMemo(() => {
    const className = [
      'gl-label',
      'hide-collapsed',
      label.textColor === '#FFFFFF'
        ? 'gl-label-text-light'
        : 'gl-label-text-dark',
    ];
    if (label.title.includes('::')) {
      className.push('gl-label-scoped');
    }
    return {
      class: clsx(className),
      style: {
        '--label-background-color': label.color,
        '--label-inset-border': `inset 0 0 0 2px ${label.color}`,
      },
    };
  }, [label]);
  return jsxs('span', {
    class: props.class,
    style: props.style,
    children: [
      jsxs('span', {
        class: 'gl-link gl-label-link gl-label-link-underline',
        children: [
          jsx('span', { class: 'gl-label-text', children: scope }),
          text &&
            jsx('span', { class: 'gl-label-text-scoped', children: text }),
        ],
      }),
      onRemove &&
        jsx('button', {
          class:
            'btn gl-label-close !gl-p-0 btn-reset btn-sm gl-button btn-reset-tertiary',
          onClick: onRemove,
          type: 'button',
          children: jsx('span', {
            class: 'gl-button-text',
            children: jsx(GitlabIcon, { icon: 'close-xs' }),
          }),
        }),
    ],
  });
}

// apps/gitlab-plus/src/components/issue-preview/blocks/IssueLabels.tsx
function IssueLabels({ issue }) {
  if (!issue.labels.nodes.length) {
    return null;
  }
  return jsx(IssueBlock, {
    className: 'issuable-show-labels',
    tile: 'Labels',
    children: issue.labels.nodes.map((label) =>
      jsx(GitlabLabel, { label }, label.id)
    ),
  });
}

// apps/gitlab-plus/src/components/common/GitlabMergeRequest.tsx
const iconMap = {
  closed: 'merge-request-close',
  locked: 'search',
  merged: 'merge',
  opened: 'merge-request',
};

function GitlabMergeRequest({ mr }) {
  return jsxs('div', {
    style: { marginTop: 10 },
    children: [
      jsxs('div', {
        class: 'item-title gl-flex gl-min-w-0 gl-gap-3',
        children: [
          jsx(GitlabIcon, {
            icon: iconMap[mr.state] || 'empty',
            className: 'merge-request-status',
            size: 16,
          }),
          jsxs('span', {
            class: 'gl-text-gray-500',
            children: ['!', mr.iid],
          }),
          jsx(GitlabUser, { withLink: true, size: 16, user: mr.author }),
        ],
      }),
      jsx('a', {
        style: {
          overflow: 'hidden',
          textOverflow: 'ellipsis',
        },
        class: 'gl-block gl-link sortable-link',
        href: mr.webUrl,
        children: mr.title,
      }),
    ],
  });
}

// apps/gitlab-plus/src/components/issue-preview/blocks/IssueMergeRequests.tsx
function IssueMergeRequests({ issue }) {
  if (!issue.relatedMergeRequests.nodes.length) {
    return null;
  }
  return jsx(IssueBlock, {
    tile: 'Merge requests',
    children: issue.relatedMergeRequests.nodes.map((mr) =>
      jsx(GitlabMergeRequest, { mr }, mr.iid)
    ),
  });
}

// apps/gitlab-plus/src/components/issue-preview/blocks/IssueMilestone.tsx
function IssueMilestone({ issue }) {
  if (!issue.milestone) {
    return null;
  }
  return jsxs(IssueBlock, {
    tile: 'Milestone',
    children: [
      jsx(GitlabIcon, { icon: 'milestone', className: 'gl-mr-2', size: 16 }),
      jsx('span', { children: issue.milestone.title }),
    ],
  });
}

// apps/gitlab-plus/src/components/issue-preview/blocks/IssueRelatedIssue.tsx
const relationMap = {
  blocks: 'Blocks:',
  is_blocked_by: 'Is blocked by:',
  relates_to: 'Related to:',
};

function IssueRelatedIssue({ isLoading, relatedIssues }) {
  const groups = useMemo(() => {
    const initValue = {
      blocks: [],
      is_blocked_by: [],
      relates_to: [],
    };
    return Object.entries(
      relatedIssues.reduce(
        (acc, issue) => ({
          ...acc,
          [issue.linkType]: [...acc[issue.linkType], issue],
        }),
        initValue
      )
    ).filter(([_, issues]) => issues.length);
  }, [relatedIssues]);
  const onHover = (e) => {
    e.stopPropagation();
    e.preventDefault();
    return false;
  };
  if (isLoading) {
    return jsx('div', {
      className: 'gl-flex gl-items-center gl-justify-center',
      children: jsx(GitlabLoader, {}),
    });
  }
  if (!relatedIssues.length) {
    return null;
  }
  return jsx(IssueBlock, {
    tile: '',
    children: groups.map(([key, issues]) =>
      jsxs(
        'div',
        {
          style: { marginTop: 10 },
          children: [
            jsx('div', {
              class: 'item-title gl-flex gl-min-w-0 gl-gap-3',
              children: jsx('span', { children: relationMap[key] }),
            }),
            issues.map((issue) =>
              jsxs(
                'a',
                {
                  style: {
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                  },
                  onMouseOver: onHover,
                  class: 'gl-block gl-link sortable-link',
                  href: issue.webUrl,
                  children: ['#', issue.iid, ' ', issue.title],
                },
                issue.iid
              )
            ),
          ],
        },
        key
      )
    ),
  });
}

// apps/gitlab-plus/src/components/issue-preview/IssueModalContent.tsx
function IssueModalContent({
  issue,
  issueLoading,
  relatedIssues,
  relatedIssuesLoading,
}) {
  if (issueLoading) {
    return jsx('div', {
      class: 'gl-flex gl-flex-1 gl-items-center gl-justify-center',
      children: jsx(GitlabLoader, { size: '3em' }),
    });
  }
  if (!issue) {
    return jsx('div', {
      class: 'gl-flex gl-flex-1 gl-items-center gl-justify-center',
      children: jsx('span', { children: 'Error' }),
    });
  }
  return jsxs('div', {
    class: 'gl-flex gl-w-full gl-flex-col',
    children: [
      jsx(IssueHeader, { issue }),
      jsx(IssueAssignee, { issue }),
      jsx(IssueLabels, { issue }),
      jsx(IssueMilestone, { issue }),
      jsx(IssueIteration, { issue }),
      jsx(IssueMergeRequests, { issue }),
      jsx(IssueRelatedIssue, {
        isLoading: relatedIssuesLoading,
        relatedIssues,
      }),
    ],
  });
}

// libs/share/src/utils/delay.ts
async function delay(ms = 1e3) {
  return await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

// libs/share/src/store/Cache.ts
class Cache {
  constructor(prefix) {
    this.prefix = prefix;
  }

  clearInvalid() {
    for (const key in localStorage) {
      if (key.startsWith(this.prefix) && !this.isValid(this.getItem(key))) {
        localStorage.removeItem(key);
      }
    }
  }

  expirationDate(minutes) {
    if (typeof minutes === 'string') {
      return minutes;
    }
    const time = new Date();
    time.setMinutes(time.getMinutes() + minutes);
    return time;
  }

  get(key) {
    try {
      const data = this.getItem(this.key(key));
      if (this.isValid(data)) {
        return data.value;
      }
    } catch (e) {
      return void 0;
    }
    return void 0;
  }

  key(key) {
    return `${this.prefix}${key}`;
  }

  set(key, value, minutes) {
    localStorage.setItem(
      this.key(key),
      JSON.stringify({
        expirationDate: this.expirationDate(minutes),
        value,
      })
    );
  }

  getItem(key) {
    try {
      return JSON.parse(localStorage.getItem(key) || '');
    } catch (e) {
      return void 0;
    }
  }

  isValid(item) {
    if (item) {
      return (
        item.expirationDate === 'lifetime' ||
        new Date(item.expirationDate) > new Date()
      );
    }
    return false;
  }
}

// libs/share/src/utils/camelizeKeys.ts
function camelizeKeys(data) {
  if (!data || ['string', 'number', 'boolean'].includes(typeof data)) {
    return data;
  }
  if (Array.isArray(data)) {
    return data.map(camelizeKeys);
  }
  const camelize = (key) => {
    const _key = key.replace(/[-_\s]+(.)?/g, (_, chr) =>
      chr ? chr.toUpperCase() : ''
    );
    return _key.substring(0, 1).toLowerCase() + _key.substring(1);
  };
  return Object.entries(data).reduce(
    (result, [key, value]) => ({
      ...result,
      [camelize(key)]: camelizeKeys(value),
    }),
    {}
  );
}

// apps/gitlab-plus/src/providers/GitlabProvider.ts
class GitlabProvider {
  constructor() {
    __publicField(this, 'cache', new Cache('glp-'));
    __publicField(this, 'url', 'https://gitlab.com/api/v4/');
    __publicField(this, 'graphqlApi', 'https://gitlab.com/api/graphql');
  }

  async get(path) {
    const response = await fetch(`${this.url}${path}`, {
      method: 'GET',
      headers: this.headers(),
    });
    const data = await response.json();
    return camelizeKeys(data);
  }

  async post(path, body) {
    const response = await fetch(`${this.url}${path}`, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: this.headers(),
    });
    const data = await response.json();
    return camelizeKeys(data);
  }

  async query(query, variables) {
    const response = await fetch(this.graphqlApi, {
      method: 'POST',
      body: JSON.stringify({ variables, query }),
      headers: this.headers(),
    });
    return response.json();
  }

  async queryCached(key, query, variables, minutes) {
    return this.cached(key, () => this.query(query, variables), minutes);
  }

  async getCached(key, path, minutes) {
    return this.cached(key, () => this.get(path), minutes);
  }

  async cached(key, getValue, minutes) {
    const cacheValue = this.cache.get(key);
    if (cacheValue) {
      return cacheValue;
    }
    const value = await getValue();
    this.cache.set(key, value, minutes);
    return value;
  }

  csrf() {
    const token = document.querySelector('meta[name=csrf-token]');
    if (token) {
      return token.getAttribute('content');
    }
    return '';
  }

  headers() {
    const headers = {
      'content-type': 'application/json',
    };
    const csrf = this.csrf();
    if (csrf) {
      headers['X-CSRF-Token'] = csrf;
    }
    return headers;
  }
}

// apps/gitlab-plus/src/providers/query/label.ts
const labelFragment = `
  fragment Label on Label {
    id
    title
    description
    color
    textColor
    __typename
  }
`;
const labelsQuery = `query projectLabels($fullPath: ID!, $searchTerm: String) {
  workspace: project(fullPath: $fullPath) {
    id
    labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
      nodes {
        ...Label
        __typename
      }
      __typename
    }
    __typename
  }
}

${labelFragment}
`;

// apps/gitlab-plus/src/providers/query/user.ts
const userFragment = `
fragment User on User {
  id
  avatarUrl
  name
  username
  webUrl
  webPath
  __typename
}
`;
const userQuery = `
query workspaceAutocompleteUsersSearch($search: String!, $fullPath: ID!, $isProject: Boolean = true) {
  groupWorkspace: group(fullPath: $fullPath) @skip(if: $isProject) {
    id
    users: autocompleteUsers(search: $search) {
      ...User
      ...UserAvailability
      __typename
    }
    __typename
  }
  workspace: project(fullPath: $fullPath) {
    id
    users: autocompleteUsers(search: $search) {
      ...User
      ...UserAvailability
      __typename
    }
    __typename
  }
}

${userFragment}
fragment UserAvailability on User {
  status {
    availability
    __typename
  }
  __typename
}
`;

// apps/gitlab-plus/src/providers/query/issue.ts
const issueQuery = `query issueEE($projectPath: ID!, $iid: String!) {
  project(fullPath: $projectPath) {
    id
    issue(iid: $iid) {
      id
      iid
      title
      description
      createdAt
      state
      confidential
      dueDate
      milestone {
        id
        title
        startDate
        dueDate
        __typename
      }
      iteration {
        id
        title
        startDate
        dueDate
        iterationCadence {
          id
          title
          __typename
        }
        __typename
      }
      labels {
        nodes {
          ...Label
        }
      }
      relatedMergeRequests {
        nodes {
          iid
          title
          state
          webUrl
          author {
            ...User
          }
        }
      }
      assignees {
        nodes {
          ...User
        }
      }
      weight
      type
      __typename
    }
    __typename
  }
}

${labelFragment}
${userFragment}
`;
const issuesQuery = `query groupWorkItems($searchTerm: String, $fullPath: ID!, $types: [IssueType!], $in: [IssuableSearchableField!], $includeAncestors: Boolean = false, $includeDescendants: Boolean = false, $iid: String = null, $searchByIid: Boolean = false, $searchByText: Boolean = true, $searchEmpty: Boolean = true) {
  workspace: group(fullPath: $fullPath) {
    id
    workItems(
      search: $searchTerm
      types: $types
      in: $in
      includeAncestors: $includeAncestors
      includeDescendants: $includeDescendants
    ) @include(if: $searchByText) {
      nodes {
        id
        iid
        title
        confidential
        project {
          fullPath
        }
        __typename
      }
      __typename
    }
    workItemsByIid: workItems(
      iid: $iid
      types: $types
      includeAncestors: $includeAncestors
      includeDescendants: $includeDescendants
    ) @include(if: $searchByIid) {
      nodes {
        id
        iid
        title
        confidential
        project {
          fullPath
        }
        __typename
      }
      __typename
    }
    workItemsEmpty: workItems(
      types: $types
      includeAncestors: $includeAncestors
      includeDescendants: $includeDescendants
    ) @include(if: $searchEmpty) {
      nodes {
        id
        iid
        title
        confidential
        project {
          fullPath
        }
        __typename
      }
      __typename
    }
    __typename
  }
}
`;
const issueMutation = `
mutation CreateIssue($input: CreateIssueInput!) {
  createIssuable: createIssue(input: $input) {
    issuable: issue {
      ...Issue
      __typename
    }
    errors
    __typename
  }
}

fragment Issue on Issue {
  ...IssueNode
  id
  weight
  blocked
  blockedByCount
  epic {
    id
    __typename
  }
  iteration {
    id
    title
    startDate
    dueDate
    iterationCadence {
      id
      title
      __typename
    }
    __typename
  }
  healthStatus
  __typename
}

fragment IssueNode on Issue {
  id
  iid
  title
  referencePath: reference(full: true)
  closedAt
  dueDate
  timeEstimate
  totalTimeSpent
  humanTimeEstimate
  humanTotalTimeSpent
  emailsDisabled
  confidential
  hidden
  webUrl
  relativePosition
  projectId
  type
  severity
  milestone {
    ...MilestoneFragment
    __typename
  }
  assignees {
    nodes {
      ...User
      __typename
    }
    __typename
  }
  labels {
    nodes {
      id
      title
      color
      description
      __typename
    }
    __typename
  }
  __typename
}

fragment MilestoneFragment on Milestone {
  expired
  id
  state
  title
  __typename
}

fragment User on User {
  id
  avatarUrl
  name
  username
  webUrl
  webPath
  __typename
}
`;

// apps/gitlab-plus/src/providers/IssueProvider.ts
class IssueProvider extends GitlabProvider {
  async getIssue(projectId, issueId) {
    return this.queryCached(
      `issue-${projectId}-${issueId}`,
      issueQuery,
      {
        projectPath: projectId,
        iid: issueId,
      },
      2
    );
  }

  async getIssues(projectId, search) {
    const searchById = !!search.match(/^\d+$/);
    return await this.query(issuesQuery, {
      iid: searchById ? search : null,
      searchByIid: searchById,
      searchEmpty: !search,
      searchByText: Boolean(search),
      fullPath: projectId,
      searchTerm: search,
      includeAncestors: true,
      includeDescendants: true,
      types: ['ISSUE'],
      in: 'TITLE',
    });
  }

  async createIssue(input) {
    return await this.query(issueMutation, { input });
  }

  async createIssueRelation(input) {
    const path = [
      'projects/:PROJECT_ID',
      '/issues/:ISSUE_ID/links',
      '?target_project_id=:TARGET_PROJECT_ID',
      '&target_issue_iid=:TARGET_ISSUE_IID',
      '&link_type=:LINK_TYPE',
    ]
      .join('')
      .replace(':PROJECT_ID', `${input.projectId}`)
      .replace(':ISSUE_ID', `${input.issueId}`)
      .replace(':TARGET_PROJECT_ID', input.targetProjectId)
      .replace(':TARGET_ISSUE_IID', input.targetIssueIid)
      .replace(':LINK_TYPE', input.linkType);
    return await this.post(path, {});
  }

  async getIssueLinks(projectId, issueId) {
    const path = 'projects/:PROJECT_ID/issues/:ISSUE_ID/links'
      .replace(':PROJECT_ID', `${projectId}`)
      .replace(':ISSUE_ID', `${issueId}`);
    return await this.getCached(`issue-${projectId}-${issueId}-links`, path, 2);
  }
}

// apps/gitlab-plus/src/components/issue-preview/useFetchIssue.ts
const initialIssueData = {
  isLoading: false,
  issue: null,
};
const initialRelatedIssuesData = {
  isLoading: false,
  issues: [],
};
const issueProvider = new IssueProvider();

function useFetchIssue() {
  const [issue, setIssue] = useState(initialIssueData);
  const [relatedIssues, setRelatedIssues] = useState(initialRelatedIssuesData);
  const fetch2 = async (link) => {
    setIssue({ ...initialIssueData, isLoading: true });
    setRelatedIssues({ ...initialRelatedIssuesData, isLoading: true });
    const response = await issueProvider.getIssue(link.projectPath, link.issue);
    setIssue({
      isLoading: false,
      issue: response.data.project.issue,
    });
    const relatedIssues2 = await issueProvider.getIssueLinks(
      response.data.project.id.replace(/\D/g, ''),
      response.data.project.issue.iid
    );
    setRelatedIssues({
      isLoading: false,
      issues: relatedIssues2,
    });
  };
  const reset = () => {
    setIssue(initialIssueData);
    setRelatedIssues(initialRelatedIssuesData);
  };
  return {
    fetch: fetch2,
    issue,
    relatedIssues,
    reset,
  };
}

// libs/share/src/ui/Events.ts
class Events {
  static intendHover(validate, mouseover, mouseleave, timeout = 500) {
    let hover = false;
    let id = 0;
    const onHover = (event) => {
      if (!event.target || !validate(event.target)) {
        return;
      }
      const element = event.target;
      hover = true;
      element.addEventListener(
        'mouseleave',
        (ev) => {
          mouseleave.call(element, ev);
          clearTimeout(id);
          hover = false;
        },
        { once: true }
      );
      clearTimeout(id);
      id = window.setTimeout(() => {
        if (hover) {
          mouseover.call(element, event);
        }
      }, timeout);
    };
    document.body.addEventListener('mouseover', onHover);
  }
}

// apps/gitlab-plus/src/helpers/IssueLink.ts
class IssueLink {
  static parseLink(link) {
    if (!IssueLink.validateLink(link)) {
      return void 0;
    }
    const [projectPath, issue] = new URL(link).pathname
      .replace(/^\//, '')
      .split('/-/issues/');
    const slashCount = (projectPath.match(/\//g) || []).length;
    const workspacePath =
      slashCount === 1 ? projectPath : projectPath.replace(/\/[^/]+$/, '');
    return {
      issue: issue.replace(/\D/g, ''),
      projectPath,
      workspacePath,
    };
  }

  static validateLink(link) {
    return Boolean(typeof link === 'string' && link.includes('/-/issues/'));
  }
}

// apps/gitlab-plus/src/components/issue-preview/useOnIssueHover.ts
function useOnIssueHover() {
  const [hoverLink, setHoverLink] = useState();
  const hoverIssueRef = useRef(false);
  const [hoverPosition, setHoverPosition] = useState({ x: 0, y: 0 });
  const onHover = (event) => {
    const anchor = event.target;
    const link = IssueLink.parseLink(anchor.href);
    if (!link) {
      return;
    }
    anchor.title = '';
    setHoverLink(link);
    setHoverPosition({
      x: event.clientX,
      y: event.clientY,
    });
  };
  useEffect(() => {
    Events.intendHover(
      (element) => IssueLink.validateLink(element.href),
      onHover,
      () => {
        setTimeout(() => {
          if (!hoverIssueRef.current) {
            setHoverLink(void 0);
          }
        }, 50);
      }
    );
  }, []);
  return {
    hoverLink,
    hoverPosition,
    onIssueEnter: () => (hoverIssueRef.current = true),
    onIssueLeave: () => {
      hoverIssueRef.current = false;
      setHoverLink(void 0);
    },
  };
}

// apps/gitlab-plus/src/components/issue-preview/useIssuePreviewModal.ts
function useIssuePreviewModal() {
  const ref = useRef(null);
  const [isVisible, setIsVisible] = useState(false);
  const [offset, setOffset] = useState(0);
  const { hoverLink, hoverPosition, onIssueEnter, onIssueLeave } =
    useOnIssueHover();
  const { fetch: fetch2, issue, relatedIssues, reset } = useFetchIssue();
  const fetchData = async (link) => {
    await fetch2(link);
    await delay(300);
    const rect = ref.current.getBoundingClientRect();
    const dY = rect.height + rect.top - window.innerHeight;
    if (dY > 0) {
      setOffset(dY + 15);
    }
  };
  useEffect(() => {
    if (hoverLink) {
      fetchData(hoverLink);
      setIsVisible(true);
    } else {
      setIsVisible(false);
      reset();
      setOffset(0);
    }
  }, [hoverLink]);
  return {
    issue,
    isVisible,
    onIssueEnter,
    onIssueLeave,
    position: {
      ...hoverPosition,
      offset,
    },
    ref,
    relatedIssues,
  };
}

// apps/gitlab-plus/src/components/issue-preview/IssuePreviewModal.tsx
function IssuePreviewModal() {
  const {
    issue,
    isVisible,
    onIssueEnter,
    onIssueLeave,
    position,
    ref,
    relatedIssues,
  } = useIssuePreviewModal();
  return jsx('div', {
    style: {
      left: position.x,
      top: position.y,
      transform: `translateY(-${position.offset}px)`,
    },
    onMouseEnter: onIssueEnter,
    onMouseLeave: onIssueLeave,
    class: clsx('glp-issue-preview-modal', isVisible && 'glp-modal-visible'),
    ref,
    children: jsx(IssueModalContent, {
      issueLoading: issue.isLoading,
      relatedIssuesLoading: relatedIssues.isLoading,
      issue: issue.issue,
      relatedIssues: relatedIssues.issues,
    }),
  });
}

// apps/gitlab-plus/src/types/Service.ts
class Service {
  root(className, parent) {
    const root = document.createElement('div');
    root.classList.add(className);
    if (parent) {
      parent.append(root);
    }
    return root;
  }

  rootBody(className) {
    return this.root(className, document.body);
  }
}

// apps/gitlab-plus/src/services/IssuePreview.tsx
class IssuePreview extends Service {
  init() {
    render(jsx(IssuePreviewModal, {}), this.rootBody('glp-issue-preview-root'));
  }
}

// apps/gitlab-plus/src/components/image-preview/useImagePreviewModal.ts
function useImagePreviewModal() {
  const [data, setData] = useState(false);
  const [src, setSrc] = useState('');
  const validate = (element) => {
    return (
      element.classList.contains('no-attachment-icon') &&
      /\.(png|jpg|jpeg|heic)$/.test(element.href.toLowerCase())
    );
  };
  const getAnchor = (element) => {
    if (!element) {
      return void 0;
    }
    if (element instanceof HTMLAnchorElement) {
      return validate(element) ? element : void 0;
    }
    if (
      element instanceof HTMLImageElement &&
      element.parentElement instanceof HTMLAnchorElement
    ) {
      return validate(element.parentElement) ? element.parentElement : void 0;
    }
    return void 0;
  };
  useEffect(() => {
    document.body.addEventListener('click', (ev) => {
      const anchor = getAnchor(ev.target);
      if (anchor) {
        setSrc(anchor.href);
        ev.preventDefault();
        ev.stopPropagation();
        return false;
      }
    });
  }, []);
  useEffect(() => {
    setData(true);
  }, [data]);
  return {
    onClose: () => setSrc(''),
    src,
  };
}

// apps/gitlab-plus/src/components/image-preview/ImagePreviewModal.tsx
function ImagePreviewModal() {
  const { onClose, src } = useImagePreviewModal();
  return jsxs('div', {
    className: clsx(
      'glp-image-preview-modal',
      Boolean(src) && 'glp-modal-visible'
    ),
    children: [
      jsx('img', { alt: 'Image preview', className: 'glp-modal-img', src }),
      jsx('div', {
        onClick: onClose,
        className: 'glp-modal-close',
        children: jsx(GitlabIcon, { icon: 'close-xs', size: 24 }),
      }),
    ],
  });
}

// apps/gitlab-plus/src/services/ImagePreview.tsx
class ImagePreview extends Service {
  init() {
    render(jsx(ImagePreviewModal, {}), this.rootBody('glp-image-preview-root'));
  }
}

// apps/gitlab-plus/src/components/create-related-issue/event.ts
const showModalEventName = 'glp-show-create-issue-modal';
const ShowModalEvent = new CustomEvent(showModalEventName);

// apps/gitlab-plus/src/components/create-related-issue/CreateIssueButton.tsx
function CreateIssueButton() {
  const onClick = () => {
    document.dispatchEvent(ShowModalEvent);
  };
  return jsx('button', {
    onClick,
    class: 'btn btn-default btn-sm gl-button',
    type: 'button',
    children: jsx('span', {
      class: 'gl-button-text',
      children: 'Create related issue',
    }),
  });
}

// apps/gitlab-plus/src/components/common/CloseButton.tsx
function CloseButton({ onClick, title = 'Close' }) {
  return jsx('button', {
    class:
      'btn js-issue-item-remove-button gl-mr-2 btn-default btn-sm gl-button btn-default-tertiary btn-icon',
    onClick,
    title,
    children: jsx(GitlabIcon, { icon: 'close-xs', size: 16 }),
  });
}

// apps/gitlab-plus/src/components/common/form/FormField.tsx
function FormField({ children, error, hint, title }) {
  return jsxs('fieldset', {
    class: clsx(
      'form-group gl-form-group gl-w-full',
      error && 'gl-show-field-errors'
    ),
    children: [
      jsx('legend', {
        class: 'bv-no-focus-ring col-form-label pt-0 col-form-label',
        children: title,
      }),
      children,
      Boolean(!error && hint) && jsx('small', { children: hint }),
      Boolean(error) &&
        jsx('small', { class: 'gl-field-error', children: error }),
    ],
  });
}

// apps/gitlab-plus/src/components/common/form/FormRow.tsx
function FormRow({ children }) {
  return jsx('div', { class: 'gl-flex gl-gap-x-3', children });
}

// apps/gitlab-plus/src/providers/UsersProvider.ts
class UsersProvider extends GitlabProvider {
  async getUsers(projectId, search = '') {
    return this.queryCached(
      `users-${projectId}-${search}`,
      userQuery,
      {
        fullPath: projectId,
        search,
      },
      search === '' ? 20 : 0.5
    );
  }
}

// apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteButton.ts
function useAsyncAutocompleteButton(hide) {
  const ref = useRef(null);
  useEffect(() => {
    document.body.addEventListener('click', (e) => {
      if (
        ref.current &&
        e.target !== ref.current &&
        !ref.current.contains(e.target)
      ) {
        hide();
      }
    });
  }, []);
  return ref;
}

// apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteButton.tsx
function AsyncAutocompleteButton({
  isOpen,
  renderLabel,
  reset,
  setIsOpen,
  value,
}) {
  const ref = useAsyncAutocompleteButton(() => setIsOpen(false));
  const icon = useMemo(() => {
    if (value.length) {
      return 'close-xs';
    }
    return isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
  }, [isOpen, value]);
  return jsx('button', {
    class: 'btn btn-default btn-md btn-block gl-button gl-new-dropdown-toggle',
    onClick: (e) => {
      e.preventDefault();
      setIsOpen(true);
    },
    ref,
    type: 'button',
    children: jsxs('span', {
      class: 'gl-button-text gl-w-full',
      children: [
        jsx('span', {
          class: 'gl-new-dropdown-button-text',
          children: renderLabel(value),
        }),
        jsx('span', {
          onClick: (e) => {
            if (value.length) {
              e.preventDefault();
              reset();
            }
          },
          children: jsx(GitlabIcon, { icon, size: 16 }),
        }),
      ],
    }),
  });
}

// apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteOption.tsx
function AsyncAutocompleteOption({
  isActive,
  onClick,
  option,
  removeFromRecent,
  renderOption,
  selected,
}) {
  const selectedIds = selected.map((i) => i.id);
  const selectedClass = (id) =>
    selectedIds.includes(id) ? 'glp-selected' : '';
  return jsx('li', {
    class: clsx(
      'gl-new-dropdown-item',
      selectedClass(option.id),
      isActive && 'glp-active'
    ),
    onClick: () => onClick(option),
    children: jsxs('span', {
      class: 'gl-new-dropdown-item-content',
      children: [
        jsx(GitlabIcon, {
          icon: 'mobile-issue-close',
          className: 'glp-item-check gl-pr-2',
          size: 16,
        }),
        renderOption(option),
        removeFromRecent &&
          jsx(CloseButton, {
            onClick: (e) => {
              e.preventDefault();
              e.stopPropagation();
              removeFromRecent(option);
            },
            title: 'Remove from recently used',
          }),
      ],
    }),
  });
}

// apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteList.tsx
function AsyncAutocompleteList({
  activeIndex,
  onClick,
  options,
  recently,
  removeRecently,
  renderOption,
  value,
}) {
  return jsx('div', {
    class:
      'gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay bottom-scrim-visible gl-new-dropdown-contents',
    style: {
      maxWidth: '800px',
      width: '100%',
      left: '0',
      top: '100%',
    },
    onClick: (e) => e.stopPropagation(),
    children: jsx('div', {
      class: 'gl-new-dropdown-inner',
      children: jsxs('ul', {
        class: 'gl-mb-0 gl-pl-0',
        children: [
          Boolean(recently.length) &&
            jsxs(Fragment, {
              children: [
                jsx('li', {
                  class:
                    'gl-pb-2 gl-pl-4 gl-pt-3 gl-text-sm gl-font-bold gl-text-strong',
                  children: 'Recently used',
                }),
                recently.map((item, index) =>
                  jsx(
                    AsyncAutocompleteOption,
                    {
                      onClick,
                      option: item,
                      removeFromRecent: removeRecently,
                      renderOption,
                      isActive: index === activeIndex,
                      selected: value,
                    },
                    item.id
                  )
                ),
              ],
            }),
          Boolean(options.length) &&
            jsxs(Fragment, {
              children: [
                jsx('li', {
                  class:
                    'gl-pb-2 gl-pl-4 gl-pt-3 gl-text-sm gl-font-bold gl-text-strong gl-border-t',
                }),
                options.map((item, index) =>
                  jsx(
                    AsyncAutocompleteOption,
                    {
                      onClick,
                      option: item,
                      renderOption,
                      isActive: recently.length + index === activeIndex,
                      selected: value,
                    },
                    item.id
                  )
                ),
              ],
            }),
          options.length + recently.length === 0 &&
            jsx('li', { class: 'gl-p-4', children: 'No options' }),
        ],
      }),
    }),
  });
}

// apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteSearch.tsx
function AsyncAutocompleteSearch({ navigate, setValue, value }) {
  return jsx('div', {
    class: 'gl-border-b-1 gl-border-b-solid gl-border-b-dropdown',
    children: jsxs('div', {
      class: 'gl-listbox-search gl-listbox-topmost',
      children: [
        jsx(GitlabIcon, {
          icon: 'search',
          className: 'gl-search-box-by-type-search-icon',
          size: 16,
        }),
        jsx('input', {
          autofocus: true,
          onInput: (e) => setValue(e.target.value),
          onKeyDown: (e) => navigate(e.key),
          class: 'gl-listbox-search-input',
          value,
        }),
        Boolean(value) &&
          jsx('div', {
            class: 'gl-search-box-by-type-right-icons',
            style: { top: '0' },
            children: jsx(CloseButton, {
              onClick: () => setValue(''),
              title: 'Clear input',
            }),
          }),
      ],
    }),
  });
}

// apps/gitlab-plus/src/components/common/form/autocomplete/useListNavigate.ts
function useListNavigate(options, recent, onClick, onClose) {
  const [activeIndex, setActiveIndex] = useState(-1);
  const navigate = (key) => {
    if (['ArrowDown', 'ArrowUp'].includes(key)) {
      const total = recent.length + options.length;
      const diff = key === 'ArrowDown' ? 1 : -1;
      setActiveIndex((activeIndex + diff + total) % total);
    } else if (key === 'Enter') {
      const allItems = [...recent, ...options];
      if (-1 < activeIndex && activeIndex < allItems.length) {
        onClick(allItems[activeIndex]);
      }
    } else if (key === 'Escape') {
      onClose();
    }
  };
  return {
    activeIndex,
    navigate,
  };
}

// apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocompleteDropdown.tsx
function AsyncAutocompleteDropdown({
  onClick,
  onClose,
  options,
  recently = [],
  removeRecently,
  renderOption,
  searchTerm,
  setSearchTerm,
  value,
}) {
  const { activeIndex, navigate } = useListNavigate(
    options,
    recently,
    onClick,
    onClose
  );
  return jsx('div', {
    style: {
      maxWidth: '800px',
      width: '100%',
      left: '0',
      top: '100%',
    },
    onClick: (e) => e.stopPropagation(),
    class: clsx('gl-new-dropdown-panel gl-absolute !gl-block'),
    children: jsxs('div', {
      class: 'gl-new-dropdown-inner',
      children: [
        jsx(AsyncAutocompleteSearch, {
          navigate,
          setValue: setSearchTerm,
          value: searchTerm,
        }),
        jsx(AsyncAutocompleteList, {
          onClick,
          options,
          removeRecently,
          renderOption,
          activeIndex,
          recently,
          value,
        }),
      ],
    }),
  });
}

// libs/share/src/utils/useDebounce.ts
function useDebounce(value, delay2 = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay2);
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay2]);
  return debouncedValue;
}

// apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteOptions.ts
function useAsyncAutocompleteOptions(searchTerm, getValues) {
  const [options, setOptions] = useState([]);
  const term = useDebounce(searchTerm);
  const loadOptions = useCallback(async (term2) => {
    const items = await getValues(term2);
    setOptions(items);
  }, []);
  useEffect(() => {
    loadOptions(term);
  }, [term]);
  return options;
}

// apps/gitlab-plus/src/providers/RecentlyProvider.ts
class RecentlyProvider {
  constructor(key) {
    __publicField(this, 'cache', new Cache('glp-'));
    __publicField(this, 'key');
    __publicField(this, 'eventName');
    this.key = `recently-${key}`;
    this.eventName = `recently-${key}-change`;
  }

  add(...items) {
    const itemsId = items.map((i) => i.id);
    this.cache.set(
      this.key,
      [...items, ...this.get().filter((el) => !itemsId.includes(el.id))],
      'lifetime'
    );
    this.triggerChange();
  }

  get() {
    return this.cache.get(this.key) || [];
  }

  onChange(callback) {
    document.addEventListener(this.eventName, callback);
  }

  remove(...items) {
    const itemsId = items.map((i) => i.id);
    this.cache.set(
      this.key,
      this.get().filter((el) => !itemsId.includes(el.id)),
      'lifetime'
    );
    this.triggerChange();
  }

  triggerChange() {
    document.dispatchEvent(new CustomEvent(this.eventName));
  }
}

// apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocompleteRecently.ts
function useAsyncAutocompleteRecently(name) {
  const store = useRef(new RecentlyProvider(name));
  const [recently, setRecently] = useState(store.current.get());
  useEffect(() => {
    store.current.onChange(() => {
      setRecently(store.current.get());
    });
  }, []);
  return {
    add: store.current.add.bind(store.current),
    recently,
    remove: store.current.remove.bind(store.current),
  };
}

// apps/gitlab-plus/src/components/common/form/autocomplete/useAsyncAutocomplete.ts
function useAsyncAutocomplete(name, value, getValues, onChange, isMultiselect) {
  const [searchTerm, setSearchTerm] = useState('');
  const [isOpen, setIsOpen] = useState(false);
  const { recently: allRecently, remove: removeRecently } =
    useAsyncAutocompleteRecently(name);
  const options = useAsyncAutocompleteOptions(searchTerm, getValues);
  const onClick = (item) => {
    if (isMultiselect) {
      if (value.find((i) => i.id === item.id)) {
        onChange(value.filter((i) => i.id !== item.id));
      } else {
        onChange([...value, item]);
      }
    } else {
      onChange([item]);
      setIsOpen(false);
    }
  };
  const recently = useMemo(() => {
    const optionsIds = options.map((i) => i.id);
    return searchTerm.length
      ? allRecently.filter((i) => optionsIds.includes(i.id))
      : allRecently;
  }, [options, allRecently]);
  return {
    isOpen,
    onClick,
    options: useMemo(() => {
      const recentlyIds = recently.map((i) => i.id);
      return options.filter((i) => !recentlyIds.includes(i.id));
    }, [options, recently]),
    recently,
    removeRecently,
    searchTerm,
    setIsOpen,
    setSearchTerm,
  };
}

// apps/gitlab-plus/src/components/common/form/autocomplete/AsyncAutocomplete.tsx
function AsyncAutocomplete({
  getValues,
  isMultiselect = false,
  name,
  onChange,
  renderLabel,
  renderOption,
  value,
}) {
  const {
    isOpen,
    onClick,
    options,
    recently,
    removeRecently,
    searchTerm,
    setIsOpen,
    setSearchTerm,
  } = useAsyncAutocomplete(name, value, getValues, onChange, isMultiselect);
  return jsxs('div', {
    class: 'gl-relative gl-w-full gl-new-dropdown !gl-block',
    children: [
      jsx(AsyncAutocompleteButton, {
        isOpen,
        renderLabel,
        reset: () => onChange([]),
        setIsOpen,
        value,
      }),
      isOpen &&
        jsx(AsyncAutocompleteDropdown, {
          onClick,
          onClose: () => setIsOpen(false),
          options,
          removeRecently,
          renderOption,
          recently,
          searchTerm,
          setSearchTerm,
          value,
        }),
    ],
  });
}

// apps/gitlab-plus/src/components/create-related-issue/fields/AssigneesField.tsx
function AssigneesField({ link, setValue, value }) {
  const getUsers = useCallback(
    async (search) => {
      const response = await new UsersProvider().getUsers(
        link.projectPath,
        search
      );
      return response.data.workspace.users;
    },
    [link]
  );
  const renderLabel = useCallback((items) => {
    const label = items.map((i) => i.name).join(', ');
    return jsx('div', {
      title: label,
      children: items.length ? label : 'Select assignee',
    });
  }, []);
  const renderOption = useCallback((item) => {
    return jsx('span', {
      class: 'gl-new-dropdown-item-text-wrapper',
      children: jsx(GitlabUser, { showUsername: true, user: item }),
    });
  }, []);
  return jsx(AsyncAutocomplete, {
    isMultiselect: true,
    onChange: setValue,
    renderOption,
    getValues: getUsers,
    name: 'assignees',
    renderLabel,
    value,
  });
}

// apps/gitlab-plus/src/components/create-related-issue/fields/ButtonField.tsx
function ButtonField({ create, isLoading, reset }) {
  return jsxs(Fragment, {
    children: [
      jsxs('button', {
        onClick: create,
        class: 'btn btn-confirm btn-sm gl-button gl-gap-2',
        disabled: isLoading,
        type: 'button',
        children: [
          jsx('span', { class: 'gl-button-text', children: 'Add' }),
          isLoading
            ? jsx(GitlabLoader, { size: 12 })
            : jsx(GitlabIcon, { icon: 'plus', size: 12 }),
        ],
      }),
      jsx('button', {
        onClick: reset,
        class: 'btn btn-sm gl-button',
        type: 'button',
        children: jsx('span', { class: 'gl-button-text', children: 'Reset' }),
      }),
    ],
  });
}

// apps/gitlab-plus/src/providers/query/iteration.ts
const iterationFragment = `fragment IterationFragment on Iteration {
  id
  title
  startDate
  dueDate
  webUrl
  iterationCadence {
    id
    title
    __typename
  }
  __typename
}`;
const iterationQuery = `query issueIterationsAliased($fullPath: ID!, $title: String, $state: IterationState) {
  workspace: group(fullPath: $fullPath) {
    id
    attributes: iterations(
      search: $title
      in: [TITLE, CADENCE_TITLE]
      state: $state
    ) {
      nodes {
        ...IterationFragment
        state
        __typename
      }
      __typename
    }
    __typename
  }
}
${iterationFragment}
`;

// apps/gitlab-plus/src/providers/IterationsProvider.ts
class IterationsProvider extends GitlabProvider {
  async getIterations(projectId, title = '') {
    return this.queryCached(
      `iterations-${projectId} `,
      iterationQuery,
      {
        fullPath: projectId,
        title,
        state: 'opened',
      },
      title !== '' ? 0.5 : 20
    );
  }
}

// apps/gitlab-plus/src/components/create-related-issue/fields/IterationField.tsx
function iterationName(iteration) {
  const start = new Date(iteration.startDate).toLocaleDateString();
  const end = new Date(iteration.dueDate).toLocaleDateString();
  return `${iteration.iterationCadence.title}: ${start} - ${end}`;
}

function IterationField({ link, setValue, value }) {
  const getUsers = useCallback(
    async (search) => {
      const response = await new IterationsProvider().getIterations(
        link.workspacePath,
        search
      );
      return response.data.workspace.attributes.nodes
        .map((iteration) => ({
          ...iteration,
          name: iterationName(iteration),
        }))
        .toSorted((a, b) => a.name.localeCompare(b.name));
    },
    [link]
  );
  const renderLabel = useCallback(([item]) => {
    return item ? item.name : 'Select iteration';
  }, []);
  const renderOption = useCallback((item) => {
    return jsx('span', {
      class: 'gl-new-dropdown-item-text-wrapper',
      children: jsx('span', {
        class: 'gl-flex gl-w-full gl-items-center',
        children: jsx('span', {
          class: 'gl-mr-2 gl-block',
          children: item.name,
        }),
      }),
    });
  }, []);
  return jsx(AsyncAutocomplete, {
    onChange: setValue,
    renderOption,
    getValues: getUsers,
    name: 'iterations',
    renderLabel,
    value,
  });
}

// apps/gitlab-plus/src/providers/LabelsProvider.ts
class LabelsProvider extends GitlabProvider {
  async getLabels(projectId, search = '') {
    return this.queryCached(
      `labels-${projectId}-${search}`,
      labelsQuery,
      {
        fullPath: projectId,
        searchTerm: search,
      },
      search === '' ? 20 : 0.5
    );
  }
}

// apps/gitlab-plus/src/components/create-related-issue/fields/LabelsField.tsx
function LabelField({ link, setValue, value }) {
  const getLabels = useCallback(
    async (search) => {
      const response = await new LabelsProvider().getLabels(
        link.projectPath,
        search
      );
      return response.data.workspace.labels.nodes;
    },
    [link]
  );
  const renderLabel = useCallback((items) => {
    return items.length
      ? items.map((i) => i.title).join(', ')
      : 'Select labels';
  }, []);
  const renderOption = useCallback((item) => {
    return jsxs('div', {
      class: 'gl-flex gl-flex-1 gl-break-anywhere gl-pb-3 gl-pl-4 gl-pt-3',
      children: [
        jsx('span', {
          class: 'dropdown-label-box gl-top-0 gl-mr-3 gl-shrink-0',
          style: { backgroundColor: item.color },
        }),
        jsx('span', { children: item.title }),
      ],
    });
  }, []);
  return jsxs(Fragment, {
    children: [
      jsx('div', {
        class: 'gl-mt-1 gl-pb-2 gl-flex gl-flex-wrap gl-gap-2',
        children: value.map((label) =>
          jsx(
            GitlabLabel,
            {
              onRemove: () =>
                setValue(value.filter((item) => label.id !== item.id)),
              label,
            },
            label.id
          )
        ),
      }),
      jsx(AsyncAutocomplete, {
        isMultiselect: true,
        onChange: setValue,
        renderOption,
        getValues: getLabels,
        name: 'labels',
        renderLabel,
        value,
      }),
    ],
  });
}

// apps/gitlab-plus/src/providers/query/milestone.ts
const milestoneQuery = `query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
  workspace: project(fullPath: $fullPath) {
    id
    attributes: milestones(
      searchTitle: $title
      state: $state
      sort: EXPIRED_LAST_DUE_DATE_ASC
      first: 20
      includeAncestors: true
    ) {
      nodes {
        ...MilestoneFragment
        state
        __typename
      }
      __typename
    }
    __typename
  }
}

fragment MilestoneFragment on Milestone {
  id
  iid
  title
  webUrl: webPath
  dueDate
  expired
  __typename
}

`;

// apps/gitlab-plus/src/providers/MilestonesProvider.ts
class MilestonesProvider extends GitlabProvider {
  async getMilestones(projectId, title = '') {
    return this.queryCached(
      `milestones-${projectId}-${title}`,
      milestoneQuery,
      {
        fullPath: projectId,
        state: 'active',
        title,
      },
      title === '' ? 20 : 0.5
    );
  }
}

// apps/gitlab-plus/src/components/create-related-issue/fields/MilestoneField.tsx
function MilestoneField({ link, setValue, value }) {
  const getMilestones = useCallback(
    async (search) => {
      const response = await new MilestonesProvider().getMilestones(
        link.projectPath,
        search
      );
      return response.data.workspace.attributes.nodes;
    },
    [link]
  );
  const renderLabel = useCallback(([item]) => {
    return item ? item.title : 'Select milestone';
  }, []);
  const renderOption = useCallback((item) => {
    return jsx('span', {
      class: 'gl-new-dropdown-item-text-wrapper',
      children: jsx('span', {
        class: 'gl-flex gl-w-full gl-items-center',
        children: jsx('span', {
          class: 'gl-mr-2 gl-block',
          children: item.title,
        }),
      }),
    });
  }, []);
  return jsx(AsyncAutocomplete, {
    onChange: setValue,
    renderOption,
    getValues: getMilestones,
    name: 'milestones',
    renderLabel,
    value,
  });
}

// apps/gitlab-plus/src/providers/query/project.ts
const projectsQuery = `query boardsGetGroupProjects($fullPath: ID!, $search: String, $after: String) {
  group(fullPath: $fullPath) {
    id
    projects(search: $search, after: $after, first: 100, includeSubgroups: true) {
      nodes {
        id
        name
        avatarUrl
        fullPath
        nameWithNamespace
        archived
        __typename
      }
      pageInfo {
        ...PageInfo
        __typename
      }
      __typename
    }
    __typename
  }
}

fragment PageInfo on PageInfo {
  hasNextPage
  hasPreviousPage
  startCursor
  endCursor
  __typename
}

`;

// apps/gitlab-plus/src/providers/ProjectsProvider.ts
class ProjectsProvider extends GitlabProvider {
  async getProjects(workspacePath, search = '') {
    return this.queryCached(
      `projects-${workspacePath}-${search}`,
      projectsQuery,
      {
        fullPath: workspacePath,
        search,
      },
      search === '' ? 20 : 0.5
    );
  }
}

// apps/gitlab-plus/src/components/common/GitlabProject.tsx
function GitlabProject({ project, size = 32 }) {
  return jsxs('span', {
    class: 'gl-flex gl-w-full gl-items-center',
    children: [
      project.avatarUrl
        ? jsx('img', {
            alt: project.name,
            class: `gl-mr-3 gl-avatar gl-avatar-s${size}`,
            src: project.avatarUrl,
          })
        : jsx('div', {
            class: `gl-mr-3 gl-avatar gl-avatar-identicon gl-avatar-s${size} gl-avatar-identicon-bg1`,
            children: project.name[0].toUpperCase(),
          }),
      jsxs('span', {
        children: [
          jsx('span', { class: 'gl-mr-2 gl-block', children: project.name }),
          jsx('span', {
            class: 'gl-block gl-text-secondary !gl-text-sm',
            children: project.nameWithNamespace,
          }),
        ],
      }),
    ],
  });
}

// apps/gitlab-plus/src/components/create-related-issue/fields/ProjectField.tsx
function ProjectField({ link, setValue, value }) {
  const getProjects = useCallback(
    async (search) => {
      const response = await new ProjectsProvider().getProjects(
        link.workspacePath,
        search
      );
      return response.data.group.projects.nodes;
    },
    [link]
  );
  const renderLabel = useCallback(([item]) => {
    return item ? item.nameWithNamespace : 'Select project';
  }, []);
  const renderOption = useCallback((item) => {
    return jsx('span', {
      class: 'gl-new-dropdown-item-text-wrapper',
      children: jsx(GitlabProject, { project: item }),
    });
  }, []);
  return jsx(AsyncAutocomplete, {
    onChange: setValue,
    renderOption,
    getValues: getProjects,
    name: 'projects',
    renderLabel,
    value,
  });
}

// apps/gitlab-plus/src/types/Issue.ts
const issueRelation = ['blocks', 'is_blocked_by', 'relates_to'];

// apps/gitlab-plus/src/components/create-related-issue/fields/RelationField.tsx
const labels = (relation) => {
  switch (relation) {
    case 'blocks':
      return 'blocks current issue';
    case 'is_blocked_by':
      return 'is blocked by current issue';
    case 'relates_to':
      return 'relates to current issue';
    default:
      return 'is not related to current issue';
  }
};

function RelationField({ setValue, value }) {
  return jsx('div', {
    class: 'linked-issue-type-radio',
    children: [...issueRelation, null].map((relation) =>
      jsxs(
        'div',
        {
          class: 'gl-form-radio custom-control custom-radio',
          children: [
            jsx('input', {
              id: `create-related-issue-relation-${relation}`,
              onChange: () => setValue(relation),
              checked: value === relation,
              class: 'custom-control-input',
              name: 'linked-issue-type-radio',
              type: 'radio',
              value: relation ?? '',
            }),
            jsx('label', {
              for: `create-related-issue-relation-${relation}`,
              class: 'custom-control-label',
              children: labels(relation),
            }),
          ],
        },
        relation
      )
    ),
  });
}

// apps/gitlab-plus/src/components/create-related-issue/fields/TitleField.tsx
function TitleField({ error, onChange, value }) {
  return jsx('input', {
    class: clsx(
      'gl-form-input form-control',
      error && 'gl-field-error-outline'
    ),
    onInput: (e) => onChange(e.target.value),
    placeholder: 'Add a title',
    value,
  });
}

// apps/gitlab-plus/src/components/create-related-issue/useCreateRelatedIssueForm.ts
const initialState = () => ({
  assignees: [],
  iteration: null,
  labels: [],
  milestone: null,
  project: null,
  relation: null,
  title: '',
});
const initialError = () => ({
  assignees: void 0,
  iteration: void 0,
  labels: void 0,
  milestone: void 0,
  project: void 0,
  relation: void 0,
  title: void 0,
});

function useCreateRelatedIssueForm(link, onClose, isVisible) {
  const [values, setValues] = useState(initialState());
  const [errors, setErrors] = useState(initialError());
  const [isLoading, setIsLoading] = useState(false);
  const [message, setMessage] = useState('');
  const [error, setError] = useState('');
  const reset = () => {
    setIsLoading(false);
    setValues(initialState());
    setErrors(initialError());
  };
  useEffect(() => {
    if (!isVisible) {
      reset();
    }
  }, [isVisible]);
  const createPayload = () => {
    const data = {
      projectPath: values.project.fullPath,
      title: values.title,
    };
    if (values.milestone) {
      data['milestoneId'] = values.milestone.id;
    }
    if (values.iteration) {
      data['iterationId'] = values.iteration.id;
      data['iterationCadenceId'] = values.iteration.iterationCadence.id;
    }
    if (values.assignees) {
      data['assigneeIds'] = values.assignees.map((a) => a.id);
    }
    data['labelIds'] = values.labels.map((label) => label.id);
    return data;
  };
  const persistRecently = () => {
    Object.entries({
      assignees: values.assignees,
      iterations: values.iteration ? [values.iteration] : [],
      labels: values.labels,
      milestones: values.milestone ? [values.milestone] : [],
      projects: values.project ? [values.project] : [],
    }).map(([key, values2]) => {
      new RecentlyProvider(key).add(...values2);
    });
  };
  const validate = () => {
    let isValid = true;
    const errors2 = {};
    if (values.title.length < 1) {
      errors2.title = 'Title is required';
      isValid = false;
    } else if (values.title.length > 255) {
      errors2.title = 'Title is too long';
      isValid = false;
    }
    if (!values.project) {
      errors2.project = 'Project must be selected';
      isValid = false;
    }
    setErrors((prev) => ({ ...prev, ...errors2 }));
    return isValid;
  };
  const createIssue = async (payload) => {
    return await new IssueProvider().createIssue(payload);
  };
  const createRelation = async (issue, relation) => {
    await new IssueProvider().createIssueRelation({
      targetIssueIid: link.issue,
      issueId: issue.iid,
      linkType: relation,
      projectId: issue.projectId,
      targetProjectId: link.projectPath.replace(/\//g, '%2F'),
    });
  };
  const submit = async () => {
    setIsLoading(true);
    if (!validate()) {
      setIsLoading(false);
      return;
    }
    try {
      const payload = createPayload();
      const response = await createIssue(payload);
      persistRecently();
      if (values.relation) {
        await createRelation(
          response.data.createIssuable.issuable,
          values.relation
        );
      }
    } catch (e) {
      setError(e.message);
    }
    setIsLoading(false);
    setMessage('Issue was created');
    window.setTimeout(() => onClose(), 3e3);
  };
  return {
    actions: {
      reset,
      submit,
    },
    error,
    form: {
      assignees: {
        errors: errors.assignees,
        onChange: (assignees) => setValues({ ...values, assignees }),
        value: values.assignees,
      },
      iteration: {
        errors: errors.iteration,
        onChange: ([iteration]) =>
          setValues({ ...values, iteration: iteration ?? null }),
        value: values.iteration ? [values.iteration] : [],
      },
      labels: {
        errors: errors.labels,
        onChange: (labels2) => setValues({ ...values, labels: labels2 }),
        value: values.labels,
      },
      milestone: {
        errors: errors.milestone,
        onChange: ([milestone]) =>
          setValues({ ...values, milestone: milestone ?? null }),
        value: values.milestone ? [values.milestone] : [],
      },
      project: {
        errors: errors.project,
        onChange: ([project]) =>
          setValues({ ...values, project: project ?? null }),
        value: values.project ? [values.project] : [],
      },
      relation: {
        errors: errors.relation,
        onChange: (relation) => setValues({ ...values, relation }),
        value: values.relation,
      },
      title: {
        errors: errors.title,
        onChange: (title) => setValues({ ...values, title }),
        value: values.title,
      },
    },
    isLoading,
    message,
  };
}

// apps/gitlab-plus/src/components/create-related-issue/CreateRelatedIssueModalContent.tsx
function CreateRelatedIssueModalContent({ isVisible, link, onClose }) {
  const { actions, error, form, isLoading, message } =
    useCreateRelatedIssueForm(link, onClose, isVisible);
  return jsxs('form', {
    class: 'crud-body add-tree-form gl-mx-5 gl-my-4 gl-rounded-b-form',
    children: [
      jsx(FormField, {
        error: form.title.errors,
        hint: 'Maximum of 255 characters',
        title: 'Title',
        children: jsx(TitleField, {
          error: form.title.errors,
          onChange: form.title.onChange,
          value: form.title.value,
        }),
      }),
      jsxs(FormRow, {
        children: [
          jsx(FormField, {
            error: form.project.errors,
            title: 'Project',
            children: jsx(ProjectField, {
              link,
              setValue: form.project.onChange,
              value: form.project.value,
            }),
          }),
          jsx(FormField, {
            error: form.assignees.errors,
            title: 'Assignees',
            children: jsx(AssigneesField, {
              link,
              setValue: form.assignees.onChange,
              value: form.assignees.value,
            }),
          }),
        ],
      }),
      jsxs(FormRow, {
        children: [
          jsx(FormField, {
            error: form.iteration.errors,
            title: 'Iteration',
            children: jsx(IterationField, {
              link,
              setValue: form.iteration.onChange,
              value: form.iteration.value,
            }),
          }),
          jsx(FormField, {
            error: form.milestone.errors,
            title: 'Milestone',
            children: jsx(MilestoneField, {
              link,
              setValue: form.milestone.onChange,
              value: form.milestone.value,
            }),
          }),
        ],
      }),
      jsx(FormField, {
        error: form.labels.errors,
        title: 'Labels',
        children: jsx(LabelField, {
          link,
          setValue: form.labels.onChange,
          value: form.labels.value,
        }),
      }),
      jsx(FormField, {
        error: form.relation.errors,
        title: 'New issue',
        children: jsx(RelationField, {
          setValue: form.relation.onChange,
          value: form.relation.value,
        }),
      }),
      jsx(FormField, {
        error,
        hint: message,
        title: '',
        children: jsx(FormRow, {
          children: jsx(ButtonField, {
            isLoading,
            create: actions.submit,
            reset: actions.reset,
          }),
        }),
      }),
    ],
  });
}

// apps/gitlab-plus/src/components/create-related-issue/CreateRelatedIssueModal.tsx
function CreateRelatedIssueModal({ link }) {
  const [isVisible, setIsVisible] = useState(false);
  useEffect(() => {
    document.addEventListener(showModalEventName, () => setIsVisible(true));
  }, []);
  return jsx('div', {
    class: clsx(
      'glp-create-related-issue-layer',
      isVisible && 'glp-modal-visible'
    ),
    children: jsxs('div', {
      className: clsx(
        'glp-create-related-issue-modal crud gl-border',
        'gl-rounded-form gl-border-section gl-bg-subtle gl-mt-5'
      ),
      children: [
        jsxs('div', {
          className: clsx(
            'crud-header gl-border-b gl-flex gl-flex-wrap',
            'gl-justify-between gl-gap-x-5 gl-gap-y-2 gl-rounded-t-form',
            'gl-border-section gl-bg-section gl-px-5 gl-py-4 gl-relative'
          ),
          children: [
            jsx('h2', {
              className: clsx(
                'gl-m-0 gl-inline-flex gl-items-center gl-gap-3',
                'gl-text-form gl-font-bold gl-leading-normal'
              ),
              children: 'Create related issue',
            }),
            jsx(CloseButton, { onClick: () => setIsVisible(false) }),
          ],
        }),
        jsx(CreateRelatedIssueModalContent, {
          onClose: () => setIsVisible(false),
          isVisible,
          link,
        }),
      ],
    }),
  });
}

// apps/gitlab-plus/src/services/CreateRelatedIssue.tsx
class CreateRelatedIssue extends Service {
  constructor() {
    super();
    __publicField(this, 'isMounted', false);
  }

  init() {
    this.mount();
    setTimeout(this.mount.bind(this), 1e3);
    setTimeout(this.mount.bind(this), 3e3);
  }

  mount() {
    if (this.isMounted) {
      return;
    }
    const link = IssueLink.parseLink(window.location.href);
    const parent = document.querySelector(
      '#related-issues [data-testid="crud-actions"]'
    );
    if (!link || !parent) {
      return;
    }
    this.isMounted = true;
    render(
      jsx(CreateIssueButton, {}),
      this.root('glp-related-issue-button', parent)
    );
    render(
      jsx(CreateRelatedIssueModal, { link }),
      this.rootBody('glp-related-issue-modal')
    );
  }
}

// apps/gitlab-plus/src/components/related-issue-autocomplete/useRelatedIssuesAutocompleteModal.ts
function useRelatedIssuesAutocompleteModal(link, input) {
  const [searchTerm, setSearchTerm] = useState('');
  const [isVisible, setIsVisible] = useState(false);
  const options = useAsyncAutocompleteOptions(searchTerm, async (term) => {
    const response = await new IssueProvider().getIssues(
      link.workspacePath,
      term
    );
    return [
      response.data.workspace.workItems,
      response.data.workspace.workItemsByIid,
      response.data.workspace.workItemsEmpty,
    ].flatMap((item) => (item == null ? void 0 : item.nodes) || []);
  });
  const onSelect = (item) => {
    input.value = `${item.project.fullPath}#${item.iid} `;
    input.dispatchEvent(new Event('input'));
    input.dispatchEvent(new Event('change'));
  };
  useEffect(() => {
    document.body.addEventListener('click', (e) => {
      if (e.target !== input && !input.contains(e.target)) {
        setIsVisible(false);
      }
    });
    input.addEventListener('click', () => setIsVisible(true));
  }, []);
  return {
    isVisible,
    onClose: () => setIsVisible(false),
    onSelect,
    options,
    searchTerm,
    setSearchTerm,
  };
}

// apps/gitlab-plus/src/components/related-issue-autocomplete/RelatedIssuesAutocompleteModal.tsx
function RelatedIssuesAutocompleteModal({ input, link }) {
  const { isVisible, onClose, onSelect, options, searchTerm, setSearchTerm } =
    useRelatedIssuesAutocompleteModal(link, input);
  return isVisible
    ? jsx('div', {
        class: 'gl-relative gl-w-full gl-new-dropdown !gl-block',
        children: jsx(AsyncAutocompleteDropdown, {
          onClick: onSelect,
          onClose,
          options,
          renderOption: (item) =>
            jsxs('div', {
              class: 'gl-flex gl-gap-x-2 gl-py-2',
              children: [
                jsx(GitlabIcon, { icon: 'issue-type-issue', size: 16 }),
                jsx('small', { children: item.iid }),
                jsx('span', {
                  class: 'gl-flex gl-flex-wrap',
                  children: item.title,
                }),
              ],
            }),
          searchTerm,
          setSearchTerm,
          value: [],
        }),
      })
    : null;
}

// apps/gitlab-plus/src/services/RelatedIssueAutocomplete.tsx
class RelatedIssueAutocomplete extends Service {
  constructor() {
    super();
    __publicField(this, 'ready', false);
    __publicField(this, 'readyClass', 'glp-input-ready');
  }

  init() {
    this.initObserver();
    window.setTimeout(this.initObserver.bind(this), 1e3);
    window.setTimeout(this.initObserver.bind(this), 3e3);
    window.setTimeout(this.initObserver.bind(this), 5e3);
  }

  initAutocomplete(section) {
    const input = section.querySelector('#add-related-issues-form-input');
    const link = IssueLink.parseLink(window.location.href);
    if (!input || this.isMounted(input) || !link) {
      return;
    }
    const container = input.closest('.add-issuable-form-input-wrapper');
    if (!container || document.querySelector('.related-issues-autocomplete')) {
      return;
    }
    const root = this.root('related-issues-autocomplete', container);
    render(jsx(RelatedIssuesAutocompleteModal, { input, link }), root);
  }

  initObserver() {
    const section = document.querySelector('#related-issues');
    if (this.ready || !section) {
      return;
    }
    this.ready = true;
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          this.initAutocomplete(section);
        }
      });
    });
    observer.observe(section, {
      childList: true,
    });
  }

  isMounted(input) {
    return input.classList.contains(this.readyClass);
  }
}

// apps/gitlab-plus/src/services/ClearCacheService.ts
class ClearCacheService extends Service {
  constructor() {
    super();
    __publicField(this, 'cache', new Cache('glp-'));
  }

  init() {
    this.cache.clearInvalid();
    window.setInterval(this.cache.clearInvalid.bind(this.cache), 60 * 1e3);
  }
}

// libs/share/src/ui/Component.ts
class Component {
  constructor(tag, props = {}) {
    this.element = Dom.create({ tag, ...props });
  }

  addClassName(...className) {
    this.element.classList.add(...className);
  }

  event(event, callback) {
    this.element.addEventListener(event, callback);
  }

  getElement() {
    return this.element;
  }

  mount(parent) {
    parent.appendChild(this.element);
  }
}

// libs/share/src/ui/SvgComponent.ts
class SvgComponent {
  constructor(tag, props = {}) {
    this.element = Dom.createSvg({ tag, ...props });
  }

  addClassName(...className) {
    this.element.classList.add(...className);
  }

  event(event, callback) {
    this.element.addEventListener(event, callback);
  }

  getElement() {
    return this.element;
  }

  mount(parent) {
    parent.appendChild(this.element);
  }
}

// libs/share/src/ui/Dom.ts
class Dom {
  static appendChildren(element, children, isSvgMode = false) {
    if (children) {
      element.append(
        ...Dom.array(children).map((item) => {
          if (typeof item === 'string') {
            return document.createTextNode(item);
          }
          if (item instanceof HTMLElement || item instanceof SVGElement) {
            return item;
          }
          if (item instanceof Component || item instanceof SvgComponent) {
            return item.getElement();
          }
          const isSvg =
            'svg' === item.tag
              ? true
              : 'foreignObject' === item.tag
              ? false
              : isSvgMode;
          if (isSvg) {
            return Dom.createSvg(item);
          }
          return Dom.create(item);
        })
      );
    }
  }

  static applyAttrs(element, attrs) {
    if (attrs) {
      Object.entries(attrs).forEach(([key, value]) => {
        if (value === void 0 || value === false) {
          element.removeAttribute(key);
        } else {
          element.setAttribute(key, `${value}`);
        }
      });
    }
  }

  static applyClass(element, classes) {
    if (classes) {
      element.classList.add(...classes.split(' ').filter(Boolean));
    }
  }

  static applyEvents(element, events) {
    if (events) {
      Object.entries(events).forEach(([name, callback]) => {
        element.addEventListener(name, callback);
      });
    }
  }

  static applyStyles(element, styles) {
    if (styles) {
      Object.entries(styles).forEach(([key, value]) => {
        const name = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
        element.style.setProperty(name, value);
      });
    }
  }

  static array(element) {
    return Array.isArray(element) ? element : [element];
  }

  static create(data) {
    const element = document.createElement(data.tag);
    Dom.appendChildren(element, data.children);
    Dom.applyClass(element, data.classes);
    Dom.applyAttrs(element, data.attrs);
    Dom.applyEvents(element, data.events);
    Dom.applyStyles(element, data.styles);
    return element;
  }

  static createSvg(data) {
    const element = document.createElementNS(
      'http://www.w3.org/2000/svg',
      data.tag
    );
    Dom.appendChildren(element, data.children, true);
    Dom.applyClass(element, data.classes);
    Dom.applyAttrs(element, data.attrs);
    Dom.applyEvents(element, data.events);
    Dom.applyStyles(element, data.styles);
    return element;
  }

  static element(tag, classes, children) {
    return Dom.create({ tag, children, classes });
  }

  static elementSvg(tag, classes, children) {
    return Dom.createSvg({ tag, children, classes });
  }
}

// libs/share/src/ui/Observer.ts
class Observer {
  start(element, callback, options) {
    this.stop();
    this.observer = new MutationObserver(callback);
    this.observer.observe(
      element,
      options || {
        attributeOldValue: true,
        attributes: true,
        characterData: true,
        characterDataOldValue: true,
        childList: true,
        subtree: true,
      }
    );
  }

  stop() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }
}

// apps/gitlab-plus/src/services/SortIssue.ts
const sortWeight = {
  ['issue']: 4,
  ['label']: 0,
  ['ownIssue']: 10,
  ['ownUserStory']: 8,
  ['unknown']: 2,
  ['userStory']: 6,
};

class SortIssue extends Service {
  init() {
    const observer = new Observer();
    const userName = this.userName();
    const board = document.querySelector('.boards-list');
    if (!userName || !board) {
      return;
    }
    observer.start(board, () => this.run(userName));
  }

  childType(child, userName) {
    if (child instanceof HTMLDivElement) {
      return 'label';
    }
    const title = child.querySelector('[data-testid="board-card-title-link"]');
    if (!title) {
      return 'unknown';
    }
    const isOwn = [...child.querySelectorAll('.gl-avatar-link img')].some(
      (img) => img.alt.includes(userName)
    );
    const isUserStory = [...child.querySelectorAll('.gl-label')].some((span) =>
      span.innerText.includes('User Story')
    );
    if (isUserStory && isOwn) {
      return 'ownUserStory';
    }
    if (isOwn) {
      return 'ownIssue';
    }
    if (isUserStory) {
      return 'userStory';
    }
    return 'issue';
  }

  initBoard(board, userName) {
    Dom.applyClass(board, 'glp-ready');
    const observer = new Observer();
    observer.start(board, () => this.sortBoard(board, userName), {
      childList: true,
    });
  }

  run(userName) {
    [...document.querySelectorAll('.board-list:not(.glp-ready)')].forEach(
      (board) => this.initBoard(board, userName)
    );
  }

  shouldSort(items) {
    return items.some((item) => {
      return ['ownIssue', 'ownUserStory'].includes(item.type);
    });
  }

  sortBoard(board, userName) {
    Dom.applyStyles(board, {
      display: 'flex',
      flexDirection: 'column',
    });
    const children = [...board.children].map((element) => ({
      element,
      type: this.childType(element, userName),
    }));
    if (!this.shouldSort(children)) {
      return;
    }
    this.sortChildren(children).forEach(({ element }, index) => {
      const order =
        index !== children.length - 1 ? index + 1 : children.length + 100;
      element.style.order = `${order}`;
    });
  }

  sortChildren(items) {
    return items.toSorted((a, b) => {
      return Math.sign(sortWeight[b.type] - sortWeight[a.type]);
    });
  }

  userName() {
    const element = document.querySelector(
      '.user-bar-dropdown-toggle .gl-button-text .gl-sr-only'
    );
    const testText = ' user’s menu';
    if (element && element.innerText.includes(testText)) {
      return element.innerText.replace(testText, '');
    }
    return void 0;
  }
}

// apps/gitlab-plus/src/main.ts
[
  ClearCacheService,
  ImagePreview,
  IssuePreview,
  CreateRelatedIssue,
  RelatedIssueAutocomplete,
  SortIssue,
].forEach((Service2) => new Service2().init());

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址