gitea源码

DashboardRepoList.vue 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <script lang="ts">
  2. import {nextTick, defineComponent} from 'vue';
  3. import {SvgIcon} from '../svg.ts';
  4. import {GET} from '../modules/fetch.ts';
  5. import {fomanticQuery} from '../modules/fomantic/base.ts';
  6. const {appSubUrl, assetUrlPrefix, pageData} = window.config;
  7. type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning' | 'skipped';
  8. type CommitStatusMap = {
  9. [status in CommitStatus]: {
  10. name: string,
  11. color: string,
  12. };
  13. };
  14. // make sure this matches templates/repo/commit_status.tmpl
  15. const commitStatus: CommitStatusMap = {
  16. pending: {name: 'octicon-dot-fill', color: 'yellow'},
  17. success: {name: 'octicon-check', color: 'green'},
  18. error: {name: 'gitea-exclamation', color: 'red'},
  19. failure: {name: 'octicon-x', color: 'red'},
  20. warning: {name: 'gitea-exclamation', color: 'yellow'},
  21. skipped: {name: 'octicon-skip', color: 'grey'},
  22. };
  23. export default defineComponent({
  24. components: {SvgIcon},
  25. data() {
  26. const params = new URLSearchParams(window.location.search);
  27. const tab = params.get('repo-search-tab') || 'repos';
  28. const reposFilter = params.get('repo-search-filter') || 'all';
  29. const privateFilter = params.get('repo-search-private') || 'both';
  30. const archivedFilter = params.get('repo-search-archived') || 'unarchived';
  31. const searchQuery = params.get('repo-search-query') || '';
  32. const page = Number(params.get('repo-search-page')) || 1;
  33. return {
  34. tab,
  35. repos: [],
  36. reposTotalCount: null,
  37. reposFilter,
  38. archivedFilter,
  39. privateFilter,
  40. page,
  41. finalPage: 1,
  42. searchQuery,
  43. isLoading: false,
  44. staticPrefix: assetUrlPrefix,
  45. counts: {},
  46. repoTypes: {
  47. all: {
  48. searchMode: '',
  49. },
  50. forks: {
  51. searchMode: 'fork',
  52. },
  53. mirrors: {
  54. searchMode: 'mirror',
  55. },
  56. sources: {
  57. searchMode: 'source',
  58. },
  59. collaborative: {
  60. searchMode: 'collaborative',
  61. },
  62. },
  63. textArchivedFilterTitles: {},
  64. textPrivateFilterTitles: {},
  65. organizations: [],
  66. isOrganization: true,
  67. canCreateOrganization: false,
  68. organizationsTotalCount: 0,
  69. organizationId: 0,
  70. subUrl: appSubUrl,
  71. ...pageData.dashboardRepoList,
  72. activeIndex: -1, // don't select anything at load, first cursor down will select
  73. };
  74. },
  75. computed: {
  76. showMoreReposLink() {
  77. return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
  78. },
  79. searchURL() {
  80. return `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
  81. }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode
  82. }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
  83. }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : ''
  84. }`;
  85. },
  86. repoTypeCount() {
  87. return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
  88. },
  89. checkboxArchivedFilterTitle() {
  90. return this.textArchivedFilterTitles[this.archivedFilter];
  91. },
  92. checkboxArchivedFilterProps() {
  93. return {checked: this.archivedFilter === 'archived', indeterminate: this.archivedFilter === 'both'};
  94. },
  95. checkboxPrivateFilterTitle() {
  96. return this.textPrivateFilterTitles[this.privateFilter];
  97. },
  98. checkboxPrivateFilterProps() {
  99. return {checked: this.privateFilter === 'private', indeterminate: this.privateFilter === 'both'};
  100. },
  101. },
  102. mounted() {
  103. const el = document.querySelector('#dashboard-repo-list');
  104. this.changeReposFilter(this.reposFilter);
  105. fomanticQuery(el.querySelector('.ui.dropdown')).dropdown();
  106. this.textArchivedFilterTitles = {
  107. 'archived': this.textShowOnlyArchived,
  108. 'unarchived': this.textShowOnlyUnarchived,
  109. 'both': this.textShowBothArchivedUnarchived,
  110. };
  111. this.textPrivateFilterTitles = {
  112. 'private': this.textShowOnlyPrivate,
  113. 'public': this.textShowOnlyPublic,
  114. 'both': this.textShowBothPrivatePublic,
  115. };
  116. },
  117. methods: {
  118. changeTab(tab: string) {
  119. this.tab = tab;
  120. this.updateHistory();
  121. },
  122. changeReposFilter(filter: string) {
  123. this.reposFilter = filter;
  124. this.repos = [];
  125. this.page = 1;
  126. this.counts[`${filter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
  127. this.searchRepos();
  128. },
  129. updateHistory() {
  130. const params = new URLSearchParams(window.location.search);
  131. if (this.tab === 'repos') {
  132. params.delete('repo-search-tab');
  133. } else {
  134. params.set('repo-search-tab', this.tab);
  135. }
  136. if (this.reposFilter === 'all') {
  137. params.delete('repo-search-filter');
  138. } else {
  139. params.set('repo-search-filter', this.reposFilter);
  140. }
  141. if (this.privateFilter === 'both') {
  142. params.delete('repo-search-private');
  143. } else {
  144. params.set('repo-search-private', this.privateFilter);
  145. }
  146. if (this.archivedFilter === 'unarchived') {
  147. params.delete('repo-search-archived');
  148. } else {
  149. params.set('repo-search-archived', this.archivedFilter);
  150. }
  151. if (this.searchQuery === '') {
  152. params.delete('repo-search-query');
  153. } else {
  154. params.set('repo-search-query', this.searchQuery);
  155. }
  156. if (this.page === 1) {
  157. params.delete('repo-search-page');
  158. } else {
  159. params.set('repo-search-page', `${this.page}`);
  160. }
  161. const queryString = params.toString();
  162. if (queryString) {
  163. window.history.replaceState({}, '', `?${queryString}`);
  164. } else {
  165. window.history.replaceState({}, '', window.location.pathname);
  166. }
  167. },
  168. toggleArchivedFilter() {
  169. if (this.archivedFilter === 'unarchived') {
  170. this.archivedFilter = 'archived';
  171. } else if (this.archivedFilter === 'archived') {
  172. this.archivedFilter = 'both';
  173. } else { // including both
  174. this.archivedFilter = 'unarchived';
  175. }
  176. this.page = 1;
  177. this.repos = [];
  178. this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
  179. this.searchRepos();
  180. },
  181. togglePrivateFilter() {
  182. if (this.privateFilter === 'both') {
  183. this.privateFilter = 'public';
  184. } else if (this.privateFilter === 'public') {
  185. this.privateFilter = 'private';
  186. } else { // including private
  187. this.privateFilter = 'both';
  188. }
  189. this.page = 1;
  190. this.repos = [];
  191. this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
  192. this.searchRepos();
  193. },
  194. async changePage(page: number) {
  195. if (this.isLoading) return;
  196. this.page = page;
  197. if (this.page > this.finalPage) {
  198. this.page = this.finalPage;
  199. }
  200. if (this.page < 1) {
  201. this.page = 1;
  202. }
  203. this.repos = [];
  204. this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
  205. await this.searchRepos();
  206. },
  207. async searchRepos() {
  208. this.isLoading = true;
  209. const searchedMode = this.repoTypes[this.reposFilter].searchMode;
  210. const searchedURL = this.searchURL;
  211. const searchedQuery = this.searchQuery;
  212. let response, json;
  213. try {
  214. const firstLoad = this.reposTotalCount === null;
  215. if (!this.reposTotalCount) {
  216. const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
  217. response = await GET(totalCountSearchURL);
  218. this.reposTotalCount = parseInt(response.headers.get('X-Total-Count') ?? '0');
  219. }
  220. if (firstLoad && this.reposTotalCount) {
  221. nextTick(() => {
  222. // MDN: If there's no focused element, this is the Document.body or Document.documentElement.
  223. if ((document.activeElement === document.body || document.activeElement === document.documentElement)) {
  224. this.$refs.search.focus({preventScroll: true});
  225. }
  226. });
  227. }
  228. response = await GET(searchedURL);
  229. json = await response.json();
  230. } catch {
  231. if (searchedURL === this.searchURL) {
  232. this.isLoading = false;
  233. }
  234. return;
  235. }
  236. if (searchedURL === this.searchURL) {
  237. this.repos = json.data.map((webSearchRepo: any) => {
  238. return {
  239. ...webSearchRepo.repository,
  240. latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status
  241. latest_commit_status_state_link: webSearchRepo.latest_commit_status?.TargetURL,
  242. locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,
  243. };
  244. });
  245. const count = Number(response.headers.get('X-Total-Count'));
  246. if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
  247. this.reposTotalCount = count;
  248. }
  249. this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = count;
  250. this.finalPage = Math.ceil(count / this.searchLimit);
  251. this.updateHistory();
  252. this.isLoading = false;
  253. }
  254. },
  255. repoIcon(repo: any) {
  256. if (repo.fork) {
  257. return 'octicon-repo-forked';
  258. } else if (repo.mirror) {
  259. return 'octicon-mirror';
  260. } else if (repo.template) {
  261. return `octicon-repo-template`;
  262. } else if (repo.private) {
  263. return 'octicon-lock';
  264. } else if (repo.internal) {
  265. return 'octicon-repo';
  266. }
  267. return 'octicon-repo';
  268. },
  269. statusIcon(status: CommitStatus) {
  270. return commitStatus[status].name;
  271. },
  272. statusColor(status: CommitStatus) {
  273. return commitStatus[status].color;
  274. },
  275. async reposFilterKeyControl(e: KeyboardEvent) {
  276. switch (e.key) {
  277. case 'Enter':
  278. document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click();
  279. break;
  280. case 'ArrowUp':
  281. if (this.activeIndex > 0) {
  282. this.activeIndex--;
  283. } else if (this.page > 1) {
  284. await this.changePage(this.page - 1);
  285. this.activeIndex = this.searchLimit - 1;
  286. }
  287. break;
  288. case 'ArrowDown':
  289. if (this.activeIndex < this.repos.length - 1) {
  290. this.activeIndex++;
  291. } else if (this.page < this.finalPage) {
  292. this.activeIndex = 0;
  293. await this.changePage(this.page + 1);
  294. }
  295. break;
  296. case 'ArrowRight':
  297. if (this.page < this.finalPage) {
  298. await this.changePage(this.page + 1);
  299. }
  300. break;
  301. case 'ArrowLeft':
  302. if (this.page > 1) {
  303. await this.changePage(this.page - 1);
  304. }
  305. break;
  306. }
  307. if (this.activeIndex === -1 || this.activeIndex > this.repos.length - 1) {
  308. this.activeIndex = 0;
  309. }
  310. },
  311. },
  312. });
  313. </script>
  314. <template>
  315. <div>
  316. <div v-if="!isOrganization" class="ui two item menu">
  317. <a :class="{item: true, active: tab === 'repos'}" @click="changeTab('repos')">{{ textRepository }}</a>
  318. <a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{ textOrganization }}</a>
  319. </div>
  320. <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos">
  321. <h4 class="ui top attached header tw-flex tw-items-center">
  322. <div class="tw-flex-1 tw-flex tw-items-center">
  323. {{ textMyRepos }}
  324. <span v-if="reposTotalCount" class="ui grey label tw-ml-2">{{ reposTotalCount }}</span>
  325. </div>
  326. <a class="tw-flex tw-items-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
  327. <svg-icon name="octicon-plus"/>
  328. </a>
  329. </h4>
  330. <div v-if="!reposTotalCount" class="ui attached segment">
  331. <div v-if="!isLoading" class="empty-repo-or-org">
  332. <svg-icon name="octicon-git-branch" :size="24"/>
  333. <p>{{ textNoRepo }}</p>
  334. </div>
  335. <!-- using the loading indicator here will cause more (unnecessary) page flickers, so at the moment, not use the loading indicator -->
  336. <!-- <div v-else class="is-loading loading-icon-2px tw-min-h-16"/> -->
  337. </div>
  338. <div v-else class="ui attached segment repos-search">
  339. <div class="ui small fluid action left icon input">
  340. <input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos">
  341. <i class="icon loading-icon-3px" :class="{'is-loading': isLoading}"><svg-icon name="octicon-search" :size="16"/></i>
  342. <div class="ui dropdown icon button" :title="textFilter">
  343. <svg-icon name="octicon-filter" :size="16"/>
  344. <div class="menu">
  345. <a class="item" @click="toggleArchivedFilter()">
  346. <div class="ui checkbox" ref="checkboxArchivedFilter" :title="checkboxArchivedFilterTitle">
  347. <!--the "tw-pointer-events-none" is necessary to prevent the checkbox from handling user's input,
  348. otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
  349. <input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxArchivedFilterProps">
  350. <label>
  351. <svg-icon name="octicon-archive" :size="16" class="tw-mr-1"/>
  352. {{ textShowArchived }}
  353. </label>
  354. </div>
  355. </a>
  356. <a class="item" @click="togglePrivateFilter()">
  357. <div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
  358. <input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxPrivateFilterProps">
  359. <label>
  360. <svg-icon name="octicon-lock" :size="16" class="tw-mr-1"/>
  361. {{ textShowPrivate }}
  362. </label>
  363. </div>
  364. </a>
  365. </div>
  366. </div>
  367. </div>
  368. <overflow-menu class="ui secondary pointing tabular borderless menu repos-filter">
  369. <div class="overflow-menu-items tw-justify-center">
  370. <a class="item" tabindex="0" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
  371. {{ textAll }}
  372. <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
  373. </a>
  374. <a class="item" tabindex="0" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
  375. {{ textSources }}
  376. <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
  377. </a>
  378. <a class="item" tabindex="0" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
  379. {{ textForks }}
  380. <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
  381. </a>
  382. <a class="item" tabindex="0" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
  383. {{ textMirrors }}
  384. <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
  385. </a>
  386. <a class="item" tabindex="0" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
  387. {{ textCollaborative }}
  388. <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
  389. </a>
  390. </div>
  391. </overflow-menu>
  392. </div>
  393. <div v-if="repos.length" class="ui attached table segment tw-rounded-b">
  394. <ul class="repo-owner-name-list">
  395. <li class="tw-flex tw-items-center tw-py-2" v-for="(repo, index) in repos" :class="{'active': index === activeIndex}" :key="repo.id">
  396. <a class="repo-list-link muted" :href="repo.link">
  397. <svg-icon :name="repoIcon(repo)" :size="16" class="repo-list-icon"/>
  398. <div class="text truncate">{{ repo.full_name }}</div>
  399. <div v-if="repo.archived">
  400. <svg-icon name="octicon-archive" :size="16"/>
  401. </div>
  402. </a>
  403. <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link || null" :data-tooltip-content="repo.locale_latest_commit_status_state">
  404. <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
  405. <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
  406. </a>
  407. </li>
  408. </ul>
  409. <div v-if="showMoreReposLink" class="tw-text-center">
  410. <div class="divider tw-my-0"/>
  411. <div class="ui borderless pagination menu narrow tw-my-2">
  412. <a
  413. class="item navigation tw-py-1" :class="{'disabled': page === 1}"
  414. @click="changePage(1)" :title="textFirstPage"
  415. >
  416. <svg-icon name="gitea-double-chevron-left" :size="16" class="tw-mr-1"/>
  417. </a>
  418. <a
  419. class="item navigation tw-py-1" :class="{'disabled': page === 1}"
  420. @click="changePage(page - 1)" :title="textPreviousPage"
  421. >
  422. <svg-icon name="octicon-chevron-left" :size="16" class="tw-mr-1"/>
  423. </a>
  424. <a class="active item tw-py-1">{{ page }}</a>
  425. <a
  426. class="item navigation" :class="{'disabled': page === finalPage}"
  427. @click="changePage(page + 1)" :title="textNextPage"
  428. >
  429. <svg-icon name="octicon-chevron-right" :size="16" class="tw-ml-1"/>
  430. </a>
  431. <a
  432. class="item navigation tw-py-1" :class="{'disabled': page === finalPage}"
  433. @click="changePage(finalPage)" :title="textLastPage"
  434. >
  435. <svg-icon name="gitea-double-chevron-right" :size="16" class="tw-ml-1"/>
  436. </a>
  437. </div>
  438. </div>
  439. </div>
  440. </div>
  441. <div v-if="!isOrganization" v-show="tab === 'organizations'" class="ui tab active list dashboard-orgs">
  442. <h4 class="ui top attached header tw-flex tw-items-center">
  443. <div class="tw-flex-1 tw-flex tw-items-center">
  444. {{ textMyOrgs }}
  445. <span class="ui grey label tw-ml-2">{{ organizationsTotalCount }}</span>
  446. </div>
  447. <a class="tw-flex tw-items-center muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg">
  448. <svg-icon name="octicon-plus"/>
  449. </a>
  450. </h4>
  451. <div v-if="!organizations.length" class="ui attached segment">
  452. <div class="empty-repo-or-org">
  453. <svg-icon name="octicon-organization" :size="24"/>
  454. <p>{{ textNoOrg }}</p>
  455. </div>
  456. </div>
  457. <div v-else class="ui attached table segment tw-rounded-b">
  458. <ul class="repo-owner-name-list">
  459. <li class="tw-flex tw-items-center tw-py-2" v-for="org in organizations" :key="org.name">
  460. <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
  461. <svg-icon name="octicon-organization" :size="16" class="repo-list-icon"/>
  462. <div class="text truncate">{{ org.full_name ? `${org.full_name} (${org.name})` : org.name }}</div>
  463. <div><!-- div to prevent underline of label on hover -->
  464. <span class="ui tiny basic label" v-if="org.org_visibility !== 'public'">
  465. {{ org.org_visibility === 'limited' ? textOrgVisibilityLimited: textOrgVisibilityPrivate }}
  466. </span>
  467. </div>
  468. </a>
  469. <div class="text light grey tw-flex tw-items-center tw-ml-2">
  470. {{ org.num_repos }}
  471. <svg-icon name="octicon-repo" :size="16" class="tw-ml-1 tw-mt-0.5"/>
  472. </div>
  473. </li>
  474. </ul>
  475. </div>
  476. </div>
  477. </div>
  478. </template>
  479. <style scoped>
  480. ul {
  481. list-style: none;
  482. margin: 0;
  483. padding-left: 0;
  484. }
  485. ul li {
  486. padding: 0 10px;
  487. }
  488. ul li:not(:last-child) {
  489. border-bottom: 1px solid var(--color-secondary);
  490. }
  491. .repos-search {
  492. padding-bottom: 0 !important;
  493. }
  494. .repos-filter {
  495. margin-top: 0 !important;
  496. border-bottom-width: 0 !important;
  497. }
  498. .repos-filter .item {
  499. padding-left: 6px !important;
  500. padding-right: 6px !important;
  501. }
  502. .repo-list-link {
  503. min-width: 0; /* for text truncation */
  504. display: flex;
  505. align-items: center;
  506. flex: 1;
  507. gap: 0.5rem;
  508. }
  509. .repo-list-link .svg {
  510. color: var(--color-text-light-2);
  511. }
  512. .repo-list-icon {
  513. min-width: 16px;
  514. margin-right: 2px;
  515. }
  516. /* octicon-mirror has no padding inside the SVG */
  517. .repo-list-icon.octicon-mirror {
  518. width: 14px;
  519. min-width: 14px;
  520. margin-left: 1px;
  521. margin-right: 3px;
  522. }
  523. .repo-owner-name-list li.active {
  524. background: var(--color-hover);
  525. }
  526. .empty-repo-or-org {
  527. margin-top: 1em;
  528. text-align: center;
  529. color: var(--color-placeholder-text);
  530. }
  531. .empty-repo-or-org p {
  532. margin: 1em auto;
  533. }
  534. </style>