Un-Grouping switch

This commit is contained in:
Johannes Zillmann 2021-03-01 23:42:02 +01:00
parent 37622577c9
commit c60bd3f737
6 changed files with 343 additions and 37 deletions

View File

@ -4,6 +4,7 @@ import Item from '../Item';
import Page, { asPages } from './Page';
import ChangeIndex from './ChangeIndex';
import ChangeTracker from './ChangeTracker';
import ItemGroup from './ItemGroup';
export default class StageResult {
constructor(
@ -20,6 +21,47 @@ export default class StageResult {
return items;
}, []);
}
selectPages(relevantChangesOnly: boolean, groupItems: boolean, pinnedPage?: number): Page[] {
let result: Page[];
// Ungroup pages
if (!groupItems && this.descriptor?.debug?.itemMerger) {
result = this.pagesWithUnpackedItems();
} else {
result = this.pages;
}
// Filter to pinned page
if (Number.isInteger(pinnedPage)) {
result = result.filter((page) => page.index === pinnedPage);
}
// Filter out item (groups) with no changes
if (relevantChangesOnly && !this.descriptor.debug?.showAll === true) {
result = result.map(
(page) =>
({
...page,
itemGroups: page.itemGroups.filter((itemGroup) => this.changes.hasChanged(itemGroup.top)),
} as Page),
);
}
return result;
}
pagesWithUnpackedItems(): Page[] {
return this.pages.map(
(page) =>
({
...page,
itemGroups: new Array<ItemGroup>().concat(
...page.itemGroups.map((itemGroup) => itemGroup.unpacked().map((item) => new ItemGroup(item))),
),
} as Page),
);
}
}
export function initialStage(inputSchema: string[], inputItems: Item[]): StageResult {

View File

@ -1,8 +1,4 @@
import ItemMerger from '../debug/ItemMerger';
import Item from '../Item';
import ItemGroup from '../debug/ItemGroup';
import Page from '../debug/Page';
import ChangeTracker from '../debug/ChangeTracker';
type KeyExtractor = (item: Item) => any;
type PageItemTransformer = (page: number, items: Item[]) => Item[];

View File

@ -0,0 +1,207 @@
import StageResult from 'src/debug/StageResult';
import { toDescriptor } from 'src/TransformDescriptor';
import ChangeTracker from 'src/debug/ChangeTracker';
import AnnotatedColumn from 'src/debug/AnnotatedColumn';
import Page, { asPages } from 'src/debug/Page';
import { items } from '../testItems';
import LineItemMerger from 'src/debug/LineItemMerger';
import ItemGroup from 'src/debug/ItemGroup';
test('itemsUnpacked', async () => {
const tracker = new ChangeTracker();
const itemMerger = new LineItemMerger(false);
const descriptor = toDescriptor({ debug: { itemMerger } });
const schema: AnnotatedColumn[] = [{ name: 'A' }];
const flatItems = [
...items(0, [
{ idx: 0, line: 1 },
{ idx: 1, line: 1 },
{ idx: 2, line: 2 },
]),
...items(1, [{ idx: 3, line: 1 }]),
...items(2, [
{ idx: 4, line: 1 },
{ idx: 5, line: 1 },
]),
];
const pages = asPages(tracker, flatItems, itemMerger);
const result = new StageResult(descriptor, schema, pages, tracker, []);
expect(result.itemsUnpacked().map((item) => item.data['idx'])).toEqual([0, 1, 2, 3, 4, 5]);
});
describe('select pages', () => {
function groupElements(page: Page, elementName: string) {
return page.itemGroups.map((group) => group.unpacked().map((item) => item.data['idx']));
}
test('Changes on group level', async () => {
const tracker = new ChangeTracker();
const itemMerger = new LineItemMerger(true);
const descriptor = toDescriptor({ debug: { itemMerger } });
const schema: AnnotatedColumn[] = [{ name: 'A' }];
const flatItems = [
...items(0, [
{ idx: 0, line: 1 },
{ idx: 1, line: 1 },
{ idx: 2, line: 2 },
]),
...items(1, [{ idx: 3, line: 1 }]),
...items(2, [
{ idx: 4, line: 1 },
{ idx: 5, line: 1 },
]),
];
const pages = asPages(tracker, flatItems, itemMerger);
const result = new StageResult(descriptor, schema, pages, tracker, []);
const allGrouped = result.selectPages(false, true);
expect(allGrouped.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(allGrouped[0], 'idx')).toEqual([[0, 1], [2]]);
expect(groupElements(allGrouped[1], 'idx')).toEqual([[3]]);
expect(groupElements(allGrouped[2], 'idx')).toEqual([[4, 5]]);
const relevantGrouped = result.selectPages(true, true);
expect(relevantGrouped.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(relevantGrouped[0], 'idx')).toEqual([[0, 1]]);
expect(groupElements(relevantGrouped[1], 'idx')).toEqual([]);
expect(groupElements(relevantGrouped[2], 'idx')).toEqual([[4, 5]]);
const relevantUnpacked = result.selectPages(true, false);
expect(relevantUnpacked.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(relevantUnpacked[0], 'idx')).toEqual([]);
expect(groupElements(relevantUnpacked[1], 'idx')).toEqual([]);
expect(groupElements(relevantUnpacked[2], 'idx')).toEqual([]);
const allUnpacked = result.selectPages(false, false);
expect(allUnpacked.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5]]);
const allGroupedWithPin = result.selectPages(false, true, 0);
expect(allGroupedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(allGroupedWithPin[0], 'idx')).toEqual([[0, 1], [2]]);
const relevantGroupedWithPin = result.selectPages(true, true, 0);
expect(relevantGroupedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(relevantGroupedWithPin[0], 'idx')).toEqual([[0, 1]]);
const relevantUnpackedWithPin = result.selectPages(true, false, 0);
expect(relevantUnpackedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(relevantUnpackedWithPin[0], 'idx')).toEqual([]);
const allUnpackedWithPin = result.selectPages(false, false, 0);
expect(allUnpackedWithPin.map((page) => page.index)).toEqual([0]);
expect(groupElements(allUnpackedWithPin[0], 'idx')).toEqual([[0], [1], [2]]);
});
test('Changes on element level', async () => {
const tracker = new ChangeTracker();
const itemMerger = new LineItemMerger(false);
const descriptor = toDescriptor({ debug: { itemMerger } });
const schema: AnnotatedColumn[] = [{ name: 'A' }];
const flatItems = [
...items(0, [
{ idx: 0, line: 1 },
{ idx: 1, line: 1 },
{ idx: 2, line: 2 },
]),
...items(1, [{ idx: 3, line: 1 }]),
...items(2, [
{ idx: 4, line: 1 },
{ idx: 5, line: 1 },
{ idx: 6, line: 2 },
]),
];
tracker.trackAddition(flatItems[3]);
tracker.trackAddition(flatItems[5]);
const pages = asPages(tracker, flatItems, itemMerger);
const result = new StageResult(descriptor, schema, pages, tracker, []);
const allGrouped = result.selectPages(false, true);
expect(allGrouped.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(allGrouped[0], 'idx')).toEqual([[0, 1], [2]]);
expect(groupElements(allGrouped[1], 'idx')).toEqual([[3]]);
expect(groupElements(allGrouped[2], 'idx')).toEqual([[4, 5], [6]]);
const relevantGrouped = result.selectPages(true, true);
expect(relevantGrouped.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(relevantGrouped[0], 'idx')).toEqual([]);
expect(groupElements(relevantGrouped[1], 'idx')).toEqual([[3]]);
expect(groupElements(relevantGrouped[2], 'idx')).toEqual([[4, 5]]);
const relevantUnpacked = result.selectPages(true, false);
expect(relevantUnpacked.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(relevantUnpacked[0], 'idx')).toEqual([]);
expect(groupElements(relevantUnpacked[1], 'idx')).toEqual([[3]]);
expect(groupElements(relevantUnpacked[2], 'idx')).toEqual([[5]]);
const allUnpacked = result.selectPages(false, false);
expect(allUnpacked.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(allUnpacked[0], 'idx')).toEqual([[0], [1], [2]]);
expect(groupElements(allUnpacked[1], 'idx')).toEqual([[3]]);
expect(groupElements(allUnpacked[2], 'idx')).toEqual([[4], [5], [6]]);
const allGroupedWithPin = result.selectPages(false, true, 2);
expect(allGroupedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(allGroupedWithPin[0], 'idx')).toEqual([[4, 5], [6]]);
const relevantGroupedWithPin = result.selectPages(true, true, 2);
expect(relevantGroupedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(relevantGroupedWithPin[0], 'idx')).toEqual([[4, 5]]);
const relevantUnpackedWithPin = result.selectPages(true, false, 2);
expect(relevantUnpackedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(relevantUnpackedWithPin[0], 'idx')).toEqual([[5]]);
const allUnpackedWithPin = result.selectPages(false, false, 2);
expect(allUnpackedWithPin.map((page) => page.index)).toEqual([2]);
expect(groupElements(allUnpackedWithPin[0], 'idx')).toEqual([[4], [5], [6]]);
});
test('showAll - grouped', async () => {
const tracker = new ChangeTracker();
const itemMerger = new LineItemMerger(false);
const descriptor = toDescriptor({ debug: { itemMerger, showAll: true } });
const schema: AnnotatedColumn[] = [{ name: 'A' }];
const flatItems = [
...items(0, [
{ idx: 0, line: 1 },
{ idx: 1, line: 1 },
{ idx: 2, line: 2 },
]),
...items(1, [{ idx: 3, line: 1 }]),
...items(2, [
{ idx: 4, line: 1 },
{ idx: 5, line: 1 },
]),
];
const pages = asPages(tracker, flatItems, itemMerger);
const result = new StageResult(descriptor, schema, pages, tracker, []);
const relevantGrouped = result.selectPages(true, true);
expect(relevantGrouped.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(relevantGrouped[0], 'idx')).toEqual([[0, 1], [2]]);
expect(groupElements(relevantGrouped[1], 'idx')).toEqual([[3]]);
expect(groupElements(relevantGrouped[2], 'idx')).toEqual([[4, 5]]);
});
test('showAll - grouped', async () => {
const tracker = new ChangeTracker();
const descriptor = toDescriptor({ debug: { showAll: true } });
const schema: AnnotatedColumn[] = [{ name: 'A' }];
const flatItems = [
...items(0, [{ idx: 0 }, { idx: 1 }, { idx: 2 }]),
...items(1, [{ idx: 3 }]),
...items(2, [{ idx: 4 }, { idx: 5 }]),
];
const pages = asPages(tracker, flatItems);
const result = new StageResult(descriptor, schema, pages, tracker, []);
const relevantGrouped = result.selectPages(true, true);
expect(relevantGrouped.map((page) => page.index)).toEqual([0, 1, 2]);
expect(groupElements(relevantGrouped[0], 'idx')).toEqual([[0], [1], [2]]);
expect(groupElements(relevantGrouped[1], 'idx')).toEqual([[3]]);
expect(groupElements(relevantGrouped[2], 'idx')).toEqual([[4], [5]]);
});
});

View File

@ -1,11 +1,13 @@
<script>
import { slide } from 'svelte/transition';
import { linear } from 'svelte/easing';
import Icon from 'fa-svelte';
import { faMapPin as pin } from '@fortawesome/free-solid-svg-icons/faMapPin';
import { BookOpen, ArrowLeft, ArrowRight } from 'svelte-hero-icons';
import type Debugger from '@core/Debugger';
import slideH from '../svelte/slideH';
import Popup from '../components/Popup.svelte';
import PageSelectionPopup from './PageSelectionPopup.svelte';
import Checkbox from '../components/Checkbox.svelte';
@ -18,14 +20,17 @@
const stageNames = debug.stageNames;
let pinnedPage: number;
let onlyRelevantItems = true;
let groupingEnabled = true;
$: canNext = $debugStage + 1 < stageNames.length;
$: canPrev = $debugStage > 0;
$: stageResult = debug.stageResults($debugStage);
$: supportsGrouping = !!stageResult.descriptor?.debug?.itemMerger;
$: supportsRelevanceFiltering = !stageResult.descriptor?.debug?.showAll;
$: pageIsPinned = !isNaN(pinnedPage);
$: pagesNumbers = new Set(stageResult.pages.map((page) => page.index));
$: maxPage = Math.max(...pagesNumbers);
$: visiblePages = pageIsPinned ? stageResult.pages.filter((page) => page.index === pinnedPage) : stageResult.pages;
$: visiblePages = stageResult.selectPages(onlyRelevantItems, groupingEnabled, pinnedPage);
</script>
<div class="mx-4">
@ -37,7 +42,7 @@
<div class="controls py-2">
<div class="flex items-center space-x-2">
{#if pageIsPinned}
<span on:click={() => (pinnedPage = undefined)} transition:slide>
<span on:click={() => (pinnedPage = undefined)} transition:slideH={{ duration: 180, easing: linear }}>
<Icon class="text-xs hover:text-green-700 hover:opacity-25 cursor-pointer opacity-75" icon={pin} />
</span>
{/if}
@ -82,7 +87,16 @@
</Popup>
</span>
<div class="w-full flex flex-row-reverse space-x-2 space-x-reverse text-sm">
{#if supportsGrouping}
<span class="inline-flex" transition:slideH={{ duration: 700 }}>
<Checkbox name="Grouped" bind:enabled={groupingEnabled} />
</span>
{/if}
{#if supportsRelevanceFiltering}
<span class="inline-flex" transition:slideH={{ duration: 700 }}>
<Checkbox name="Relevant Items" bind:enabled={onlyRelevantItems} />
</span>
{/if}
</div>
</div>
</div>
@ -97,15 +111,33 @@
</ul>
<!-- Items -->
{#if visiblePages.find((page) => page.itemGroups.length > 0)}
<ItemTable
fontMap={debug.fontMap}
schema={stageResult.schema}
pages={visiblePages}
{maxPage}
{pageIsPinned}
showAllAsRelevant={stageResult.descriptor?.debug?.showAll}
bind:onlyRelevantItems
changes={stageResult.changes} />
{:else}
<div class="flex space-x-1 items-center justify-center text-xl">
<div>No visible changes from the transformation.</div>
{#if supportsRelevanceFiltering && onlyRelevantItems}
<div>Disabled the</div>
<div class="font-bold cursor-pointer hover:underline" on:click={() => (onlyRelevantItems = false)}>
relevance filter
</div>
<div>?</div>
{/if}
{#if supportsGrouping && !groupingEnabled}
<div>Enable</div>
<div class="font-bold cursor-pointer hover:underline" on:click={() => (groupingEnabled = true)}>
grouping
</div>
<div>?</div>
{/if}
</div>
{/if}
</div>
<style>

View File

@ -16,8 +16,6 @@
export let pages: Page[];
export let maxPage: number;
export let pageIsPinned: boolean;
export let onlyRelevantItems: boolean;
export let showAllAsRelevant = false;
export let changes: ChangeIndex;
let maxItemsToRenderInOneLoad = 200;
let renderedMaxPage = 0;
@ -79,7 +77,7 @@
{/if}
<!-- Page items -->
{#each page.itemGroups.filter((group) => showAllAsRelevant || !onlyRelevantItems || changes.hasChanged(group.top)) as itemGroup, itemIdx}
{#each page.itemGroups as itemGroup, itemIdx}
<tr
class:expandable={itemGroup.hasMany()}
class:expanded={expandedItemGroup && isExpanded(page.index, itemIdx)}
@ -171,24 +169,13 @@
</tbody>
</table>
{#if onlyRelevantItems && changes.changeCount() === 0}
<div class="flex space-x-1 items-center justify-center text-xl">
<div>No changes from the transformation.</div>
<div>Want to see</div>
<div class="font-bold cursor-pointer hover:underline" on:click={() => (onlyRelevantItems = false)}>
all items
</div>
<div>?</div>
</div>
{:else}
{#if !pageIsPinned}
{#if !pageIsPinned}
{#if renderedMaxPage < pages.length}
<span use:inView on:intersect={({ detail }) => detail && calculateNextPageToRenderTo()} />
<div class="my-6 text-center text-2xl">...</div>
{:else}
<div class="my-6 text-center text-2xl">FIN!</div>
{/if}
{/if}
{/if}
<style>
@ -249,6 +236,6 @@
margin-right: 15px;
right: 100%;
with: 200px;
z-index: 4;
z-index: 1;
}
</style>

42
ui/src/svelte/slideH.ts Normal file
View File

@ -0,0 +1,42 @@
import { backInOut as easing } from 'svelte/easing';
const defaultOptions = {
delay: 0,
duration: 400,
easing,
};
/**
* Slide the element horizontally, across the X-axis.
* @param node
* @param options
*/
export default function slideH(node: HTMLElement, options: object) {
const mergedOptions = { ...defaultOptions, ...options };
const style = getComputedStyle(node);
const opacity = +style.opacity;
const width = parseFloat(style.width);
const padding_left = parseFloat(style.paddingLeft);
const padding_right = parseFloat(style.paddingRight);
const margin_left = parseFloat(style.marginLeft);
const margin_right = parseFloat(style.marginRight);
const border_left_width = parseFloat(style.borderLeftWidth);
const border_right_width = parseFloat(style.borderRightWidth);
return {
delay: mergedOptions.delay,
duration: mergedOptions.duration,
easing: mergedOptions.easing,
css: (t: number) =>
`overflow: hidden;` +
`opacity: ${Math.min(t * 20, 1) * opacity};` +
`width: ${t * width}px;` +
`padding-left: ${t * padding_left}px;` +
`padding-right: ${t * padding_right}px;` +
`margin-left: ${t * margin_left}px;` +
`margin-right: ${t * margin_right}px;` +
`border-left-width: ${t * border_left_width}px;` +
`border-right-width: ${t * border_right_width}px;`,
};
}