Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3184b7b31 | ||
|
|
1d0360d77d |
@@ -158,6 +158,10 @@ async function filteredOf(page,apply,undo) {
|
||||
await apply();
|
||||
await expect.poll(()=>captionOf(page)).not.toBe(before);
|
||||
await expect.poll(()=>page.locator(CARDS).count()).toBeLessThan(CONCEPT_PROMPTS);
|
||||
// Production empties the shelf when the filter applies, writes the answer's caption at once and places its cards
|
||||
// in the next animation frame (one paint for a viewer), so the empty shelf already counts as "fewer": read the
|
||||
// titles only after that frame, as the Images and Inspector comparisons do.
|
||||
await settle(page);
|
||||
const result={caption:await captionOf(page),titles:await titlesOf(page)};
|
||||
await undo();
|
||||
await expect.poll(()=>captionOf(page)).toBe(before);
|
||||
|
||||
@@ -1487,6 +1487,30 @@ test('a star press leaves the continuation status, count and caption date untouc
|
||||
expect(await page.evaluate(() => window.__statusMutations)).toEqual([]);
|
||||
});
|
||||
|
||||
// Opened before the source and folder discovery answered (a slow server), Refine lists them once it does.
|
||||
test('Refine opened before discovery settles lists the discovered sources and folders', async ({ page }) => {
|
||||
let release, started;
|
||||
const held = new Promise(resolve => { release = resolve; }), hit = new Promise(resolve => { started = resolve; });
|
||||
await page.route('**/prompt_manager/gallery/subfolders?*', async route => { started(); await held; await route.continue(); }, { times: 1 });
|
||||
await page.goto(workspace);
|
||||
await expect(cards(page)).toHaveCount(2);
|
||||
await hit;
|
||||
await filtersButton(page).click();
|
||||
const source = refineDialog(page).getByLabel('Source', { exact: true });
|
||||
const folder = refineDialog(page).getByLabel('Folder', { exact: true });
|
||||
const values = select => select.locator('option').evaluateAll(options => options.map(option => option.value));
|
||||
expect(await values(source)).toEqual(['all']);
|
||||
expect(await values(folder)).toEqual(['']);
|
||||
release();
|
||||
await expect.poll(() => values(source)).toEqual(['all', '0', '1']);
|
||||
expect(await values(folder)).toEqual(['', 'shared']);
|
||||
await expect(source).toHaveValue('all');
|
||||
await source.selectOption('1');
|
||||
await refineDialog(page).getByRole('button', { name: 'Apply filters', exact: true }).click();
|
||||
await expect(cards(page)).toHaveCount(1);
|
||||
await expect(cards(page)).toHaveAttribute('data-image-key', '1:shared/same.png');
|
||||
});
|
||||
|
||||
// #249 final review m1: as in the old field row, a Source offers only its own folders.
|
||||
test('Refine offers only the chosen source folders', async ({ page, request }) => {
|
||||
await expect(await request.post('/__test__/add-nested-image')).toBeOK();
|
||||
|
||||
@@ -342,6 +342,56 @@ test('refine dialog round-trips values and clear resets them', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('Refine lists options discovered after it opened and keeps the wanted values', async () => {
|
||||
const { openRefineDialog, openImportDialog, closeWorkspaceDialog } = await dialogsModule;
|
||||
const dom = fakeDOM();
|
||||
const scope = (await coreModule).createScope();
|
||||
try {
|
||||
let bySource = {};
|
||||
const optionsFor = source => [['', 'All output folders'], ...(bySource[source] ?? []).map(path => [path, path])];
|
||||
const fields = [{ key: 'source', options: [['all', 'All sources']] }, { key: 'folder', options: optionsFor('all'), optionsFor },
|
||||
REFINE_FIELDS[2]];
|
||||
const applied = [];
|
||||
const refine = openRefineDialog({ scope, fields, values: { source: '1', folder: 'temp-only', media: 'video' },
|
||||
onApply: value => applied.push(value), onClear() {} });
|
||||
const [source, folder, media] = byId(dom, 'modal-body').children.filter(child => child.className === 'field')
|
||||
.map(field => field.children[1]);
|
||||
// Opened before discovery: only the cleared values.
|
||||
assert.deepEqual(optionPairs(source), [['all', 'All sources']]);
|
||||
assert.equal(source.value, 'all');
|
||||
assert.equal(folder.value, '');
|
||||
bySource = { all: ['portraits', 'temp-only'], 0: ['portraits'], 1: ['temp-only'] };
|
||||
refine.setOptions('source', [['all', 'All sources'], ['0', 'output'], ['1', 'archive']]);
|
||||
// The values it opened with, now offered, and the chosen Source's folders.
|
||||
assert.deepEqual(optionPairs(source).map(([value]) => value), ['all', '0', '1']);
|
||||
assert.equal(source.value, '1');
|
||||
assert.deepEqual(optionPairs(folder), [['', 'All output folders'], ['temp-only', 'temp-only']]);
|
||||
assert.equal(folder.value, 'temp-only');
|
||||
// A choice made since wins over the opening value; tags and unknown keys are ignored.
|
||||
source.value = '0';
|
||||
source.dispatchEvent(new Event('change'));
|
||||
refine.setOptions('source', [['all', 'All sources'], ['0', 'output'], ['1', 'archive']]);
|
||||
refine.setOptions('tags', [['x', 'x']]);
|
||||
assert.equal(source.value, '0');
|
||||
assert.deepEqual(optionPairs(folder).map(([value]) => value), ['', 'portraits']);
|
||||
assert.equal(media.value, 'video');
|
||||
button(byId(dom, 'modal-body'), 'Apply filters').click();
|
||||
assert.deepEqual(applied, [{ source: '0', folder: '', media: 'video' }]);
|
||||
// Closed: a late discovery leaves the shell alone.
|
||||
refine.setOptions('source', [['all', 'All sources']]);
|
||||
assert.equal(source.options.length, 3);
|
||||
// Replaced by another dialog: the same.
|
||||
const again = openRefineDialog({ scope, fields, values: {}, onApply() {}, onClear() {} });
|
||||
openImportDialog({ scope, onInspect() {}, onRescan() {} });
|
||||
again.setOptions('source', [['all', 'All sources'], ['9', 'late']]);
|
||||
assert.equal(byId(dom, 'modal-title').textContent, 'Import & inspect');
|
||||
} finally {
|
||||
closeWorkspaceDialog();
|
||||
scope.dispose();
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the Folder list follows the chosen Source', async () => {
|
||||
const { openRefineDialog, closeWorkspaceDialog } = await dialogsModule;
|
||||
const dom = fakeDOM();
|
||||
|
||||
@@ -133,6 +133,8 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
let state, geometry = { positions: [], columns: 1, height: 0, maxHeight: 0 }, galleryWidth = 0, optionsRequest, categoryRequest, recovering = false, rescanning = false;
|
||||
// Discovered sources (each with its own folders) and every folder, for "Refine this view" and the scope chip.
|
||||
let roots = [], folders = [];
|
||||
// The open "Refine this view", so a discovery that settles after it opened still lists its sources and folders.
|
||||
let refine = null;
|
||||
const rendered = new Map();
|
||||
const itemIndexes = new Map();
|
||||
const filters = { root: 'all', q: '', category: '', tags: [], subfolder: '', media: 'all', sort: 'newest', ...visit?.filters, ...saved,
|
||||
@@ -643,8 +645,8 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
/** "Refine this view": Source, Folder, Media and Tags, the filters the old field row held (D14). */
|
||||
function openFilters(trigger) {
|
||||
if (recovering) return;
|
||||
openRefineDialog({scope, trigger, fields:[
|
||||
{key:'source', options:[['all', 'All sources'], ...roots.map(root => [String(root.index), root.label])]},
|
||||
refine = openRefineDialog({scope, trigger, fields:[
|
||||
{key:'source', options:sourceOptions()},
|
||||
{key:'folder', options:folderOptions('all'), optionsFor:folderOptions},
|
||||
{key:'media', options:MEDIA_OPTIONS},
|
||||
{key:'tags'},
|
||||
@@ -654,6 +656,9 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
applyRefinement(cleared);
|
||||
}});
|
||||
}
|
||||
function sourceOptions() {
|
||||
return [['all', 'All sources'], ...roots.map(root => [String(root.index), root.label])];
|
||||
}
|
||||
/** The chosen Source's folders, as the old field row reloaded them; every folder under All sources. */
|
||||
function folderOptions(source) {
|
||||
const root = source === 'all' ? null : roots.find(item => String(item.index) === source);
|
||||
@@ -750,6 +755,7 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
roots = data.roots.filter(root => Number.isSafeInteger(root?.index) && typeof root.label === 'string')
|
||||
.map(root => ({index:root.index, label:root.label, subfolders:paths(root.subfolders)}));
|
||||
folders = paths(data.subfolders);
|
||||
refine?.setOptions('source', sourceOptions());
|
||||
updateScope();
|
||||
sourcesRetry = null;
|
||||
return true;
|
||||
|
||||
@@ -152,6 +152,9 @@ function choose(select, value) {
|
||||
* Tags; Folder may add `optionsFor(source)`, the options of one Source, so its list follows the chosen Source and a
|
||||
* folder that Source lacks returns to the first option. `values` holds the current value per key. Apply reports the edited values; Clear resets every field and
|
||||
* reports the cleared values plus `category: ''`, so the page clears its toolbar category too. Both close first.
|
||||
* Returns `{setOptions(key, options)}` for options discovered after the dialog opened (a slow folder discovery): the
|
||||
* select is refilled and shows the value the field opened with, or the one chosen since, once it is offered; a Folder
|
||||
* that follows its Source is refilled from `optionsFor` again. It does nothing once this dialog closed or was replaced.
|
||||
*/
|
||||
export function openRefineDialog({ scope, fields, values = {}, onApply, onClear, trigger = null }) {
|
||||
requireScope(scope);
|
||||
@@ -172,16 +175,21 @@ export function openRefineDialog({ scope, fields, values = {}, onApply, onClear,
|
||||
choose(select, values[key]);
|
||||
return [{ key, control: select, cleared: item.options[0][0], optionsFor: item.optionsFor }];
|
||||
});
|
||||
// Each select's wanted value: the one it opened with until the person picks another (see setOptions).
|
||||
const wanted = new Map(controls.map(({ key }) => [key, values[key]]));
|
||||
for (const { key, control } of controls) {
|
||||
if (key !== 'tags') control.addEventListener('change', () => wanted.set(key, control.value));
|
||||
}
|
||||
const source = controls.find(item => item.key === 'source');
|
||||
const folder = controls.find(item => item.key === 'folder' && typeof item.optionsFor === 'function');
|
||||
const followSource = () => {
|
||||
const chosen = folder.control.value;
|
||||
const followSource = (chosen = folder.control.value) => {
|
||||
fillSelect(folder.control, 'folder', folder.optionsFor(source.control.value));
|
||||
choose(folder.control, chosen);
|
||||
};
|
||||
if (source && folder) {
|
||||
followSource();
|
||||
source.control.addEventListener('change', followSource);
|
||||
followSource(values.folder);
|
||||
// A Source that lacks the chosen folder clears it, and that cleared folder is the wanted one from then on.
|
||||
source.control.addEventListener('change', () => { followSource(); wanted.set('folder', folder.control.value); });
|
||||
}
|
||||
const read = () => Object.fromEntries(controls.map(({ key, control }) =>
|
||||
[key, key === 'tags' ? control.value.trim() : control.value]));
|
||||
@@ -207,6 +215,15 @@ export function openRefineDialog({ scope, fields, values = {}, onApply, onClear,
|
||||
...controls.map(({ key, control }) => field(FIELD_LABELS[key], control, `filter-${key}`)),
|
||||
actions,
|
||||
], { scope, trigger });
|
||||
const opened = session;
|
||||
function setOptions(key, options) {
|
||||
const item = controls.find(candidate => candidate.key === key);
|
||||
if (session !== opened || !item || key === 'tags') return;
|
||||
fillSelect(item.control, key, options);
|
||||
choose(item.control, wanted.get(key));
|
||||
if (source && folder && (item === source || item === folder)) followSource(wanted.get('folder'));
|
||||
}
|
||||
return { setOptions };
|
||||
}
|
||||
|
||||
/** One operation row; a press on its busy (aria-disabled) action is ignored and keeps the dialog open. */
|
||||
|
||||
Reference in New Issue
Block a user