Add tabbed sub-views to bins section, create-bin-from-source flow
Bins section now mirrors the grids section with two tabs: - Bins: gallery of processed bin records - Sources: source images tagged with uses=['bin'], with upload and '+ Bin' button to create a bin record from an existing source image Server: POST /api/bins/from-source accepts source_id, creates bin record and adds 'bin' to the source image's uses array. URL state: /bins → bins tab, /bins/sources → sources tab. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
117
public/app.mjs
117
public/app.mjs
@@ -29,7 +29,8 @@ let all_drafts = [];
|
||||
let all_templates = [];
|
||||
let all_pdfs = [];
|
||||
let all_bins = [];
|
||||
let bin_editor_instance = null; // { bin, setup: Grid_Setup }
|
||||
let bin_tab = 'bins'; // 'bins' | 'sources'
|
||||
let bin_editor_instance = null;
|
||||
let bin_editor_bin_id = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1947,47 +1948,111 @@ function confirm_delete(message, on_confirm) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function render_bins() {
|
||||
const main = document.getElementById('main');
|
||||
main.innerHTML = '';
|
||||
const sec = document.getElementById('t-section-bins').content.cloneNode(true);
|
||||
main.appendChild(sec);
|
||||
let sec = document.getElementById('section-bins');
|
||||
if (!sec) {
|
||||
const main = document.getElementById('main');
|
||||
main.replaceChildren(document.getElementById('t-section-bins').content.cloneNode(true));
|
||||
sec = document.getElementById('section-bins');
|
||||
|
||||
qs(sec, '#btn-tab-bins').addEventListener('click', () => {
|
||||
bin_tab = 'bins';
|
||||
history.replaceState(null, '', '/bins');
|
||||
update_bin_tabs(sec);
|
||||
});
|
||||
qs(sec, '#btn-tab-bin-sources').addEventListener('click', () => {
|
||||
bin_tab = 'sources';
|
||||
history.replaceState(null, '', '/bins/sources');
|
||||
update_bin_tabs(sec);
|
||||
});
|
||||
qs(sec, '#bin-source-upload-input').addEventListener('change', async (e) => {
|
||||
const files = [...e.target.files];
|
||||
if (!files.length) return;
|
||||
for (const file of files) {
|
||||
try {
|
||||
const result = await api.upload_bin(file, file.name.replace(/\.[^.]+$/, ''));
|
||||
all_bins.unshift(result.bin);
|
||||
const src = all_sources.find(s => s.id === result.bin.source_id);
|
||||
if (!src) {
|
||||
const r2 = await api.get_source_images();
|
||||
all_sources = r2.sources;
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
e.target.value = '';
|
||||
render_bin_source_list();
|
||||
});
|
||||
}
|
||||
|
||||
update_bin_tabs(sec);
|
||||
render_bin_list();
|
||||
render_bin_source_list();
|
||||
}
|
||||
|
||||
function update_bin_tabs(sec) {
|
||||
qs(sec, '#btn-tab-bins').classList.toggle('active', bin_tab === 'bins');
|
||||
qs(sec, '#btn-tab-bin-sources').classList.toggle('active', bin_tab === 'sources');
|
||||
qs(sec, '#btn-upload-bin-sources').hidden = (bin_tab !== 'sources');
|
||||
qs(sec, '#tab-bins-content').hidden = (bin_tab !== 'bins');
|
||||
qs(sec, '#tab-bin-sources-content').hidden = (bin_tab !== 'sources');
|
||||
}
|
||||
|
||||
function render_bin_list() {
|
||||
const gallery = document.getElementById('bin-gallery');
|
||||
if (!gallery) return;
|
||||
gallery.replaceChildren();
|
||||
for (const bin of all_bins) {
|
||||
const card = document.getElementById('t-bin-card').content.cloneNode(true);
|
||||
const img = card.querySelector('.bin-card-img');
|
||||
const img_wrap = card.querySelector('.bin-card-img-wrap');
|
||||
card.querySelector('.bin-card-name').textContent = bin.name;
|
||||
const card = clone('t-bin-card');
|
||||
const img = qs(card, '.bin-card-img');
|
||||
const img_wrap = qs(card, '.bin-card-img-wrap');
|
||||
qs(card, '.bin-card-name').textContent = bin.name;
|
||||
if (bin.image_filename) {
|
||||
img.src = `/img/${bin.image_filename}`;
|
||||
img_wrap.classList.add('has-image');
|
||||
} else {
|
||||
img.hidden = true;
|
||||
}
|
||||
card.querySelector('.btn-edit').addEventListener('click', () => open_bin_editor(bin));
|
||||
card.querySelector('.btn-delete').addEventListener('click', () => {
|
||||
qs(card, '.btn-edit').addEventListener('click', () => open_bin_editor(bin));
|
||||
qs(card, '.btn-delete').addEventListener('click', () => {
|
||||
confirm_delete(`Delete bin "${bin.name}"?`, async () => {
|
||||
await api.delete_bin(bin.id);
|
||||
all_bins = all_bins.filter(b => b.id !== bin.id);
|
||||
render_bins();
|
||||
render_bin_list();
|
||||
});
|
||||
});
|
||||
gallery.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('bin-upload-input').addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const name = file.name.replace(/\.[^.]+$/, '');
|
||||
const result = await api.upload_bin(file, name);
|
||||
all_bins.unshift(result.bin);
|
||||
render_bins();
|
||||
open_bin_editor(result.bin);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
});
|
||||
function render_bin_source_list() {
|
||||
const list_el = document.getElementById('bin-source-image-list');
|
||||
if (!list_el) return;
|
||||
const bin_sources = all_sources.filter(s => s.uses?.includes('bin'));
|
||||
if (bin_sources.length === 0) {
|
||||
const el = clone('t-empty-block');
|
||||
el.textContent = 'No bin source images yet. Upload photos of your bins.';
|
||||
list_el.replaceChildren(el);
|
||||
return;
|
||||
}
|
||||
list_el.replaceChildren(...bin_sources.map(src => {
|
||||
const card = build_source_card(src, false);
|
||||
const create_btn = card.querySelector('.source-card-create-bin');
|
||||
create_btn.hidden = false;
|
||||
create_btn.addEventListener('click', async () => {
|
||||
try {
|
||||
const result = await api.create_bin_from_source(src.id);
|
||||
all_bins.unshift(result.bin);
|
||||
render_bin_list();
|
||||
open_bin_editor(result.bin);
|
||||
bin_tab = 'bins';
|
||||
update_bin_tabs(document.getElementById('section-bins'));
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
});
|
||||
return card;
|
||||
}));
|
||||
}
|
||||
|
||||
function open_bin_editor(bin) {
|
||||
@@ -2021,6 +2086,7 @@ function parse_url() {
|
||||
section = 'components';
|
||||
grid_view_state = 'list';
|
||||
grid_tab = 'grids';
|
||||
bin_tab = 'bins';
|
||||
current_grid_id = null;
|
||||
current_panel_idx = null;
|
||||
grid_draft = null;
|
||||
@@ -2045,6 +2111,7 @@ function parse_url() {
|
||||
section = 'templates';
|
||||
} else if (p0 === 'bins') {
|
||||
section = 'bins';
|
||||
bin_tab = p1 === 'sources' ? 'sources' : 'bins';
|
||||
} else if (p0 === 'grids') {
|
||||
section = 'grids';
|
||||
if (p1 === 'sources') {
|
||||
|
||||
Reference in New Issue
Block a user