519 lines
22 KiB
TypeScript
519 lines
22 KiB
TypeScript
import {contrastColor} from '../utils/color.ts';
|
|
import {createSortable} from '../modules/sortable.ts';
|
|
import {POST, request} from '../modules/fetch.ts';
|
|
import {hideFomanticModal} from '../modules/fomantic/modal.ts';
|
|
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
|
|
import type {SortableEvent} from 'sortablejs';
|
|
import {toggleFullScreen} from '../utils.ts';
|
|
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
|
import {localUserSettings} from '../modules/user-settings.ts';
|
|
|
|
type ProjectFilterField = 'assignee' | 'author' | 'label' | 'milestone' | 'repo' | 'state' | 'status' | 'title' | 'type';
|
|
export type ProjectGroupBy = 'none' | 'assignee' | 'label' | 'milestone' | 'repository' | 'state' | 'status' | 'type';
|
|
|
|
export type ProjectFilterParseError = {
|
|
kind: 'malformed' | 'unclosed' | 'unknown';
|
|
value?: string;
|
|
};
|
|
|
|
export type ProjectFilterTerm = {
|
|
field: ProjectFilterField;
|
|
negated: boolean;
|
|
values: string[];
|
|
};
|
|
|
|
export type ProjectFilterParseResult = {
|
|
error?: ProjectFilterParseError;
|
|
terms: ProjectFilterTerm[];
|
|
};
|
|
|
|
export type ProjectCardMetadata = {
|
|
assignees: string[];
|
|
author: string;
|
|
labels: string[];
|
|
milestone: string;
|
|
repository: string;
|
|
state: string;
|
|
status: string;
|
|
title: string;
|
|
type: string;
|
|
};
|
|
|
|
type ProjectCard = {
|
|
element: HTMLElement;
|
|
list: HTMLElement;
|
|
metadata: ProjectCardMetadata;
|
|
};
|
|
|
|
const projectFilterAliases: Readonly<Record<string, ProjectFilterField | 'is'>> = {
|
|
assignee: 'assignee', author: 'author', column: 'status', is: 'is', label: 'label', milestone: 'milestone',
|
|
repo: 'repo', repository: 'repo', state: 'state', status: 'status', type: 'type',
|
|
};
|
|
|
|
function tokenizeProjectFilter(query: string): {error?: ProjectFilterParseError; tokens: string[]} {
|
|
const tokens: string[] = [];
|
|
let current = '';
|
|
let quoted = false;
|
|
for (const char of query) {
|
|
if (char === '"') {
|
|
quoted = !quoted;
|
|
current += char;
|
|
} else if (/\s/.test(char) && !quoted) {
|
|
if (current) tokens.push(current);
|
|
current = '';
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
if (quoted) return {error: {kind: 'unclosed'}, tokens: []};
|
|
if (current) tokens.push(current);
|
|
return {tokens};
|
|
}
|
|
|
|
function parseProjectFilterValues(value: string): {error?: ProjectFilterParseError; values: string[]} {
|
|
if (!value || value.startsWith(',') || value.endsWith(',')) return {error: {kind: 'malformed', value}, values: []};
|
|
const values: string[] = [];
|
|
let offset = 0;
|
|
while (offset < value.length) {
|
|
if (value[offset] === ',') return {error: {kind: 'malformed', value}, values: []};
|
|
if (value[offset] === '"') {
|
|
const endQuote = value.indexOf('"', offset + 1);
|
|
if (endQuote === -1 || endQuote === offset + 1) return {error: {kind: 'malformed', value}, values: []};
|
|
values.push(value.slice(offset + 1, endQuote).toLowerCase());
|
|
offset = endQuote + 1;
|
|
if (offset < value.length && value[offset] !== ',') return {error: {kind: 'malformed', value}, values: []};
|
|
} else {
|
|
const comma = value.indexOf(',', offset);
|
|
const end = comma === -1 ? value.length : comma;
|
|
const current = value.slice(offset, end).trim();
|
|
if (!current || current.includes('"')) return {error: {kind: 'malformed', value}, values: []};
|
|
values.push(current.toLowerCase());
|
|
offset = end;
|
|
}
|
|
if (offset < value.length) offset++;
|
|
}
|
|
return {values};
|
|
}
|
|
|
|
function normalizeProjectFilter(field: ProjectFilterField | 'is', values: string[]): {error?: ProjectFilterParseError; field?: ProjectFilterField; values?: string[]} {
|
|
if (field === 'is') {
|
|
const fields = values.map((value) => value === 'open' || value === 'closed' ? 'state' : value === 'issue' || value === 'pr' || value === 'pull' ? 'type' : undefined);
|
|
const normalizedField = fields[0];
|
|
if (!normalizedField || fields.some((value) => value !== normalizedField)) return {error: {kind: 'malformed', value: values.join(',')}};
|
|
return {field: normalizedField, values: values.map((value) => value === 'pr' ? 'pull' : value)};
|
|
}
|
|
if (field === 'state' && values.some((value) => value !== 'open' && value !== 'closed')) return {error: {kind: 'malformed', value: values.join(',')}};
|
|
if (field === 'type' && values.some((value) => value !== 'issue' && value !== 'pr' && value !== 'pull')) return {error: {kind: 'malformed', value: values.join(',')}};
|
|
return {field, values: values.map((value) => value === 'pr' ? 'pull' : value)};
|
|
}
|
|
|
|
export function parseProjectFilter(query: string): ProjectFilterParseResult {
|
|
const tokenized = tokenizeProjectFilter(query.trim());
|
|
if (tokenized.error) return {error: tokenized.error, terms: []};
|
|
const terms: ProjectFilterTerm[] = [];
|
|
for (const rawToken of tokenized.tokens) {
|
|
const negated = rawToken.startsWith('-');
|
|
const token = negated ? rawToken.slice(1) : rawToken;
|
|
if (!token) return {error: {kind: 'malformed', value: rawToken}, terms: []};
|
|
const separator = token.indexOf(':');
|
|
if (separator === -1) {
|
|
const values = parseProjectFilterValues(token);
|
|
if (values.error) return {error: values.error, terms: []};
|
|
terms.push({field: 'title', negated, values: values.values});
|
|
continue;
|
|
}
|
|
const qualifier = token.slice(0, separator).toLowerCase();
|
|
const field = projectFilterAliases[qualifier];
|
|
if (!field) return {error: {kind: 'unknown', value: qualifier}, terms: []};
|
|
const values = parseProjectFilterValues(token.slice(separator + 1));
|
|
if (values.error) return {error: values.error, terms: []};
|
|
const normalized = normalizeProjectFilter(field, values.values);
|
|
if (normalized.error) return {error: normalized.error, terms: []};
|
|
terms.push({field: normalized.field!, negated, values: normalized.values!});
|
|
}
|
|
return {terms};
|
|
}
|
|
|
|
function parseProjectCardValues(value: string | null): string[] {
|
|
if (!value) return [];
|
|
try {
|
|
const parsed: unknown = JSON.parse(value);
|
|
return Array.isArray(parsed) && parsed.every((item) => typeof item === 'string') ? parsed : [];
|
|
} catch (error) {
|
|
console.warn('Invalid project card metadata', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function projectCardMetadata(element: HTMLElement): ProjectCardMetadata {
|
|
return {
|
|
assignees: parseProjectCardValues(element.getAttribute('data-project-card-assignees')),
|
|
author: element.getAttribute('data-project-card-author') || '', labels: parseProjectCardValues(element.getAttribute('data-project-card-labels')),
|
|
milestone: element.getAttribute('data-project-card-milestone') || '', repository: element.getAttribute('data-project-card-repository') || '',
|
|
state: element.getAttribute('data-project-card-state') || '', status: element.getAttribute('data-project-card-status') || '',
|
|
title: element.getAttribute('data-project-card-title') || '', type: element.getAttribute('data-project-card-type') || '',
|
|
};
|
|
}
|
|
|
|
function projectCardValues(metadata: ProjectCardMetadata, field: ProjectFilterField): string[] {
|
|
switch (field) {
|
|
case 'assignee': return metadata.assignees;
|
|
case 'author': return [metadata.author];
|
|
case 'label': return metadata.labels;
|
|
case 'milestone': return [metadata.milestone];
|
|
case 'repo': return [metadata.repository];
|
|
case 'state': return [metadata.state];
|
|
case 'status': return [metadata.status];
|
|
case 'title': return [metadata.title];
|
|
case 'type': return [metadata.type];
|
|
}
|
|
}
|
|
|
|
export function matchesProjectFilter(metadata: ProjectCardMetadata, terms: ProjectFilterTerm[]): boolean {
|
|
return terms.every((term) => {
|
|
const matches = term.values.some((value) => projectCardValues(metadata, term.field).some((cardValue) => {
|
|
const normalizedCardValue = cardValue.toLowerCase();
|
|
if (term.field === 'title') return normalizedCardValue.includes(value);
|
|
if (term.field === 'repo') return normalizedCardValue === value || normalizedCardValue.endsWith(`/${value}`);
|
|
return normalizedCardValue === value;
|
|
}));
|
|
return term.negated ? !matches : matches;
|
|
});
|
|
}
|
|
|
|
function normalizeProjectGroupBy(groupBy: string): ProjectGroupBy {
|
|
if (groupBy === 'column') return 'status';
|
|
if (groupBy === 'repo') return 'repository';
|
|
if (groupBy === 'assignee' || groupBy === 'label' || groupBy === 'milestone' || groupBy === 'repository' || groupBy === 'state' || groupBy === 'status' || groupBy === 'type') return groupBy;
|
|
return 'none';
|
|
}
|
|
|
|
export function projectCardGroupValue(metadata: ProjectCardMetadata, groupBy: ProjectGroupBy, noValue: string): string {
|
|
// Multi-valued labels and assignees use their first loaded value so one movable card is never duplicated.
|
|
switch (groupBy) {
|
|
case 'assignee': return metadata.assignees[0] || noValue;
|
|
case 'label': return metadata.labels[0] || noValue;
|
|
case 'milestone': return metadata.milestone || noValue;
|
|
case 'repository': return metadata.repository || noValue;
|
|
case 'state': return metadata.state || noValue;
|
|
case 'type': return metadata.type || noValue;
|
|
default: return noValue;
|
|
}
|
|
}
|
|
|
|
function createProjectCardGroup(title: string, cards: ProjectCard[]): HTMLElement {
|
|
const group = document.createElement('section');
|
|
group.classList.add('project-card-group');
|
|
const header = document.createElement('h3');
|
|
header.classList.add('project-card-group-header');
|
|
header.textContent = title;
|
|
const groupCards = document.createElement('div');
|
|
groupCards.classList.add('project-card-group-cards');
|
|
groupCards.append(...cards.map((card) => card.element));
|
|
group.append(header, groupCards);
|
|
return group;
|
|
}
|
|
|
|
function setProjectIssueCount(list: HTMLElement, count: number): void {
|
|
list.closest<HTMLElement>('.project-column')!.querySelector<HTMLElement>('.project-column-issue-count')!.textContent = String(count);
|
|
}
|
|
|
|
function initRepoProjectFilters(elProjectsView: HTMLElement): void {
|
|
const board = elProjectsView.querySelector<HTMLElement>('#project-board')!;
|
|
const input = elProjectsView.querySelector<HTMLInputElement>('[data-project-filter-input]')!;
|
|
const clear = elProjectsView.querySelector<HTMLButtonElement>('[data-project-filter-clear]')!;
|
|
const select = elProjectsView.querySelector<HTMLSelectElement>('[data-project-group-by]')!;
|
|
const errorElement = elProjectsView.querySelector<HTMLElement>('[data-project-filter-error]')!;
|
|
const emptyElement = elProjectsView.querySelector<HTMLElement>('[data-project-filter-empty]')!;
|
|
const lists = Array.from(board.querySelectorAll<HTMLElement>('.project-column > .cards'));
|
|
const cardsByList = new Map<HTMLElement, ProjectCard[]>();
|
|
let viewWasModified = false;
|
|
|
|
const snapshotCards = () => {
|
|
for (const list of lists) {
|
|
cardsByList.set(list, Array.from(list.querySelectorAll<HTMLElement>('.issue-card'), (element) => ({element, list, metadata: projectCardMetadata(element)})));
|
|
}
|
|
};
|
|
|
|
const initializeFromUrl = () => {
|
|
const url = new URL(window.location.href);
|
|
input.value = url.searchParams.get('filter') || '';
|
|
select.value = normalizeProjectGroupBy(url.searchParams.get('group_by') || select.value);
|
|
};
|
|
const apply = (replaceUrl: boolean) => {
|
|
const parsed = parseProjectFilter(input.value);
|
|
const groupBy = normalizeProjectGroupBy(select.value);
|
|
select.value = groupBy;
|
|
if (!viewWasModified) snapshotCards();
|
|
board.setAttribute('data-project-board-view-modified', String(Boolean(input.value.trim()) || (groupBy !== 'none' && groupBy !== 'status')));
|
|
errorElement.hidden = !parsed.error;
|
|
errorElement.textContent = parsed.error ? (errorElement.getAttribute(`data-project-filter-error-${parsed.error.kind}`)!.replace('%s', parsed.error.value || '')) : '';
|
|
let matchingCount = 0;
|
|
for (const list of lists) {
|
|
const cards = cardsByList.get(list)!;
|
|
for (const card of cards) {
|
|
card.metadata = projectCardMetadata(card.element);
|
|
}
|
|
const matchingCards = parsed.error ? cards : cards.filter((card) => matchesProjectFilter(card.metadata, parsed.terms));
|
|
matchingCount += matchingCards.length;
|
|
setProjectIssueCount(list, matchingCards.length);
|
|
if (groupBy === 'none' || groupBy === 'status') {
|
|
list.replaceChildren(...matchingCards.map((card) => card.element));
|
|
continue;
|
|
}
|
|
const groups = new Map<string, ProjectCard[]>();
|
|
for (const card of matchingCards) {
|
|
const title = projectCardGroupValue(card.metadata, groupBy, board.getAttribute('data-project-group-no-value')!);
|
|
const groupedCards = groups.get(title);
|
|
if (groupedCards) groupedCards.push(card);
|
|
else groups.set(title, [card]);
|
|
}
|
|
list.replaceChildren(...Array.from(groups, ([title, groupedCards]) => createProjectCardGroup(title, groupedCards)));
|
|
}
|
|
emptyElement.hidden = matchingCount !== 0 || Boolean(parsed.error);
|
|
viewWasModified = Boolean(input.value.trim()) || (groupBy !== 'none' && groupBy !== 'status');
|
|
if (!replaceUrl) return;
|
|
const url = new URL(window.location.href);
|
|
if (input.value.trim()) url.searchParams.set('filter', input.value);
|
|
else url.searchParams.delete('filter');
|
|
if (groupBy === 'none') url.searchParams.delete('group_by');
|
|
else url.searchParams.set('group_by', groupBy);
|
|
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`);
|
|
};
|
|
|
|
initializeFromUrl();
|
|
apply(false);
|
|
input.addEventListener('input', () => apply(true));
|
|
select.addEventListener('change', () => apply(true));
|
|
clear.addEventListener('click', () => {
|
|
input.value = '';
|
|
input.focus();
|
|
apply(true);
|
|
});
|
|
window.addEventListener('popstate', () => {
|
|
initializeFromUrl();
|
|
apply(false);
|
|
});
|
|
}
|
|
|
|
function isProjectBoardViewModified(): boolean {
|
|
return document.querySelector<HTMLElement>('#project-board')?.getAttribute('data-project-board-view-modified') === 'true';
|
|
}
|
|
|
|
function updateIssueCount(card: HTMLElement): void {
|
|
const parent = card.parentElement!;
|
|
const count = parent.querySelectorAll('.issue-card').length;
|
|
parent.querySelector('.project-column-issue-count')!.textContent = String(count);
|
|
}
|
|
|
|
async function moveIssue({item, from, to, oldIndex}: SortableEvent): Promise<void> {
|
|
if (isProjectBoardViewModified()) {
|
|
if (oldIndex !== undefined) {
|
|
const siblings = Array.from(from.children).filter((child) => child !== item);
|
|
from.insertBefore(item, siblings[oldIndex] || null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const columnCards = to.querySelectorAll('.issue-card');
|
|
updateIssueCount(from);
|
|
updateIssueCount(to);
|
|
|
|
const columnSorting = {
|
|
issues: Array.from(columnCards, (card, i) => ({
|
|
issueID: parseInt(card.getAttribute('data-issue')!),
|
|
sorting: i,
|
|
})),
|
|
};
|
|
|
|
try {
|
|
await POST(`${to.getAttribute('data-url')}/move`, {
|
|
data: columnSorting,
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
if (oldIndex !== undefined) {
|
|
from.insertBefore(item, from.children[oldIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function initRepoProjectSortable(): Promise<void> {
|
|
// the HTML layout is: #project-board.board > .project-column .cards > .issue-card
|
|
const mainBoard = document.querySelector<HTMLElement>('#project-board')!;
|
|
let boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column');
|
|
createSortable(mainBoard, {
|
|
group: 'project-column',
|
|
draggable: '.project-column',
|
|
handle: '.project-column-header',
|
|
delayOnTouchOnly: true,
|
|
delay: 500,
|
|
onMove: () => !isProjectBoardViewModified(),
|
|
onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
|
|
if (isProjectBoardViewModified()) return;
|
|
boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column');
|
|
|
|
const columnSorting = {
|
|
columns: Array.from(boardColumns, (column, i) => ({
|
|
columnID: parseInt(column.getAttribute('data-id')!),
|
|
sorting: i,
|
|
})),
|
|
};
|
|
|
|
try {
|
|
await POST(mainBoard.getAttribute('data-url')!, {
|
|
data: columnSorting,
|
|
});
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
},
|
|
});
|
|
|
|
for (const boardColumn of boardColumns) {
|
|
const boardCardList = boardColumn.querySelector<HTMLElement>('.cards')!;
|
|
createSortable(boardCardList, {
|
|
group: 'shared',
|
|
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
|
|
onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
|
|
delayOnTouchOnly: true,
|
|
delay: 500,
|
|
onMove: () => !isProjectBoardViewModified(),
|
|
});
|
|
}
|
|
}
|
|
|
|
function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
|
|
const elModal = document.querySelector<HTMLElement>('.ui.modal#project-column-modal-edit')!;
|
|
const elForm = elModal.querySelector<HTMLFormElement>('form')!;
|
|
|
|
const elColumnId = elForm.querySelector<HTMLInputElement>('input[name="id"]')!;
|
|
const elColumnTitle = elForm.querySelector<HTMLInputElement>('input[name="title"]')!;
|
|
const elColumnColor = elForm.querySelector<HTMLInputElement>('input[name="color"]')!;
|
|
|
|
const attrDataColumnId = 'data-modal-project-column-id';
|
|
const attrDataColumnTitle = 'data-modal-project-column-title-input';
|
|
const attrDataColumnColor = 'data-modal-project-column-color-input';
|
|
|
|
// the "new" button is not in project board, so need to query from document
|
|
queryElems(document, '.show-project-column-modal-edit', (el) => {
|
|
el.addEventListener('click', () => {
|
|
elColumnId.value = el.getAttribute(attrDataColumnId)!;
|
|
elColumnTitle.value = el.getAttribute(attrDataColumnTitle)!;
|
|
elColumnColor.value = el.getAttribute(attrDataColumnColor)!;
|
|
elColumnColor.dispatchEvent(new Event('input', {bubbles: true})); // trigger the color picker
|
|
});
|
|
});
|
|
|
|
elForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const columnId = elColumnId.value;
|
|
const actionBaseLink = elForm.getAttribute('data-action-base-link');
|
|
|
|
const formData = new FormData(elForm);
|
|
const formLink = columnId ? `${actionBaseLink}/${columnId}` : `${actionBaseLink}/columns/new`;
|
|
const formMethod = columnId ? 'PUT' : 'POST';
|
|
|
|
try {
|
|
elForm.classList.add('is-loading');
|
|
await request(formLink, {method: formMethod, data: formData});
|
|
if (!columnId) {
|
|
window.location.reload(); // newly added column, need to reload the page
|
|
return;
|
|
}
|
|
|
|
// update the newly saved column title and color in the project board (to avoid reload)
|
|
const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${CSS.escape(attrDataColumnId)}="${CSS.escape(columnId)}"]`)!;
|
|
elEditButton.setAttribute(attrDataColumnTitle, elColumnTitle.value);
|
|
elEditButton.setAttribute(attrDataColumnColor, elColumnColor.value);
|
|
|
|
const elBoardColumn = writableProjectBoard.querySelector<HTMLElement>(`.project-column[data-id="${CSS.escape(columnId)}"]`)!;
|
|
const elBoardColumnTitle = elBoardColumn.querySelector<HTMLElement>(`.project-column-title-text`)!;
|
|
elBoardColumnTitle.textContent = elColumnTitle.value;
|
|
for (const card of elBoardColumn.querySelectorAll<HTMLElement>('.issue-card')) {
|
|
card.setAttribute('data-project-card-status', elColumnTitle.value);
|
|
}
|
|
if (elColumnColor.value) {
|
|
const textColor = contrastColor(elColumnColor.value);
|
|
elBoardColumn.style.setProperty('background', elColumnColor.value, 'important');
|
|
elBoardColumn.style.setProperty('color', textColor, 'important');
|
|
queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.color = textColor);
|
|
} else {
|
|
elBoardColumn.style.removeProperty('background');
|
|
elBoardColumn.style.removeProperty('color');
|
|
queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.removeProperty('color'));
|
|
}
|
|
|
|
hideFomanticModal(elModal);
|
|
} finally {
|
|
elForm.classList.remove('is-loading');
|
|
}
|
|
});
|
|
}
|
|
|
|
function initRepoProjectToggleFullScreen(elProjectsView: HTMLElement): void {
|
|
const enterFullscreenBtn = document.querySelector('.screen-full');
|
|
const exitFullscreenBtn = document.querySelector('.screen-normal');
|
|
if (!enterFullscreenBtn || !exitFullscreenBtn) return;
|
|
|
|
const settingKey = 'projects-view-options';
|
|
type ProjectsViewOptions = {
|
|
fullScreen: boolean;
|
|
};
|
|
const opts = localUserSettings.getJsonObject<ProjectsViewOptions>(settingKey, {fullScreen: false});
|
|
const toggleFullscreenState = (isFullScreen: boolean) => {
|
|
toggleFullScreen(elProjectsView, isFullScreen);
|
|
toggleElem(enterFullscreenBtn, !isFullScreen);
|
|
toggleElem(exitFullscreenBtn, isFullScreen);
|
|
|
|
opts.fullScreen = isFullScreen;
|
|
localUserSettings.setJsonObject(settingKey, opts);
|
|
};
|
|
|
|
enterFullscreenBtn.addEventListener('click', () => toggleFullscreenState(true));
|
|
exitFullscreenBtn.addEventListener('click', () => toggleFullscreenState(false));
|
|
if (opts.fullScreen) {
|
|
// a temporary solution to remember the full screen state, not perfect,
|
|
// just make UX better than before, especially for users who need to change the label filter frequently and want to keep full screen mode.
|
|
toggleFullscreenState(true);
|
|
}
|
|
}
|
|
|
|
function initRepoProjectRefresh(): void {
|
|
let wasHidden = document.hidden;
|
|
let isReloading = false;
|
|
const reloadProject = () => {
|
|
if (isReloading) return;
|
|
isReloading = true;
|
|
window.location.reload();
|
|
};
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
wasHidden = true;
|
|
} else if (wasHidden) {
|
|
reloadProject();
|
|
}
|
|
});
|
|
|
|
window.addEventListener('pageshow', (event) => {
|
|
if (event.persisted) {
|
|
reloadProject();
|
|
}
|
|
});
|
|
}
|
|
|
|
export function initRepoProjectsView(): void {
|
|
registerGlobalInitFunc('initRepoProjectsView', (elProjectsView) => {
|
|
initRepoProjectRefresh();
|
|
initRepoProjectToggleFullScreen(elProjectsView);
|
|
initRepoProjectFilters(elProjectsView);
|
|
|
|
const writableProjectBoard = document.querySelector('#project-board[data-project-board-writable="true"]');
|
|
if (!writableProjectBoard) return;
|
|
|
|
initRepoProjectSortable(); // no await
|
|
initRepoProjectColumnEdit(writableProjectBoard);
|
|
});
|
|
}
|