aboutsummaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
Diffstat (limited to 'web_src')
-rw-r--r--web_src/css/repo/home.css5
-rw-r--r--web_src/js/components/RepoActionView.vue31
-rw-r--r--web_src/js/features/common-button.test.ts14
-rw-r--r--web_src/js/features/common-button.ts34
-rw-r--r--web_src/js/features/common-issue-list.ts6
-rw-r--r--web_src/js/features/repo-actions.ts1
-rw-r--r--web_src/js/features/repo-issue-list.ts6
-rw-r--r--web_src/js/utils/dom.test.ts6
-rw-r--r--web_src/js/utils/dom.ts49
9 files changed, 95 insertions, 57 deletions
diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css
index 69c454d611..61b0a1f962 100644
--- a/web_src/css/repo/home.css
+++ b/web_src/css/repo/home.css
@@ -58,6 +58,11 @@
flex: 0 0 15%;
min-width: 0;
max-height: 100vh;
+ position: sticky;
+ top: 0;
+ bottom: 0;
+ height: 100%;
+ overflow-y: hidden;
}
.repo-view-content {
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 447347890b..af300622b4 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -495,14 +495,24 @@ export default defineComponent({
{{ locale.artifactsTitle }}
</div>
<ul class="job-artifacts-list">
- <li class="job-artifacts-item" v-for="artifact in artifacts" :key="artifact.name">
- <a class="job-artifacts-link" target="_blank" :href="run.link+'/artifacts/'+artifact.name">
- <SvgIcon name="octicon-file" class="ui text black job-artifacts-icon"/>{{ artifact.name }}
- </a>
- <a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)" class="job-artifacts-delete">
- <SvgIcon name="octicon-trash" class="ui text black job-artifacts-icon"/>
- </a>
- </li>
+ <template v-for="artifact in artifacts" :key="artifact.name">
+ <li class="job-artifacts-item">
+ <template v-if="artifact.status !== 'expired'">
+ <a class="flex-text-inline" target="_blank" :href="run.link+'/artifacts/'+artifact.name">
+ <SvgIcon name="octicon-file" class="text black"/>
+ <span class="gt-ellipsis">{{ artifact.name }}</span>
+ </a>
+ <a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)">
+ <SvgIcon name="octicon-trash" class="text black"/>
+ </a>
+ </template>
+ <span v-else class="flex-text-inline text light grey">
+ <SvgIcon name="octicon-file"/>
+ <span class="gt-ellipsis">{{ artifact.name }}</span>
+ <span class="ui label text light grey tw-flex-shrink-0">{{ locale.artifactExpired }}</span>
+ </span>
+ </li>
+ </template>
</ul>
</div>
</div>
@@ -664,6 +674,7 @@ export default defineComponent({
padding: 6px;
display: flex;
justify-content: space-between;
+ align-items: center;
}
.job-artifacts-list {
@@ -671,10 +682,6 @@ export default defineComponent({
list-style: none;
}
-.job-artifacts-icon {
- padding-right: 3px;
-}
-
.job-brief-list {
display: flex;
flex-direction: column;
diff --git a/web_src/js/features/common-button.test.ts b/web_src/js/features/common-button.test.ts
new file mode 100644
index 0000000000..f41bafbc79
--- /dev/null
+++ b/web_src/js/features/common-button.test.ts
@@ -0,0 +1,14 @@
+import {assignElementProperty} from './common-button.ts';
+
+test('assignElementProperty', () => {
+ const elForm = document.createElement('form');
+ assignElementProperty(elForm, 'action', '/test-link');
+ expect(elForm.action).contains('/test-link'); // the DOM always returns absolute URL
+ assignElementProperty(elForm, 'text-content', 'dummy');
+ expect(elForm.textContent).toBe('dummy');
+
+ const elInput = document.createElement('input');
+ expect(elInput.readOnly).toBe(false);
+ assignElementProperty(elInput, 'read-only', 'true');
+ expect(elInput.readOnly).toBe(true);
+});
diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts
index 003bfbce5d..ae399e48b3 100644
--- a/web_src/js/features/common-button.ts
+++ b/web_src/js/features/common-button.ts
@@ -1,5 +1,5 @@
import {POST} from '../modules/fetch.ts';
-import {addDelegatedEventListener, hideElem, showElem, toggleElem} from '../utils/dom.ts';
+import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {camelize} from 'vue';
@@ -79,10 +79,11 @@ function onShowPanelClick(el: HTMLElement, e: MouseEvent) {
// if it has "toggle" class, it toggles the panel
e.preventDefault();
const sel = el.getAttribute('data-panel');
- if (el.classList.contains('toggle')) {
- toggleElem(sel);
- } else {
- showElem(sel);
+ const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel);
+ for (const elem of elems) {
+ if (isElemVisible(elem as HTMLElement)) {
+ elem.querySelector<HTMLElement>('[autofocus]')?.focus();
+ }
}
}
@@ -102,6 +103,21 @@ function onHidePanelClick(el: HTMLElement, e: MouseEvent) {
throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code
}
+export function assignElementProperty(el: any, name: string, val: string) {
+ name = camelize(name);
+ const old = el[name];
+ if (typeof old === 'boolean') {
+ el[name] = val === 'true';
+ } else if (typeof old === 'number') {
+ el[name] = parseFloat(val);
+ } else if (typeof old === 'string') {
+ el[name] = val;
+ } else {
+ // in the future, we could introduce a better typing system like `data-modal-form.action:string="..."`
+ throw new Error(`cannot assign element property ${name} by value ${val}`);
+ }
+}
+
function onShowModalClick(el: HTMLElement, e: MouseEvent) {
// A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
// Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
@@ -109,7 +125,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
// * Then, try to query '[name=target]'
// * Then, try to query '.target'
// * Then, try to query 'target' as HTML tag
- // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
+ // If there is a ".{prop-name}" part like "data-modal-form.action", the "form" element's "action" property will be set, the "prop-name" will be camel-cased to "propName".
e.preventDefault();
const modalSelector = el.getAttribute('data-modal');
const elModal = document.querySelector(modalSelector);
@@ -122,7 +138,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
}
const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length);
- const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.');
+ const [attrTargetName, attrTargetProp] = attrTargetCombo.split('.');
// try to find target by: "#target" -> "[name=target]" -> ".target" -> "<target> tag"
const attrTarget = elModal.querySelector(`#${attrTargetName}`) ||
elModal.querySelector(`[name=${attrTargetName}]`) ||
@@ -133,8 +149,8 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
continue;
}
- if (attrTargetAttr) {
- (attrTarget as any)[camelize(attrTargetAttr)] = attrib.value;
+ if (attrTargetProp) {
+ assignElementProperty(attrTarget, attrTargetProp, attrib.value);
} else if (attrTarget.matches('input, textarea')) {
(attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value; // FIXME: add more supports like checkbox
} else {
diff --git a/web_src/js/features/common-issue-list.ts b/web_src/js/features/common-issue-list.ts
index e207364794..037529bd10 100644
--- a/web_src/js/features/common-issue-list.ts
+++ b/web_src/js/features/common-issue-list.ts
@@ -1,4 +1,4 @@
-import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.ts';
+import {isElemVisible, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.ts';
import {GET} from '../modules/fetch.ts';
const {appSubUrl} = window.config;
@@ -28,7 +28,7 @@ export function parseIssueListQuickGotoLink(repoLink: string, searchText: string
}
export function initCommonIssueListQuickGoto() {
- const goto = document.querySelector('#issue-list-quick-goto');
+ const goto = document.querySelector<HTMLElement>('#issue-list-quick-goto');
if (!goto) return;
const form = goto.closest('form');
@@ -37,7 +37,7 @@ export function initCommonIssueListQuickGoto() {
form.addEventListener('submit', (e) => {
// if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
- let doQuickGoto = !isElemHidden(goto);
+ let doQuickGoto = isElemVisible(goto);
const submitter = submitEventSubmitter(e);
if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false;
if (!doQuickGoto) return;
diff --git a/web_src/js/features/repo-actions.ts b/web_src/js/features/repo-actions.ts
index cbd0429c04..8d93fce53f 100644
--- a/web_src/js/features/repo-actions.ts
+++ b/web_src/js/features/repo-actions.ts
@@ -24,6 +24,7 @@ export function initRepositoryActionView() {
pushedBy: el.getAttribute('data-locale-runs-pushed-by'),
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
areYouSure: el.getAttribute('data-locale-are-you-sure'),
+ artifactExpired: el.getAttribute('data-locale-artifact-expired'),
confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts
index 8cd4483357..3ea5fb70c0 100644
--- a/web_src/js/features/repo-issue-list.ts
+++ b/web_src/js/features/repo-issue-list.ts
@@ -1,5 +1,5 @@
import {updateIssuesMeta} from './repo-common.ts';
-import {toggleElem, isElemHidden, queryElems} from '../utils/dom.ts';
+import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts';
import {htmlEscape} from 'escape-goat';
import {confirmModal} from './comp/ConfirmModal.ts';
import {showErrorToast} from '../modules/toast.ts';
@@ -33,8 +33,8 @@ function initRepoIssueListCheckboxes() {
toggleElem('#issue-filters', !anyChecked);
toggleElem('#issue-actions', anyChecked);
// there are two panels but only one select-all checkbox, so move the checkbox to the visible panel
- const panels = document.querySelectorAll('#issue-filters, #issue-actions');
- const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el));
+ const panels = document.querySelectorAll<HTMLElement>('#issue-filters, #issue-actions');
+ const visiblePanel = Array.from(panels).find((el) => isElemVisible(el));
const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left');
toolbarLeft.prepend(issueSelectAll);
};
diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts
index 6a3af91556..057ea9808c 100644
--- a/web_src/js/utils/dom.test.ts
+++ b/web_src/js/utils/dom.test.ts
@@ -25,10 +25,14 @@ test('createElementFromAttrs', () => {
});
test('querySingleVisibleElem', () => {
- let el = createElementFromHTML('<div><span>foo</span></div>');
+ let el = createElementFromHTML('<div></div>');
+ expect(querySingleVisibleElem(el, 'span')).toBeNull();
+ el = createElementFromHTML('<div><span>foo</span></div>');
expect(querySingleVisibleElem(el, 'span').textContent).toEqual('foo');
el = createElementFromHTML('<div><span style="display: none;">foo</span><span>bar</span></div>');
expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar');
+ el = createElementFromHTML('<div><span class="some-class tw-hidden">foo</span><span>bar</span></div>');
+ expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar');
el = createElementFromHTML('<div><span>foo</span><span>bar</span></div>');
expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element');
});
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index 4386d38632..83a0d9c8df 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -9,24 +9,24 @@ type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
export type DOMEvent<E extends Event, T extends Element = HTMLElement> = E & { target: Partial<T>; };
-function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
+function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]): ArrayLikeIterable<Element> {
if (typeof el === 'string' || el instanceof String) {
el = document.querySelectorAll(el as string);
}
if (el instanceof Node) {
func(el, ...args);
+ return [el];
} else if (el.length !== undefined) {
// this works for: NodeList, HTMLCollection, Array, jQuery
- for (const e of (el as ArrayLikeIterable<Element>)) {
- func(e, ...args);
- }
- } else {
- throw new Error('invalid argument to be shown/hidden');
+ const elems = el as ArrayLikeIterable<Element>;
+ for (const elem of elems) func(elem, ...args);
+ return elems;
}
+ throw new Error('invalid argument to be shown/hidden');
}
-export function toggleClass(el: ElementArg, className: string, force?: boolean) {
- elementsCall(el, (e: Element) => {
+export function toggleClass(el: ElementArg, className: string, force?: boolean): ArrayLikeIterable<Element> {
+ return elementsCall(el, (e: Element) => {
if (force === true) {
e.classList.add(className);
} else if (force === false) {
@@ -43,23 +43,16 @@ export function toggleClass(el: ElementArg, className: string, force?: boolean)
* @param el ElementArg
* @param force force=true to show or force=false to hide, undefined to toggle
*/
-export function toggleElem(el: ElementArg, force?: boolean) {
- toggleClass(el, 'tw-hidden', force === undefined ? force : !force);
-}
-
-export function showElem(el: ElementArg) {
- toggleElem(el, true);
+export function toggleElem(el: ElementArg, force?: boolean): ArrayLikeIterable<Element> {
+ return toggleClass(el, 'tw-hidden', force === undefined ? force : !force);
}
-export function hideElem(el: ElementArg) {
- toggleElem(el, false);
+export function showElem(el: ElementArg): ArrayLikeIterable<Element> {
+ return toggleElem(el, true);
}
-export function isElemHidden(el: ElementArg) {
- const res: boolean[] = [];
- elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden')));
- if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`);
- return res[0];
+export function hideElem(el: ElementArg): ArrayLikeIterable<Element> {
+ return toggleElem(el, false);
}
function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
@@ -275,14 +268,12 @@ export function initSubmitEventPolyfill() {
document.body.addEventListener('focus', submitEventPolyfillListener);
}
-/**
- * Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
- * Note: This function doesn't account for all possible visibility scenarios.
- */
-export function isElemVisible(element: HTMLElement): boolean {
- if (!element) return false;
- // checking element.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout
- return Boolean((element.offsetWidth || element.offsetHeight || element.getClientRects().length) && element.style.display !== 'none');
+export function isElemVisible(el: HTMLElement): boolean {
+ // Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
+ // This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem"
+ if (!el) return false;
+ // checking el.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout
+ return !el.classList.contains('tw-hidden') && Boolean((el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none');
}
// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this