|
| 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