| 
 | 1 | +<script setup lang="ts">  | 
 | 2 | +import type {SortableOptions} from 'sortablejs';  | 
 | 3 | +import DashboardRepoGroupItem from './DashboardRepoGroupItem.vue';  | 
 | 4 | +import {Sortable} from 'sortablejs-vue3';  | 
 | 5 | +import hash from 'object-hash';  | 
 | 6 | +import {computed, inject, nextTick, type ComputedRef, type WritableComputedRef} from 'vue';  | 
 | 7 | +import {GET, POST} from '../modules/fetch.ts';  | 
 | 8 | +import type {GroupMapType} from './DashboardRepoList.vue';  | 
 | 9 | +const {curGroup, depth} = defineProps<{ curGroup: number; depth: number; }>();  | 
 | 10 | +const emitter = defineEmits<{  | 
 | 11 | +  loadChanged: [ boolean ],  | 
 | 12 | +  itemAdded: [ item: any, index: number ],  | 
 | 13 | +  itemRemoved: [ item: any, index: number ]  | 
 | 14 | +}>();  | 
 | 15 | +const groupData = inject<WritableComputedRef<Map<number, GroupMapType>>>('groups');  | 
 | 16 | +const searchUrl = inject<string>('searchURL');  | 
 | 17 | +const orgName = inject<string>('orgName');  | 
 | 18 | +
  | 
 | 19 | +const combined = computed(() => {  | 
 | 20 | +  let groups = groupData.value.get(curGroup)?.subgroups ?? [];  | 
 | 21 | +  groups = Array.from(new Set(groups));  | 
 | 22 | +
  | 
 | 23 | +  const repos = (groupData.value.get(curGroup)?.repos ?? []).filter((a, pos, arr) => arr.findIndex((b) => b.id === a.id) === pos);  | 
 | 24 | +  const c = [  | 
 | 25 | +    ...groups, // ,  | 
 | 26 | +    ...repos,  | 
 | 27 | +  ];  | 
 | 28 | +  return c;  | 
 | 29 | +});  | 
 | 30 | +function repoMapper(webSearchRepo: any) {  | 
 | 31 | +  return {  | 
 | 32 | +    ...webSearchRepo.repository,  | 
 | 33 | +    latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status  | 
 | 34 | +    latest_commit_status_state_link: webSearchRepo.latest_commit_status?.TargetURL,  | 
 | 35 | +    locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,  | 
 | 36 | +  };  | 
 | 37 | +}  | 
 | 38 | +function mapper(item: any) {  | 
 | 39 | +  groupData.value.set(item.group.id, {  | 
 | 40 | +    repos: item.repos.map((a: any) => repoMapper(a)),  | 
 | 41 | +    subgroups: item.subgroups.map((a: {group: any}) => a.group.id),  | 
 | 42 | +    ...item.group,  | 
 | 43 | +    latest_commit_status_state: item.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status  | 
 | 44 | +    latest_commit_status_state_link: item.latest_commit_status?.TargetURL,  | 
 | 45 | +    locale_latest_commit_status_state: item.locale_latest_commit_status,  | 
 | 46 | +  });  | 
 | 47 | +  // return {  | 
 | 48 | +  // ...item.group,  | 
 | 49 | +  // subgroups: item.subgroups.map((a) => mapper(a)),  | 
 | 50 | +  // repos: item.repos.map((a) => repoMapper(a)),  | 
 | 51 | +  // };  | 
 | 52 | +}  | 
 | 53 | +async function searchGroup(gid: number) {  | 
 | 54 | +  emitter('loadChanged', true);  | 
 | 55 | +  const searchedURL = `${searchUrl}&group_id=${gid}`;  | 
 | 56 | +  let response, json;  | 
 | 57 | +  try {  | 
 | 58 | +    response = await GET(searchedURL);  | 
 | 59 | +    json = await response.json();  | 
 | 60 | +  } catch {  | 
 | 61 | +    emitter('loadChanged', false);  | 
 | 62 | +    return;  | 
 | 63 | +  }  | 
 | 64 | +  mapper(json.data);  | 
 | 65 | +  for (const g of json.data.subgroups) {  | 
 | 66 | +    mapper(g);  | 
 | 67 | +  }  | 
 | 68 | +  emitter('loadChanged', false);  | 
 | 69 | +  const tmp = groupData.value;  | 
 | 70 | +  groupData.value = tmp;  | 
 | 71 | +}  | 
 | 72 | +const orepos = inject<ComputedRef<any[]>>('repos');  | 
 | 73 | +
  | 
 | 74 | +const dynKey = computed(() => hash(combined.value));  | 
 | 75 | +function getId(it: any) {  | 
 | 76 | +  if (typeof it === 'number') {  | 
 | 77 | +    return `group-${it}`;  | 
 | 78 | +  }  | 
 | 79 | +  return `repo-${it.id}`;  | 
 | 80 | +}  | 
 | 81 | +
  | 
 | 82 | +const options: SortableOptions = {  | 
 | 83 | +  group: {  | 
 | 84 | +    name: 'repo-group',  | 
 | 85 | +    put(to, _from, _drag, _ev) {  | 
 | 86 | +      const closestLi = to.el?.closest('li');  | 
 | 87 | +      const base = to.el.getAttribute('data-is-group').toLowerCase() === 'true';  | 
 | 88 | +      if (closestLi) {  | 
 | 89 | +        const input = Array.from(closestLi?.querySelector('label')?.children).find((a) => a instanceof HTMLInputElement && a.checked);  | 
 | 90 | +        return base && Boolean(input);  | 
 | 91 | +      }  | 
 | 92 | +      return base;  | 
 | 93 | +    },  | 
 | 94 | +    pull: true,  | 
 | 95 | +  },  | 
 | 96 | +  delay: 500,  | 
 | 97 | +  emptyInsertThreshold: 50,  | 
 | 98 | +  delayOnTouchOnly: true,  | 
 | 99 | +  dataIdAttr: 'data-sort-id',  | 
 | 100 | +  draggable: '.expandable-menu-item',  | 
 | 101 | +  dragClass: 'active',  | 
 | 102 | +  store: {  | 
 | 103 | +    get() {  | 
 | 104 | +      return combined.value.map((a) => getId(a)).filter((a, i, arr) => arr.indexOf(a) === i);  | 
 | 105 | +    },  | 
 | 106 | +    // eslint-disable-next-line @typescript-eslint/no-misused-promises  | 
 | 107 | +    async set(sortable) {  | 
 | 108 | +      const arr = sortable.toArray();  | 
 | 109 | +      const groups = Array.from(new Set(arr.filter((a) => a.startsWith('group')).map((a) => parseInt(a.split('-')[1]))));  | 
 | 110 | +      const repos = arr  | 
 | 111 | +        .filter((a) => a.startsWith('repo'))  | 
 | 112 | +        .map((a) => orepos.value.filter(Boolean).find((b) => b.id === parseInt(a.split('-')[1])))  | 
 | 113 | +        .map((a, i) => ({...a, group_sort_order: i + 1}))  | 
 | 114 | +        .filter((a, pos, arr) => arr.findIndex((b) => b.id === a.id) === pos);  | 
 | 115 | +
  | 
 | 116 | +      for (let i = 0; i < groups.length; i++) {  | 
 | 117 | +        const cur = groupData.value.get(groups[i]);  | 
 | 118 | +        groupData.value.set(groups[i], {  | 
 | 119 | +          ...cur,  | 
 | 120 | +          sort_order: i + 1,  | 
 | 121 | +        });  | 
 | 122 | +      }  | 
 | 123 | +      const cur = groupData.value.get(curGroup);  | 
 | 124 | +      const ndata: GroupMapType = {  | 
 | 125 | +        ...cur,  | 
 | 126 | +        subgroups: groups.toSorted((a, b) => groupData.value.get(a).sort_order - groupData.value.get(b).sort_order),  | 
 | 127 | +        repos: repos.toSorted((a, b) => a.group_sort_order - b.group_sort_order),  | 
 | 128 | +      };  | 
 | 129 | +      groupData.value.set(curGroup, ndata);  | 
 | 130 | +      // const tmp = groupData.value;  | 
 | 131 | +      // groupData.value = tmp;  | 
 | 132 | +      for (let i = 0; i < ndata.subgroups.length; i++) {  | 
 | 133 | +        const sg = ndata.subgroups[i];  | 
 | 134 | +        const data = {  | 
 | 135 | +          newParent: curGroup,  | 
 | 136 | +          id: sg,  | 
 | 137 | +          newPos: i + 1,  | 
 | 138 | +          isGroup: true,  | 
 | 139 | +        };  | 
 | 140 | +        try {  | 
 | 141 | +          await POST(`/${orgName}/groups/items/move`, {  | 
 | 142 | +            data,  | 
 | 143 | +          });  | 
 | 144 | +        } catch (error) {  | 
 | 145 | +          console.error(error);  | 
 | 146 | +        }  | 
 | 147 | +      }  | 
 | 148 | +      for (const r of ndata.repos) {  | 
 | 149 | +        const data = {  | 
 | 150 | +          newParent: curGroup,  | 
 | 151 | +          id: r.id,  | 
 | 152 | +          newPos: r.group_sort_order,  | 
 | 153 | +          isGroup: false,  | 
 | 154 | +        };  | 
 | 155 | +        try {  | 
 | 156 | +          await POST(`/${orgName}/groups/items/move`, {  | 
 | 157 | +            data,  | 
 | 158 | +          });  | 
 | 159 | +        } catch (error) {  | 
 | 160 | +          console.error(error);  | 
 | 161 | +        }  | 
 | 162 | +      }  | 
 | 163 | +      nextTick(() => {  | 
 | 164 | +        const finalSorted = [  | 
 | 165 | +          ...ndata.subgroups,  | 
 | 166 | +          ...ndata.repos,  | 
 | 167 | +        ].map(getId);  | 
 | 168 | +        try {  | 
 | 169 | +          sortable.sort(finalSorted, true);  | 
 | 170 | +        } catch {}  | 
 | 171 | +      });  | 
 | 172 | +    },  | 
 | 173 | +  },  | 
 | 174 | +};  | 
 | 175 | +
  | 
 | 176 | +</script>  | 
 | 177 | +<template>  | 
 | 178 | +  <Sortable  | 
 | 179 | +    :options="options" tag="ul"  | 
 | 180 | +    :class="{ 'expandable-menu': curGroup === 0, 'repo-owner-name-list': curGroup === 0, 'expandable-ul': true }"  | 
 | 181 | +    v-model:list="combined"  | 
 | 182 | +    :data-is-group="true"  | 
 | 183 | +    :item-key="(it) => getId(it)"  | 
 | 184 | +    :key="dynKey"  | 
 | 185 | +  >  | 
 | 186 | +    <template #item="{ element, index }">  | 
 | 187 | +      <dashboard-repo-group-item  | 
 | 188 | +        :index="index + 1"  | 
 | 189 | +        :item="element"  | 
 | 190 | +        :depth="depth + 1"  | 
 | 191 | +        :key="getId(element)"  | 
 | 192 | +        @load-requested="searchGroup"  | 
 | 193 | +      />  | 
 | 194 | +    </template>  | 
 | 195 | +  </Sortable>  | 
 | 196 | +</template>  | 
 | 197 | +<style scoped>  | 
 | 198 | +ul.expandable-ul {  | 
 | 199 | +  list-style: none;  | 
 | 200 | +  margin: 0;  | 
 | 201 | +  padding-left: 0;  | 
 | 202 | +}  | 
 | 203 | +
  | 
 | 204 | +ul.expandable-ul li {  | 
 | 205 | +  padding: 0 10px;  | 
 | 206 | +}  | 
 | 207 | +.repos-search {  | 
 | 208 | +  padding-bottom: 0 !important;  | 
 | 209 | +}  | 
 | 210 | +
  | 
 | 211 | +.repos-filter {  | 
 | 212 | +  margin-top: 0 !important;  | 
 | 213 | +  border-bottom-width: 0 !important;  | 
 | 214 | +}  | 
 | 215 | +
  | 
 | 216 | +.repos-filter .item {  | 
 | 217 | +  padding-left: 6px !important;  | 
 | 218 | +  padding-right: 6px !important;  | 
 | 219 | +}  | 
 | 220 | +
  | 
 | 221 | +.repo-owner-name-list li.active {  | 
 | 222 | +  background: var(--color-hover);  | 
 | 223 | +}  | 
 | 224 | +ul.expandable-ul > li:not(:last-child) {  | 
 | 225 | +  border-bottom: 1px solid var(--color-secondary);  | 
 | 226 | +}  | 
 | 227 | +ul.expandable-ul > li:first-child {  | 
 | 228 | +  border-top: 1px solid var(--color-secondary);  | 
 | 229 | +}  | 
 | 230 | +</style>  | 
0 commit comments