- Move datsets from the "Unselected" column to the "Selected" column below to compose the list
- in the intended order and with the intended datasets.
+ Move datasets from the "Unselected" column to the "Selected" column below to compose the
+ list in the intended order and with the intended datasets.
The filter textbox can be used to rapidly find the datasets of interest by name.
diff --git a/client/src/components/Collections/ListDatasetCollectionElementView.vue b/client/src/components/Collections/ListDatasetCollectionElementView.vue
index 537b3e40f6cb..47dc9327f7f4 100644
--- a/client/src/components/Collections/ListDatasetCollectionElementView.vue
+++ b/client/src/components/Collections/ListDatasetCollectionElementView.vue
@@ -32,6 +32,13 @@ watch(elementName, () => {
function clickDiscard() {
emit("element-is-discarded", props.element);
}
+
+watch(
+ () => props.element.name,
+ () => {
+ elementName.value = props.element.name || "...";
+ }
+);
diff --git a/client/src/components/Collections/ListWizard.vue b/client/src/components/Collections/ListWizard.vue
new file mode 100644
index 000000000000..2f493c06ee22
--- /dev/null
+++ b/client/src/components/Collections/ListWizard.vue
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+ {{ creationError }}
+
+
+
+ Collection created and it has been added to your history.
+
+ This options creates a simple flat list of files. If your data isn't nested and does not contained
+ paired datasets, this is the the option to choose.
+
+
+
+
+ List of Paired Datasets
+
+
+ This options creates a uniform list of paired datasets, use this option if all of your data should
+ be paired off.
+
+
+
+
+ Rule Builder
+
+
+ The rule builder allows building arbitrarily nested collections of data with reproducible metadata
+ parsing and other advanced features.
+
+
+
+
+
+ Advanced Options
+
+
+
+
+ Mixed List of Paired and Unpaired Datasets
+
+
+ For studies that have a mix of paired and unpaired data, this may be the right choice. Existing
+ tools and workflows may have to be updated to allow this modality though.
+
+
+
+
+ Nested List of Datasets
+
+
+ A nested list of datasets. This type of collection is a list where each element is in turn a
+ list of datasets. The outer list identifier might group samples together by condition, sample
+ type, etc..
+
+
+
+
+ Nested List of Dataset Pairs
+
+
+ A nested list of datasets. This type of collection is a list where each element is in turn a
+ list of pairs of datasets.
+
+
+
+
+
+
diff --git a/client/src/components/Collections/ListWizard/types.ts b/client/src/components/Collections/ListWizard/types.ts
new file mode 100644
index 000000000000..429f92f6c40a
--- /dev/null
+++ b/client/src/components/Collections/ListWizard/types.ts
@@ -0,0 +1,7 @@
+export type WhichListBuilder =
+ | "list"
+ | "list:paired"
+ | "rules"
+ | "list:paired_or_unpaired"
+ | "list:list"
+ | "list:list:paired";
diff --git a/client/src/components/Collections/PairCollectionCreator.vue b/client/src/components/Collections/PairCollectionCreator.vue
index c00c2c0f9b1e..8933d0340552 100644
--- a/client/src/components/Collections/PairCollectionCreator.vue
+++ b/client/src/components/Collections/PairCollectionCreator.vue
@@ -5,12 +5,12 @@ import { BAlert, BButton } from "bootstrap-vue";
import { computed, ref, watch } from "vue";
import type { HDASummary, HistoryItemSummary } from "@/api";
+import { type CollectionElementIdentifiers, type CreateNewCollectionPayload } from "@/api/datasetCollections";
import { Toast } from "@/composables/toast";
-import STATES from "@/mvc/dataset/states";
-import { useDatatypesMapperStore } from "@/stores/datatypesMapperStore";
import localize from "@/utils/localization";
-import type { DatasetPair } from "../History/adapters/buildCollectionModal";
+import { type Mode, useCollectionCreator } from "./common/useCollectionCreator";
+import { guessNameForPair } from "./pairing";
import DatasetCollectionElementView from "./ListDatasetCollectionElementView.vue";
import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue";
@@ -28,17 +28,17 @@ interface Props {
defaultHideSourceItems?: boolean;
fromSelection?: boolean;
extensions?: string[];
+ mode: Mode;
}
const props = defineProps();
const emit = defineEmits<{
- (event: "clicked-create", selectedPair: DatasetPair, collectionName: string, hideSourceItems: boolean): void;
+ (event: "on-create", options: CreateNewCollectionPayload): void;
(event: "on-cancel"): void;
}>();
const state = ref("build");
-const removeExtensions = ref(true);
const initialSuggestedName = ref("");
const invalidElements = ref([]);
const workingElements = ref([]);
@@ -55,7 +55,6 @@ const noElementsSelected = computed(() => {
const exactlyTwoValidElements = computed(() => {
return pairElements.value.forward && pairElements.value.reverse;
});
-const hideSourceItems = ref(props.defaultHideSourceItems || false);
const pairElements = computed(() => {
if (props.fromSelection) {
return {
@@ -67,12 +66,15 @@ const pairElements = computed(() => {
}
});
-// variables for datatype mapping and then filtering
-const datatypesMapperStore = useDatatypesMapperStore();
-const datatypesMapper = computed(() => datatypesMapperStore.datatypesMapper);
-
-/** Are we filtering by datatype? */
-const filterExtensions = computed(() => !!datatypesMapper.value && !!props.extensions?.length);
+const {
+ removeExtensions,
+ hideSourceItems,
+ onUpdateHideSourceItems,
+ isElementInvalid,
+ onCollectionCreate,
+ showButtonsForModal,
+ onUpdateCollectionName,
+} = useCollectionCreator(props);
watch(
() => props.initialElements,
@@ -118,7 +120,7 @@ function _elementsSetUp() {
const element = workingElements.value.find(
(e) => e.id === inListElementsPrev[key as keyof SelectedDatasetPair]?.id
);
- const problem = _isElementInvalid(prevElem);
+ const problem = isElementInvalid(prevElem);
if (element) {
inListElements.value[key as keyof SelectedDatasetPair] = element;
} else if (problem) {
@@ -153,7 +155,7 @@ function _ensureElementIds() {
// /** separate working list into valid and invalid elements for this collection */
function _validateElements() {
workingElements.value = workingElements.value.filter((element) => {
- var problem = _isElementInvalid(element);
+ var problem = isElementInvalid(element);
if (problem) {
invalidElements.value.push(element.name + " " + problem);
@@ -165,33 +167,6 @@ function _validateElements() {
return workingElements.value;
}
-/** describe what is wrong with a particular element if anything */
-function _isElementInvalid(element: HistoryItemSummary) {
- if (element.history_content_type === "dataset_collection") {
- return localize("is a collection, this is not allowed");
- }
-
- var validState = element.state === STATES.OK || STATES.NOT_READY_STATES.includes(element.state as string);
-
- if (!validState) {
- return localize("has errored, is paused, or is not accessible");
- }
-
- if (element.deleted || element.purged) {
- return localize("has been deleted or purged");
- }
-
- // is the element's extension not a subtype of any of the required extensions?
- if (
- filterExtensions.value &&
- element.extension &&
- !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!)
- ) {
- return localize(`has an invalid extension: ${element.extension}`);
- }
- return null;
-}
-
function getPairElement(key: string) {
return pairElements.value[key as "forward" | "reverse"];
}
@@ -232,7 +207,7 @@ function addUploadedFiles(files: HDASummary[]) {
// Check for validity of uploads
files.forEach((file) => {
- const problem = _isElementInvalid(file);
+ const problem = isElementInvalid(file);
if (problem) {
const invalidMsg = `${file.hid}: ${file.name} ${problem} and ${NOT_VALID_ELEMENT_MSG}`;
invalidElements.value.push(invalidMsg);
@@ -241,14 +216,15 @@ function addUploadedFiles(files: HDASummary[]) {
});
}
-function clickedCreate(collectionName: string) {
+function attemptCreate() {
if (state.value !== "error" && exactlyTwoValidElements.value) {
- const returnedPair = {
- forward: pairElements.value.forward as HDASummary,
- reverse: pairElements.value.reverse as HDASummary,
- name: collectionName,
- };
- emit("clicked-create", returnedPair, collectionName, hideSourceItems.value);
+ const forward = pairElements.value.forward as HDASummary;
+ const reverse = pairElements.value.reverse as HDASummary;
+ const returnedElems = [
+ { name: "forward", src: "src" in forward ? forward.src : "hda", id: forward.id },
+ { name: "reverse", src: "src" in reverse ? reverse.src : "hda", id: reverse.id },
+ ] as CollectionElementIdentifiers;
+ onCollectionCreate("paired", returnedElems);
}
}
@@ -264,76 +240,7 @@ function removeExtensionsToggle() {
function _guessNameForPair(fwd: HDASummary, rev: HDASummary, removeExtensions: boolean) {
removeExtensions = removeExtensions ? removeExtensions : removeExtensions;
-
- var fwdName = fwd.name ?? "";
- var revName = rev.name ?? "";
- var lcs = _naiveStartingAndEndingLCS(fwdName, revName);
-
- /** remove url prefix if files were uploaded by url */
- var lastDotIndex = lcs.lastIndexOf(".");
- var lastSlashIndex = lcs.lastIndexOf("/");
- var extension = lcs.slice(lastDotIndex, lcs.length);
-
- if (lastSlashIndex > 0) {
- var urlprefix = lcs.slice(0, lastSlashIndex + 1);
-
- lcs = lcs.replace(urlprefix, "");
- fwdName = fwdName.replace(extension, "");
- revName = revName.replace(extension, "");
- }
-
- if (removeExtensions) {
- if (lastDotIndex > 0) {
- lcs = lcs.replace(extension, "");
- fwdName = fwdName.replace(extension, "");
- revName = revName.replace(extension, "");
- }
- }
-
- return lcs || `${fwdName} & ${revName}`;
-}
-
-function onUpdateHideSourceItems(newHideSourceItems: boolean) {
- hideSourceItems.value = newHideSourceItems;
-}
-
-function _naiveStartingAndEndingLCS(s1: string, s2: string) {
- var i = 0;
- var j = 0;
- var fwdLCS = "";
- var revLCS = "";
-
- while (i < s1.length && i < s2.length) {
- if (s1[i] !== s2[i]) {
- break;
- }
-
- fwdLCS += s1[i];
- i += 1;
- }
-
- if (i === s1.length) {
- return s1;
- }
-
- if (i === s2.length) {
- return s2;
- }
-
- i = s1.length - 1;
- j = s2.length - 1;
-
- while (i >= 0 && j >= 0) {
- if (s1[i] !== s2[j]) {
- break;
- }
-
- revLCS = [s1[i], revLCS].join("");
- i -= 1;
- j -= 1;
- }
-
- return fwdLCS + revLCS;
+ return guessNameForPair(fwd, rev, "", "", removeExtensions);
}
@@ -377,9 +284,11 @@ function _naiveStartingAndEndingLCS(s1: string, s2: string) {
collection-type="paired"
:no-items="props.initialElements.length == 0 && !props.fromSelection"
:show-upload="!fromSelection"
+ :show-buttons="showButtonsForModal"
+ @on-update-collection-name="onUpdateCollectionName"
@add-uploaded-files="addUploadedFiles"
@onUpdateHideSourceItems="onUpdateHideSourceItems"
- @clicked-create="clickedCreate"
+ @clicked-create="attemptCreate"
@remove-extensions-toggle="removeExtensionsToggle">
diff --git a/client/src/components/Collections/PairedListCollectionCreator.test.js b/client/src/components/Collections/PairedListCollectionCreator.test.js
deleted file mode 100644
index d62270b04335..000000000000
--- a/client/src/components/Collections/PairedListCollectionCreator.test.js
+++ /dev/null
@@ -1,147 +0,0 @@
-import { createTestingPinia } from "@pinia/testing";
-import DATA from "@tests/test-data/paired-collection-creator.data.js";
-import { mount, shallowMount } from "@vue/test-utils";
-import PairedListCollectionCreator from "components/Collections/PairedListCollectionCreator";
-import flushPromises from "flush-promises";
-import Vue from "vue";
-
-import { useServerMock } from "@/api/client/__mocks__";
-
-// Mock the localize directive
-// (otherwise we get: [Vue warn]: Failed to resolve directive: localize)
-Vue.directive("localize", {
- bind(el, binding) {
- el.textContent = binding.value;
- },
-});
-
-const { server, http } = useServerMock();
-
-describe("PairedListCollectionCreator", () => {
- let wrapper;
- const pinia = createTestingPinia();
-
- beforeEach(() => {
- server.use(
- http.get("/api/configuration", ({ response }) => {
- return response(200).json({
- chunk_upload_size: 100,
- file_sources_configured: true,
- });
- })
- );
- });
-
- it("performs an autopair on startup if we have a selection", async () => {
- // Kind of deprecated because we are never using `props.fromSelection: true` anywhere
-
- wrapper = shallowMount(PairedListCollectionCreator, {
- propsData: {
- historyId: "history_id",
- initialElements: DATA._1,
- fromSelection: true,
- },
- pinia,
- });
-
- await flushPromises();
- // Autopair is called on startup
- const pairsCountDisplay = wrapper.find('[data-description="number of pairs"]');
- expect(pairsCountDisplay.text()).toContain(`${DATA._1.length / 2} pairs`);
- });
-
- it("selects the correct name for an autopair", async () => {
- wrapper = mount(PairedListCollectionCreator, {
- propsData: {
- historyId: "history_id",
- initialElements: DATA._2,
- },
- pinia,
- stubs: {
- FontAwesomeIcon: true,
- BPopover: true,
- },
- });
-
- await flushPromises();
- //change filter to .1.fastq/.2.fastq
- await wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click");
- await wrapper
- .findAll("div.dropdown-menu > a.dropdown-item")
- .wrappers.find((e) => e.text() == ".1.fastq")
- .trigger("click");
- //assert forward filter
- const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value;
- expect(forwardFilter).toBe(".1.fastq");
- //assert reverse filter
- const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value;
- expect(reverseFilter).toBe(".2.fastq");
- // click Autopair
- await wrapper.find(".autopair-link").trigger("click");
- //assert pair-name longer name
- const pairname = wrapper.find("span.pair-name");
- expect(pairname.text()).toBe("DP134_1_FS_PSII_FSB_42C_A10");
- });
-
- it("removes the period from autopair name", async () => {
- wrapper = mount(PairedListCollectionCreator, {
- propsData: {
- historyId: "history_id",
- initialElements: DATA._3,
- },
- pinia,
- stubs: {
- FontAwesomeIcon: true,
- BPopover: true,
- },
- });
-
- await flushPromises();
- //change filter to .1.fastq/.2.fastq
- await wrapper.find("div.forward-unpaired-filter > div.input-group-append > button").trigger("click");
- await wrapper
- .findAll("div.dropdown-menu > a.dropdown-item")
- .wrappers.find((e) => e.text() == ".1.fastq")
- .trigger("click");
- //assert forward filter
- const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value;
- expect(forwardFilter).toBe(".1.fastq");
- //assert reverse filter
- const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value;
- expect(reverseFilter).toBe(".2.fastq");
- // click Autopair
- await wrapper.find(".autopair-link").trigger("click");
- //assert pair-name longer name
- const pairname = wrapper.find("span.pair-name");
- expect(pairname.text()).toBe("UII_moo_1");
- });
-
- it("autopairs correctly when filters are typed in", async () => {
- wrapper = mount(PairedListCollectionCreator, {
- propsData: {
- historyId: "history_id",
- initialElements: DATA._4,
- },
- pinia,
- stubs: {
- FontAwesomeIcon: true,
- BPopover: true,
- },
- });
-
- await flushPromises();
- //change filter to _R1/_R2
- await wrapper.find("div.forward-unpaired-filter > input").setValue("_R1");
- await wrapper.find("div.reverse-unpaired-filter > input").setValue("_R2");
- //assert forward filter
- const forwardFilter = wrapper.find("div.forward-unpaired-filter > input").element.value;
- expect(forwardFilter).toBe("_R1");
- //assert reverse filter
- const reverseFilter = wrapper.find("div.reverse-unpaired-filter > input").element.value;
- expect(reverseFilter).toBe("_R2");
- // click Autopair
- await wrapper.find(".autopair-link").trigger("click");
- //assert all pairs matched
- expect(wrapper.findAll("li.dataset unpaired").length == 0).toBeTruthy();
- });
-});
diff --git a/client/src/components/Collections/PairedListCollectionCreator.vue b/client/src/components/Collections/PairedListCollectionCreator.vue
index 1f831e66a79c..7974dd0ffe20 100644
--- a/client/src/components/Collections/PairedListCollectionCreator.vue
+++ b/client/src/components/Collections/PairedListCollectionCreator.vue
@@ -16,12 +16,24 @@ import type { HDASummary, HistoryItemSummary } from "@/api";
import { useConfirmDialog } from "@/composables/confirmDialog";
import { Toast } from "@/composables/toast";
import STATES from "@/mvc/dataset/states";
-import { useDatatypesMapperStore } from "@/stores/datatypesMapperStore";
-// import levenshteinDistance from '@/utils/levenshtein';
import localize from "@/utils/localization";
import { naturalSort } from "@/utils/naturalSort";
import type { DatasetPair } from "../History/adapters/buildCollectionModal";
+import { useExtensionFiltering } from "./common/useExtensionFilter";
+import {
+ COMMON_FILTERS,
+ type CommonFiltersType,
+ createPair,
+ DEFAULT_FILTER,
+ guessInitialFilterType,
+ guessNameForPair,
+ type MatchingFunction,
+ matchOnlyIfExact,
+ matchOnPercentOfStartingAndEndingLCS,
+ splitElementsByFilter,
+ statelessAutoPairFnBuilder,
+} from "./pairing";
import Heading from "../Common/Heading.vue";
import PairedElementView from "./PairedElementView.vue";
@@ -29,12 +41,6 @@ import UnpairedDatasetElementView from "./UnpairedDatasetElementView.vue";
import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue";
const NOT_VALID_ELEMENT_MSG: string = localize("is not a valid element for this collection");
-const COMMON_FILTERS = {
- illumina: ["_1", "_2"],
- Rs: ["_R1", "_R2"],
- dot12s: [".1.fastq", ".2.fastq"],
-};
-const DEFAULT_FILTER: keyof typeof COMMON_FILTERS = "illumina";
const MATCH_PERCENTAGE = 0.99;
// Titles and help text
@@ -57,7 +63,7 @@ interface Props {
const props = defineProps();
const emit = defineEmits<{
- (e: "clicked-create", workingElements: DatasetPair[], collectionName: string, hideSourceItems: boolean): void;
+ (e: "on-create", workingElements: DatasetPair[], collectionName: string, hideSourceItems: boolean): void;
(e: "on-cancel"): void;
}>();
@@ -183,12 +189,7 @@ const noUnpairedElementsDisplayed = computed(() => {
return numOfUnpairedForwardElements.value + numOfUnpairedReverseElements.value === 0;
});
-// variables for datatype mapping and then filtering
-const datatypesMapperStore = useDatatypesMapperStore();
-const datatypesMapper = computed(() => datatypesMapperStore.datatypesMapper);
-
-/** Are we filtering by datatype? */
-const filterExtensions = computed(() => !!datatypesMapper.value && !!props.extensions?.length);
+const { hasInvalidExtension } = useExtensionFiltering(props);
function removeExtensionsToggle() {
removeExtensions.value = !removeExtensions.value;
@@ -234,45 +235,15 @@ function _elementsSetUp() {
}
function initialFiltersSet() {
- let illumina = 0;
- let dot12s = 0;
- let Rs = 0;
- //should we limit the forEach? What if there are 1000s of elements?
- props.initialElements.forEach((element) => {
- if (element.name?.includes(".1.fastq") || element.name?.includes(".2.fastq")) {
- dot12s++;
- } else if (element.name?.includes("_R1") || element.name?.includes("_R2")) {
- Rs++;
- } else if (element.name?.includes("_1") || element.name?.includes("_2")) {
- illumina++;
- }
- });
- // if we cannot filter don't set an initial filter and hide all the data
- if (illumina == 0 && dot12s == 0 && Rs == 0) {
+ const filterType = guessInitialFilterType(props.initialElements);
+ if (filterType == null) {
forwardFilter.value = "";
reverseFilter.value = "";
- } else if (illumina > dot12s && illumina > Rs) {
- changeFilters("illumina");
- } else if (dot12s > illumina && dot12s > Rs) {
- changeFilters("dot12s");
- } else if (Rs > illumina && Rs > dot12s) {
- changeFilters("Rs");
} else {
- changeFilters("illumina");
+ changeFilters(filterType);
}
}
-// TODO: Don't know if this is really necessary
-// function _ensureElementIds() {
-// workingElements.value.forEach((element) => {
-// if (!Object.prototype.hasOwnProperty.call(element, "id")) {
-// console.warn("Element missing id", element);
-// }
-// });
-
-// return workingElements.value;
-// }
-
function _validateElements() {
workingElements.value = workingElements.value.filter((element) => {
var problem = _isElementInvalid(element);
@@ -299,11 +270,7 @@ function _isElementInvalid(element: HistoryItemSummary) {
}
// is the element's extension not a subtype of any of the required extensions?
- if (
- filterExtensions.value &&
- element.extension &&
- !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!)
- ) {
+ if (hasInvalidExtension(element)) {
return localize(`has an invalid extension: ${element.extension}`);
}
return null;
@@ -378,45 +345,13 @@ function removePairFromUnpaired(fwd: HDASummary, rev: HDASummary) {
* (will guess if not given)
*/
function _createPair(fwd: HDASummary, rev: HDASummary, name?: string) {
- // ensure existance and don't pair something with itself
- if (!(fwd && rev) || fwd === rev) {
- throw new Error(`Bad pairing: ${[JSON.stringify(fwd), JSON.stringify(rev)]}`);
- }
name = name || _guessNameForPair(fwd, rev);
- return { forward: fwd, name: name, reverse: rev };
+ return createPair(fwd, rev, name);
}
-/** Try to find a good pair name for the given fwd and rev datasets */
function _guessNameForPair(fwd: HDASummary, rev: HDASummary, willRemoveExtensions?: boolean) {
willRemoveExtensions = willRemoveExtensions ? willRemoveExtensions : removeExtensions.value;
- let fwdName = fwd.name;
- let revName = rev.name;
- const fwdNameFilter = fwdName?.replace(new RegExp(forwardFilter.value || ""), "");
- const revNameFilter = revName?.replace(new RegExp(reverseFilter.value || ""), "");
- if (!fwdNameFilter || !revNameFilter || !fwdName || !revName) {
- return `${fwdName} & ${revName}`;
- }
- let lcs = _naiveStartingAndEndingLCS(fwdNameFilter, revNameFilter);
- // remove url prefix if files were uploaded by url
- const lastSlashIndex = lcs.lastIndexOf("/");
- if (lastSlashIndex > 0) {
- const urlprefix = lcs.slice(0, lastSlashIndex + 1);
- lcs = lcs.replace(urlprefix, "");
- }
-
- if (willRemoveExtensions) {
- const lastDotIndex = lcs.lastIndexOf(".");
- if (lastDotIndex > 0) {
- const extension = lcs.slice(lastDotIndex, lcs.length);
- lcs = lcs.replace(extension, "");
- fwdName = fwdName.replace(extension, "");
- revName = revName.replace(extension, "");
- }
- }
- if (lcs.endsWith(".") || lcs.endsWith("_")) {
- lcs = lcs.substring(0, lcs.length - 1);
- }
- return lcs || `${fwdName} & ${revName}`;
+ return guessNameForPair(fwd, rev, forwardFilter.value, reverseFilter.value, willRemoveExtensions);
}
function clickAutopair() {
@@ -440,16 +375,7 @@ function clickClearFilters() {
}
function splitByFilter() {
- const filters = [new RegExp(forwardFilter.value), new RegExp(reverseFilter.value)];
- const split: [HDASummary[], HDASummary[]] = [[], []];
- workingElements.value.forEach((e) => {
- filters.forEach((filter, i) => {
- if (e.name && filter.test(e.name)) {
- split[i]?.push(e);
- }
- });
- });
- return split;
+ return splitElementsByFilter(workingElements.value, forwardFilter.value, reverseFilter.value);
}
// ===========================================================================
@@ -457,55 +383,15 @@ function splitByFilter() {
/** Autopair by exact match */
function autoPairSimple(params: { listA: HDASummary[]; listB: HDASummary[] }) {
return autoPairFnBuilder({
- match: (params) => {
- params = params || {};
- if (params.matchTo === params.possible) {
- return {
- index: params.index,
- score: 1.0,
- };
- }
- return params.bestMatch;
- },
+ match: matchOnlyIfExact, // will only issue 1.0 scores if exact
scoreThreshold: 0.6,
})(params);
}
-// TODO: Currently unused?
-/** Autopair by levenstein distance */
-// function autoPairLevenshtein(params: { listA: HDASummary[]; listB: HDASummary[] }) {
-// return autoPairFnBuilder({
-// match: (params) => {
-// params = params || {};
-// const distance = levenshteinDistance(params.matchTo, params.possible);
-// const score = 1.0 - distance / Math.max(params.matchTo.length, params.possible.length);
-// if (score > params.bestMatch.score) {
-// return {
-// index: params.index,
-// score: score,
-// };
-// }
-// return params.bestMatch;
-// },
-// scoreThreshold: MATCH_PERCENTAGE,
-// })(params);
-// }
-
/** Autopair by longest common substrings scoring */
function autoPairLCS(params: { listA: HDASummary[]; listB: HDASummary[] }) {
return autoPairFnBuilder({
- match: (params) => {
- params = params || {};
- const match = _naiveStartingAndEndingLCS(params.matchTo, params.possible).length;
- const score = match / Math.max(params.matchTo.length, params.possible.length);
- if (score > params.bestMatch.score) {
- return {
- index: params.index,
- score: score,
- };
- }
- return params.bestMatch;
- },
+ match: matchOnPercentOfStartingAndEndingLCS,
scoreThreshold: MATCH_PERCENTAGE,
})(params);
}
@@ -599,128 +485,25 @@ function unpairAll() {
// ===========================================================================
/** Returns an autopair function that uses the provided options.match function */
-function autoPairFnBuilder(options: {
- match: (params: {
- matchTo: string;
- possible: string;
- index: number;
- bestMatch: { score: number; index: number };
- }) => { score: number; index: number };
- createPair?: (params: {
- listA: HDASummary[];
- indexA: number;
- listB: HDASummary[];
- indexB: number;
- }) => DatasetPair | undefined;
- preprocessMatch?: (params: {
- matchTo: HDASummary;
- possible: HDASummary;
- index: number;
- bestMatch: { score: number; index: number };
- }) => { matchTo: string; possible: string; index: number; bestMatch: { score: number; index: number } };
- scoreThreshold?: number;
-}) {
- options = options || {};
- options.createPair =
- options.createPair ||
- function _defaultCreatePair(params) {
- params = params || {};
- const a = params.listA.splice(params.indexA, 1)[0];
- const b = params.listB.splice(params.indexB, 1)[0];
- if (!a || !b) {
- return undefined;
- }
- const aInBIndex = params.listB.indexOf(a);
- const bInAIndex = params.listA.indexOf(b);
- if (aInBIndex !== -1) {
- params.listB.splice(aInBIndex, 1);
- }
- if (bInAIndex !== -1) {
- params.listA.splice(bInAIndex, 1);
- }
- // return _pair(a, b, { silent: true });
- return _pair(a, b);
- };
- // compile these here outside of the loop
- let _regexps: RegExp[] = [];
- function getRegExps() {
- if (!_regexps.length) {
- _regexps = [new RegExp(forwardFilter.value), new RegExp(reverseFilter.value)];
- }
- return _regexps;
- }
- // mangle params as needed
- options.preprocessMatch =
- options.preprocessMatch ||
- function _defaultPreprocessMatch(params) {
- const regexps = getRegExps();
- return Object.assign(params, {
- matchTo: params.matchTo.name?.replace(regexps[0] || "", ""),
- possible: params.possible.name?.replace(regexps[1] || "", ""),
- index: params.index,
- bestMatch: params.bestMatch,
- });
- };
-
+function autoPairFnBuilder(options: { match: MatchingFunction; scoreThreshold?: number }) {
return function _strategy(params: { listA: HDASummary[]; listB: HDASummary[] }) {
- params = params || {};
- const listA = params.listA;
- const listB = params.listB;
- let indexA = 0;
- let indexB;
-
- let bestMatch = {
- score: 0.0,
- index: -1,
- };
-
- const paired = [];
- while (indexA < listA.length) {
- const matchTo = listA[indexA];
- bestMatch.score = 0.0;
-
- if (!matchTo) {
- continue;
- }
- for (indexB = 0; indexB < listB.length; indexB++) {
- const possible = listB[indexB] as HDASummary;
- if (listA[indexA] !== listB[indexB]) {
- bestMatch = options.match(
- options.preprocessMatch!({
- matchTo: matchTo,
- possible: possible,
- index: indexB,
- bestMatch: bestMatch,
- })
- );
- if (bestMatch.score === 1.0) {
- break;
- }
- }
- }
- const scoreThreshold = options.scoreThreshold ? options.scoreThreshold : MATCH_PERCENTAGE;
- if (bestMatch.score >= scoreThreshold) {
- const createdPair = options.createPair!({
- listA: listA,
- indexA: indexA,
- listB: listB,
- indexB: bestMatch.index,
- });
- if (createdPair) {
- paired.push(createdPair);
- }
- } else {
- indexA += 1;
- }
- if (!listA.length || !listB.length) {
- return paired;
- }
- }
+ const scoreThreshold = options.scoreThreshold ? options.scoreThreshold : MATCH_PERCENTAGE;
+ const pairs = statelessAutoPairFnBuilder(
+ options.match,
+ scoreThreshold,
+ forwardFilter.value,
+ reverseFilter.value,
+ removeExtensions.value
+ )(params);
+ const paired: DatasetPair[] = [];
+ pairs.forEach((pair: { forward: HDASummary; reverse: HDASummary; name: string }) => {
+ paired.push(_pair(pair.forward, pair.reverse, { name: pair.name }));
+ });
return paired;
};
}
-function changeFilters(filter: keyof typeof COMMON_FILTERS) {
+function changeFilters(filter: CommonFiltersType) {
forwardFilter.value = COMMON_FILTERS[filter][0] as string;
reverseFilter.value = COMMON_FILTERS[filter][1] as string;
}
@@ -756,7 +539,7 @@ async function clickedCreate(collectionName: string) {
}
if (state.value == "build" && (atLeastOnePair.value || confirmed)) {
- emit("clicked-create", generatedPairs.value, collectionName, hideSourceItems.value);
+ emit("on-create", generatedPairs.value, collectionName, hideSourceItems.value);
}
}
@@ -773,44 +556,6 @@ function checkForDuplicates() {
});
state.value = valid ? "build" : "duplicates";
}
-
-// TODO: Where is this being used?
-// function stripExtension(name: string) {
-// return name.includes(".") ? name.substring(0, name.lastIndexOf(".")) : name;
-// }
-
-/** Return the concat'd longest common prefix and suffix from two strings */
-function _naiveStartingAndEndingLCS(s1: string, s2: string) {
- var fwdLCS = "";
- var revLCS = "";
- var i = 0;
- var j = 0;
- while (i < s1.length && i < s2.length) {
- if (s1[i] !== s2[i]) {
- break;
- }
- fwdLCS += s1[i];
- i += 1;
- }
- if (i === s1.length) {
- return s1;
- }
- if (i === s2.length) {
- return s2;
- }
-
- i = s1.length - 1;
- j = s2.length - 1;
- while (i >= 0 && j >= 0) {
- if (s1[i] !== s2[j]) {
- break;
- }
- revLCS = [s1[i], revLCS].join("");
- i -= 1;
- j -= 1;
- }
- return fwdLCS + revLCS;
-}
diff --git a/client/src/components/Collections/PairedOrUnpairedComponents.ts b/client/src/components/Collections/PairedOrUnpairedComponents.ts
new file mode 100644
index 000000000000..f974a34bc55a
--- /dev/null
+++ b/client/src/components/Collections/PairedOrUnpairedComponents.ts
@@ -0,0 +1,13 @@
+// Vue 2.7 <-> AG Grid - legacy stuff
+// see comments around "export default {" in PairedOrUnpairedListCollectionCreator
+import CellDiscardComponent from "./common/CellDiscardComponent.vue";
+import CellStatusComponent from "./common/CellStatusComponent.vue";
+import PairedDatasetCellComponent from "./common/PairedDatasetCellComponent.vue";
+import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue";
+
+export const components = {
+ CellDiscardComponent,
+ CellStatusComponent,
+ PairedDatasetCellComponent,
+ CollectionCreator,
+};
diff --git a/client/src/components/Collections/PairedOrUnpairedListCollectionCreator.vue b/client/src/components/Collections/PairedOrUnpairedListCollectionCreator.vue
new file mode 100644
index 000000000000..1a1f5ac12396
--- /dev/null
+++ b/client/src/components/Collections/PairedOrUnpairedListCollectionCreator.vue
@@ -0,0 +1,684 @@
+
+
+
+
+
+