summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/access_tokens/components/projects_token_selector.vue
blob: 4843c52fcbbcc2619cba93abaaeac269f179088f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<script>
import {
  GlTokenSelector,
  GlAvatar,
  GlAvatarLabeled,
  GlIntersectionObserver,
  GlLoadingIcon,
} from '@gitlab/ui';
import produce from 'immer';

import { convertToGraphQLIds, convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';

import getProjectsQuery from '../graphql/queries/get_projects.query.graphql';

const DEBOUNCE_DELAY = 250;
const PROJECTS_PER_PAGE = 20;
const GRAPHQL_ENTITY_TYPE = 'Project';

export default {
  name: 'ProjectsTokenSelector',
  components: {
    GlTokenSelector,
    GlAvatar,
    GlAvatarLabeled,
    GlIntersectionObserver,
    GlLoadingIcon,
  },
  model: {
    prop: 'selectedProjects',
  },
  props: {
    selectedProjects: {
      type: Array,
      required: true,
    },
    initialProjectIds: {
      type: Array,
      required: true,
    },
  },
  apollo: {
    projects: {
      query: getProjectsQuery,
      debounce: DEBOUNCE_DELAY,
      variables() {
        return {
          search: this.searchQuery,
          after: null,
          first: PROJECTS_PER_PAGE,
        };
      },
      update({ projects }) {
        return {
          list: convertNodeIdsFromGraphQLIds(projects.nodes),
          pageInfo: projects.pageInfo,
        };
      },
      result() {
        this.isLoadingMoreProjects = false;
        this.isSearching = false;
      },
    },
    initialProjects: {
      query: getProjectsQuery,
      variables() {
        return {
          ids: convertToGraphQLIds(GRAPHQL_ENTITY_TYPE, this.initialProjectIds),
        };
      },
      manual: true,
      skip() {
        return !this.initialProjectIds.length;
      },
      result({ data: { projects } }) {
        this.$emit('input', convertNodeIdsFromGraphQLIds(projects.nodes));
      },
    },
  },
  data() {
    return {
      projects: {
        list: [],
        pageInfo: {},
      },
      searchQuery: '',
      isLoadingMoreProjects: false,
      isSearching: false,
    };
  },
  methods: {
    handleSearch(query) {
      this.isSearching = true;
      this.searchQuery = query;
    },
    loadMoreProjects() {
      this.isLoadingMoreProjects = true;

      this.$apollo.queries.projects.fetchMore({
        variables: {
          after: this.projects.pageInfo.endCursor,
          first: PROJECTS_PER_PAGE,
        },
        updateQuery(previousResult, { fetchMoreResult: { projects: newProjects } }) {
          const { projects: previousProjects } = previousResult;

          return produce(previousResult, (draftData) => {
            draftData.projects.nodes = [...previousProjects.nodes, ...newProjects.nodes];
            draftData.projects.pageInfo = newProjects.pageInfo;
          });
        },
      });
    },
  },
};
</script>

<template>
  <div class="gl-relative">
    <gl-token-selector
      :selected-tokens="selectedProjects"
      :dropdown-items="projects.list"
      :loading="isSearching"
      :placeholder="__('Select projects')"
      menu-class="gl-w-full! gl-max-w-full!"
      @input="$emit('input', $event)"
      @focus="$emit('focus', $event)"
      @text-input="handleSearch"
      @keydown.enter.prevent
    >
      <template #token-content="{ token: project }">
        <gl-avatar
          :entity-id="project.id"
          :entity-name="project.name"
          :src="project.avatarUrl"
          :size="16"
        />
        {{ project.nameWithNamespace }}
      </template>
      <template #dropdown-item-content="{ dropdownItem: project }">
        <gl-avatar-labeled
          :entity-id="project.id"
          :entity-name="project.name"
          :size="32"
          :src="project.avatarUrl"
          :label="project.name"
          :sub-label="project.nameWithNamespace"
        />
      </template>
      <template #dropdown-footer>
        <gl-intersection-observer v-if="projects.pageInfo.hasNextPage" @appear="loadMoreProjects">
          <gl-loading-icon v-if="isLoadingMoreProjects" class="gl-mb-3" size="sm" />
        </gl-intersection-observer>
      </template>
    </gl-token-selector>
  </div>
</template>