1
0
Fork 0
timelinize/frontend/resources/js/entities.js
changeling 2cddac7c2b
Add Select All checkbox to Entities page. (#174)
Add checkbox to table header in entities.html.
Add logic to handle `Select All` checkbox to entities.js.
2025-11-12 13:10:46 -07:00

240 lines
No EOL
7.4 KiB
JavaScript

async function entitiesPageMain() {
filterAndRenderEntitiesList();
}
async function filterAndRenderEntitiesList() {
const searchParams = {
repo: tlz.openRepos[0].instance_id,
order_by: 'id',
or_fields: true,
limit: 100
}
const filter = $('.filter-entities').value.trim();
if (filter) {
searchParams.name = [filter];
searchParams.birth_place = [filter];
searchParams.attributes = [{ value: filter }];
}
const entities = await app.SearchEntities(searchParams);
console.log("GOT ENTITIES:", entities);
$('#results-container').replaceChildren();
if (!entities) {
return; // there SHOULD always be one, but just in case...
}
const owner = await getOwner();
// Ensure the "select all" checkbox is unchecked when the list is refreshed/filtered
$('#select-all-entities').checked = false;
for (const ent of entities) {
const tpl = cloneTemplate('#tpl-entity');
$('.entity-id', tpl).innerText = ent.id;//String(ent.id).padStart(4, '0');
$('.entity-picture', tpl).innerHTML = avatar(true, ent, "avatar me-2").replace(/span/g, "a");
$('.entity-name', tpl).innerText = ent.name; // TODO: and if no name...?
if (ent.type && ent.type != "person") {
$('.entity-type', tpl).innerText = ent.type;
}
$('.entity-stored', tpl).innerText = DateTime.fromISO(ent.stored).toLocaleString(DateTime.DATE_MED);
$('.select-entity', tpl).dataset.entityID = ent.id;
const entHref = `/entities/${tlz.openRepos[0].instance_id}/${ent.id}`;
$('.entity-name', tpl).href = entHref;
$('.entity-picture a', tpl).href = entHref;
$('.entity-view', tpl).href = entHref;
// find some attributes to display; display up to 2 (we may change this
// depending on viability)... generally email and phone are the most useful,
// so default to those, but if one or both of those aren't available,
// show the first 1 or 2 identity attributes as they are more useful than
// generic attributes... if there aren't any identity attributes, just show
// the first generic attributes, doesn't really matter at that point
const attrs = {};
for (const attr of ent.attributes) {
if (!attrs[attr.name]) {
attrs[attr.name] = [];
}
attrs[attr.name].push(attr);
}
let attr1, attr2;
attr1 = attrs["email_address"]?.slice(0, 5) || attrs["phone_number"]?.slice(0, 5);
if (!attr1) {
for (const name in attrs) {
if (attrs[name][0].identity) {
attr1 = attrs[name].slice(0, 5);
break;
}
}
}
if (attr1) {
for (const name in attrs) {
if (attrs[name][0].identity && name != attr1[0].name) {
attr2 = attrs[name].slice(0, 5);
break;
}
}
}
if (attr1 && !attr2) {
for (const name in attrs) {
if (name != attr1[0].name) {
attr2 = attrs[name].slice(0, 5);
break;
}
}
}
function renderAttributes(attrs, container) {
for (const attr of attrs) {
let tagName = attr.name == "email_address" || attr.name == "phone_number" || attr.name == "url" ? 'a' : 'span';
const elem = document.createElement(tagName);
if (attr.name == "email_address") {
elem.href = "mailto:" + attr.value;
} else if (attr.name == "phone_number") {
elem.href = "tel:" + attr.value;
} else if (attr.name == "url") {
elem.href = attr.value;
}
if (attr.latitude && attr.longitude) {
elem.innerText = `${attr.latitude}, ${attr.longitude}`;
} else if (attr.value) {
elem.innerText = attr.value;
}
container.append(elem);
container.append(document.createElement("br"));
}
const elem = document.createElement('span');
elem.classList.add('text-secondary');
elem.innerText = tlz.attributeLabels[attrs[0].name] || attrs[0].name;
container.append(elem);
}
if (attr1) {
renderAttributes(attr1, $('.entity-attr1', tpl));
}
if (attr2) {
renderAttributes(attr2, $('.entity-attr2', tpl));
}
// show the "You" badge next to help identify self
if (ent.id == owner.id) {
const badge = document.createElement('span');
badge.classList.add('badge', 'bg-purple-lt', 'ms-2');
badge.innerText = "You";
$('.entity-name', tpl).append(badge);
}
// if the user clicks the Merge action, this will pre-populate the modal
$('.entity-list-merge-link', tpl).dataset.entityIDMerge = ent.id;
$('#results-container').append(tpl);
}
}
on('keyup', '.filter-entities', async e => {
// skip Shift, Ctrl, etc. (but allow Backspace)
if ((e.keyCode < 32 && e.keyCode != 8) || e.ctrlKey || e.metaKey) {
return;
}
// TODO: debounce
filterAndRenderEntitiesList();
});
on('change', '.select-entity', e => {
if ($$('.select-entity:checked').length > 1) {
$('#merge-entities').classList.remove('disabled');
} else {
$('#merge-entities').classList.add('disabled');
}
});
// Function to check the state of the merge button enablement
function updateMergeButtonState() {
const checkedCount = $$('.select-entity:checked').length;
if (checkedCount > 1) {
$('#merge-entities').classList.remove('disabled');
} else {
$('#merge-entities').classList.add('disabled');
}
// Uncheck the "select all" box if not all items are checked
const allCheckboxes = $$('.select-entity');
if (checkedCount < allCheckboxes.length) {
$('#select-all-entities').checked = false;
} else if (allCheckboxes.length > 0 && checkedCount === allCheckboxes.length) {
// If all are checked, make sure the master is checked (useful when filtering might leave a few items)
$('#select-all-entities').checked = true;
}
}
// Event listener for the individual checkboxes, for the merge button check
on('change', '.select-entity', e => {
updateMergeButtonState();
});
// Event listener for the "Select All" checkbox
on('change', '#select-all-entities', e => {
const isChecked = e.target.checked;
// Iterate over all individual checkboxes and set their state to match the master
$$('.select-entity').forEach(checkbox => {
checkbox.checked = isChecked;
});
// Update the merge button state after toggling all checkboxes
updateMergeButtonState();
});
on('click', '#confirm-merge-entities', async e => {
const button = $('#confirm-merge-entities');
const done = function() {
button.classList.remove('disabled');
$('spinner', button)?.remove();
bootstrap.Modal.getInstance('#modal-merge-multiple-entities').hide();
};
// get all the IDs of entities to be merged
const ids = Array.from($$('.select-entity:checked'), check => Number(check.dataset.entityID));
if (ids.length < 2) {
done();
return;
}
// show loading indicator on button and disable clicks until done
const spinner = document.createElement('spinner');
spinner.classList.add('spinner-border', 'spinner-border-sm', 'me-2');
button.prepend(spinner);
button.classList.add('disabled');
// choose an entity to use as the keeper, I'm going to go with the lowest ID (oldest)
ids.sort();
keepID = ids.shift();
// perform merge
await app.MergeEntities(tlz.openRepos[0].instance_id, keepID, ids);
notify({
type: "success",
title: `${ids.length+1} entit${ids.length+1 == 1 ? "y" : "ies"} merged`,
duration: 2000
});
// close modal and update list
done();
filterAndRenderEntitiesList();
// also update owner picture at the top if its entity was in the merge
if (keepID == 1 || ids[0] == 1) {
updateRepoOwners(true);
}
});
on('hidden.bs.modal', '#confirm-merge-entities', async e => {
// reset UI state
$('#confirm-merge-entities').classList.remove('disabled');
$('#confirm-merge-entities .spinner-border').remove();
});