Skip to content

Commit b0c79d2

Browse files
committed
Improve Identification list view
1 parent 3a16d34 commit b0c79d2

18 files changed

+390
-205
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/annotations/AnnotationForm.vue

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}
1515
</Message>
1616
</FormField>
17-
<!-- <FormField v-slot="$field" name="isExecutive" :initial-value="false" class="flex ml-auto items-center gap-2">
17+
<FormField v-if="$can('change', annotationSubject, 'is_decisive')" v-slot="$field" name="isExecutive"
18+
:initial-value="false" class="flex ml-auto items-center gap-2">
1819
<label>Is executive?</label>
1920
<ToggleSwitch v-model="isExecutive" :disabled="isFlagged" />
2021
<Message v-if="$field?.invalid" severity="error" size="small" variant="simple">{{ $field.error?.message }}
2122
</Message>
22-
</FormField> -->
23+
</FormField>
2324
</div>
2425

2526
<div v-if="isExtended" class="flex flex-col w-full">
@@ -108,6 +109,8 @@ import { ref, watch, computed } from 'vue';
108109
import { Form } from '@primevue/forms';
109110
import { useToast } from 'primevue/usetoast';
110111
112+
import { subject } from '@casl/ability';
113+
111114
import type { AssignedObservation, Photo, SimplePhoto, Taxon, AnnotationRequest, AnnotationClassificationRequest, AnnotationFeedbackRequest, AnnotationCharacteristicsRequest } from 'mosquito-alert';
112115
import { AnnotationType, AnnotationClassificationConfidenceLabel, AnnotationCharacteristicsSex } from 'mosquito-alert';
113116
@@ -148,6 +151,18 @@ const props = withDefaults(defineProps<{
148151
canSetIsExecutive: true
149152
});
150153
154+
const annotationSubject = computed(() =>
155+
subject('Annotation', {
156+
observation: {
157+
location: {
158+
country: {
159+
id: props.observation.location.country?.id
160+
}
161+
}
162+
}
163+
})
164+
)
165+
151166
watch(
152167
selectedSex,
153168
(newSex) => {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<template>
2+
<Button v-if="$can('add', 'Annotation')" label="Start annotating" icon="pi pi-arrow-right" iconPos="right"
3+
:loading="isLoading" @click="onStartAnnotationClicked" />
4+
</template>
5+
6+
<script setup lang="ts">
7+
import { ref } from 'vue';
8+
import { useRouter } from 'vue-router'
9+
10+
import { useToast } from "primevue/usetoast";
11+
12+
import { useAssignmentStore } from '@/stores/assignmentStore';
13+
14+
const router = useRouter()
15+
const toast = useToast();
16+
const assignmentStore = useAssignmentStore();
17+
18+
const isLoading = ref<boolean>(false);
19+
20+
const onStartAnnotationClicked = async () => {
21+
try {
22+
isLoading.value = true;
23+
await assignmentStore.fetchNewAssignment();
24+
} catch (error) {
25+
toast.add(
26+
{ severity: 'error', summary: 'Error', detail: error, life: 3000 }
27+
)
28+
return;
29+
} finally {
30+
isLoading.value = false;
31+
}
32+
33+
const assignment = assignmentStore.assignment
34+
if (assignment) {
35+
router.push({
36+
name: 'annotate_identification_task',
37+
params: {
38+
observationUuid: assignment.observation.uuid
39+
}
40+
})
41+
} else {
42+
toast.add(
43+
{ severity: 'info', summary: 'Task queue empty', detail: 'No identification tasks available at the moment', life: 3000 }
44+
)
45+
}
46+
}
47+
48+
</script>

src/components/CountryMultipleSelect.vue renamed to src/components/countries/CountryMultipleSelect.vue

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
<MultiSelect v-model="selectedCountries" :options="sortedCountries" optionLabel="name_en" :loading="loading"
33
display="chip" dropdown-icon="pi pi-plus-circle" filter showClear resetFilterOnClear :maxSelectedLabels="3">
44
<template #option="slotProps">
5-
<div class="flex items-center gap-2">
6-
<i :class="`flag flag-${slotProps.option.iso3_code.toLowerCase()} rounded`" style="width: 24px" />
7-
<span>{{ slotProps.option.name_en }}</span>
8-
</div>
5+
<CountryTag :country="slotProps.option" />
96
</template>
107
</MultiSelect>
118
</template>
@@ -14,17 +11,29 @@
1411
import { ref, onMounted, computed } from 'vue';
1512
1613
import { useCountryStore } from '@/stores/countryStore';
14+
import CountryTag from './CountryTag.vue';
1715
1816
import type { Country } from 'mosquito-alert';
1917
2018
const selectedCountries = defineModel<Country[]>();
2119
20+
const props = defineProps<{
21+
allowedCountryIds?: number[];
22+
}>();
23+
2224
const loading = ref<boolean>(false);
2325
2426
const countries = ref<Country[]>();
2527
const sortedCountries = computed<Country[]>(() => {
2628
if (!countries.value) return [];
27-
return [...countries.value].sort((a, b) => a.name_en.localeCompare(b.name_en));
29+
30+
let filtered = countries.value;
31+
32+
if (props.allowedCountryIds?.length) {
33+
filtered = filtered.filter(country => props.allowedCountryIds?.includes(country.id));
34+
}
35+
36+
return [...filtered].sort((a, b) => a.name_en.localeCompare(b.name_en));
2837
})
2938
3039
const countryStore = useCountryStore();
@@ -34,7 +43,7 @@ onMounted(async () => {
3443
})
3544
3645
async function fetchCountries() {
37-
if (!countryStore.countries) {
46+
if (countryStore.countries.length === 0) {
3847
loading.value = true
3948
await countryStore.fetchCountries();
4049
loading.value = false
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<div class="flex items-center gap-2">
3+
<i :class="`flag flag-${country.iso3_code.toLowerCase()} rounded`" style="width: 24px" />
4+
<span>{{ country.name_en }}</span>
5+
</div>
6+
</template>
7+
8+
<script setup lang="ts">
9+
import type { Country } from 'mosquito-alert';
10+
11+
defineProps<{
12+
country: Country
13+
}>();
14+
15+
16+
</script>

src/components/IdentificationTaskGrid.vue renamed to src/components/identificationTasks/IdentificationTaskGrid.vue

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,12 @@
88
image-class="aspect-square object-cover transition-all duration-300 cursor-pointer"
99
class="w-full h-full group-hover:brightness-50" />
1010
<figcaption class="absolute top-2 left-2 text-white rounded-md gap-1">
11-
<Tag v-if="item.status" :value="item.status.toUpperCase()" :severity="getStatusSeverity(item.status)" />
11+
<IdentificationTaskStatusTag v-if="item.status" :status="item.status" />
1212
</figcaption>
1313
<figcaption v-if="item.observation.location.country"
1414
class="absolute top-2 right-2 rounded-md gap-1 flex flex-col">
15-
<div class="text-white bg-black/50 flex gap-1 p-2 rounded-md">
16-
<i :class="`flag flag-${item.observation.location.country?.iso3_code.toLowerCase()} rounded`"
17-
style="width: 24px" />
18-
<span>{{ item.observation.location.country?.name_en }}</span>
19-
</div>
15+
<CountryTag v-if="item.observation.location.country" :country="item.observation.location.country"
16+
class="text-white bg-black/50 rounded-md p-2" />
2017
<div class="gap-1 flex justify-end">
2118
<Tag v-if="item.is_flagged" icon="pi pi-flag" severity="danger" />
2219
<Tag v-if="item.is_safe" icon="pi pi-shield" severity="success" />
@@ -26,7 +23,7 @@
2623

2724
</figcaption>
2825
<figcaption v-if="item.result.source" class="absolute bottom-2 left-2">
29-
<TaxonClassificationTag class="bg-white/80!" :classification="item.result" />
26+
<IdentificationTaskResultTag class="bg-white/80!" :result="item.result" />
3027
</figcaption>
3128
</figure>
3229
</router-link>
@@ -38,8 +35,10 @@
3835
<script setup lang="ts">
3936
4037
import type { IdentificationTask } from 'mosquito-alert';
41-
import { getStatusSeverity } from '@/utils/IdentificationTaskUtils';
42-
import TaxonClassificationTag from './taxa/TaxonClassificationTag.vue';
38+
39+
import CountryTag from '@/components/countries/CountryTag.vue';
40+
import IdentificationTaskStatusTag from '@/components/identificationTasks/IdentificationTaskStatusTag.vue';
41+
import IdentificationTaskResultTag from '@/components/identificationTasks/IdentificationTaskResultTag.vue';
4342
4443
defineProps<{
4544
tasks: IdentificationTask[]

src/components/IdentificationTaskList.vue renamed to src/components/identificationTasks/IdentificationTaskList.vue

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
<template>
22
<DataTable :value="tasks" ref="dt" stripedRows :loading="loading" data-key="observation.uuid" row-hover
33
selectionMode="single" @rowClick="goToAnnotation">
4-
<template #header>
4+
<!-- <template #header>
55
<div style="text-align:left">
66
<MultiSelect id="column_select" :modelValue="selectedColumns" :options="columns" optionLabel="header"
77
@update:modelValue="onSelectColumn" display="chip" :maxSelectedLabels="0" filter selectedItemsLabel="Columns"
88
placeholder="Columns" />
99
</div>
10-
</template>
10+
</template> -->
1111
<Column field="observation.uuid" header="UUID" />
12+
<Column header="Status">
13+
<template #body="slotProps">
14+
<IdentificationTaskStatusTag :status="slotProps.data.status" />
15+
</template>
16+
</Column>
1217
<Column header="Image">
1318
<template #body="slotProps">
1419
<Image :src="`${slotProps.data.public_photo.url}`" preview
1520
imageClass="aspect-square object-cover rounded w-16" />
1621
</template>
1722
</Column>
18-
23+
<Column header="Taxon">
24+
<template #body="slotProps">
25+
<IdentificationTaskResultTag :result="slotProps.data.result" />
26+
</template>
27+
</Column>
1928
<Column header="Country">
2029
<template #body="slotProps">
21-
<div class="flex items-center gap-2">
22-
<i :class="`flag flag-${slotProps.data.observation.location.country?.iso3_code.toLowerCase()} rounded`"
23-
style="width: 24px" />
24-
<span>{{ slotProps.data.observation.location.country?.name_en }}</span>
25-
</div>
30+
<CountryTag v-if="slotProps.data.observation.location.country"
31+
:country="slotProps.data.observation.location.country" />
2632
</template>
2733
</Column>
2834
<Column header="Assignations">
@@ -43,17 +49,12 @@
4349
<Column field="is_safe" header="Is Safe" dataType="boolean" style="min-width: 6rem">
4450
<template #body="slotProps">
4551
<i v-if='slotProps.data.is_safe' class="pi pi-shield text-green-500" />
46-
4752
</template>
4853
</Column>
49-
<Column v-for="(col, index) of selectedColumns" :field="col.field" :header="col.header"
54+
<!-- <Column v-for="(col, index) of selectedColumns" :field="col.field" :header="col.header"
5055
:key="col.field + '_' + index">
51-
</Column>
52-
<Column header="Status">
53-
<template #body="slotProps">
54-
<Tag :value="slotProps.data.status.toUpperCase()" :severity="getStatusSeverity(slotProps.data.status)" />
55-
</template>
56-
</Column>
56+
</Column> -->
57+
5758
<Column header="Created at">
5859
<template #body="slotProps">
5960
{{ formatLocalDateTime(slotProps.data.created_at) }}
@@ -77,9 +78,11 @@ import type { DataTableRowClickEvent } from 'primevue';
7778
7879
import type { IdentificationTask } from 'mosquito-alert';
7980
80-
import UserAvatar from './users/UserAvatar.vue';
81+
import CountryTag from '@/components/countries/CountryTag.vue';
82+
import IdentificationTaskStatusTag from '@/components/identificationTasks/IdentificationTaskStatusTag.vue';
83+
import IdentificationTaskResultTag from '@/components/identificationTasks/IdentificationTaskResultTag.vue';
84+
import UserAvatar from '@/components/users/UserAvatar.vue';
8185
82-
import { getStatusSeverity } from '@/utils/IdentificationTaskUtils';
8386
import { formatLocalDateTime } from '@/utils/DateUtils';
8487
8588
defineProps<{

src/components/IdentificationTaskResultTag.vue renamed to src/components/identificationTasks/IdentificationTaskResultTag.vue

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
<template>
2-
<div class="relative">
3-
<TaxonClassificationTag :classification="result || null" />
2+
<TaxonClassificationTag :classification="result || null">
43
<div v-if="result.source"
5-
class="flex absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 origin-[100%_0%] m-0 outline-2 outline-solid outline-white bg-white justify-items-center items-center p-1 rounded-full">
4+
class="flex absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 origin-[100%_0%] m-0 outline-2 outline-solid outline-white text-surface-900 bg-white justify-items-center items-center p-1 rounded-full">
65
<i v-if='result.source === IdentificationTaskResultSource.Ai' class="pi pi-microchip-ai"
76
v-tooltip.right="'From artificial intelligence'" />
87
<i v-if='result.source === IdentificationTaskResultSource.Expert' class="pi pi-users"
98
v-tooltip.right="'From experts agreement'" />
109
</div>
11-
</div>
12-
10+
</TaxonClassificationTag>
1311
</template>
1412

1513
<script setup lang="ts">
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<template>
2+
<MultiSelect v-model="selectedIdentificationTaskResultSource" display="chip" :options="options" optionValue="value"
3+
optionLabel="label" data-key="value" :maxSelectedLabels="3" dropdown-icon="pi pi-plus-circle" filter showClear
4+
resetFilterOnClear>
5+
<template #option="slotProps">
6+
<div class="flex items-center gap-2">
7+
<i :class="slotProps.option.icon"></i>
8+
<span>{{ slotProps.option.label }}</span>
9+
</div>
10+
</template>
11+
</MultiSelect>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { ref } from 'vue';
16+
import { IdentificationTaskResultSource } from 'mosquito-alert';
17+
18+
const selectedIdentificationTaskResultSource = defineModel<IdentificationTaskResultSource[]>();
19+
20+
const options = ref<Array<{ value: IdentificationTaskResultSource; label: string; icon: string }>>([
21+
{ value: IdentificationTaskResultSource.Expert, label: 'Expert community', icon: 'pi pi-users' },
22+
{ value: IdentificationTaskResultSource.Ai, label: 'Artificial Intelligence', icon: 'pi pi-microchip-ai' },
23+
]);
24+
25+
</script>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<template>
2+
<MultiSelect id="status_filter" v-model="selectedIdentificationTaskStatus" display="chip" :options="options"
3+
optionValue="value" optionLabel="label" data-key="value" :maxSelectedLabels="3" dropdown-icon="pi pi-plus-circle"
4+
filter showClear resetFilterOnClear>
5+
<template #option="slotProps">
6+
<div class="flex items-center">
7+
<IdentificationTaskStatusTag :status="slotProps.option.value" />
8+
</div>
9+
</template>
10+
</MultiSelect>
11+
</template>
12+
13+
<script setup lang="ts">
14+
import { ref } from 'vue';
15+
import { IdentificationTaskStatus } from 'mosquito-alert';
16+
import IdentificationTaskStatusTag from './IdentificationTaskStatusTag.vue';
17+
18+
const selectedIdentificationTaskStatus = defineModel<IdentificationTaskStatus[]>();
19+
20+
const options = ref<Array<{ value: IdentificationTaskStatus; label: string }>>([
21+
{ value: IdentificationTaskStatus.Open, label: 'Open' },
22+
{ value: IdentificationTaskStatus.Conflict, label: 'Conflict' },
23+
{ value: IdentificationTaskStatus.Review, label: 'Review' },
24+
{ value: IdentificationTaskStatus.Done, label: 'Done' },
25+
{ value: IdentificationTaskStatus.Archived, label: 'Archived' },
26+
]);
27+
28+
</script>

0 commit comments

Comments
 (0)