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

Feature/project vcf #3902

Merged
merged 11 commits into from
Aug 28, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ public Plugins.VCFSamplesMappingDto getSamplesMapping(@PathParam("name") String
return Dtos.asDto(vcfSamplesMappingService.getVCFSamplesMapping(name));
}

@OPTIONS
@Path("/samples")
public Response getSamplesMappingOptions() {
return Response.ok().build();
}

/**
* Update or create a specific {@link org.obiba.opal.core.domain.VCFSamplesMapping}.
*
Expand Down Expand Up @@ -165,6 +171,11 @@ public List<Plugins.VCFSummaryDto> getVCFList(@PathParam("name") String name) {
.collect(Collectors.toList());
}

@OPTIONS
@Path("/vcfs")
public Response getVCFListOptions() {
return Response.ok().build();
}
/**
* Delete a VCF file. Does not fail if such VCF is not found.
*
Expand Down
42 changes: 27 additions & 15 deletions opal-ui/src/components/ProjectDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
</q-item-section>
</q-item>

<q-item-label header class="text-weight-bolder">{{
$t('content')
}}</q-item-label>
<q-item-label header class="text-weight-bolder">{{ $t('content') }}</q-item-label>

<q-item :to="`/project/${projectsStore.project.name}/tables`">
<q-item-section avatar>
Expand All @@ -35,6 +33,15 @@
</q-item-section>
</q-item>

<q-item v-show="hasVcfStorePermission" :to="`/project/${projectsStore.project.name}/genotypes`">
<q-item-section avatar>
<q-icon name="science" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('genotypes') }}</q-item-label>
</q-item-section>
</q-item>

<q-item :to="`/project/${projectsStore.project.name}/files`">
<q-item-section avatar>
<q-icon name="folder" />
Expand All @@ -53,9 +60,7 @@
</q-item-section>
</q-item>

<q-item-label header class="text-weight-bolder">{{
$t('administration')
}}</q-item-label>
<q-item-label header class="text-weight-bolder">{{ $t('administration') }}</q-item-label>

<q-item :to="`/project/${projectsStore.project.name}/tasks`">
<q-item-section avatar>
Expand Down Expand Up @@ -94,16 +99,23 @@ export default defineComponent({
</script>
<script setup lang="ts">
const projectsStore = useProjectsStore();
const pluginsStore = usePluginsStore();
const hasAdminPermission = ref(false);
const hasVcfStorePermission = ref(false);
const hasVcfPlugins = ref(false);

watchEffect(() => {
hasAdminPermission.value =
projectsStore.perms.project?.canCreate() ||
projectsStore.perms.project?.canUpdate() ||
projectsStore.perms.project?.canDelete() ||
false;

watch(
() => projectsStore.perms.project,
(newValue) => {
if (!!newValue) {
hasAdminPermission.value = projectsStore.perms.project?.canRead() || false;
}
},
{ immediate: true }
);
hasVcfStorePermission.value =
hasVcfPlugins && projectsStore.perms.vcfstore?.canRead() && !!projectsStore.project.vcfStoreService ? true : false;
});

onMounted(() => {
pluginsStore.hasPlugin('vcf-store').then((status) => (hasVcfPlugins.value = status));
});
</script>
2 changes: 1 addition & 1 deletion opal-ui/src/components/project/AddProjectDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
v-model="newProject.vcfStoreService"
:options="vcfStores"
dense
:label="$t('vcf_store')"
:label="$t('vcf_store.label')"
class="q-mb-md q-pt-md"
emit-value
map-options
Expand Down
281 changes: 281 additions & 0 deletions opal-ui/src/components/project/AddVcfMappingTableDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
<template>
<template>
<q-dialog v-model="showDialog" persistent @hide="onHide">
<q-card class="dialog-sm">
<q-card-section>
<div class="text-h6">{{ dialogTitle }}</div>
</q-card-section>

<q-separator />

<q-card-section v-if="tables.length < 1">
<q-banner inline-actions rounded class="bg-orange text-white">{{
$t('vcf_store.no_mapping_tables')
}}</q-banner>
</q-card-section>

<q-card-section v-else>
<q-select
v-model="selectedTable"
:options="filterOptions"
:label="$t('table')"
:hint="$t('vcf_store.mapping_table_hint')"
class="q-mb-md"
dense
emit-value
map-options
use-input
use-chips
hide-selection
input-debounce="0"
@update:model-value="onSelectTable"
@filter="onFilterFn"
>
<template v-slot:option="scope">
<q-item v-show="!!!scope.opt.value" class="text-help" dense clickable disable :label="scope.opt.group">
<q-item-section class="q-pa-none">
{{ scope.opt.label }}
</q-item-section>
</q-item>
<q-item v-show="!!scope.opt.value" dense clickable v-close-popup @click="onSelectTable(scope.opt.value)">
<q-item-section class="q-pl-md">
{{ scope.opt.label }}
</q-item-section>
</q-item>
</template>
</q-select>

<q-select
v-model="selectedParticipantIdVariable"
dense
class="q-mb-md"
:label="$t('vcf_store.participant_id')"
:hint="$t('vcf_store.participant_id_hint')"
:options="participantIdOptions"
emit-value
map-options
@update:model-value="newMapping.participantIdVariable = $event.name"
/>

<q-select
v-model="selectedRoleVariable"
dense
class="q-mb-md"
:label="$t('vcf_store.participant_id')"
:hint="$t('vcf_store.participant_id_hint')"
:options="roleOptions"
emit-value
map-options
@update:model-value="newMapping.sampleRoleVariable = $event.name"
/>
</q-card-section>

<q-separator />

<q-card-actions align="right" class="bg-grey-3">
<q-btn flat :label="$t('cancel')" color="secondary" v-close-popup />
<q-btn flat :label="submitCaption" color="primary" :disable="!canAdd" @click="onAdd" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</template>
</template>

<script lang="ts">
export default defineComponent({
name: 'AddVcfMappingTableDialog',
});
</script>

<script setup lang="ts">
import { VCFSamplesMappingDto } from 'src/models/Plugins';
import { TableDto, VariableDto, CategoryDto } from 'src/models/Magma';
import { notifyError } from 'src/utils/notify';

interface DialogProps {
modelValue: boolean;
project: string;
mapping?: VCFSamplesMappingDto;
}

type SelectOption = { label: string; value: TableDto | undefined };

const props = defineProps<DialogProps>();
const emit = defineEmits(['update:modelValue', 'update']);
const projectsStore = useProjectsStore();
const datasourceStore = useDatasourceStore();
const tables = ref([] as TableDto[]);
const { t } = useI18n();
const showDialog = ref(props.modelValue);
const newMapping = ref({} as VCFSamplesMappingDto);
const selectedTable = ref<TableDto | null>(null);
const selectedParticipantIdVariable = ref<VariableDto | null>(null);
const selectedRoleVariable = ref<VariableDto | null>(null);
let mappingOptions = [] as SelectOption[];
const filterOptions = ref([] as SelectOption[]);
const participantIdOptions = ref([] as { label: string; value: VariableDto }[]);
const roleOptions = ref([] as { label: string; value: VariableDto }[]);
const editMode = computed(() => !!props.mapping && !!props.mapping.projectName);
const submitCaption = computed(() => (editMode.value ? t('update') : t('add')));
const dialogTitle = computed(() => (editMode.value ? t('vcf_store.edit_mapping') : t('vcf_store.add_mapping')));
const canAdd = computed(() => !!selectedTable.value && !!selectedParticipantIdVariable.value && !!selectedRoleVariable.value);

function initMappingOptions() {
if (tables.value.length > 0) {
let lastGroup = '';
tables.value.forEach((table) => {
const tableRef = `${table.datasourceName}.${table.name}`;
if (!!!selectedTable.value && newMapping.value.tableReference === tableRef) {
selectedTable.value = table;
}

if (!!table.datasourceName && table.datasourceName !== lastGroup) {
lastGroup = table.datasourceName;
mappingOptions.push({ label: lastGroup } as SelectOption);
}
mappingOptions.push({ label: table.name, value: table });
});
filterOptions.value = [...mappingOptions];

if (!!selectedTable.value) {
getVariables();
}
}
}

function initializeVariableOptions(variables: VariableDto[]) {
let roleVariableSuggestion: VariableDto | undefined = undefined;
let participantIdVariableSuggestion: VariableDto | undefined = undefined;
roleOptions.value = [];
participantIdOptions.value = [];

(variables || []).forEach((variable) => {
const variableName = variable.name;
let roleCategory = null;
const categories: CategoryDto[] = variable.categories || [];

if (!!!roleVariableSuggestion) {
if (categories.length > 0) {
roleCategory = categories.find((category: CategoryDto) => {
const categoryName = category.name.toLowerCase();
return ['control', 'sample'].includes(categoryName);
});
}

if (!!newMapping.value.sampleRoleVariable) {
if (variableName === newMapping.value.sampleRoleVariable) {
roleVariableSuggestion = variable;
}
} else if (!!roleCategory || variableName.match(/role/i) != null) {
roleVariableSuggestion = variable;
}
}

if (!!!participantIdVariableSuggestion ) {
if (!!newMapping.value.participantIdVariable) {
if (variableName === newMapping.value.participantIdVariable) {
participantIdVariableSuggestion = variable;
}
}
else if (variableName.match(/participant/i) != null) {
participantIdVariableSuggestion = variable;
}
}

roleOptions.value.push({ label: variableName, value: variable });
participantIdOptions.value.push({ label: variableName, value: variable });
});

if (!!participantIdVariableSuggestion) {
selectedParticipantIdVariable.value = participantIdVariableSuggestion;
newMapping.value.participantIdVariable = participantIdVariableSuggestion.name;
}

if (!!roleVariableSuggestion) {
selectedRoleVariable.value = roleVariableSuggestion;
newMapping.value.sampleRoleVariable = roleVariableSuggestion.name;
}
}

async function getVariables() {
if (selectedTable.value && selectedTable.value.datasourceName && selectedTable.value.name) {
datasourceStore
.getTableVariables(selectedTable.value.datasourceName, selectedTable.value.name)
.then((variables) => {
initializeVariableOptions(variables);
});
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function onFilterFn(val: string, update: any) {
update(() => {
if (val.trim().length === 0) {
filterOptions.value = [...mappingOptions];
} else {
const needle = val.toLowerCase();
filterOptions.value = [
...mappingOptions.filter((v: SelectOption) => 'label' in v && v.label.toLowerCase().indexOf(needle) > -1),
];
}
});
}

// Handlers

watch(
() => props.modelValue,
(value) => {
if (value) {
datasourceStore.getAllTables('Sample').then((response) => {
tables.value = response;

if (props.mapping && props.mapping.projectName) {
newMapping.value = { ...props.mapping };
} else {
newMapping.value = { projectName: props.project } as VCFSamplesMappingDto;
}

initMappingOptions();
});

showDialog.value = true;
}
}
);

function onSelectTable(table: TableDto | null) {
selectedTable.value = table;
if (!!table) {
newMapping.value.tableReference = `${table.datasourceName}.${table.name}`;
getVariables();
} else {
selectedParticipantIdVariable.value = null;
selectedRoleVariable.value = null;
participantIdOptions.value = [];
roleOptions.value = [];
}
}

function onHide() {
mappingOptions = [];
newMapping.value = {} as VCFSamplesMappingDto;
selectedTable.value = null;
selectedParticipantIdVariable.value = null;
selectedRoleVariable.value = null;
filterOptions.value = [];
roleOptions.value = [];
participantIdOptions.value = [];
emit('update:modelValue', false);
}

async function onAdd() {
try {
await projectsStore.addVcfSamplesMapping(props.project, newMapping.value);
emit('update');
onHide();
} catch (error) {
notifyError(error);
}
}
</script>
Loading