255 lines
12 KiB
JavaScript
255 lines
12 KiB
JavaScript
(function (root, factory) {
|
|
const createSearchPreview = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = createSearchPreview;
|
|
if (root) {
|
|
root.createSearchPreview = createSearchPreview;
|
|
root.safeSearchUrl = value => {
|
|
try {
|
|
const url = new URL(value);
|
|
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
|
|
} catch (_) { return ''; }
|
|
};
|
|
root.searchPreviewUrl = (state, defaultScope, location) => {
|
|
const { query, preview, scope = defaultScope } = state;
|
|
const params = new URLSearchParams({
|
|
search:query || '', preview:preview.kind + ':' + preview.repository + ':' + preview.number,
|
|
search_kind:scope.kind, search_state:scope.state,
|
|
});
|
|
if (scope.repository) params.set('search_repository', scope.repository);
|
|
return new URL('?' + params, location.origin + location.pathname).href;
|
|
};
|
|
root.searchPreviewPath = item => {
|
|
const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
|
|
return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
|
|
'/preview?kind=' + encodeURIComponent(item.kind);
|
|
};
|
|
root.searchPreviewConversationPath = (item, page) => {
|
|
const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
|
|
const query = new URLSearchParams({ kind:item.kind, limit:'20' });
|
|
if (page) query.set('page', String(page));
|
|
return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
|
|
'/preview/conversation?' + query.toString();
|
|
};
|
|
root.renderSearchPreviewConversation = (conversation, document, escapeHtml, formatTime, renderMarkdown) => {
|
|
const comments = document.querySelector('#search-preview-comments');
|
|
const status = document.querySelector('#search-preview-conversation-status');
|
|
const retry = document.querySelector('#retry-search-preview-conversation');
|
|
const older = document.querySelector('#load-older-search-preview-comments');
|
|
if (!conversation) {
|
|
comments.textContent = status.textContent = '';
|
|
retry.hidden = older.hidden = true;
|
|
return;
|
|
}
|
|
comments.innerHTML = (conversation.comments || []).map(comment =>
|
|
'<article class="search-preview-comment" data-comment-id="' + Number(comment.id || 0) + '">' +
|
|
'<div class="small">' + escapeHtml(comment.author || 'Unknown author') +
|
|
(comment.created_at ? ' · ' + escapeHtml(formatTime(comment.created_at)) : '') + '</div>' +
|
|
'<div class="markdown-content">' + renderMarkdown(comment.body || '') + '</div></article>'
|
|
).join('');
|
|
retry.hidden = conversation.status !== 'error';
|
|
older.hidden = conversation.status !== 'ready' || !conversation.olderPage;
|
|
older.disabled = conversation.status === 'loading';
|
|
if (conversation.status === 'loading') status.textContent = conversation.comments?.length
|
|
? 'Loading older messages…' : 'Loading current conversation…';
|
|
else if (conversation.status === 'error') status.textContent =
|
|
'Conversation unavailable. Preview and planning actions still work.';
|
|
else if (!conversation.comments?.length) status.textContent = 'No conversation yet.';
|
|
else status.textContent = conversation.comments.length +
|
|
(conversation.comments.length === 1 ? ' message.' : ' messages.');
|
|
};
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
return function createSearchPreview({ fetchJson, fetchConversation, mutate, share, session, getSession, loadMore, onNavigate, navigationRoot, onState }) {
|
|
if (Array.isArray(session)) {
|
|
getSession = session[0];
|
|
loadMore = () => session[1].loadMore();
|
|
onNavigate = session[2];
|
|
navigationRoot = document;
|
|
}
|
|
let generation = 0;
|
|
let current = null;
|
|
let mutationRequest = null;
|
|
let shareRequest = null;
|
|
let moveRequest = null;
|
|
let conversation = null;
|
|
|
|
function sameItem(left, right) {
|
|
return left && right && left.kind === right.kind && left.repository === right.repository &&
|
|
Number(left.number) === Number(right.number);
|
|
}
|
|
|
|
function navigation(item) {
|
|
const session = typeof getSession === 'function' ? getSession() : null;
|
|
const items = Array.isArray(session?.items) ? session.items : [];
|
|
const index = items.findIndex(candidate => sameItem(candidate, item));
|
|
if (index < 0) return null;
|
|
return {
|
|
position:index + 1,
|
|
total:items.length,
|
|
hasPrevious:index > 0,
|
|
hasNext:index < items.length - 1 || session.more === true,
|
|
};
|
|
}
|
|
|
|
function publish(state) {
|
|
const position = navigation(state.item || current);
|
|
if (navigationRoot) {
|
|
const bar = navigationRoot.querySelector('.search-preview-navigation');
|
|
bar.hidden = !position;
|
|
if (position) {
|
|
navigationRoot.querySelector('#search-preview-position').textContent = position.position + ' of ' + position.total;
|
|
navigationRoot.querySelector('#previous-search-result').disabled = !position.hasPrevious;
|
|
navigationRoot.querySelector('#next-search-result').disabled = !position.hasNext;
|
|
}
|
|
}
|
|
onState(position ? { ...state, navigation:position } : state);
|
|
if (state.complete && navigationRoot) {
|
|
navigationRoot.querySelector('#search-preview-status').textContent = 'Search pass complete.';
|
|
}
|
|
}
|
|
|
|
function run(action, pending, success, detail) {
|
|
if (mutationRequest) return mutationRequest;
|
|
publish({ status: pending, item: current, detail });
|
|
mutationRequest = mutate(detail, action).then(result => {
|
|
publish({ status: success, item: current, detail, result });
|
|
return result;
|
|
}).catch(error => {
|
|
publish({ status: 'ready', item: current, detail, error });
|
|
throw error;
|
|
}).finally(() => { mutationRequest = null; });
|
|
return mutationRequest;
|
|
}
|
|
|
|
function loadConversation(detail, requestGeneration, page) {
|
|
if (typeof fetchConversation !== 'function') return Promise.resolve(null);
|
|
const previousComments = page && Array.isArray(conversation?.comments)
|
|
? conversation.comments : [];
|
|
conversation = { status:'loading', comments:previousComments, olderPage:page ?? null };
|
|
publish({ status:'ready', item:current, detail, conversation });
|
|
return fetchConversation(detail, page).then(result => {
|
|
if (requestGeneration !== generation) return result;
|
|
const incoming = Array.isArray(result?.comments) ? result.comments : [];
|
|
const comments = page
|
|
? [...incoming, ...previousComments].filter((comment, index, all) =>
|
|
all.findIndex(candidate => candidate?.id === comment?.id) === index)
|
|
: incoming;
|
|
conversation = {
|
|
status:'ready',
|
|
comments,
|
|
olderPage:result?.older_page ?? null,
|
|
};
|
|
publish({ status:'ready', item:current, detail:current, conversation });
|
|
return result;
|
|
}).catch(error => {
|
|
if (requestGeneration === generation) {
|
|
conversation = {
|
|
status:'error', comments:previousComments,
|
|
olderPage:page ?? conversation?.olderPage ?? null, error,
|
|
};
|
|
publish({ status:'ready', item:current, detail:current, conversation });
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
|
|
const api = {
|
|
open(item) {
|
|
generation += 1;
|
|
const requestGeneration = generation;
|
|
current = { ...item };
|
|
conversation = null;
|
|
publish({ status: 'loading', item: current });
|
|
return fetchJson(current).then(detail => {
|
|
if (requestGeneration === generation) {
|
|
current = { ...current, ...detail };
|
|
if (typeof fetchConversation === 'function') {
|
|
loadConversation(current, requestGeneration);
|
|
} else {
|
|
publish({ status: 'ready', item: current, detail });
|
|
}
|
|
}
|
|
return detail;
|
|
}).catch(error => {
|
|
if (requestGeneration === generation) {
|
|
publish({ status: 'error', item: current, error });
|
|
}
|
|
throw error;
|
|
});
|
|
},
|
|
close() {
|
|
generation += 1;
|
|
current = null;
|
|
onState({ status: 'closed' });
|
|
},
|
|
previous() {
|
|
const session = typeof getSession === 'function' ? getSession() : null;
|
|
const items = Array.isArray(session?.items) ? session.items : [];
|
|
const index = items.findIndex(item => sameItem(item, current));
|
|
if (index <= 0) return Promise.resolve(current);
|
|
const item = items[index - 1];
|
|
if (typeof onNavigate === 'function') onNavigate(item);
|
|
return api.open(item);
|
|
},
|
|
next() {
|
|
if (moveRequest) return moveRequest;
|
|
moveRequest = (async () => {
|
|
let session = typeof getSession === 'function' ? getSession() : null;
|
|
let items = Array.isArray(session?.items) ? session.items : [];
|
|
let index = items.findIndex(item => sameItem(item, current));
|
|
if (index < 0) return current;
|
|
if (index === items.length - 1 && session?.more === true && typeof loadMore === 'function') {
|
|
await loadMore();
|
|
session = getSession();
|
|
items = Array.isArray(session?.items) ? session.items : [];
|
|
index = items.findIndex(item => sameItem(item, current));
|
|
}
|
|
if (index >= 0 && index < items.length - 1) {
|
|
const item = items[index + 1];
|
|
if (typeof onNavigate === 'function') onNavigate(item);
|
|
return api.open(item);
|
|
}
|
|
publish({ status:'ready', item:current, detail:current, complete:true });
|
|
return current;
|
|
})().finally(() => { moveRequest = null; });
|
|
return moveRequest;
|
|
},
|
|
retryConversation() {
|
|
if (!current) return Promise.resolve(null);
|
|
return loadConversation(current, generation);
|
|
},
|
|
loadOlderConversation() {
|
|
if (!current || !conversation?.olderPage) return Promise.resolve(null);
|
|
return loadConversation(current, generation, conversation.olderPage);
|
|
},
|
|
share(url) {
|
|
if (shareRequest) return shareRequest;
|
|
if (!current || typeof share !== 'function') return Promise.reject(new Error('Sharing is unavailable.'));
|
|
const detail = current;
|
|
publish({ status:'sharing', item:current, detail });
|
|
shareRequest = share(url).then(result => {
|
|
publish({ status:result, item:current, detail });
|
|
return result;
|
|
}).catch(error => {
|
|
publish({ status:error?.name === 'AbortError' ? 'share-canceled' : 'share-error', item:current, detail, error });
|
|
throw error;
|
|
}).finally(() => { shareRequest = null; });
|
|
return shareRequest;
|
|
},
|
|
claim(detail) {
|
|
return run('claim', 'claiming', 'claimed', detail);
|
|
},
|
|
reopen(detail) {
|
|
return run('reopen', 'reopening', 'reopened', detail);
|
|
},
|
|
};
|
|
if (navigationRoot) {
|
|
navigationRoot.querySelector('#previous-search-result').addEventListener('click', () => api.previous().catch(() => {}));
|
|
navigationRoot.querySelector('#next-search-result').addEventListener('click', () => api.next().catch(() => {}));
|
|
navigationRoot.querySelector('#retry-search-preview-conversation').addEventListener('click', () => api.retryConversation());
|
|
navigationRoot.querySelector('#load-older-search-preview-comments').addEventListener('click', () => api.loadOlderConversation());
|
|
}
|
|
return api;
|
|
};
|
|
});
|