Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HOTFIX: Prevent sharing of locked samples #1403

Merged
merged 11 commits into from
Nov 4, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [Developer/UI]: Fixed bug preventing managers from sharing project samples. See [PR 1398](https://github.com/phac-nml/irida/pull/1398)
* [UI]: Fixed bug where a sample added to the cart from the sample detail viewer still had a `Add to Cart` button if the viewer was closed and relaunched. See [PR 1397](https://github.com/phac-nml/irida/pull/1397)
* [Galaxy]: Fixed missing "deferred" state found in the Galaxy API but not in the IRIDA API for getting status of Galaxy histories. See [PR 1402](https://github.com/phac-nml/irida/pull/1402).
* [UI]: Fixed a bug that allowed the sharing and moving of locked samples. See [PR 1403](https://github.com/phac-nml/irida/pull/1403)

## [22.09.1] - 2022/10/21
* [UI]: Fixed when sharing or exporting sample on the project sample page, and other minor bugs. See [PR 1382](https://github.com/phac-nml/irida/pull/1382)
Expand Down
5 changes: 3 additions & 2 deletions src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,8 @@ ShareSamples.title=Share Samples with Another Project
ShareSamples.projects=Select a project to share samples with
ShareSamples.ready=These samples are ready to copy
ShareSamples.exists=Samples that exist in the target project and will not be copied
ShareSamples.avatar.unlocked=This sample is modifiable in this project.
ShareSamples.avatar.locked=This sample is not modifiable in this project.
ShareSamples.remove=Remove
ShareSamples.locked=The follow samples are locked and cannot be shared
ShareSamplesList.title=Review samples to share
ShareSamples.no-samples.message=All samples exist in the target project
ShareSamples.no-samples.description=Since these samples exists, there is no reason to re-share them.
Expand Down Expand Up @@ -2872,6 +2871,8 @@ SampleMenu.excel=Export to Excel
SampleMenu.csv=Export to CSV
SampleMenu.fileFilter=Filter by File
SampleMenu.fileFilter.clear=Clear File Filter: {0}
SampleMenu.share-all-locked=All samples are locked and cannot be shared.
SampleMenu.share-some-locked={0} samples are locked and cannot be shared:
SampleIcon.locked=You do not have ownership of this sample and cannot modify it.
SamplesTable.Column.sampleName=Name
SamplesTable.Column.quality=QC
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import { LockOutlined } from "@ant-design/icons";
import { List, Space, Typography } from "antd";
import { LockTwoTone } from "@ant-design/icons";
import { Avatar, Button, List } from "antd";
import { red6 } from "../../../../styles/colors";
import { SampleDetailViewer } from "../../../../components/samples/SampleDetailViewer";

/**
* React Element to render a list of locked samples. Use this when they
Expand All @@ -12,18 +14,30 @@ import { List, Space, Typography } from "antd";
export default function LockedSamplesList({ locked }) {
return (
<List
style={{ maxHeight: 400, overflowY: "auto" }}
size="small"
bordered
header={
<Space>
<LockOutlined />
<Typography.Text>{i18n("LockedSamplesList.header")}</Typography.Text>
</Space>
}
dataSource={locked}
renderItem={(sample) => (
<List.Item>
<List.Item.Meta title={sample.sampleName} />
<List.Item.Meta
avatar={
<Avatar
icon={<LockTwoTone twoToneColor={red6} />}
style={{ backgroundColor: "transparent" }}
/>
}
title={
<SampleDetailViewer
sampleId={sample.id}
projectId={sample.projectId}
>
<Button className="t-locked-name">
{sample.sampleName || sample.name}
</Button>
</SampleDetailViewer>
}
/>
</List.Item>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import {
Radio,
Row,
Space,
Typography,
} from "antd";
import React from "react";
import { useSelector } from "react-redux";
import {
serverValidateSampleName
} from "../../../../utilities/validation-utilities";
import { serverValidateSampleName } from "../../../../utilities/validation-utilities";
import { useMergeMutation } from "../../../../apis/projects/samples";
import LockedSamplesList from "./LockedSamplesList";

Expand Down Expand Up @@ -178,6 +177,9 @@ export default function MergeModal({ samples, visible, onComplete, onCancel }) {
)}
{samples.locked.length ? (
<Col span={24}>
<Typography.Text strong>
{i18n("LockedSamplesList.header")}
</Typography.Text>
<LockedSamplesList locked={samples.locked} />
</Col>
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { lazy, Suspense } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Button, Dropdown, Menu, message, Row, Space } from "antd";
import { Button, Dropdown, Menu, notification, Row, Space } from "antd";
import {
addToCart,
clearFilterByFile,
Expand All @@ -12,7 +12,7 @@ import {
import { setBaseUrl } from "../../../../utilities/url-utilities";
import {
validateSamplesForLinker,
validateSamplesForMerge,
validateSamplesForMergeOrShare,
validateSamplesForRemove,
} from "../services/sample.utilities";
import {
Expand Down Expand Up @@ -118,24 +118,25 @@ export default function SamplesMenu() {

const onNCBI = () => {
if (selected.size === 0) return;
formatAndStoreSamples(`ncbi`);
formatAndStoreSamples(`ncbi`, Object.values(selected));
window.location.href = setBaseUrl(`/projects/${projectId}/ncbi`);
};

const onExport = (type) => {
dispatch(exportSamplesToFile(type));
};

const formatAndStoreSamples = (path) => {
const samples = Object.values(selected).map(
({ id, sampleName: name, owner, projectId }) => ({
const formatAndStoreSamples = (path, samples) => {
storeSamples({
samples: samples.map(({ id, sampleName: name, owner, projectId }) => ({
id,
name,
owner,
projectId,
})
);
storeSamples({ samples, projectId, path });
})),
projectId,
path,
});
};

/**
Expand All @@ -144,9 +145,14 @@ export default function SamplesMenu() {
*/
const shareSamples = () => {
if (selected.size === 0) return;
formatAndStoreSamples(`share`);
// Redirect user to share page
window.location.href = setBaseUrl(`/projects/${projectId}/share`);
const { valid, locked } = validateSamplesForMergeOrShare(selected);
if (locked.length && valid.length === 0) {
notification.error({ message: i18n("SampleMenu.share-all-locked") });
} else {
formatAndStoreSamples(`share`, Object.values(selected));
// Redirect user to share page
window.location.href = setBaseUrl(`/projects/${projectId}/share`);
}
};

/**
Expand All @@ -156,23 +162,23 @@ export default function SamplesMenu() {
*/
const validateAndOpenModalFor = (name) => {
if (name === "merge") {
const validated = validateSamplesForMerge(selected);
const validated = validateSamplesForMergeOrShare(selected);
if (validated.valid.length >= 2) {
setSorted(validated);
setMergeVisible(true);
} else {
message.error(i18n("SamplesMenu.merge.error"));
notification.error({ message: i18n("SamplesMenu.merge.error") });
}
} else if (name === "remove") {
const validated = validateSamplesForRemove(selected, projectId);
if (validated.valid.length > 0) {
setSorted(validated);
setRemovedVisible(true);
} else message.error(i18n("SamplesMenu.remove.error"));
} else notification.error({ message: i18n("SamplesMenu.remove.error") });
} else if (name === "linker") {
const validated = validateSamplesForLinker(selected, projectId);
if (validated.associated.length > 0) {
message.error(i18n("SampleMenu.linker.error"));
notification.error({ message: i18n("SampleMenu.linker.error") });
} else {
setSorted(validated.valid);
setLinkerVisible(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Valid => samples that the user has ownership of
* Invalid => no ownership
*/
export function validateSamplesForMerge(samples) {
export function validateSamplesForMergeOrShare(samples) {
const values = Object.values(samples),
valid = [],
locked = [];
Expand All @@ -24,7 +24,7 @@ export function validateSamplesForMerge(samples) {
* associated => samples that do not belong to the current project.
* @param {array} samples
* @param {number | string} projectId
* @returns {{valid: *[], associated: *[], locked: *[]}}
* @returns {{valid: *[], associated: *[]}}
*/
export function validateSamplesForRemove(samples, projectId) {
const values = Object.values(samples),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IconLocked, IconUnlocked } from "../../../components/icons/Icons";
import { SampleDetailViewer } from "../../../components/samples/SampleDetailViewer";
import { green6 } from "../../../styles/colors";
import { removeSample } from "./shareSlice";

import { UnlockTwoTone } from "@ant-design/icons";

/**
* Render a list item for the samples to be shared with another project.
Expand Down Expand Up @@ -40,28 +40,11 @@ export default function ShareSamplesListItem({
>
<List.Item.Meta
avatar={
sample.owner ? (
<Tooltip
title={i18n("ShareSamples.avatar.unlocked")}
placement="right"
color={green6}
>
<Avatar
style={{ backgroundColor: green6 }}
className="t-unlocked-sample"
size="small"
icon={<IconUnlocked />}
/>
</Tooltip>
) : (
<Tooltip title={i18n("ShareSamples.avatar.locked")}>
<Avatar
className="t-locked-sample"
size="small"
icon={<IconLocked />}
/>
</Tooltip>
)
<Avatar
style={{ backgroundColor: `transparent` }}
className="t-unlocked-sample"
icon={<UnlockTwoTone twoToneColor={green6} />}
/>
}
title={
<SampleDetailViewer
Expand Down
14 changes: 11 additions & 3 deletions src/main/webapp/resources/js/pages/projects/share/ShareSamples.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SharedSamplesList } from "./SharedSamplesList";
import { updatedLocked, updateMoveSamples } from "./shareSlice";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import { SPACE_XS } from "../../../styles/spacing";
import LockedSamplesList from "../samples/components/LockedSamplesList";

const { Panel } = Collapse;
/**
Expand All @@ -24,15 +25,14 @@ export function ShareSamples({
const {
associated,
samples: originalSamples,
lockedSamples,
locked,
remove,
} = useSelector((state) => state.shareReducer);

return (
<Space direction="vertical" style={{ width: `100%` }}>
<Typography.Title level={5}>
{i18n("ShareSamplesList.title")}
</Typography.Title>
<Typography.Text strong>{i18n("ShareSamplesList.title")}</Typography.Text>
{associated.length > 0 && <ShareAssociated />}
{samples.length > 0 && (
<>
Expand Down Expand Up @@ -113,6 +113,14 @@ export function ShareSamples({
</Panel>
</Collapse>
)}
{lockedSamples.length > 0 && (
<Space direction="vertical" style={{ width: `100%` }}>
<Typography.Text strong>
{i18n("ShareSamples.locked")}
</Typography.Text>
<LockedSamplesList locked={lockedSamples} />
</Space>
)}
</Space>
);
}
12 changes: 9 additions & 3 deletions src/main/webapp/resources/js/pages/projects/share/shareSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,24 @@ const initialState = (() => {

const { samples: allSamples, projectId: currentProject } =
JSON.parse(stringData);
const samples = [];
const associated = [];
const samples = [],
lockedSamples = [],
associated = [];
allSamples.forEach((sample) => {
if (Number(sample.projectId) === Number(currentProject)) {
samples.push(sample);
if (sample.owner) {
samples.push(sample);
} else {
lockedSamples.push(sample);
}
} else {
associated.push(sample);
}
});

return {
samples,
lockedSamples,
associated,
currentProject,
locked: false,
Expand Down
Loading