Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions webapp/cypress/component/EquipmentTableTest.cy.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe("EquipmentTable Component Tests", () => {
.within(() => {
cy.get("td").eq(columnIndices["ID"]).should("contain.text", "equipment1");
cy.get("td").eq(columnIndices["Name"]).should("contain.text", "Equipment One");
cy.get("td").eq(columnIndices["Date"]).should("contain.text", "9/1/2023");
cy.get("td").eq(columnIndices["Date"]).should("contain.text", "2023-09-01");
cy.get("td").eq(columnIndices["Location"]).should("contain.text", "Warehouse A");
cy.get("td").eq(columnIndices["Maintainers"]).find(".avatar").should("have.length", 1);
});
Expand All @@ -113,7 +113,7 @@ describe("EquipmentTable Component Tests", () => {
.within(() => {
cy.get("td").eq(columnIndices["ID"]).should("contain.text", "equipment2");
cy.get("td").eq(columnIndices["Name"]).should("contain.text", "Equipment Two");
cy.get("td").eq(columnIndices["Date"]).should("contain.text", "8/15/2023");
cy.get("td").eq(columnIndices["Date"]).should("contain.text", "2023-08-15");
cy.get("td").eq(columnIndices["Location"]).should("contain.text", "Warehouse B");
cy.get("td").eq(columnIndices["Maintainers"]).find(".avatar").should("have.length", 2);
});
Expand Down
138 changes: 123 additions & 15 deletions webapp/src/components/CollectionInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,31 @@
</template>

<script>
import { createComputedSetterForCollectionField } from "@/field_utils.js";
import { createComputedSetterForCollectionField, formatRelativeDate } from "@/field_utils.js";
import TiptapInline from "@/components/TiptapInline";
import Creators from "@/components/Creators";
import CollectionRelationshipVisualization from "@/components/CollectionRelationshipVisualization";
import DynamicDataTable from "@/components/DynamicDataTable";
import FormattedItemName from "@/components/FormattedItemName";
import FormattedItemStatus from "@/components/FormattedItemStatus.vue";
import ChemicalFormula from "@/components/ChemicalFormula";
import BlocksIconCounter from "@/components/BlocksIconCounter";
import FilesIconCounter from "@/components/FilesIconCounter";
import ExportButton from "@/components/ExportButton";
import ToggleableCreatorsFormGroup from "@/components/ToggleableCreatorsFormGroup";
import ToggleableGroupsFormGroup from "@/components/ToggleableGroupsFormGroup";

import TextFilter from "@/components/TextFilter";
import MultiSelectFilter from "@/components/MultiSelectFilter";

import { FilterOperator, FilterMatchMode } from "@primevue/core/api";
import { matchStatus, statusOptions } from "@/utils/filterMatchers";

export default {
components: {
TiptapInline,
Creators,
CollectionRelationshipVisualization,
DynamicDataTable,
FormattedItemStatus,
ExportButton,
ToggleableCreatorsFormGroup,
ToggleableGroupsFormGroup,
Expand All @@ -100,38 +108,138 @@ export default {
{
field: "item_id",
header: "ID",
body: "FormattedItemName",
filter: true,
label: "ID",
body: {
component: FormattedItemName,
props: (row) => ({
item_id: row.item_id,
itemType: row.type,
enableClick: true,
enableModifiedClick: true,
}),
},
filter: {
component: TextFilter,
componentProps: { placeholder: "Search by ID" },
matchMode: FilterMatchMode.CONTAINS,
operator: FilterOperator.AND,
},
},
{
field: "type",
header: "Type",
label: "Type",
filter: {
component: MultiSelectFilter,
componentProps: { optionLabel: "type", placeholder: "Select item types" },
match: (value, filterValue) => {
if (!filterValue || (Array.isArray(filterValue) && filterValue.length === 0))
return true;
if (Array.isArray(filterValue)) return filterValue.some((f) => f.type === value);
return filterValue.type === value;
},
operator: FilterOperator.AND,
options: (data) =>
Array.from(new Set(data.map((item) => item.type).filter(Boolean))).map((type) => ({
type,
})),
noOperator: true,
},
},
{
field: "status",
header: "Status",
body: {
component: FormattedItemStatus,
props: (row) => ({ status: row.status }),
},
filter: {
component: MultiSelectFilter,
componentProps: {
optionLabel: "status",
placeholder: "Select status",
optionComponent: FormattedItemStatus,
optionProps: (opt) => ({ status: opt.status, dotOnly: false }),
valueComponent: FormattedItemStatus,
valueProps: (val) => ({ status: val.status, dotOnly: false }),
},
match: matchStatus,
operator: FilterOperator.OR,
options: statusOptions,
noOperator: true,
},
},
{ field: "type", header: "Type", filter: true, label: "Type" },
{ field: "status", header: "Status", body: "FormattedItemStatus", filter: true },
{ field: "name", header: "Name", label: "Sample name" },
{ field: "chemform", header: "Formula", body: "ChemicalFormula", label: "Formula" },
{ field: "date", header: "Date", label: "Date", filter: true },
{
field: "chemform",
header: "Formula",
label: "Formula",
body: {
component: ChemicalFormula,
props: (row) => ({
formula: row.chemform,
smiles: row.smiles,
inchiKey: row.inchi_key,
ghsCodes: row.GHS_codes,
molarMass: row.molar_mass,
cas: row.CAS,
}),
},
},
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file got missed during the filter migration. On main, these three columns all had filter: true:

{ field: "date", header: "Date", label: "Date", filter: true },
{ field: "creatorsAndGroups", ..., filter: true },
{ field: "blocks", ..., filter: true },
In the refactored version they only kept getValue/body for rendering the filter key is just gone, and none of DateRangeFilter, CreatorsAndGroupsFilter, or matchBlocks/blocksOptions are imported at the top of the file (only TextFilter and MultiSelectFilter made it in, for the id/type/status columns).

So they are lost kinda, maybe we should return them? or it was a plan to do like that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the filters should indeed stay the same unless this is intentional ?

field: "date",
header: "Date",
label: "Date",
getValue: (row) => (row.date ? row.date.substring(0, 10) : row.date),
},
{
field: "creatorsAndGroups",
header: "Creators",
body: "Creators",
label: "Creators",
filter: true,
body: {
component: Creators,
props: (row) => ({
creators: row.creatorsAndGroups
? row.creatorsAndGroups.filter((item) => item.type === "creator")
: row.creators || [],
groups: row.creatorsAndGroups
? row.creatorsAndGroups.filter((item) => item.type === "group")
: row.groups || [],
showNames:
(row.creatorsAndGroups || row.creators || []).filter(
(item) => !item.type || item.type === "creator",
).length === 1,
showBubble: true,
}),
},
},
{
field: "blocks",
header: "",
body: "BlocksIconCounter",
icon: ["fa", "cubes"],
label: "Blocks",
filter: true,
body: {
component: BlocksIconCounter,
props: (row) => ({ count: row.nblocks, blockInfo: row.blocks }),
},
},
{
field: "nfiles",
header: "",
body: "FilesIconCounter",
icon: ["fa", "file"],
label: "Files",
body: {
component: FilesIconCounter,
props: (row) => ({ count: row.nfiles }),
},
},
{
field: "last_modified",
header: "",
label: "Last modified",
icon: ["fa", "clock"],
getValue: (row) => formatRelativeDate(row.last_modified),
},
{ field: "last_modified", header: "", label: "Last modified", icon: ["fa", "clock"] },
],
};
},
Expand Down
49 changes: 45 additions & 4 deletions webapp/src/components/CollectionTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
import DynamicDataTable from "@/components/DynamicDataTable";
import { getCollectionList } from "@/server_fetch_utils.js";

import FormattedCollectionName from "@/components/FormattedCollectionName";
import Creators from "@/components/Creators";

import TextFilter from "@/components/TextFilter";
import CreatorsAndGroupsFilter from "@/components/CreatorsAndGroupsFilter";

import { FilterOperator, FilterMatchMode } from "@primevue/core/api";
import { matchCreatorsAndGroups, creatorsAndGroupsOptions } from "@/utils/filterMatchers";

export default {
components: { DynamicDataTable },
data() {
Expand All @@ -20,17 +29,49 @@ export default {
{
field: "collection_id",
header: "ID",
body: "FormattedCollectionName",
filter: true,
label: "Collections",
body: {
component: FormattedCollectionName,
props: (row) => ({
collection_id: row.collection_id,
enableClick: true,
enableModifiedClick: true,
}),
},
filter: {
component: TextFilter,
componentProps: { placeholder: "Search by ID" },
matchMode: FilterMatchMode.CONTAINS,
operator: FilterOperator.AND,
},
},
{ field: "title", header: "Title", label: "Title" },
{
field: "creatorsAndGroups",
header: "Creators",
body: "Creators",
filter: true,
label: "Creators",
body: {
component: Creators,
props: (row) => ({
creators: row.creatorsAndGroups
? row.creatorsAndGroups.filter((item) => item.type === "creator")
: row.creators || [],
groups: row.creatorsAndGroups
? row.creatorsAndGroups.filter((item) => item.type === "group")
: row.groups || [],
showNames:
(row.creatorsAndGroups || row.creators || []).filter(
(item) => !item.type || item.type === "creator",
).length === 1,
showBubble: true,
}),
},
filter: {
component: CreatorsAndGroupsFilter,
match: matchCreatorsAndGroups,
operator: FilterOperator.AND,
options: creatorsAndGroupsOptions,
},
},
],
};
Expand Down
69 changes: 69 additions & 0 deletions webapp/src/components/CreatorsAndGroupsFilter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<MultiSelect
ref="multiselect"
:model-value="modelValue"
:options="options"
option-label="display_name"
placeholder="Any"
class="d-flex w-full"
:filter="true"
@update:model-value="onValueChange"
@click.stop
>
<template #option="slotProps">
<div class="flex items-center">
<UserBubble
v-if="slotProps.option.type === 'creator'"
:creator="slotProps.option"
:size="24"
/>
<FormattedGroupName
v-if="slotProps.option.type === 'group'"
:group="slotProps.option"
:size="24"
/>
<span v-if="slotProps.option.type === 'creator'" class="ml-1">{{
slotProps.option.display_name
}}</span>
</div>
</template>
<template #value="slotProps">
<div class="flex flex-wrap gap-2 items-center">
<template v-if="slotProps.value && slotProps.value.length">
<span
v-for="(option, index) in slotProps.value"
:key="index"
class="inline-flex items-center mr-2"
>
<UserBubble v-if="option.type === 'creator'" :creator="option" :size="20" />
<FormattedGroupName v-if="option.type === 'group'" :group="option" :size="20" />
<span v-if="option.type === 'creator'" class="ml-1">{{ option.display_name }}</span>
</span>
</template>
<span v-else class="text-gray-400">Any</span>
</div>
</template>
</MultiSelect>
</template>

<script>
import MultiSelect from "primevue/multiselect";
import UserBubble from "@/components/UserBubble.vue";
import FormattedGroupName from "@/components/FormattedGroupName.vue";

export default {
components: { MultiSelect, UserBubble, FormattedGroupName },
props: {
modelValue: { type: Array, default: null },
options: { type: Array, default: () => [] },
},
emits: ["update:modelValue", "apply"],
methods: {
onValueChange(value) {
this.$emit("update:modelValue", value);
this.$emit("apply");
this.$nextTick(() => this.$refs.multiselect?.hide?.());
},
},
};
</script>
Loading
Loading