mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-22 17:58:03 +02:00
Merge branch 'main' into lunny/issue_dev
This commit is contained in:
@@ -65,6 +65,7 @@
|
||||
@import "./repo/linebutton.css";
|
||||
@import "./repo/wiki.css";
|
||||
@import "./repo/header.css";
|
||||
@import "./repo/home.css";
|
||||
@import "./repo/reactions.css";
|
||||
|
||||
@import "./editor/fileeditor.css";
|
||||
|
||||
+4
-36
@@ -422,14 +422,6 @@ td .commit-summary {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
.repository.file.list .sidebar {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.repository.file.list .sidebar .svg {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.repo-editor-header {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1822,16 +1814,6 @@ td .commit-summary {
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
|
||||
.repository .repository-summary .segment.language-stats {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 0;
|
||||
height: 10px;
|
||||
white-space: nowrap;
|
||||
border-radius: 0 0 3px 3px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#cite-repo-modal #citation-panel {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -2079,17 +2061,6 @@ td .commit-summary {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.edit-label.modal .form .column,
|
||||
.new-label.modal .form .column {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.edit-label.modal .form .buttons,
|
||||
.new-label.modal .form .buttons {
|
||||
margin-left: auto;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.stats-table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
@@ -2172,11 +2143,7 @@ td .commit-summary {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.repo-button-row[data-is-homepage="false"] .repo-button-row-right {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
@media (max-width: 1200px) {
|
||||
.repository:not(.wiki) .repo-button-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
@@ -2302,6 +2269,7 @@ tbody.commit-list {
|
||||
font-weight: var(--font-weight-normal);
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
#new-dependency-drop-list.ui.selection.dropdown {
|
||||
@@ -2820,9 +2788,9 @@ tbody.commit-list {
|
||||
/* FIXME: These media selectors are not ideal (just keep them from old code).
|
||||
There are many different pages, some need the max-width while some others don't,
|
||||
they should be tested and improved in the future. */
|
||||
@media (min-width: 768px) and (max-width: 991.98px) {
|
||||
@media (min-width: 768px) and (max-width: 1235px) {
|
||||
.branch-selector-dropdown .branch-dropdown-button {
|
||||
max-width: 185px;
|
||||
max-width: 301px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
.repo-grid-filelist-sidebar {
|
||||
display: grid;
|
||||
grid-template-columns: auto 300px;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.repo-grid-filelist-sidebar .repo-home-filelist {
|
||||
min-width: 0;
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
.repo-grid-filelist-sidebar .repo-home-sidebar-top {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
padding-left: 1em;
|
||||
}
|
||||
.repo-grid-filelist-sidebar .repo-home-sidebar-bottom {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
padding-left: 1em;
|
||||
}
|
||||
.repo-home-sidebar-bottom > :first-child {
|
||||
border-top: 1px solid var(--color-secondary); /* same to .flex-list > .flex-item + .flex-item */
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.repo-grid-filelist-sidebar {
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
.repo-grid-filelist-sidebar .repo-home-filelist {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
.repo-grid-filelist-sidebar .repo-home-sidebar-top {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
padding-left: 0;
|
||||
}
|
||||
.repo-grid-filelist-sidebar .repo-home-sidebar-bottom {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
padding-left: 0;
|
||||
}
|
||||
.repo-home-sidebar-bottom > :first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.language-stats {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 0;
|
||||
height: 10px;
|
||||
white-space: nowrap;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.language-stats-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.language-stats-details .item {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25em;
|
||||
padding: 0 0.5em; /* make the UI look better for narrow (mobile) view */
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -6,8 +6,17 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
|
||||
|
||||
type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning';
|
||||
|
||||
type CommitStatusMap = {
|
||||
[status in CommitStatus]: {
|
||||
name: string,
|
||||
color: string,
|
||||
};
|
||||
};
|
||||
|
||||
// make sure this matches templates/repo/commit_status.tmpl
|
||||
const commitStatus = {
|
||||
const commitStatus: CommitStatusMap = {
|
||||
pending: {name: 'octicon-dot-fill', color: 'yellow'},
|
||||
success: {name: 'octicon-check', color: 'green'},
|
||||
error: {name: 'gitea-exclamation', color: 'red'},
|
||||
@@ -281,18 +290,18 @@ const sfc = {
|
||||
return 'octicon-repo';
|
||||
},
|
||||
|
||||
statusIcon(status) {
|
||||
statusIcon(status: CommitStatus) {
|
||||
return commitStatus[status].name;
|
||||
},
|
||||
|
||||
statusColor(status) {
|
||||
statusColor(status: CommitStatus) {
|
||||
return commitStatus[status].color;
|
||||
},
|
||||
|
||||
reposFilterKeyControl(e) {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
document.querySelector('.repo-owner-name-list li.active a')?.click();
|
||||
document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (this.activeIndex > 0) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
issueLink: el.getAttribute('data-issuelink'),
|
||||
locale: {
|
||||
filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'),
|
||||
},
|
||||
} as Record<string, string>,
|
||||
commits: [],
|
||||
hoverActivated: false,
|
||||
lastReviewCommitSha: null,
|
||||
@@ -41,16 +41,16 @@ export default {
|
||||
this.$el.removeEventListener('keyup', this.onKeyUp);
|
||||
},
|
||||
methods: {
|
||||
onBodyClick(event) {
|
||||
onBodyClick(event: MouseEvent) {
|
||||
// close this menu on click outside of this element when the dropdown is currently visible opened
|
||||
if (this.$el.contains(event.target)) return;
|
||||
if (this.menuVisible) {
|
||||
this.toggleMenu();
|
||||
}
|
||||
},
|
||||
onKeyDown(event) {
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
if (!this.menuVisible) return;
|
||||
const item = document.activeElement;
|
||||
const item = document.activeElement as HTMLElement;
|
||||
if (!this.$el.contains(item)) return;
|
||||
switch (event.key) {
|
||||
case 'ArrowDown': // select next element
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
if (commitIdx) this.highlight(this.commits[commitIdx]);
|
||||
}
|
||||
},
|
||||
onKeyUp(event) {
|
||||
onKeyUp(event: KeyboardEvent) {
|
||||
if (!this.menuVisible) return;
|
||||
const item = document.activeElement;
|
||||
if (!this.$el.contains(item)) return;
|
||||
@@ -95,7 +95,7 @@ export default {
|
||||
}
|
||||
},
|
||||
/** Focus given element */
|
||||
focusElem(elem, prevElem) {
|
||||
focusElem(elem: HTMLElement, prevElem: HTMLElement) {
|
||||
if (elem) {
|
||||
elem.tabIndex = 0;
|
||||
if (prevElem) prevElem.tabIndex = -1;
|
||||
@@ -149,7 +149,7 @@ export default {
|
||||
window.location.assign(`${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`);
|
||||
},
|
||||
/** Clicking on a single commit opens this specific commit */
|
||||
commitClicked(commitId, newWindow = false) {
|
||||
commitClicked(commitId: string, newWindow = false) {
|
||||
const url = `${this.issueLink}/commits/${commitId}${this.queryParams}`;
|
||||
if (newWindow) {
|
||||
window.open(url);
|
||||
|
||||
@@ -2,10 +2,22 @@
|
||||
import {SvgIcon} from '../svg.ts';
|
||||
import ActionRunStatus from './ActionRunStatus.vue';
|
||||
import {createApp} from 'vue';
|
||||
import {toggleElem} from '../utils/dom.ts';
|
||||
import {createElementFromAttrs, toggleElem} from '../utils/dom.ts';
|
||||
import {formatDatetime} from '../utils/time.ts';
|
||||
import {renderAnsi} from '../render/ansi.ts';
|
||||
import {GET, POST, DELETE} from '../modules/fetch.ts';
|
||||
import {POST, DELETE} from '../modules/fetch.ts';
|
||||
|
||||
// see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts"
|
||||
type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
|
||||
|
||||
type LogLine = {
|
||||
index: number;
|
||||
timestamp: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const LogLinePrefixGroup = '::group::';
|
||||
const LogLinePrefixEndGroup = '::endgroup::';
|
||||
|
||||
const sfc = {
|
||||
name: 'RepoActionView',
|
||||
@@ -23,7 +35,7 @@ const sfc = {
|
||||
data() {
|
||||
return {
|
||||
// internal state
|
||||
loading: false,
|
||||
loadingAbortController: null,
|
||||
intervalID: null,
|
||||
currentJobStepsStates: [],
|
||||
artifacts: [],
|
||||
@@ -39,6 +51,7 @@ const sfc = {
|
||||
run: {
|
||||
link: '',
|
||||
title: '',
|
||||
titleHTML: '',
|
||||
status: '',
|
||||
canCancel: false,
|
||||
canApprove: false,
|
||||
@@ -89,9 +102,7 @@ const sfc = {
|
||||
// load job data and then auto-reload periodically
|
||||
// need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener
|
||||
await this.loadJob();
|
||||
this.intervalID = setInterval(() => {
|
||||
this.loadJob();
|
||||
}, 1000);
|
||||
this.intervalID = setInterval(() => this.loadJob(), 1000);
|
||||
document.body.addEventListener('click', this.closeDropdown);
|
||||
this.hashChangeListener();
|
||||
window.addEventListener('hashchange', this.hashChangeListener);
|
||||
@@ -113,38 +124,44 @@ const sfc = {
|
||||
|
||||
methods: {
|
||||
// get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
|
||||
getLogsContainer(idx) {
|
||||
const el = this.$refs.logs[idx];
|
||||
getLogsContainer(stepIndex: number) {
|
||||
const el = this.$refs.logs[stepIndex];
|
||||
return el._stepLogsActiveContainer ?? el;
|
||||
},
|
||||
// begin a log group
|
||||
beginLogGroup(idx) {
|
||||
const el = this.$refs.logs[idx];
|
||||
|
||||
const elJobLogGroup = document.createElement('div');
|
||||
elJobLogGroup.classList.add('job-log-group');
|
||||
|
||||
const elJobLogGroupSummary = document.createElement('div');
|
||||
elJobLogGroupSummary.classList.add('job-log-group-summary');
|
||||
|
||||
const elJobLogList = document.createElement('div');
|
||||
elJobLogList.classList.add('job-log-list');
|
||||
|
||||
elJobLogGroup.append(elJobLogGroupSummary);
|
||||
elJobLogGroup.append(elJobLogList);
|
||||
beginLogGroup(stepIndex: number, startTime: number, line: LogLine) {
|
||||
const el = this.$refs.logs[stepIndex];
|
||||
const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'},
|
||||
this.createLogLine(stepIndex, startTime, {
|
||||
index: line.index,
|
||||
timestamp: line.timestamp,
|
||||
message: line.message.substring(LogLinePrefixGroup.length),
|
||||
}),
|
||||
);
|
||||
const elJobLogList = createElementFromAttrs('div', {class: 'job-log-list'});
|
||||
const elJobLogGroup = createElementFromAttrs('details', {class: 'job-log-group'},
|
||||
elJobLogGroupSummary,
|
||||
elJobLogList,
|
||||
);
|
||||
el.append(elJobLogGroup);
|
||||
el._stepLogsActiveContainer = elJobLogList;
|
||||
},
|
||||
// end a log group
|
||||
endLogGroup(idx) {
|
||||
const el = this.$refs.logs[idx];
|
||||
endLogGroup(stepIndex: number, startTime: number, line: LogLine) {
|
||||
const el = this.$refs.logs[stepIndex];
|
||||
el._stepLogsActiveContainer = null;
|
||||
el.append(this.createLogLine(stepIndex, startTime, {
|
||||
index: line.index,
|
||||
timestamp: line.timestamp,
|
||||
message: line.message.substring(LogLinePrefixEndGroup.length),
|
||||
}));
|
||||
},
|
||||
|
||||
// show/hide the step logs for a step
|
||||
toggleStepLogs(idx) {
|
||||
toggleStepLogs(idx: number) {
|
||||
this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded;
|
||||
if (this.currentJobStepsStates[idx].expanded) {
|
||||
this.loadJob(); // try to load the data immediately instead of waiting for next timer interval
|
||||
this.loadJobForce(); // try to load the data immediately instead of waiting for next timer interval
|
||||
}
|
||||
},
|
||||
// cancel a run
|
||||
@@ -156,62 +173,53 @@ const sfc = {
|
||||
POST(`${this.run.link}/approve`);
|
||||
},
|
||||
|
||||
createLogLine(line, startTime, stepIndex) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('job-log-line');
|
||||
div.setAttribute('id', `jobstep-${stepIndex}-${line.index}`);
|
||||
div._jobLogTime = line.timestamp;
|
||||
createLogLine(stepIndex: number, startTime: number, line: LogLine) {
|
||||
const lineNum = createElementFromAttrs('a', {class: 'line-num muted', href: `#jobstep-${stepIndex}-${line.index}`},
|
||||
String(line.index),
|
||||
);
|
||||
|
||||
const lineNumber = document.createElement('a');
|
||||
lineNumber.classList.add('line-num', 'muted');
|
||||
lineNumber.textContent = line.index;
|
||||
lineNumber.setAttribute('href', `#jobstep-${stepIndex}-${line.index}`);
|
||||
div.append(lineNumber);
|
||||
const logTimeStamp = createElementFromAttrs('span', {class: 'log-time-stamp'},
|
||||
formatDatetime(new Date(line.timestamp * 1000)), // for "Show timestamps"
|
||||
);
|
||||
|
||||
const logMsg = createElementFromAttrs('span', {class: 'log-msg'});
|
||||
logMsg.innerHTML = renderAnsi(line.message);
|
||||
|
||||
const seconds = Math.floor(line.timestamp - startTime);
|
||||
const logTimeSeconds = createElementFromAttrs('span', {class: 'log-time-seconds'},
|
||||
`${seconds}s`, // for "Show seconds"
|
||||
);
|
||||
|
||||
// for "Show timestamps"
|
||||
const logTimeStamp = document.createElement('span');
|
||||
logTimeStamp.className = 'log-time-stamp';
|
||||
const date = new Date(parseFloat(line.timestamp * 1000));
|
||||
const timeStamp = formatDatetime(date);
|
||||
logTimeStamp.textContent = timeStamp;
|
||||
toggleElem(logTimeStamp, this.timeVisible['log-time-stamp']);
|
||||
// for "Show seconds"
|
||||
const logTimeSeconds = document.createElement('span');
|
||||
logTimeSeconds.className = 'log-time-seconds';
|
||||
const seconds = Math.floor(parseFloat(line.timestamp) - parseFloat(startTime));
|
||||
logTimeSeconds.textContent = `${seconds}s`;
|
||||
toggleElem(logTimeSeconds, this.timeVisible['log-time-seconds']);
|
||||
|
||||
const logMessage = document.createElement('span');
|
||||
logMessage.className = 'log-msg';
|
||||
logMessage.innerHTML = renderAnsi(line.message);
|
||||
div.append(logTimeStamp);
|
||||
div.append(logMessage);
|
||||
div.append(logTimeSeconds);
|
||||
|
||||
return div;
|
||||
return createElementFromAttrs('div', {id: `jobstep-${stepIndex}-${line.index}`, class: 'job-log-line'},
|
||||
lineNum, logTimeStamp, logMsg, logTimeSeconds,
|
||||
);
|
||||
},
|
||||
|
||||
appendLogs(stepIndex, logLines, startTime) {
|
||||
appendLogs(stepIndex: number, startTime: number, logLines: LogLine[]) {
|
||||
for (const line of logLines) {
|
||||
// TODO: group support: ##[group]GroupTitle , ##[endgroup]
|
||||
const el = this.getLogsContainer(stepIndex);
|
||||
el.append(this.createLogLine(line, startTime, stepIndex));
|
||||
if (line.message.startsWith(LogLinePrefixGroup)) {
|
||||
this.beginLogGroup(stepIndex, startTime, line);
|
||||
continue;
|
||||
} else if (line.message.startsWith(LogLinePrefixEndGroup)) {
|
||||
this.endLogGroup(stepIndex, startTime, line);
|
||||
continue;
|
||||
}
|
||||
el.append(this.createLogLine(stepIndex, startTime, line));
|
||||
}
|
||||
},
|
||||
|
||||
async fetchArtifacts() {
|
||||
const resp = await GET(`${this.actionsURL}/runs/${this.runIndex}/artifacts`);
|
||||
return await resp.json();
|
||||
},
|
||||
|
||||
async deleteArtifact(name) {
|
||||
async deleteArtifact(name: string) {
|
||||
if (!window.confirm(this.locale.confirmDeleteArtifact.replace('%s', name))) return;
|
||||
// TODO: should escape the "name"?
|
||||
await DELETE(`${this.run.link}/artifacts/${name}`);
|
||||
await this.loadJob();
|
||||
await this.loadJobForce();
|
||||
},
|
||||
|
||||
async fetchJob() {
|
||||
async fetchJobData(abortController: AbortController) {
|
||||
const logCursors = this.currentJobStepsStates.map((it, idx) => {
|
||||
// cursor is used to indicate the last position of the logs
|
||||
// it's only used by backend, frontend just reads it and passes it back, it and can be any type.
|
||||
@@ -219,30 +227,27 @@ const sfc = {
|
||||
return {step: idx, cursor: it.cursor, expanded: it.expanded};
|
||||
});
|
||||
const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, {
|
||||
signal: abortController.signal,
|
||||
data: {logCursors},
|
||||
});
|
||||
return await resp.json();
|
||||
},
|
||||
|
||||
async loadJobForce() {
|
||||
this.loadingAbortController?.abort();
|
||||
this.loadingAbortController = null;
|
||||
await this.loadJob();
|
||||
},
|
||||
|
||||
async loadJob() {
|
||||
if (this.loading) return;
|
||||
if (this.loadingAbortController) return;
|
||||
const abortController = new AbortController();
|
||||
this.loadingAbortController = abortController;
|
||||
try {
|
||||
this.loading = true;
|
||||
const job = await this.fetchJobData(abortController);
|
||||
if (this.loadingAbortController !== abortController) return;
|
||||
|
||||
let job, artifacts;
|
||||
try {
|
||||
[job, artifacts] = await Promise.all([
|
||||
this.fetchJob(),
|
||||
this.fetchArtifacts(), // refresh artifacts if upload-artifact step done
|
||||
]);
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) return; // avoid network error while unloading page
|
||||
throw err;
|
||||
}
|
||||
|
||||
this.artifacts = artifacts['artifacts'] || [];
|
||||
|
||||
// save the state to Vue data, then the UI will be updated
|
||||
this.artifacts = job.artifacts || [];
|
||||
this.run = job.state.run;
|
||||
this.currentJob = job.state.currentJob;
|
||||
|
||||
@@ -254,26 +259,30 @@ const sfc = {
|
||||
}
|
||||
}
|
||||
// append logs to the UI
|
||||
for (const logs of job.logs.stepsLog) {
|
||||
for (const logs of job.logs.stepsLog ?? []) {
|
||||
// save the cursor, it will be passed to backend next time
|
||||
this.currentJobStepsStates[logs.step].cursor = logs.cursor;
|
||||
this.appendLogs(logs.step, logs.lines, logs.started);
|
||||
this.appendLogs(logs.step, logs.started, logs.lines);
|
||||
}
|
||||
|
||||
if (this.run.done && this.intervalID) {
|
||||
clearInterval(this.intervalID);
|
||||
this.intervalID = null;
|
||||
}
|
||||
} catch (e) {
|
||||
// avoid network error while unloading page, and ignore "abort" error
|
||||
if (e instanceof TypeError || abortController.signal.aborted) return;
|
||||
throw e;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
if (this.loadingAbortController === abortController) this.loadingAbortController = null;
|
||||
}
|
||||
},
|
||||
|
||||
isDone(status) {
|
||||
isDone(status: RunStatus) {
|
||||
return ['success', 'skipped', 'failure', 'cancelled'].includes(status);
|
||||
},
|
||||
|
||||
isExpandable(status) {
|
||||
isExpandable(status: RunStatus) {
|
||||
return ['success', 'running', 'failure', 'cancelled'].includes(status);
|
||||
},
|
||||
|
||||
@@ -281,7 +290,7 @@ const sfc = {
|
||||
if (this.menuVisible) this.menuVisible = false;
|
||||
},
|
||||
|
||||
toggleTimeDisplay(type) {
|
||||
toggleTimeDisplay(type: string) {
|
||||
this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`];
|
||||
for (const el of this.$refs.steps.querySelectorAll(`.log-time-${type}`)) {
|
||||
toggleElem(el, this.timeVisible[`log-time-${type}`]);
|
||||
@@ -294,7 +303,7 @@ const sfc = {
|
||||
const outerEl = document.querySelector('.full.height');
|
||||
const actionBodyEl = document.querySelector('.action-view-body');
|
||||
const headerEl = document.querySelector('#navbar');
|
||||
const contentEl = document.querySelector('.page-content.repository');
|
||||
const contentEl = document.querySelector('.page-content');
|
||||
const footerEl = document.querySelector('.page-footer');
|
||||
toggleElem(headerEl, !this.isFullScreen);
|
||||
toggleElem(contentEl, !this.isFullScreen);
|
||||
@@ -332,7 +341,7 @@ export function initRepositoryActionView() {
|
||||
|
||||
// TODO: the parent element's full height doesn't work well now,
|
||||
// but we can not pollute the global style at the moment, only fix the height problem for pages with this component
|
||||
const parentFullHeight = document.querySelector('body > div.full.height');
|
||||
const parentFullHeight = document.querySelector<HTMLElement>('body > div.full.height');
|
||||
if (parentFullHeight) parentFullHeight.style.paddingBottom = '0';
|
||||
|
||||
const view = createApp(sfc, {
|
||||
@@ -375,9 +384,8 @@ export function initRepositoryActionView() {
|
||||
<div class="action-info-summary">
|
||||
<div class="action-info-summary-title">
|
||||
<ActionRunStatus :locale-status="locale.status[run.status]" :status="run.status" :size="20"/>
|
||||
<h2 class="action-info-summary-title-text">
|
||||
{{ run.title }}
|
||||
</h2>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<h2 class="action-info-summary-title-text" v-html="run.titleHTML"/>
|
||||
</div>
|
||||
<button class="ui basic small compact button primary" @click="approveRun()" v-if="run.canApprove">
|
||||
{{ locale.approve }}
|
||||
@@ -858,7 +866,7 @@ export function initRepositoryActionView() {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.job-step-section .job-step-logs .job-log-line .log-msg {
|
||||
.job-step-logs .job-log-line .log-msg {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
white-space: break-spaces;
|
||||
@@ -884,15 +892,18 @@ export function initRepositoryActionView() {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* TODO: group support
|
||||
|
||||
.job-log-group {
|
||||
|
||||
.job-log-group .job-log-list .job-log-line .log-msg {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.job-log-group-summary {
|
||||
|
||||
position: relative;
|
||||
}
|
||||
.job-log-list {
|
||||
|
||||
} */
|
||||
.job-log-group-summary > .job-log-line {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1; /* to avoid hiding the triangle of the "details" element */
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
PointElement,
|
||||
LineElement,
|
||||
Filler,
|
||||
type ChartOptions,
|
||||
type ChartData,
|
||||
} from 'chart.js';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {Line as ChartLine} from 'vue-chartjs';
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
firstStartDateAfterDate,
|
||||
fillEmptyStartDaysWithZeroes,
|
||||
type DayData,
|
||||
type DayDataObject,
|
||||
} from '../utils/time.ts';
|
||||
import {chartJsColors} from '../utils/color.ts';
|
||||
import {sleep} from '../utils.ts';
|
||||
@@ -64,12 +67,12 @@ async function fetchGraphData() {
|
||||
}
|
||||
} while (response.status === 202);
|
||||
if (response.ok) {
|
||||
data.value = await response.json();
|
||||
const weekValues = Object.values(data.value);
|
||||
const dayDataObject: DayDataObject = await response.json();
|
||||
const weekValues = Object.values(dayDataObject);
|
||||
const start = weekValues[0].week;
|
||||
const end = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(start, end);
|
||||
data.value = fillEmptyStartDaysWithZeroes(startDays, data.value);
|
||||
data.value = fillEmptyStartDaysWithZeroes(startDays, dayDataObject);
|
||||
errorText.value = '';
|
||||
} else {
|
||||
errorText.value = response.statusText;
|
||||
@@ -81,7 +84,7 @@ async function fetchGraphData() {
|
||||
}
|
||||
}
|
||||
|
||||
function toGraphData(data) {
|
||||
function toGraphData(data: Array<Record<string, any>>): ChartData<'line'> {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
@@ -108,10 +111,9 @@ function toGraphData(data) {
|
||||
};
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options: ChartOptions<'line'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
PointElement,
|
||||
LineElement,
|
||||
Filler,
|
||||
type ChartOptions,
|
||||
type ChartData,
|
||||
type Plugin,
|
||||
} from 'chart.js';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||
@@ -22,8 +25,9 @@ import {chartJsColors} from '../utils/color.ts';
|
||||
import {sleep} from '../utils.ts';
|
||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import type {Entries} from 'type-fest';
|
||||
|
||||
const customEventListener = {
|
||||
const customEventListener: Plugin = {
|
||||
id: 'customEventListener',
|
||||
afterEvent: (chart, args, opts) => {
|
||||
// event will be replayed from chart.update when reset zoom,
|
||||
@@ -65,10 +69,10 @@ export default {
|
||||
data: () => ({
|
||||
isLoading: false,
|
||||
errorText: '',
|
||||
totalStats: {},
|
||||
sortedContributors: {},
|
||||
totalStats: {} as Record<string, any>,
|
||||
sortedContributors: {} as Record<string, any>,
|
||||
type: 'commits',
|
||||
contributorsStats: [],
|
||||
contributorsStats: {} as Record<string, any>,
|
||||
xAxisStart: null,
|
||||
xAxisEnd: null,
|
||||
xAxisMin: null,
|
||||
@@ -99,7 +103,7 @@ export default {
|
||||
async fetchGraphData() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
let response;
|
||||
let response: Response;
|
||||
do {
|
||||
response = await GET(`${this.repoLink}/activity/contributors/data`);
|
||||
if (response.status === 202) {
|
||||
@@ -112,7 +116,7 @@ export default {
|
||||
// below line might be deleted if we are sure go produces map always sorted by keys
|
||||
total.weeks = Object.fromEntries(Object.entries(total.weeks).sort());
|
||||
|
||||
const weekValues = Object.values(total.weeks);
|
||||
const weekValues = Object.values(total.weeks) as any;
|
||||
this.xAxisStart = weekValues[0].week;
|
||||
this.xAxisEnd = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd);
|
||||
@@ -120,7 +124,7 @@ export default {
|
||||
this.xAxisMin = this.xAxisStart;
|
||||
this.xAxisMax = this.xAxisEnd;
|
||||
this.contributorsStats = {};
|
||||
for (const [email, user] of Object.entries(rest)) {
|
||||
for (const [email, user] of Object.entries(rest) as Entries<Record<string, Record<string, any>>>) {
|
||||
user.weeks = fillEmptyStartDaysWithZeroes(startDays, user.weeks);
|
||||
this.contributorsStats[email] = user;
|
||||
}
|
||||
@@ -146,7 +150,7 @@ export default {
|
||||
user.total_additions = 0;
|
||||
user.total_deletions = 0;
|
||||
user.max_contribution_type = 0;
|
||||
const filteredWeeks = user.weeks.filter((week) => {
|
||||
const filteredWeeks = user.weeks.filter((week: Record<string, number>) => {
|
||||
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
||||
if (week.week >= this.xAxisMin - oneWeek && week.week <= this.xAxisMax + oneWeek) {
|
||||
user.total_commits += week.commits;
|
||||
@@ -195,7 +199,7 @@ export default {
|
||||
return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
|
||||
},
|
||||
|
||||
toGraphData(data) {
|
||||
toGraphData(data: Array<Record<string, any>>): ChartData<'line'> {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
@@ -211,9 +215,9 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
updateOtherCharts(event, reset) {
|
||||
const minVal = event.chart.options.scales.x.min;
|
||||
const maxVal = event.chart.options.scales.x.max;
|
||||
updateOtherCharts({chart}: {chart: Chart}, reset?: boolean = false) {
|
||||
const minVal = chart.options.scales.x.min;
|
||||
const maxVal = chart.options.scales.x.max;
|
||||
if (reset) {
|
||||
this.xAxisMin = this.xAxisStart;
|
||||
this.xAxisMax = this.xAxisEnd;
|
||||
@@ -225,7 +229,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
getOptions(type) {
|
||||
getOptions(type: string): ChartOptions<'line'> {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
@@ -238,6 +242,7 @@ export default {
|
||||
position: 'top',
|
||||
align: 'center',
|
||||
},
|
||||
// @ts-expect-error: bug in chart.js types
|
||||
customEventListener: {
|
||||
chartType: type,
|
||||
instance: this,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
type ChartOptions,
|
||||
type ChartData,
|
||||
} from 'chart.js';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {Bar} from 'vue-chartjs';
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
firstStartDateAfterDate,
|
||||
fillEmptyStartDaysWithZeroes,
|
||||
type DayData,
|
||||
type DayDataObject,
|
||||
} from '../utils/time.ts';
|
||||
import {chartJsColors} from '../utils/color.ts';
|
||||
import {sleep} from '../utils.ts';
|
||||
@@ -61,11 +63,11 @@ async function fetchGraphData() {
|
||||
}
|
||||
} while (response.status === 202);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const start = Object.values(data)[0].week;
|
||||
const dayDataObj: DayDataObject = await response.json();
|
||||
const start = Object.values(dayDataObj)[0].week;
|
||||
const end = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(start, end);
|
||||
data.value = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
|
||||
data.value = fillEmptyStartDaysWithZeroes(startDays, dayDataObj).slice(-52);
|
||||
errorText.value = '';
|
||||
} else {
|
||||
errorText.value = response.statusText;
|
||||
@@ -77,10 +79,11 @@ async function fetchGraphData() {
|
||||
}
|
||||
}
|
||||
|
||||
function toGraphData(data) {
|
||||
function toGraphData(data: DayData[]): ChartData<'bar'> {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
// @ts-expect-error -- bar chart expects one-dimensional data, but apparently x/y still works
|
||||
data: data.map((i) => ({x: i.week, y: i.commits})),
|
||||
label: 'Commits',
|
||||
backgroundColor: chartJsColors['commits'],
|
||||
@@ -91,10 +94,9 @@ function toGraphData(data) {
|
||||
};
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options: ChartOptions<'bar'> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: true,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
|
||||
@@ -41,35 +41,28 @@ export async function initCitationFileCopyContent() {
|
||||
citationCopyApa.classList.toggle('primary', !isBibtex);
|
||||
};
|
||||
|
||||
document.querySelector('#cite-repo-button')?.addEventListener('click', async (e: MouseEvent & {target: HTMLAnchorElement}) => {
|
||||
const dropdownBtn = e.target.closest('.ui.dropdown.button');
|
||||
dropdownBtn.classList.add('is-loading');
|
||||
|
||||
document.querySelector('#cite-repo-button')?.addEventListener('click', async () => {
|
||||
try {
|
||||
try {
|
||||
await initInputCitationValue(citationCopyApa, citationCopyBibtex);
|
||||
} catch (e) {
|
||||
console.error(`initCitationFileCopyContent error: ${e}`, e);
|
||||
return;
|
||||
}
|
||||
updateUi();
|
||||
|
||||
citationCopyApa.addEventListener('click', () => {
|
||||
localStorage.setItem('citation-copy-format', 'apa');
|
||||
updateUi();
|
||||
});
|
||||
|
||||
citationCopyBibtex.addEventListener('click', () => {
|
||||
localStorage.setItem('citation-copy-format', 'bibtex');
|
||||
updateUi();
|
||||
});
|
||||
|
||||
inputContent.addEventListener('click', () => {
|
||||
inputContent.select();
|
||||
});
|
||||
} finally {
|
||||
dropdownBtn.classList.remove('is-loading');
|
||||
await initInputCitationValue(citationCopyApa, citationCopyBibtex);
|
||||
} catch (e) {
|
||||
console.error(`initCitationFileCopyContent error: ${e}`, e);
|
||||
return;
|
||||
}
|
||||
updateUi();
|
||||
|
||||
citationCopyApa.addEventListener('click', () => {
|
||||
localStorage.setItem('citation-copy-format', 'apa');
|
||||
updateUi();
|
||||
});
|
||||
|
||||
citationCopyBibtex.addEventListener('click', () => {
|
||||
localStorage.setItem('citation-copy-format', 'bibtex');
|
||||
updateUi();
|
||||
});
|
||||
|
||||
inputContent.addEventListener('click', () => {
|
||||
inputContent.select();
|
||||
});
|
||||
|
||||
fomanticQuery('#cite-repo-modal').modal('show');
|
||||
});
|
||||
|
||||
@@ -12,5 +12,5 @@ export function initCommonOrganization() {
|
||||
});
|
||||
|
||||
// Labels
|
||||
initCompLabelEdit('.organization.settings.labels');
|
||||
initCompLabelEdit('.page-content.organization.settings.labels');
|
||||
}
|
||||
|
||||
@@ -1,96 +1,81 @@
|
||||
import $ from 'jquery';
|
||||
import {toggleElem} from '../../utils/dom.ts';
|
||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||
|
||||
function isExclusiveScopeName(name) {
|
||||
function nameHasScope(name: string): boolean {
|
||||
return /.*[^/]\/[^/].*/.test(name);
|
||||
}
|
||||
|
||||
function updateExclusiveLabelEdit(form) {
|
||||
const nameInput = document.querySelector(`${form} .label-name-input`);
|
||||
const exclusiveField = document.querySelector(`${form} .label-exclusive-input-field`);
|
||||
const exclusiveCheckbox = document.querySelector(`${form} .label-exclusive-input`);
|
||||
const exclusiveWarning = document.querySelector(`${form} .label-exclusive-warning`);
|
||||
export function initCompLabelEdit(pageSelector: string) {
|
||||
const pageContent = document.querySelector<HTMLElement>(pageSelector);
|
||||
if (!pageContent) return;
|
||||
|
||||
if (isExclusiveScopeName(nameInput.value)) {
|
||||
exclusiveField?.classList.remove('muted');
|
||||
exclusiveField?.removeAttribute('aria-disabled');
|
||||
if (exclusiveCheckbox.checked && exclusiveCheckbox.getAttribute('data-exclusive-warn')) {
|
||||
exclusiveWarning?.classList.remove('tw-hidden');
|
||||
} else {
|
||||
exclusiveWarning?.classList.add('tw-hidden');
|
||||
}
|
||||
} else {
|
||||
exclusiveField?.classList.add('muted');
|
||||
exclusiveField?.setAttribute('aria-disabled', 'true');
|
||||
exclusiveWarning?.classList.add('tw-hidden');
|
||||
// for guest view, the modal is not available, the "labels" are read-only
|
||||
const elModal = pageContent.querySelector<HTMLElement>('#issue-label-edit-modal');
|
||||
if (!elModal) return;
|
||||
|
||||
const elLabelId = elModal.querySelector<HTMLInputElement>('input[name="id"]');
|
||||
const elNameInput = elModal.querySelector<HTMLInputElement>('.label-name-input');
|
||||
const elExclusiveField = elModal.querySelector('.label-exclusive-input-field');
|
||||
const elExclusiveInput = elModal.querySelector<HTMLInputElement>('.label-exclusive-input');
|
||||
const elExclusiveWarning = elModal.querySelector('.label-exclusive-warning');
|
||||
const elIsArchivedField = elModal.querySelector('.label-is-archived-input-field');
|
||||
const elIsArchivedInput = elModal.querySelector<HTMLInputElement>('.label-is-archived-input');
|
||||
const elDescInput = elModal.querySelector<HTMLInputElement>('.label-desc-input');
|
||||
const elColorInput = elModal.querySelector<HTMLInputElement>('.js-color-picker-input input');
|
||||
|
||||
const syncModalUi = () => {
|
||||
const hasScope = nameHasScope(elNameInput.value);
|
||||
elExclusiveField.classList.toggle('disabled', !hasScope);
|
||||
const showExclusiveWarning = hasScope && elExclusiveInput.checked && elModal.hasAttribute('data-need-warn-exclusive');
|
||||
toggleElem(elExclusiveWarning, showExclusiveWarning);
|
||||
if (!hasScope) elExclusiveInput.checked = false;
|
||||
};
|
||||
|
||||
const showLabelEditModal = (btn:HTMLElement) => {
|
||||
// the "btn" should contain the label's attributes by its `data-label-xxx` attributes
|
||||
const form = elModal.querySelector<HTMLFormElement>('form');
|
||||
elLabelId.value = btn.getAttribute('data-label-id') || '';
|
||||
elNameInput.value = btn.getAttribute('data-label-name') || '';
|
||||
elIsArchivedInput.checked = btn.getAttribute('data-label-is-archived') === 'true';
|
||||
elExclusiveInput.checked = btn.getAttribute('data-label-exclusive') === 'true';
|
||||
elDescInput.value = btn.getAttribute('data-label-description') || '';
|
||||
elColorInput.value = btn.getAttribute('data-label-color') || '';
|
||||
elColorInput.dispatchEvent(new Event('input', {bubbles: true})); // trigger the color picker
|
||||
|
||||
// if label id exists: "edit label" mode; otherwise: "new label" mode
|
||||
const isEdit = Boolean(elLabelId.value);
|
||||
|
||||
// if a label was not exclusive but has issues, then it should warn user if it will become exclusive
|
||||
const numIssues = parseInt(btn.getAttribute('data-label-num-issues') || '0');
|
||||
elModal.toggleAttribute('data-need-warn-exclusive', !elExclusiveInput.checked && numIssues > 0);
|
||||
elModal.querySelector('.header').textContent = isEdit ? elModal.getAttribute('data-text-edit-label') : elModal.getAttribute('data-text-new-label');
|
||||
|
||||
const curPageLink = elModal.getAttribute('data-current-page-link');
|
||||
form.action = isEdit ? `${curPageLink}/edit` : `${curPageLink}/new`;
|
||||
toggleElem(elIsArchivedField, isEdit);
|
||||
syncModalUi();
|
||||
fomanticQuery(elModal).modal({
|
||||
onApprove() {
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return false;
|
||||
}
|
||||
form.submit();
|
||||
},
|
||||
}).modal('show');
|
||||
};
|
||||
|
||||
elModal.addEventListener('input', () => syncModalUi());
|
||||
|
||||
// theoretically, if the modal exists, the "new label" button should also exist, just in case it doesn't, use "?."
|
||||
const elNewLabel = pageContent.querySelector<HTMLElement>('.ui.button.new-label');
|
||||
elNewLabel?.addEventListener('click', () => showLabelEditModal(elNewLabel));
|
||||
|
||||
const elEditLabelButtons = pageContent.querySelectorAll<HTMLElement>('.edit-label-button');
|
||||
for (const btn of elEditLabelButtons) {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
showLabelEditModal(btn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initCompLabelEdit(selector) {
|
||||
if (!$(selector).length) return;
|
||||
|
||||
// Create label
|
||||
$('.new-label.button').on('click', () => {
|
||||
updateExclusiveLabelEdit('.new-label');
|
||||
$('.new-label.modal').modal({
|
||||
onApprove() {
|
||||
const form = document.querySelector('.new-label.form');
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return false;
|
||||
}
|
||||
$('.new-label.form').trigger('submit');
|
||||
},
|
||||
}).modal('show');
|
||||
return false;
|
||||
});
|
||||
|
||||
// Edit label
|
||||
$('.edit-label-button').on('click', function () {
|
||||
$('#label-modal-id').val($(this).data('id'));
|
||||
|
||||
const $nameInput = $('.edit-label .label-name-input');
|
||||
$nameInput.val($(this).data('title'));
|
||||
|
||||
const $isArchivedCheckbox = $('.edit-label .label-is-archived-input');
|
||||
$isArchivedCheckbox[0].checked = this.hasAttribute('data-is-archived');
|
||||
|
||||
const $exclusiveCheckbox = $('.edit-label .label-exclusive-input');
|
||||
$exclusiveCheckbox[0].checked = this.hasAttribute('data-exclusive');
|
||||
// Warn when label was previously not exclusive and used in issues
|
||||
$exclusiveCheckbox.data('exclusive-warn',
|
||||
$(this).data('num-issues') > 0 &&
|
||||
(!this.hasAttribute('data-exclusive') || !isExclusiveScopeName($nameInput.val())));
|
||||
updateExclusiveLabelEdit('.edit-label');
|
||||
|
||||
$('.edit-label .label-desc-input').val(this.getAttribute('data-description'));
|
||||
|
||||
const colorInput = document.querySelector('.edit-label .js-color-picker-input input');
|
||||
colorInput.value = this.getAttribute('data-color');
|
||||
colorInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
|
||||
$('.edit-label.modal').modal({
|
||||
onApprove() {
|
||||
const form = document.querySelector('.edit-label.form');
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return false;
|
||||
}
|
||||
$('.edit-label.form').trigger('submit');
|
||||
},
|
||||
}).modal('show');
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.new-label .label-name-input').on('input', () => {
|
||||
updateExclusiveLabelEdit('.new-label');
|
||||
});
|
||||
$('.new-label .label-exclusive-input').on('change', () => {
|
||||
updateExclusiveLabelEdit('.new-label');
|
||||
});
|
||||
$('.edit-label .label-name-input').on('input', () => {
|
||||
updateExclusiveLabelEdit('.edit-label');
|
||||
});
|
||||
$('.edit-label .label-exclusive-input').on('change', () => {
|
||||
updateExclusiveLabelEdit('.edit-label');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
export function initRepoTopicBar() {
|
||||
const mgrBtn = document.querySelector('#manage_topic');
|
||||
const mgrBtn = document.querySelector<HTMLButtonElement>('#manage_topic');
|
||||
if (!mgrBtn) return;
|
||||
|
||||
const editDiv = document.querySelector('#topic_edit');
|
||||
@@ -18,7 +18,7 @@ export function initRepoTopicBar() {
|
||||
mgrBtn.addEventListener('click', () => {
|
||||
hideElem(viewDiv);
|
||||
showElem(editDiv);
|
||||
topicDropdown.querySelector('input.search').focus();
|
||||
topicDropdown.querySelector<HTMLInputElement>('input.search').focus();
|
||||
});
|
||||
|
||||
document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
|
||||
@@ -28,9 +28,9 @@ export function initRepoTopicBar() {
|
||||
mgrBtn.focus();
|
||||
});
|
||||
|
||||
document.querySelector('#save_topic').addEventListener('click', async (e) => {
|
||||
document.querySelector('#save_topic').addEventListener('click', async (e: MouseEvent & {target: HTMLButtonElement}) => {
|
||||
lastErrorToast?.hideToast();
|
||||
const topics = editDiv.querySelector('input[name=topics]').value;
|
||||
const topics = editDiv.querySelector<HTMLInputElement>('input[name=topics]').value;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('topics', topics);
|
||||
@@ -45,12 +45,13 @@ export function initRepoTopicBar() {
|
||||
const topicArray = topics.split(',');
|
||||
topicArray.sort();
|
||||
for (const topic of topicArray) {
|
||||
// it should match the code in repo/home.tmpl
|
||||
// TODO: sort items in topicDropdown, or items in edit div will have different order to the items in view div
|
||||
// !!!! it SHOULD and MUST match the code in "home_sidebar_top.tmpl" !!!!
|
||||
const link = document.createElement('a');
|
||||
link.classList.add('repo-topic', 'ui', 'large', 'label');
|
||||
link.classList.add('repo-topic', 'ui', 'large', 'label', 'gt-ellipsis');
|
||||
link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`;
|
||||
link.textContent = topic;
|
||||
mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button
|
||||
viewDiv.append(link);
|
||||
}
|
||||
}
|
||||
hideElem(editDiv);
|
||||
|
||||
@@ -43,7 +43,7 @@ export function initRepository() {
|
||||
initRepoCommentFormAndSidebar();
|
||||
|
||||
// Labels
|
||||
initCompLabelEdit('.repository.labels');
|
||||
initCompLabelEdit('.page-content.repository.labels');
|
||||
initRepoMilestone();
|
||||
initRepoNew();
|
||||
|
||||
|
||||
@@ -49,7 +49,11 @@ export type DayData = {
|
||||
commits: number,
|
||||
}
|
||||
|
||||
export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData[]): DayData[] {
|
||||
export type DayDataObject = {
|
||||
[timestamp: string]: DayData,
|
||||
}
|
||||
|
||||
export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayDataObject): DayData[] {
|
||||
const result = {};
|
||||
|
||||
for (const startDay of startDays) {
|
||||
|
||||
Reference in New Issue
Block a user