/
uzer_007
/
Rixea
Обзор
Документация
Войти
/
uzer_007
/
Rixea
Код
Запросы
0
Задачи
Вики
Пакеты
5
Релизы
2
CI/CD
Аналитика
Безопасность
master
web_src/js/components/ViewFileTreeStore.ts
142 строки
6 KB
uzer_007
feat: первая версия Rixea
02 авг 2026, 22:16
02 авг 2026, 22:16
d4f7504
Код
Авторство
О чём код?
import {reactive} from 'vue'; import {GET} from '../modules/fetch.ts'; import {pathEscapeSegments} from '../utils/url.ts'; import {createElementFromHTML} from '../utils/dom.ts'; import {html} from '../utils/html.ts'; export type FileTreeItem = { entryName: string; entryMode: 'blob' | 'exec' | 'tree' | 'commit' | 'symlink' | 'unknown'; entryIcon: string; entryIconOpen?: string; fullPath: string; submoduleUrl?: string; children?: Array<FileTreeItem>; }; type FileTreeStoreProps = { repoLink: string; treePath: string; currentRefNameSubURL: string; loadErrorText: string; contentLoadErrorText: string; retryText: string; }; const fileTreeEntryModes = new Set<FileTreeItem['entryMode']>(['blob', 'exec', 'tree', 'commit', 'symlink', 'unknown']); function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null; } function isFileTreeItem(value: unknown): value is FileTreeItem { if (!isRecord(value)) return false; if ( typeof value.entryName !== 'string' || typeof value.entryMode !== 'string' || !fileTreeEntryModes.has(value.entryMode as FileTreeItem['entryMode']) || typeof value.entryIcon !== 'string' || (value.entryIconOpen !== undefined && typeof value.entryIconOpen !== 'string') || typeof value.fullPath !== 'string' || (value.submoduleUrl !== undefined && typeof value.submoduleUrl !== 'string') ) { return false; } return value.children === undefined || (Array.isArray(value.children) && value.children.every(isFileTreeItem)); } export function createViewFileTreeStore(props: FileTreeStoreProps) { const store = reactive({ rootFiles: [] as Array<FileTreeItem>, selectedItem: props.treePath, loadErrorText: props.loadErrorText, contentLoadErrorText: props.contentLoadErrorText, retryText: props.retryText, navigationLoadError: false, failedNavigationPath: '', failedNavigationUrl: '', failedNavigationShouldPush: false, async loadChildren(treePath: string, subPath: string = ''): Promise<Array<FileTreeItem>> { // there is no git ref if no commits were made yet (an empty repo) if (!props.currentRefNameSubURL) return []; const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`); if (!response.ok) throw new Error(`Unexpected response status: ${response.status}`); const json: unknown = await response.json(); if (!isRecord(json) || !Array.isArray(json.fileTreeNodes) || !json.fileTreeNodes.every(isFileTreeItem)) { throw new TypeError('Unexpected file tree response'); } if (json.renderedIconPool !== undefined && ( !isRecord(json.renderedIconPool) || Object.values(json.renderedIconPool).some((svg) => typeof svg !== 'string') )) { throw new TypeError('Unexpected file tree icon pool'); } const poolSvgs: Array<string> = []; for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) { if (!document.querySelector(`.global-svg-icon-pool #${CSS.escape(svgId)}`)) poolSvgs.push(svgContent as string); } if (poolSvgs.length) { const svgContainer = createElementFromHTML(html`<div class="global-svg-icon-pool svg-icon-container"></div>`); svgContainer.innerHTML = poolSvgs.join(''); document.body.append(svgContainer); } return json.fileTreeNodes; }, async loadViewContent(url: string) { const u = new URL(url, window.location.origin); u.searchParams.set('only_content', 'true'); const response = await GET(u.href); if (!response.ok) throw new Error(`Unexpected response status: ${response.status}`); const nextContent = document.createElement('template'); nextContent.innerHTML = await response.text(); const elViewContentData = nextContent.content.querySelector('.repo-view-content-data'); if (!elViewContentData) throw new TypeError('Unexpected repository content response'); const t1 = elViewContentData.getAttribute('data-document-title'); const t2 = elViewContentData.getAttribute('data-document-title-common'); if (t1 === null || t2 === null) throw new TypeError('Missing repository content title'); const elViewContent = document.querySelector('.repo-view-content')!; elViewContent.replaceChildren(nextContent.content); document.title = `${t1} - ${t2}`; // follow the format in head.tmpl: <head><title>...</title></head> }, async navigateTreeView( treePath: string, {url = store.buildTreePathWebUrl(treePath), pushHistory = true}: {url?: string; pushHistory?: boolean} = {}, ): Promise<boolean> { try { await store.loadViewContent(url); if (pushHistory) window.history.pushState({treePath, url}, '', url); store.selectedItem = treePath; store.navigationLoadError = false; store.failedNavigationPath = ''; store.failedNavigationUrl = ''; store.failedNavigationShouldPush = false; return true; } catch { store.navigationLoadError = true; store.failedNavigationPath = treePath; store.failedNavigationUrl = url; store.failedNavigationShouldPush = pushHistory; return false; } }, async retryNavigation(): Promise<boolean> { if (!store.failedNavigationUrl) return false; return store.navigateTreeView(store.failedNavigationPath, { url: store.failedNavigationUrl, pushHistory: store.failedNavigationShouldPush, }); }, buildTreePathWebUrl: (treePath: string) => `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`, }); return store; }