-
-
-
-
+
@@ -32,21 +29,14 @@ export default {
},
};
-
diff --git a/frontend/components/feedback-task/container/fields/RecordFieldsAndSimilarity.vue b/frontend/components/feedback-task/container/fields/RecordFieldsAndSimilarity.vue
deleted file mode 100644
index 51b64546c2..0000000000
--- a/frontend/components/feedback-task/container/fields/RecordFieldsAndSimilarity.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/frontend/components/feedback-task/container/fields/RecordFieldsHeader.vue b/frontend/components/feedback-task/container/fields/RecordFieldsHeader.vue
new file mode 100644
index 0000000000..4fec58e5b0
--- /dev/null
+++ b/frontend/components/feedback-task/container/fields/RecordFieldsHeader.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
diff --git a/frontend/components/feedback-task/container/fields/RecordStatus.vue b/frontend/components/feedback-task/container/fields/RecordStatus.vue
index a1877934bb..8ab2bc69c0 100644
--- a/frontend/components/feedback-task/container/fields/RecordStatus.vue
+++ b/frontend/components/feedback-task/container/fields/RecordStatus.vue
@@ -69,7 +69,7 @@ export default {
display: flex;
align-items: center;
gap: $base-space;
- @include font-size(14px);
+ @include font-size(13px);
font-weight: 500;
&.--discarded {
diff --git a/frontend/components/feedback-task/container/mode/BulkAnnotation.vue b/frontend/components/feedback-task/container/mode/BulkAnnotation.vue
new file mode 100644
index 0000000000..9180e65135
--- /dev/null
+++ b/frontend/components/feedback-task/container/mode/BulkAnnotation.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/feedback-task/container/mode/FocusAnnotation.vue b/frontend/components/feedback-task/container/mode/FocusAnnotation.vue
new file mode 100644
index 0000000000..0397f8d9cf
--- /dev/null
+++ b/frontend/components/feedback-task/container/mode/FocusAnnotation.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
diff --git a/frontend/components/feedback-task/container/mode/useBulkAnnotationViewModel.ts b/frontend/components/feedback-task/container/mode/useBulkAnnotationViewModel.ts
new file mode 100644
index 0000000000..124a6d54c2
--- /dev/null
+++ b/frontend/components/feedback-task/container/mode/useBulkAnnotationViewModel.ts
@@ -0,0 +1,97 @@
+import { useResolve } from "ts-injecty";
+import { ref } from "vue-demi";
+import { Notification } from "~/models/Notifications";
+import { Record } from "~/v1/domain/entities/record/Record";
+import { DiscardBulkAnnotationUseCase } from "~/v1/domain/usecases/discard-bulk-annotation-use-case";
+import { SaveDraftBulkAnnotationUseCase } from "~/v1/domain/usecases/save-draft-bulk-annotation-use-case";
+import { SubmitBulkAnnotationUseCase } from "~/v1/domain/usecases/submit-bulk-annotation-use-case";
+import { useDebounce } from "~/v1/infrastructure/services/useDebounce";
+import { useTranslate } from "~/v1/infrastructure/services/useTranslate";
+
+export const useBulkAnnotationViewModel = () => {
+ const debounceForSubmit = useDebounce(300);
+
+ const isDraftSaving = ref(false);
+ const isDiscarding = ref(false);
+ const isSubmitting = ref(false);
+ const discardUseCase = useResolve(DiscardBulkAnnotationUseCase);
+ const submitUseCase = useResolve(SubmitBulkAnnotationUseCase);
+ const saveDraftUseCase = useResolve(SaveDraftBulkAnnotationUseCase);
+ const t = useTranslate();
+
+ const discard = async (records: Record[], recordReference: Record) => {
+ try {
+ isDiscarding.value = true;
+
+ const allSuccessful = await discardUseCase.execute(
+ records,
+ recordReference
+ );
+
+ if (!allSuccessful) {
+ Notification.dispatch("notify", {
+ message: t("some_records_failed_to_annotate"),
+ type: "error",
+ });
+ }
+
+ await debounceForSubmit.wait();
+
+ return allSuccessful;
+ } catch (error) {
+ } finally {
+ isDiscarding.value = false;
+ }
+
+ return false;
+ };
+
+ const submit = async (records: Record[], recordReference: Record) => {
+ try {
+ isSubmitting.value = true;
+
+ const allSuccessful = await submitUseCase.execute(
+ records,
+ recordReference
+ );
+
+ if (!allSuccessful) {
+ Notification.dispatch("notify", {
+ message: t("some_records_failed_to_annotate"),
+ type: "error",
+ });
+ }
+
+ await debounceForSubmit.wait();
+
+ return allSuccessful;
+ } catch (error) {
+ } finally {
+ isSubmitting.value = false;
+ }
+
+ return false;
+ };
+
+ const saveAsDraft = async (records: Record[], recordReference: Record) => {
+ try {
+ isDraftSaving.value = true;
+
+ await saveDraftUseCase.execute(records, recordReference);
+
+ await debounceForSubmit.wait();
+ } catch (error) {
+ } finally {
+ isDraftSaving.value = false;
+ }
+ };
+
+ return {
+ isDraftSaving,
+ isDiscarding,
+ isSubmitting,
+ submit,
+ discard,
+ saveAsDraft,
+ };
+};
diff --git a/frontend/components/feedback-task/container/questions/useQuestionsFormViewModel.ts b/frontend/components/feedback-task/container/mode/useFocusAnnotationViewModel.ts
similarity index 54%
rename from frontend/components/feedback-task/container/questions/useQuestionsFormViewModel.ts
rename to frontend/components/feedback-task/container/mode/useFocusAnnotationViewModel.ts
index b40cd147a7..722562ac20 100644
--- a/frontend/components/feedback-task/container/questions/useQuestionsFormViewModel.ts
+++ b/frontend/components/feedback-task/container/mode/useFocusAnnotationViewModel.ts
@@ -1,17 +1,12 @@
import { useResolve } from "ts-injecty";
import { ref } from "vue-demi";
import { Record } from "~/v1/domain/entities/record/Record";
-import { ClearRecordUseCase } from "~/v1/domain/usecases/clear-record-use-case";
import { DiscardRecordUseCase } from "~/v1/domain/usecases/discard-record-use-case";
import { SubmitRecordUseCase } from "~/v1/domain/usecases/submit-record-use-case";
-import { SaveDraftRecord } from "~/v1/domain/usecases/save-draft-use-case";
+import { SaveDraftUseCase } from "~/v1/domain/usecases/save-draft-use-case";
import { useDebounce } from "~/v1/infrastructure/services/useDebounce";
-import { useQueue } from "~/v1/infrastructure/services/useQueue";
-import { useBeforeUnload } from "~/v1/infrastructure/services/useBeforeUnload";
-export const useQuestionFormViewModel = () => {
- const beforeUnload = useBeforeUnload();
- const queue = useQueue();
+export const useFocusAnnotationViewModel = () => {
const debounceForSubmit = useDebounce(300);
const isDraftSaving = ref(false);
@@ -19,16 +14,12 @@ export const useQuestionFormViewModel = () => {
const isSubmitting = ref(false);
const discardUseCase = useResolve(DiscardRecordUseCase);
const submitUseCase = useResolve(SubmitRecordUseCase);
- const clearUseCase = useResolve(ClearRecordUseCase);
- const saveDraftUseCase = useResolve(SaveDraftRecord);
+ const saveDraftUseCase = useResolve(SaveDraftUseCase);
const discard = async (record: Record) => {
isDiscarding.value = true;
- beforeUnload.destroy();
- await queue.enqueue(() => {
- return discardUseCase.execute(record);
- });
+ await discardUseCase.execute(record);
await debounceForSubmit.wait();
@@ -37,11 +28,8 @@ export const useQuestionFormViewModel = () => {
const submit = async (record: Record) => {
isSubmitting.value = true;
- beforeUnload.destroy();
- await queue.enqueue(() => {
- return submitUseCase.execute(record);
- });
+ await submitUseCase.execute(record);
await debounceForSubmit.wait();
@@ -50,29 +38,18 @@ export const useQuestionFormViewModel = () => {
const saveAsDraft = async (record: Record) => {
isDraftSaving.value = true;
- beforeUnload.destroy();
- await queue.enqueue(() => {
- return saveDraftUseCase.execute(record);
- });
+ await saveDraftUseCase.execute(record);
await debounceForSubmit.wait();
- isDraftSaving.value = false;
- };
- const clear = (record: Record) => {
- beforeUnload.destroy();
-
- queue.enqueue(() => {
- return clearUseCase.execute(record);
- });
+ isDraftSaving.value = false;
};
return {
isDraftSaving,
isDiscarding,
isSubmitting,
- clear,
submit,
discard,
saveAsDraft,
diff --git a/frontend/components/feedback-task/container/questions/QuestionsForm.component.vue b/frontend/components/feedback-task/container/questions/QuestionsForm.component.vue
index 2105e32d78..289d0736a8 100644
--- a/frontend/components/feedback-task/container/questions/QuestionsForm.component.vue
+++ b/frontend/components/feedback-task/container/questions/QuestionsForm.component.vue
@@ -30,23 +30,31 @@
@@ -86,7 +89,7 @@ export default {
newFiltersChanged() {
if (!this.recordCriteria.hasChanges) return;
if (!this.recordCriteria.isChangingAutomatically) {
- this.recordCriteria.page = 1;
+ this.recordCriteria.page.goToFirst();
}
this.$root.$emit("on-change-record-criteria-filter", this.recordCriteria);
@@ -181,6 +184,10 @@ export default {
}
}
}
+ &--right {
+ flex-shrink: 0;
+ margin-left: auto;
+ }
.search-area {
width: min(100%, 400px);
}
diff --git a/frontend/components/feedback-task/header/RadioButtonsSelect.base.vue b/frontend/components/feedback-task/header/RadioButtonsSelect.base.vue
index 116ade3d45..36af5518dc 100644
--- a/frontend/components/feedback-task/header/RadioButtonsSelect.base.vue
+++ b/frontend/components/feedback-task/header/RadioButtonsSelect.base.vue
@@ -19,7 +19,11 @@
-
+
{{ currentOptionName }}
@@ -204,4 +208,9 @@ $selector-width: 140px;
border-radius: 50%;
}
}
+
+[data-title] {
+ overflow: visible;
+ @include tooltip-mini("top");
+}
diff --git a/frontend/components/feedback-task/header/SearchBar.base.vue b/frontend/components/feedback-task/header/SearchBar.base.vue
index 0765a3e19d..203e1b1a1b 100644
--- a/frontend/components/feedback-task/header/SearchBar.base.vue
+++ b/frontend/components/feedback-task/header/SearchBar.base.vue
@@ -20,7 +20,11 @@
class="search-area"
:class="{ active: isSearchActive, expanded: isExpanded }"
>
-
+
diff --git a/frontend/components/feedback-task/header/ToggleAnnotationType.vue b/frontend/components/feedback-task/header/ToggleAnnotationType.vue
new file mode 100644
index 0000000000..ddbf24f678
--- /dev/null
+++ b/frontend/components/feedback-task/header/ToggleAnnotationType.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/feedback-task/header/filters/LabelsSelector.vue b/frontend/components/feedback-task/header/filters/LabelsSelector.vue
index efd7d2c62a..44377a44a4 100644
--- a/frontend/components/feedback-task/header/filters/LabelsSelector.vue
+++ b/frontend/components/feedback-task/header/filters/LabelsSelector.vue
@@ -109,7 +109,7 @@ export default {
margin-top: $base-space;
}
&__item {
- &.re-checkbox {
+ &.checkbox {
display: flex;
padding: 6px $base-space;
border-radius: $border-radius;
@@ -117,7 +117,7 @@ export default {
&--highlighted {
background: $black-4;
}
- :deep(.checkbox-container) {
+ :deep(.checkbox__container) {
background: none !important;
border: 0 !important;
}
@@ -126,7 +126,7 @@ export default {
overflow: hidden;
text-overflow: ellipsis;
}
- &.re-checkbox :deep(.checkbox-container .svg-icon) {
+ &.checkbox :deep(.checkbox__container .svg-icon) {
fill: $primary-color;
min-width: 16px;
}
diff --git a/frontend/components/feedback-task/header/load-line/LoadLine.vue b/frontend/components/feedback-task/header/load-line/LoadLine.vue
new file mode 100644
index 0000000000..c29af801c5
--- /dev/null
+++ b/frontend/components/feedback-task/header/load-line/LoadLine.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/components/feedback-task/pagination/PageSizeSelector.vue b/frontend/components/feedback-task/pagination/PageSizeSelector.vue
new file mode 100644
index 0000000000..2d001dde7a
--- /dev/null
+++ b/frontend/components/feedback-task/pagination/PageSizeSelector.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
diff --git a/frontend/components/feedback-task/pagination/Pagination.component.vue b/frontend/components/feedback-task/pagination/Pagination.component.vue
new file mode 100644
index 0000000000..719b575d61
--- /dev/null
+++ b/frontend/components/feedback-task/pagination/Pagination.component.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
diff --git a/frontend/components/feedback-task/pagination/PaginationFeedbackTask.component.vue b/frontend/components/feedback-task/pagination/PaginationFeedbackTask.component.vue
new file mode 100644
index 0000000000..c3c192bd39
--- /dev/null
+++ b/frontend/components/feedback-task/pagination/PaginationFeedbackTask.component.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
diff --git a/frontend/components/feedback-task/footer/usePaginationFeedbackTaskViewModel.ts b/frontend/components/feedback-task/pagination/usePaginationFeedbackTaskViewModel.ts
similarity index 100%
rename from frontend/components/feedback-task/footer/usePaginationFeedbackTaskViewModel.ts
rename to frontend/components/feedback-task/pagination/usePaginationFeedbackTaskViewModel.ts
diff --git a/frontend/components/feedback-task/sidebar/FeedbackTaskProgress.vue b/frontend/components/feedback-task/sidebar/FeedbackTaskProgress.vue
index 125366aed8..fa408c7cc9 100644
--- a/frontend/components/feedback-task/sidebar/FeedbackTaskProgress.vue
+++ b/frontend/components/feedback-task/sidebar/FeedbackTaskProgress.vue
@@ -17,7 +17,7 @@
diff --git a/frontend/components/feedback-task/sidebar/sidebar-feedback-task/SidebarFeedbackTaskProgress.vue b/frontend/components/feedback-task/sidebar/sidebar-feedback-task/SidebarFeedbackTaskProgress.vue
index b1d4fa57a8..6bd8154a29 100644
--- a/frontend/components/feedback-task/sidebar/sidebar-feedback-task/SidebarFeedbackTaskProgress.vue
+++ b/frontend/components/feedback-task/sidebar/sidebar-feedback-task/SidebarFeedbackTaskProgress.vue
@@ -20,20 +20,19 @@
Progress
Total
-
{{ metrics.respondedProgress }}%
+
{{
+ metrics.progress | percent
+ }}
- {{ metrics.responded | formatNumber }}/{{ metrics.total | formatNumber }}
+ /{{ metrics.total }}
{
const recordCriteria = ref(
new RecordCriteria(
datasetId,
- routes.getQueryParams("_page"),
- routes.getQueryParams("_status"),
- routes.getQueryParams("_search"),
- routes.getQueryParams("_metadata"),
- routes.getQueryParams("_sort"),
- routes.getQueryParams("_response"),
- routes.getQueryParams("_suggestion"),
- routes.getQueryParams("_similarity")
+ routes.getQueryParams("page"),
+ routes.getQueryParams("status"),
+ routes.getQueryParams("search"),
+ routes.getQueryParams("metadata"),
+ routes.getQueryParams("sort"),
+ routes.getQueryParams("response"),
+ routes.getQueryParams("suggestion"),
+ routes.getQueryParams("similarity")
)
);
routes.watchBrowserNavigation(() => {
recordCriteria.value.complete(
- routes.getQueryParams("_page"),
- routes.getQueryParams("_status"),
- routes.getQueryParams("_search"),
- routes.getQueryParams("_metadata"),
- routes.getQueryParams("_sort"),
- routes.getQueryParams("_response"),
- routes.getQueryParams("_suggestion"),
- routes.getQueryParams("_similarity")
+ routes.getQueryParams("page"),
+ routes.getQueryParams("status"),
+ routes.getQueryParams("search"),
+ routes.getQueryParams("metadata"),
+ routes.getQueryParams("sort"),
+ routes.getQueryParams("response"),
+ routes.getQueryParams("suggestion"),
+ routes.getQueryParams("similarity")
);
});
const updateQueryParams = async () => {
await routes.setQueryParams(
{
- key: "_page",
- value: recordCriteria.value.committed.page.toString(),
+ key: "page",
+ value: recordCriteria.value.committed.page.urlParams,
},
{
- key: "_status",
+ key: "status",
value: recordCriteria.value.committed.status,
},
{
- key: "_search",
+ key: "search",
value: recordCriteria.value.committed.searchText,
},
{
- key: "_metadata",
+ key: "metadata",
value: recordCriteria.value.committed.metadata.urlParams,
},
{
- key: "_sort",
+ key: "sort",
value: recordCriteria.value.committed.sortBy.urlParams,
},
{
- key: "_response",
+ key: "response",
value: recordCriteria.value.committed.response.urlParams,
},
{
- key: "_suggestion",
+ key: "suggestion",
value: recordCriteria.value.committed.suggestion.urlParams,
},
{
- key: "_similarity",
+ key: "similarity",
value: recordCriteria.value.committed.similaritySearch.urlParams,
}
);
diff --git a/frontend/pages/dataset/_id/useDatasetSettingViewModel.ts b/frontend/pages/dataset/_id/useDatasetSettingViewModel.ts
index c2c53c821f..526f808346 100644
--- a/frontend/pages/dataset/_id/useDatasetSettingViewModel.ts
+++ b/frontend/pages/dataset/_id/useDatasetSettingViewModel.ts
@@ -88,7 +88,7 @@ export const useDatasetSettingViewModel = () => {
...createRootBreadCrumbs(datasetSetting.dataset),
{
link: {},
- name: "settings",
+ name: t("breadcrumbs.datasetSettings"),
},
];
});
diff --git a/frontend/pages/dataset/_id/useDatasetViewModel.ts b/frontend/pages/dataset/_id/useDatasetViewModel.ts
index 8814f56c49..56968bcf6c 100644
--- a/frontend/pages/dataset/_id/useDatasetViewModel.ts
+++ b/frontend/pages/dataset/_id/useDatasetViewModel.ts
@@ -2,10 +2,12 @@ import { ref, useRoute } from "@nuxtjs/composition-api";
import { Notification } from "@/models/Notifications";
import { DATASET_API_ERRORS } from "@/v1/infrastructure/repositories/DatasetRepository";
import { Dataset } from "@/v1/domain/entities/Dataset";
+import { useTranslate } from "~/v1/infrastructure/services";
export const useDatasetViewModel = () => {
const isLoadingDataset = ref(false);
const route = useRoute();
+ const t = useTranslate();
const datasetId = route.value.params.id;
const handleError = (response: string) => {
@@ -33,7 +35,7 @@ export const useDatasetViewModel = () => {
const createRootBreadCrumbs = (dataset: Dataset) => {
return [
- { link: { name: "datasets" }, name: "Home" },
+ { link: { name: "datasets" }, name: t("breadcrumbs.home") },
{
link: { path: `/datasets?workspaces=${dataset.workspace}` },
name: dataset.workspace,
diff --git a/frontend/pages/user-settings.vue b/frontend/pages/user-settings.vue
index 0db19e4d0d..f0cb665d45 100644
--- a/frontend/pages/user-settings.vue
+++ b/frontend/pages/user-settings.vue
@@ -1,7 +1,12 @@
-
+
diff --git a/frontend/plugins/directives/copy-code.directive.js b/frontend/plugins/directives/copy-code.directive.js
index eed3bea3c0..ab1ee5a9ff 100644
--- a/frontend/plugins/directives/copy-code.directive.js
+++ b/frontend/plugins/directives/copy-code.directive.js
@@ -6,7 +6,7 @@ Vue.directive("copy-code", {
const preElements = el.getElementsByTagName("PRE");
for (const pre of preElements) {
- const code = pre.children[0].innerText;
+ const code = pre.children[0] ? pre.children[0].innerText : pre.innerText;
const container = document.createElement("div");
container.style.position = "relative";
diff --git a/frontend/static/icons/bulk-mode.svg b/frontend/static/icons/bulk-mode.svg
new file mode 100644
index 0000000000..b053321678
--- /dev/null
+++ b/frontend/static/icons/bulk-mode.svg
@@ -0,0 +1,8 @@
+
diff --git a/frontend/static/icons/change-height.svg b/frontend/static/icons/change-height.svg
new file mode 100644
index 0000000000..b6a6926de5
--- /dev/null
+++ b/frontend/static/icons/change-height.svg
@@ -0,0 +1,6 @@
+
diff --git a/frontend/static/icons/focus-mode.svg b/frontend/static/icons/focus-mode.svg
new file mode 100644
index 0000000000..442fb3b7db
--- /dev/null
+++ b/frontend/static/icons/focus-mode.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/translation/en.json b/frontend/translation/en.json
index c4149dbc92..0e2fede0d8 100644
--- a/frontend/translation/en.json
+++ b/frontend/translation/en.json
@@ -4,12 +4,13 @@
"label_selection": "Label",
"text": "Text",
"rating": "Rating",
- "saving": "Saving...",
- "saved": "Saved",
"minimize": "Minimize",
+ "select": "Select",
+ "search": "Search",
"expand": "Expand",
"copied": "Copied",
"title": "Title",
+ "annotation": "Annotation",
"description": "Description",
"useMarkdown": "Use Markdown",
"visibleForAnnotators": "Visible for annotators",
@@ -19,6 +20,14 @@
"visibleOptions": "Visible options",
"annotationGuidelines": "Annotation guidelines",
"noAnnotationGuidelines": "This dataset has no annotation guidelines",
+ "breadcrumbs": {
+ "home": "Home",
+ "datasetSettings": "settings",
+ "userSettings": "my settings"
+ },
+ "userSettings": {
+ "title": "My settings"
+ },
"settings": {
"title": "Dataset settings",
"seeYourDataset": "See your dataset",
@@ -35,8 +44,18 @@
"button": {
"ignore_and_continue": "Ignore and continue"
},
- "to_submit_complete_required": "To submit complete required responses",
+ "to_submit_complete_required": "To submit complete \nrequired responses",
+ "some_records_failed_to_annotate": "Some records failed to annotate",
"changes_no_submit": "You didn't submit your changes",
+ "bulkAnnotation": {
+ "recordsSelected": "1 record selected | {count} records selected",
+ "recordsViewSettings": "Record size",
+ "fixedHeight": "Collapse records",
+ "defaultHeight": "Expand records",
+ "to_annotate_record_bulk_required": "No record selected",
+ "select_to_annotate": "Select all",
+ "pageSize": "Page size"
+ },
"shortcuts": {
"label": "Shortcuts",
"pagination": {
@@ -69,12 +88,16 @@
"suggested-rank": "✨ Suggested rank",
"name": "✨ Suggestion"
},
+ "similarity": {
+ "records": "Records",
+ "findSimilar": "Find similar",
+ "similarTo": "Similar to",
+ "similarityScore": "Similarity Score",
+ "similarUsing": "similar using",
+ "expand": "Expand"
+ },
"filters": "Filters",
"filterBy": "Filter by...",
- "findSimilar": "Find similar",
- "similarTo": "Similar to",
- "similarityScore": "Similarity Score",
- "similarUsing": "similar using",
"fields": "Fields",
"questions": "Questions",
"metadata": "Metadata",
@@ -86,6 +109,8 @@
"with": "with",
"find": "Find",
"cancel": "Cancel",
+ "focus_mode": "Focus view",
+ "bulk_mode": "Bulk view",
"update": "Update",
"youAreOnlineAgain": "You are online again",
"youAreOffline": "You are offline",
@@ -94,4 +119,4 @@
"errors::UnauthorizedError": "Could not validate credentials"
}
}
-}
+}
\ No newline at end of file
diff --git a/frontend/v1/di/di.ts b/frontend/v1/di/di.ts
index 044efa0d3c..4716a6e5ca 100644
--- a/frontend/v1/di/di.ts
+++ b/frontend/v1/di/di.ts
@@ -26,9 +26,11 @@ import { GetDatasetByIdUseCase } from "@/v1/domain/usecases/get-dataset-by-id-us
import { DeleteDatasetUseCase } from "@/v1/domain/usecases/delete-dataset-use-case";
import { LoadRecordsToAnnotateUseCase } from "@/v1/domain/usecases/load-records-to-annotate-use-case";
import { SubmitRecordUseCase } from "@/v1/domain/usecases/submit-record-use-case";
-import { SaveDraftRecord } from "@/v1/domain/usecases/save-draft-use-case";
-import { ClearRecordUseCase } from "@/v1/domain/usecases/clear-record-use-case";
+import { SubmitBulkAnnotationUseCase } from "@/v1/domain/usecases/submit-bulk-annotation-use-case";
+import { SaveDraftUseCase } from "@/v1/domain/usecases/save-draft-use-case";
+import { SaveDraftBulkAnnotationUseCase } from "@/v1/domain/usecases/save-draft-bulk-annotation-use-case";
import { DiscardRecordUseCase } from "@/v1/domain/usecases/discard-record-use-case";
+import { DiscardBulkAnnotationUseCase } from "@/v1/domain/usecases/discard-bulk-annotation-use-case";
import { GetUserMetricsUseCase } from "@/v1/domain/usecases/get-user-metrics-use-case";
import { GetDatasetSettingsUseCase } from "@/v1/domain/usecases/dataset-setting/get-dataset-settings-use-case";
import { UpdateQuestionSettingUseCase } from "@/v1/domain/usecases/dataset-setting/update-question-setting-use-case";
@@ -78,22 +80,30 @@ export const loadDependencyContainer = (context: Context) => {
.withDependencies(RecordRepository, useEventDispatcher)
.build(),
+ register(DiscardBulkAnnotationUseCase)
+ .withDependencies(RecordRepository, useEventDispatcher)
+ .build(),
+
register(SubmitRecordUseCase)
.withDependencies(RecordRepository, useEventDispatcher)
.build(),
- register(ClearRecordUseCase)
+ register(SubmitBulkAnnotationUseCase)
.withDependencies(RecordRepository, useEventDispatcher)
.build(),
- register(GetUserMetricsUseCase)
- .withDependencies(MetricsRepository, useMetrics)
+ register(SaveDraftUseCase)
+ .withDependencies(RecordRepository, useEventDispatcher)
.build(),
- register(SaveDraftRecord)
+ register(SaveDraftBulkAnnotationUseCase)
.withDependencies(RecordRepository, useEventDispatcher)
.build(),
+ register(GetUserMetricsUseCase)
+ .withDependencies(MetricsRepository, useMetrics)
+ .build(),
+
register(GetDatasetSettingsUseCase)
.withDependencies(
useRole,
diff --git a/frontend/v1/domain/entities/Metrics.test.ts b/frontend/v1/domain/entities/Metrics.test.ts
new file mode 100644
index 0000000000..792450fe5b
--- /dev/null
+++ b/frontend/v1/domain/entities/Metrics.test.ts
@@ -0,0 +1,86 @@
+import { Metrics } from "./Metrics";
+
+describe("Metrics", () => {
+ describe("hasMetrics", () => {
+ it("should return true when there are records", () => {
+ const metrics = new Metrics(1, 0, 0, 0, 0);
+
+ const result = metrics.hasMetrics;
+
+ expect(result).toBeTruthy();
+ });
+ it("should return false when there are no records", () => {
+ const metrics = new Metrics(0, 0, 0, 0, 0);
+
+ const result = metrics.hasMetrics;
+
+ expect(result).toBeFalsy();
+ });
+ });
+
+ describe("total", () => {
+ it("should return the total number of records", () => {
+ const metrics = new Metrics(1, 0, 0, 0, 0);
+
+ const result = metrics.total;
+
+ expect(result).toEqual(1);
+ });
+ });
+
+ describe("responded", () => {
+ it("should return the number of responded records", () => {
+ const metrics = new Metrics(5, 5, 3, 1, 1);
+
+ const result = metrics.responded;
+
+ expect(result).toEqual(5);
+ });
+ });
+
+ describe("pending", () => {
+ it("should return the number of pending records", () => {
+ const metrics = new Metrics(5, 4, 3, 1, 0);
+
+ const result = metrics.pending;
+
+ expect(result).toEqual(1);
+ });
+ });
+
+ describe("progress", () => {
+ it("should return the progress of responded records", () => {
+ const metrics = new Metrics(5, 4, 3, 1, 0);
+
+ const result = metrics.progress;
+
+ expect(result).toEqual(0.8);
+ });
+ });
+
+ describe("percentage", () => {
+ it("should return the percentage of draft records", () => {
+ const metrics = new Metrics(5, 4, 3, 1, 1);
+
+ const result = metrics.percentage.draft;
+
+ expect(result).toEqual(20);
+ });
+
+ it("should return the percentage of submitted records", () => {
+ const metrics = new Metrics(5, 4, 3, 1, 1);
+
+ const result = metrics.percentage.submitted;
+
+ expect(result).toEqual(60);
+ });
+
+ it("should return the percentage of discarded records", () => {
+ const metrics = new Metrics(5, 4, 3, 1, 1);
+
+ const result = metrics.percentage.discarded;
+
+ expect(result).toEqual(20);
+ });
+ });
+});
diff --git a/frontend/v1/domain/entities/Metrics.ts b/frontend/v1/domain/entities/Metrics.ts
index 3c1db80652..ff3dc4b63f 100644
--- a/frontend/v1/domain/entities/Metrics.ts
+++ b/frontend/v1/domain/entities/Metrics.ts
@@ -1,41 +1,41 @@
export class Metrics {
+ public readonly percentage: {
+ draft: number;
+ submitted: number;
+ discarded: number;
+ };
+
constructor(
- public readonly records: number,
+ private readonly records: number,
public readonly responses: number,
public readonly submitted: number,
public readonly discarded: number,
public readonly draft: number
- ) {}
-
- public get total(): number {
- return this.records;
- }
-
- public get pending(): number {
- return this.records - this.responded;
- }
-
- public get pendingProgress(): number {
- return Math.round((this.pending / this.total) * 100 * 10) / 10;
+ ) {
+ this.percentage = {
+ draft: (this.draft * 100) / this.total,
+ submitted: (this.submitted * 100) / this.total,
+ discarded: (this.discarded * 100) / this.total,
+ };
}
- public get draftProgress(): number {
- return Math.round((this.draft / this.total) * 100 * 10) / 10;
+ get hasMetrics() {
+ return this.records > 0;
}
- public get submittedProgress(): number {
- return Math.round((this.submitted / this.total) * 100 * 10) / 10;
+ get total() {
+ return this.records;
}
- public get discardedProgress(): number {
- return Math.round((this.discarded / this.total) * 100 * 10) / 10;
+ get responded() {
+ return this.submitted + this.discarded + this.draft;
}
- public get responded(): number {
- return this.submitted + this.draft + this.discarded;
+ get pending() {
+ return this.total - this.responded;
}
- public get respondedProgress(): number {
- return Math.round((this.responded / this.total) * 100 * 10) / 10;
+ get progress() {
+ return this.responded / this.total;
}
}
diff --git a/frontend/v1/domain/entities/Pagination.ts b/frontend/v1/domain/entities/Pagination.ts
deleted file mode 100644
index 8189d05dba..0000000000
--- a/frontend/v1/domain/entities/Pagination.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export interface Pagination {
- from: number;
- many: number;
-}
diff --git a/frontend/v1/domain/entities/common/Criteria.ts b/frontend/v1/domain/entities/common/Criteria.ts
index 494a6af6e8..48f5c163f7 100644
--- a/frontend/v1/domain/entities/common/Criteria.ts
+++ b/frontend/v1/domain/entities/common/Criteria.ts
@@ -5,12 +5,16 @@ export abstract class Criteria {
this.reset();
}
- abstract get isCompleted(): boolean;
+ get isCompleted(): boolean {
+ return true;
+ }
abstract get urlParams(): string;
abstract complete(urlParams: string);
+ abstract withValue(criteria: Criteria);
+
abstract reset();
protected getRangeValue(value: string): RangeValue {
diff --git a/frontend/v1/domain/entities/metadata/MetadataCriteria.ts b/frontend/v1/domain/entities/metadata/MetadataCriteria.ts
index 60d03aba83..813955ade7 100644
--- a/frontend/v1/domain/entities/metadata/MetadataCriteria.ts
+++ b/frontend/v1/domain/entities/metadata/MetadataCriteria.ts
@@ -31,8 +31,8 @@ export class MetadataCriteria extends Criteria {
});
}
- withValue(value: MetadataSearch[]) {
- this.value = value.map((v) => {
+ withValue(metadataCriteria: MetadataCriteria) {
+ this.value = metadataCriteria.value.map((v) => {
return {
name: v.name,
value: v.value,
diff --git a/frontend/v1/domain/entities/page/PageCriteria.test.ts b/frontend/v1/domain/entities/page/PageCriteria.test.ts
new file mode 100644
index 0000000000..f63cbbffb3
--- /dev/null
+++ b/frontend/v1/domain/entities/page/PageCriteria.test.ts
@@ -0,0 +1,151 @@
+import { PageCriteria } from "./PageCriteria";
+
+describe("PageCriteria", () => {
+ describe("next", () => {
+ it("should return the next page", () => {
+ const pageCriteria = new PageCriteria();
+
+ pageCriteria.client = {
+ page: 1,
+ many: 10,
+ };
+
+ expect(pageCriteria.next).toEqual(2);
+ });
+ });
+
+ describe("previous", () => {
+ test("should return the previous page", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.client = {
+ page: 2,
+ many: 10,
+ };
+
+ expect(pageCriteria.previous).toEqual(1);
+ });
+ });
+
+ describe("urlParams", () => {
+ it("should return the url params for focus", () => {
+ const pageCriteria = new PageCriteria();
+
+ pageCriteria.focusMode();
+
+ expect(pageCriteria.urlParams).toEqual("1");
+ });
+
+ it("should return the url params for bulk", () => {
+ const pageCriteria = new PageCriteria();
+
+ pageCriteria.bulkMode();
+
+ expect(pageCriteria.urlParams).toEqual("1~10");
+ });
+ });
+
+ describe("complete", () => {
+ it("should set page and many from url params", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.complete("1~10");
+ expect(pageCriteria.client).toEqual({
+ page: 1,
+ many: 10,
+ });
+ });
+
+ it("should set default page and many from url params", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.complete("1~");
+ expect(pageCriteria.client).toEqual({
+ page: 1,
+ many: 10,
+ });
+ });
+
+ it("should set the default page and many from url params when the `many` no exists in options", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.complete("1~11");
+ expect(pageCriteria.client).toEqual({
+ page: 1,
+ many: 10,
+ });
+ });
+ });
+
+ describe("reset", () => {
+ test("should reset the client page criteria", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.client = {
+ page: 1,
+ many: 25,
+ };
+
+ pageCriteria.reset();
+
+ expect(pageCriteria.client).toEqual({
+ page: 1,
+ many: 10,
+ });
+ });
+
+ test("should reset the server page criteria", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.client = {
+ page: 10,
+ many: 25,
+ };
+
+ pageCriteria.synchronizePagination({
+ from: 9,
+ many: 25,
+ });
+
+ pageCriteria.reset();
+
+ expect(pageCriteria.server).toEqual({
+ from: 1,
+ many: 10,
+ });
+ });
+ });
+
+ describe("goToFirst", () => {
+ test("should set the client page to 1", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.client = {
+ page: 10,
+ many: 10,
+ };
+
+ pageCriteria.goToFirst();
+
+ expect(pageCriteria.client).toEqual({
+ page: 1,
+ many: 10,
+ });
+ });
+ });
+
+ describe("isFirstPage", () => {
+ test("should return true when the client page is 1", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.client = {
+ page: 1,
+ many: 25,
+ };
+
+ expect(pageCriteria.isFirstPage()).toBeTruthy();
+ });
+
+ test("should return false when the client page is not 1", () => {
+ const pageCriteria = new PageCriteria();
+ pageCriteria.client = {
+ page: 2,
+ many: 25,
+ };
+
+ expect(pageCriteria.isFirstPage()).toBeFalsy();
+ });
+ });
+});
diff --git a/frontend/v1/domain/entities/page/PageCriteria.ts b/frontend/v1/domain/entities/page/PageCriteria.ts
new file mode 100644
index 0000000000..6687e0e40d
--- /dev/null
+++ b/frontend/v1/domain/entities/page/PageCriteria.ts
@@ -0,0 +1,139 @@
+import { Criteria } from "../common/Criteria";
+
+const DEFAULT_RECORDS_TO_FETCH = 10;
+
+interface BrowserPagination {
+ page: number;
+ many: number;
+}
+
+interface ServerPagination {
+ from: number;
+ many: number;
+}
+
+export class PageCriteria extends Criteria {
+ public readonly options = [10, 25, 50, 100];
+ public mode: "focus" | "bulk" = "focus";
+ public client: BrowserPagination;
+ private _server: ServerPagination;
+ constructor() {
+ super();
+
+ this.reset();
+ }
+
+ get server(): ServerPagination {
+ return {
+ ...this._server,
+ };
+ }
+
+ get next(): number {
+ if (this.isBulkMode) return this.client.page + this.client.many;
+
+ return this.client.page + 1;
+ }
+
+ get previous(): number {
+ if (this.isBulkMode)
+ return Math.max(this.client.page - this.client.many, 1);
+
+ return this.client.page - 1;
+ }
+
+ get isBulkMode() {
+ return this.mode === "bulk";
+ }
+
+ get isFocusMode() {
+ return this.mode === "focus";
+ }
+
+ get urlParams(): string {
+ if (this.isFocusMode) return this.client.page.toString();
+
+ return `${this.client.page}~${this.client.many}`;
+ }
+
+ withValue({ client, mode }: PageCriteria) {
+ this.client = {
+ page: client.page,
+ many: client.many,
+ };
+
+ this.mode = mode;
+ }
+
+ complete(urlParams = "") {
+ const pageParams = urlParams
+ .split("~")
+ .filter((param) => !isNaN(Number(param)) && param !== "");
+
+ if (pageParams.length === 0) return;
+
+ if (pageParams.length === 2) {
+ const [page, many] = pageParams;
+
+ if (!this.options.includes(Number(many))) return;
+
+ this.client = {
+ page: Number(page),
+ many: Number(many),
+ };
+ this.mode = "bulk";
+ } else {
+ const [page] = pageParams;
+
+ this.client = {
+ page: Number(page),
+ many: DEFAULT_RECORDS_TO_FETCH,
+ };
+ }
+ }
+
+ reset() {
+ this.client = {
+ page: 1,
+ many: DEFAULT_RECORDS_TO_FETCH,
+ };
+
+ this._server = {
+ from: this.client.page,
+ many: this.client.many,
+ };
+ }
+
+ synchronizePagination(server: ServerPagination) {
+ this._server = {
+ ...server,
+ };
+ }
+
+ goTo(page: number) {
+ this.client = {
+ ...this.client,
+ page,
+ };
+ }
+
+ goToFirst() {
+ this.goTo(1);
+ }
+
+ isFirstPage() {
+ return this.client.page === 1;
+ }
+
+ bulkMode() {
+ this.mode = "bulk";
+
+ this.reset();
+ }
+
+ focusMode() {
+ this.mode = "focus";
+
+ this.reset();
+ }
+}
diff --git a/frontend/v1/domain/entities/question/Question.ts b/frontend/v1/domain/entities/question/Question.ts
index 6650554540..3b76884f3d 100644
--- a/frontend/v1/domain/entities/question/Question.ts
+++ b/frontend/v1/domain/entities/question/Question.ts
@@ -1,4 +1,4 @@
-import { RecordAnswer } from "../record/RecordAnswer";
+import { Answer } from "../IAnswer";
import {
QuestionAnswer,
QuestionType,
@@ -147,18 +147,22 @@ export class Question {
this.initializeOriginal();
}
- complete(answer: RecordAnswer) {
+ clone(questionReference: Question) {
+ this.answer = questionReference.answer;
+ }
+
+ responseIfUnanswered(answer: Answer) {
if (this.suggestion) {
- this.answer.complete(this.suggestion);
+ this.answer.responseIfUnanswered(this.suggestion);
} else if (answer) {
- this.answer.complete(answer);
+ this.answer.responseIfUnanswered(answer);
}
}
- forceComplete(answer: RecordAnswer) {
+ response(answer: Answer) {
if (!answer) return;
- this.answer.forceComplete(answer);
+ this.answer.response(answer);
}
addSuggestion(suggestion: Suggestion) {
diff --git a/frontend/v1/domain/entities/question/QuestionAnswer.ts b/frontend/v1/domain/entities/question/QuestionAnswer.ts
index 04c460aae2..6f535bdd21 100644
--- a/frontend/v1/domain/entities/question/QuestionAnswer.ts
+++ b/frontend/v1/domain/entities/question/QuestionAnswer.ts
@@ -7,34 +7,37 @@ export type QuestionType =
| "ranking"
| "label_selection"
| "multi_label_selection";
+
export abstract class QuestionAnswer {
- private answer: Answer;
+ private _answer: Answer;
constructor(public readonly type: QuestionType) {}
- complete(answer: Answer) {
- if (this.answer) return;
+ get isPartiallyValid(): boolean {
+ return false;
+ }
- this.answer = answer;
- this.fill(answer);
+ get hasValidValues(): boolean {
+ return true;
}
- forceComplete(answer: Answer) {
- this.answer = answer;
- this.fill(answer);
+ get answer() {
+ return this._answer;
}
- protected abstract fill(answer: Answer);
- abstract clear();
- abstract get isValid(): boolean;
+ responseIfUnanswered(answer: Answer) {
+ if (this._answer) return;
- get isPartiallyValid(): boolean {
- return false;
+ this.response(answer);
}
- get hasValidValues(): boolean {
- return true;
+ response(answer: Answer) {
+ this._answer = answer;
+ this.fill(this._answer);
}
+ protected abstract fill(answer: Answer);
+ abstract clear();
+ abstract get isValid(): boolean;
abstract get valuesAnswered();
abstract matchSuggestion(suggestion: Suggestion): boolean;
diff --git a/frontend/v1/domain/entities/record/Record.ts b/frontend/v1/domain/entities/record/Record.ts
index ab7971ec49..48775d7d89 100644
--- a/frontend/v1/domain/entities/record/Record.ts
+++ b/frontend/v1/domain/entities/record/Record.ts
@@ -75,6 +75,18 @@ export class Record {
this.initialize();
}
+ answerWith(recordReference: Record) {
+ this.questions.forEach((question) => {
+ const questionReference = recordReference.questions.find(
+ (q) => q.id === question.id
+ );
+
+ if (!questionReference) return;
+
+ question.clone(questionReference);
+ });
+ }
+
initialize() {
this.completeQuestion();
@@ -111,16 +123,15 @@ export class Record {
private completeQuestion() {
return this.questions.map((question) => {
- const answerForQuestion = this.answer?.value[question.name];
+ const answer = this.answer?.value[question.name];
const suggestion = this.suggestions?.find(
(s) => s.questionId === question.id
);
question.addSuggestion(suggestion);
-
if (this.isPending || this.isDraft) {
- question.complete(answerForQuestion);
+ question.responseIfUnanswered(answer);
} else {
- question.forceComplete(answerForQuestion);
+ question.response(answer);
}
return question;
diff --git a/frontend/v1/domain/entities/record/RecordCriteria.test.ts b/frontend/v1/domain/entities/record/RecordCriteria.test.ts
index 13e5befe04..348d9f0979 100644
--- a/frontend/v1/domain/entities/record/RecordCriteria.test.ts
+++ b/frontend/v1/domain/entities/record/RecordCriteria.test.ts
@@ -5,14 +5,14 @@ describe("RecordCriteria", () => {
test("should return true if searchText is not empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"searchText",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteringByText).toBe(true);
@@ -21,14 +21,14 @@ describe("RecordCriteria", () => {
test("should return false if searchText is empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteringByText).toBe(false);
@@ -37,14 +37,14 @@ describe("RecordCriteria", () => {
test("should return false if searchText is undefined", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
undefined,
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteringByText).toBe(false);
@@ -55,7 +55,7 @@ describe("RecordCriteria", () => {
test("should return true if similaritySearch is not empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
@@ -71,14 +71,14 @@ describe("RecordCriteria", () => {
test("should return false if similaritySearch is empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteringBySimilarity).toBe(false);
@@ -89,14 +89,14 @@ describe("RecordCriteria", () => {
test("should return true if response is range", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"response.ge.1le.5",
"",
- null
+ ""
);
expect(criteria.isFilteringByResponse).toBe(true);
@@ -105,14 +105,14 @@ describe("RecordCriteria", () => {
test("should return true if response is terms", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"response.option1~option2",
"",
- null
+ ""
);
expect(criteria.isFilteringByResponse).toBe(true);
@@ -121,14 +121,14 @@ describe("RecordCriteria", () => {
test("should return false if response is empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteringByResponse).toBe(false);
@@ -139,14 +139,14 @@ describe("RecordCriteria", () => {
test("should return true if suggestion is not empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"content_class.value.operator.and.values.hate.pii",
- null
+ ""
);
expect(criteria.isFilteringBySuggestion).toBe(true);
@@ -155,14 +155,14 @@ describe("RecordCriteria", () => {
test("should return false if suggestion is empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteringBySuggestion).toBe(false);
@@ -173,14 +173,14 @@ describe("RecordCriteria", () => {
test("should return true if sortBy is not empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"suggestion.relevant.score.asc",
"",
"",
- null
+ ""
);
expect(criteria.isSortingBy).toBe(true);
@@ -189,14 +189,14 @@ describe("RecordCriteria", () => {
test("should return false if sortBy is empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isSortingBy).toBe(false);
@@ -207,14 +207,14 @@ describe("RecordCriteria", () => {
test("should return true if searchText is not empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"searchText",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteredByText).toBe(true);
@@ -223,14 +223,14 @@ describe("RecordCriteria", () => {
test("should return false if searchText is empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteredByText).toBe(false);
@@ -239,14 +239,14 @@ describe("RecordCriteria", () => {
test("should return false if searchText is undefined", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
undefined,
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteredByText).toBe(false);
@@ -257,14 +257,14 @@ describe("RecordCriteria", () => {
test("return true if metadata is range", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"metadata.ge.1le.5",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteredByMetadata).toBe(true);
@@ -273,14 +273,14 @@ describe("RecordCriteria", () => {
test("return true if metadata is terms", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"metadata.option1~option2",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteredByMetadata).toBe(true);
@@ -289,14 +289,14 @@ describe("RecordCriteria", () => {
test("return false if metadata is empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteredByMetadata).toBe(false);
@@ -305,14 +305,14 @@ describe("RecordCriteria", () => {
test("return false if metadata is undefined", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
undefined,
"",
"",
"",
- null
+ ""
);
expect(criteria.isFilteredByMetadata).toBe(false);
@@ -323,14 +323,14 @@ describe("RecordCriteria", () => {
test("should return true if response is range", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"response.ge.1le.5",
"",
- null
+ ""
);
expect(criteria.isFilteredByResponse).toBe(true);
@@ -339,14 +339,14 @@ describe("RecordCriteria", () => {
test("should return true if response is terms", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"response.option1~option2",
"",
- null
+ ""
);
expect(criteria.isFilteredByResponse).toBe(true);
@@ -357,14 +357,14 @@ describe("RecordCriteria", () => {
test("should return true if suggestion is not empty", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"content_class.score.ge0.le0.24",
- null
+ ""
);
expect(criteria.isFilteredBySuggestion).toBe(true);
@@ -375,17 +375,17 @@ describe("RecordCriteria", () => {
test("return true if page is different", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
- criteria.page = 2;
+ criteria.page.goTo(2);
expect(criteria.hasChanges).toBe(true);
});
@@ -393,14 +393,14 @@ describe("RecordCriteria", () => {
test("return true if status is different", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
criteria.status = "submitted";
@@ -411,14 +411,14 @@ describe("RecordCriteria", () => {
test("return true if searchText is different", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"Can AI help us?",
"",
"",
"",
"",
- null
+ ""
);
criteria.searchText = "Can ML help to improve your business processes?";
@@ -429,14 +429,14 @@ describe("RecordCriteria", () => {
test("return true if metadata is different", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
criteria.metadata.complete("your_feel.happy~sad");
@@ -447,14 +447,14 @@ describe("RecordCriteria", () => {
test("return true if sortBy is different", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
criteria.sortBy.complete(
@@ -473,7 +473,7 @@ describe("RecordCriteria", () => {
test("return true if similaritySearch is different", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
@@ -491,17 +491,17 @@ describe("RecordCriteria", () => {
test("return false if page, status, searchText, metadata, sortBy or similaritySearch are same after commit", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"",
"",
"",
"",
"",
- null
+ ""
);
- criteria.page = 2;
+ criteria.page.goTo(2);
criteria.status = "submitted";
criteria.searchText = "Love ML";
criteria.metadata.value = [
@@ -527,7 +527,7 @@ describe("RecordCriteria", () => {
test("restore committed changes", () => {
const criteria = new RecordCriteria(
"datasetId",
- 1,
+ "1",
"pending",
"Do you love ML?",
"your_feel.happy~sad",
@@ -537,7 +537,7 @@ describe("RecordCriteria", () => {
"record:1,vector:2,limit:50,order:most"
);
- criteria.page = 1;
+ criteria.page.goTo(1);
criteria.status = "discarded";
criteria.searchText = "Do you love AI?";
criteria.metadata.complete("your_feel.sad");
@@ -556,4 +556,280 @@ describe("RecordCriteria", () => {
);
});
});
+
+ describe("nextPage", () => {
+ test("should increment page focus mode", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "Do you love ML?",
+ "your_feel.happy~sad",
+ "inserted_at:desc",
+ "",
+ "",
+ "record:1,vector:2,limit:50,order:most"
+ );
+
+ criteria.nextPage();
+
+ expect(criteria.page.client.page).toEqual(2);
+ });
+
+ test("should increment page bulk mode", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "Do you love ML?",
+ "your_feel.happy~sad",
+ "inserted_at:desc",
+ "",
+ "",
+ "record:1,vector:2,limit:50,order:most"
+ );
+
+ criteria.page.mode = "bulk";
+ criteria.page.client.many = 10;
+ criteria.commit();
+
+ criteria.nextPage();
+
+ expect(criteria.page.client.page).toEqual(11);
+ });
+ });
+
+ describe("previousPage", () => {
+ test("should decrement page", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "3",
+ "pending",
+ "Do you love ML?",
+ "your_feel.happy~sad",
+ "inserted_at:desc",
+ "",
+ "",
+ "record:1,vector:2,limit:50,order:most"
+ );
+
+ criteria.previousPage();
+
+ expect(criteria.page.client.page).toEqual(2);
+ });
+
+ test("should decrement page bulk mode", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "11",
+ "pending",
+ "Do you love ML?",
+ "your_feel.happy~sad",
+ "inserted_at:desc",
+ "",
+ "",
+ "record:1,vector:2,limit:50,order:most"
+ );
+
+ criteria.page.mode = "bulk";
+ criteria.page.client.many = 10;
+ criteria.commit();
+
+ criteria.previousPage();
+
+ expect(criteria.page.client.page).toEqual(1);
+ });
+ });
+
+ describe("isComingToBulkMode", () => {
+ test("should return true when previous mode has focus and now is bulk", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "3",
+ "pending",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ );
+
+ criteria.page.bulkMode();
+
+ criteria.commit();
+
+ expect(criteria.isComingToBulkMode).toBeTruthy();
+ });
+
+ test("should return true when the criteria does not have any change", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1~10",
+ "pending",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ );
+
+ criteria.commit();
+
+ expect(criteria.isComingToBulkMode).toBeTruthy();
+ });
+ });
+
+ describe("reset", () => {
+ test("should reset page", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ );
+
+ criteria.page.goTo(2);
+
+ criteria.reset();
+
+ expect(criteria.page.client.page).toEqual(1);
+ });
+
+ test("should reset metadata", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "",
+ "metadata.ge.1le.5",
+ "",
+ "",
+ "",
+ ""
+ );
+
+ criteria.reset();
+
+ expect(criteria.metadata.value).toEqual([]);
+ });
+
+ test("should reset sortBy", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "",
+ "",
+ "suggestion.relevant.score.asc",
+ "",
+ "",
+ ""
+ );
+
+ criteria.reset();
+
+ expect(criteria.sortBy.value).toEqual([]);
+ });
+
+ test("should reset response", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "",
+ "",
+ "",
+ "response.ge.1le.5",
+ "",
+ ""
+ );
+
+ criteria.reset();
+
+ expect(criteria.response.value).toEqual([]);
+ });
+
+ test("should reset suggestion", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "",
+ "",
+ "",
+ "",
+ "content_class.value.operator.and.values.hate.pii",
+ ""
+ );
+
+ criteria.reset();
+
+ expect(criteria.suggestion.value).toEqual([]);
+ });
+
+ test("should NO reset similaritySearch", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "record.1.vector.2.limit.50.order.most"
+ );
+
+ criteria.reset();
+
+ expect(criteria.similaritySearch.urlParams).toEqual(
+ "record.1.vector.2.limit.50.order.most"
+ );
+ });
+
+ test("should NO reset status", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ );
+
+ criteria.status = "submitted";
+
+ criteria.reset();
+
+ expect(criteria.status).toEqual("submitted");
+ });
+
+ test("should NO reset searchText", () => {
+ const criteria = new RecordCriteria(
+ "datasetId",
+ "1",
+ "pending",
+ "Can AI help us?",
+ "",
+ "",
+ "",
+ "",
+ ""
+ );
+
+ criteria.reset();
+
+ expect(criteria.searchText).toEqual("Can AI help us?");
+ });
+ });
});
diff --git a/frontend/v1/domain/entities/record/RecordCriteria.ts b/frontend/v1/domain/entities/record/RecordCriteria.ts
index 5daf8dbf2a..5b951c2cbe 100644
--- a/frontend/v1/domain/entities/record/RecordCriteria.ts
+++ b/frontend/v1/domain/entities/record/RecordCriteria.ts
@@ -3,23 +3,66 @@ import { SortCriteria } from "../sort/SortCriteria";
import { MetadataCriteria } from "../metadata/MetadataCriteria";
import { ResponseCriteria } from "../response/ResponseCriteria";
import { SuggestionCriteria } from "../suggestion/SuggestionCriteria";
+import { PageCriteria } from "../page/PageCriteria";
import { RecordStatus } from "./RecordAnswer";
-interface CommittedRecordCriteria {
- page: number;
- status: RecordStatus;
- searchText: string;
- metadata: MetadataCriteria;
- sortBy: SortCriteria;
- response: ResponseCriteria;
- suggestion: SuggestionCriteria;
- similaritySearch: SimilarityCriteria;
+interface IRecordCriteria {
+ readonly page: PageCriteria;
+ readonly status: RecordStatus;
+ readonly searchText: string;
+ readonly metadata: MetadataCriteria;
+ readonly sortBy: SortCriteria;
+ readonly response: ResponseCriteria;
+ readonly suggestion: SuggestionCriteria;
+ readonly similaritySearch: SimilarityCriteria;
}
-export class RecordCriteria {
+class CommittedRecordCriteria implements IRecordCriteria {
+ public readonly page: PageCriteria;
+ public readonly status: RecordStatus;
+ public readonly searchText: string;
+ public readonly metadata: MetadataCriteria;
+ public readonly sortBy: SortCriteria;
+ public readonly response: ResponseCriteria;
+ public readonly suggestion: SuggestionCriteria;
+ public readonly similaritySearch: SimilarityCriteria;
+
+ constructor(recordCriteria: IRecordCriteria) {
+ const pageCommitted = new PageCriteria();
+ const similaritySearchCommitted = new SimilarityCriteria();
+ const metadataCommitted = new MetadataCriteria();
+ const sortByCommitted = new SortCriteria();
+ const responseCommitted = new ResponseCriteria();
+ const suggestionCommitted = new SuggestionCriteria();
+
+ pageCommitted.withValue(recordCriteria.page);
+ metadataCommitted.withValue(recordCriteria.metadata);
+ sortByCommitted.withValue(recordCriteria.sortBy);
+ suggestionCommitted.withValue(recordCriteria.suggestion);
+ responseCommitted.withValue(recordCriteria.response);
+ similaritySearchCommitted.withValue(recordCriteria.similaritySearch);
+
+ this.status = recordCriteria.status;
+ this.searchText = recordCriteria.searchText;
+ this.page = pageCommitted;
+ this.metadata = metadataCommitted;
+ this.sortBy = sortByCommitted;
+ this.response = responseCommitted;
+ this.suggestion = suggestionCommitted;
+ this.similaritySearch = similaritySearchCommitted;
+ }
+
+ get isPending() {
+ return this.status === "pending";
+ }
+}
+
+export class RecordCriteria implements IRecordCriteria {
public isChangingAutomatically = false;
public committed: CommittedRecordCriteria;
+ public previous?: CommittedRecordCriteria;
+ public page: PageCriteria;
public metadata: MetadataCriteria;
public sortBy: SortCriteria;
public response: ResponseCriteria;
@@ -28,7 +71,7 @@ export class RecordCriteria {
constructor(
public readonly datasetId: string,
- public page: number,
+ page: string,
public status: RecordStatus,
public searchText: string,
metadata: string,
@@ -37,6 +80,7 @@ export class RecordCriteria {
suggestion: string,
similaritySearch: string
) {
+ this.page = new PageCriteria();
this.metadata = new MetadataCriteria();
this.sortBy = new SortCriteria();
this.response = new ResponseCriteria();
@@ -116,24 +160,19 @@ export class RecordCriteria {
return this.committed.similaritySearch.isCompleted;
}
- get hasChanges(): boolean {
- if (this.committed.page !== this.page) return true;
- if (this.committed.status !== this.status) return true;
-
- if (this.committed.searchText !== this.searchText) return true;
-
- if (!this.metadata.isEqual(this.committed.metadata)) return true;
- if (!this.sortBy.isEqual(this.committed.sortBy)) return true;
- if (!this.response.isEqual(this.committed.response)) return true;
- if (!this.suggestion.isEqual(this.committed.suggestion)) return true;
- if (!this.similaritySearch.isEqual(this.committed.similaritySearch))
- return true;
+ get isComingToBulkMode(): boolean {
+ return (
+ (!this.previous || this.previous.page.isFocusMode) &&
+ this.committed.page.isBulkMode
+ );
+ }
- return false;
+ get hasChanges(): boolean {
+ return this.areDifferent(this, this.committed);
}
complete(
- page: number,
+ page: string,
status: RecordStatus,
searchText: string,
metadata: string,
@@ -144,10 +183,10 @@ export class RecordCriteria {
) {
this.isChangingAutomatically = true;
- this.page = Number(page ?? 1);
this.status = status ?? "pending";
this.searchText = searchText ?? "";
+ this.page.complete(page);
this.metadata.complete(metadata);
this.sortBy.complete(sortBy);
this.response.complete(response);
@@ -156,69 +195,71 @@ export class RecordCriteria {
}
commit() {
- // TODO: Move to instance of commit
- const similaritySearchCommitted = new SimilarityCriteria();
- const metadataCommitted = new MetadataCriteria();
- const sortByCommitted = new SortCriteria();
- const responseCommitted = new ResponseCriteria();
- const suggestionCommitted = new SuggestionCriteria();
+ if (this.committed) {
+ this.previous = new CommittedRecordCriteria(this.committed);
+ }
- similaritySearchCommitted.withValue(
- this.similaritySearch.recordId,
- this.similaritySearch.vectorName,
- this.similaritySearch.limit,
- this.similaritySearch.order
- );
- metadataCommitted.withValue(this.metadata.value);
- sortByCommitted.witValue(this.sortBy.value);
- responseCommitted.withValue(this.response.value);
- suggestionCommitted.withValue(this.suggestion.value);
-
- this.committed = {
- page: this.page,
- status: this.status,
- searchText: this.searchText,
-
- metadata: metadataCommitted,
- sortBy: sortByCommitted,
- response: responseCommitted,
- suggestion: suggestionCommitted,
- similaritySearch: similaritySearchCommitted,
- };
+ this.committed = new CommittedRecordCriteria(this);
+
+ if (!this.areDifferent(this.committed, this.previous)) {
+ this.previous = null;
+ }
this.isChangingAutomatically = false;
}
rollback() {
- this.page = this.committed.page;
this.status = this.committed.status;
this.searchText = this.committed.searchText;
- this.metadata = this.committed.metadata;
-
- this.metadata.withValue(this.committed.metadata.value);
- this.sortBy.witValue(this.committed.sortBy.value);
- this.response.withValue(this.committed.response.value);
- this.suggestion.withValue(this.committed.suggestion.value);
- this.similaritySearch.withValue(
- this.committed.similaritySearch.recordId,
- this.committed.similaritySearch.vectorName,
- this.committed.similaritySearch.limit,
- this.committed.similaritySearch.order
- );
+
+ this.page.withValue(this.committed.page);
+ this.metadata.withValue(this.committed.metadata);
+ this.sortBy.withValue(this.committed.sortBy);
+ this.response.withValue(this.committed.response);
+ this.suggestion.withValue(this.committed.suggestion);
+ this.similaritySearch.withValue(this.committed.similaritySearch);
}
reset() {
+ // Not call the similaritySearch.reset() because it is not managed as a global filter.
+ // Not clear the searchText because it is not managed as a global filter.
+
+ this.page.reset();
this.metadata.reset();
this.sortBy.reset();
this.response.reset();
this.suggestion.reset();
}
+ get queuePage(): number {
+ return this.isFilteringBySimilarity
+ ? this.page.server.from
+ : this.page.client.page;
+ }
+
nextPage() {
- this.page = this.committed.page + 1;
+ this.page.goTo(this.committed.page.next);
}
previousPage() {
- this.page = this.committed.page - 1;
+ this.page.goTo(this.committed.page.previous);
+ }
+
+ private areDifferent(previous: IRecordCriteria, actual: IRecordCriteria) {
+ if (!previous) return false;
+ if (!actual) return false;
+
+ if (actual.status !== previous.status) return true;
+ if (actual.searchText !== previous.searchText) return true;
+
+ if (!previous.page.isEqual(actual.page)) return true;
+ if (!previous.metadata.isEqual(actual.metadata)) return true;
+ if (!previous.sortBy.isEqual(actual.sortBy)) return true;
+ if (!previous.response.isEqual(actual.response)) return true;
+ if (!previous.suggestion.isEqual(actual.suggestion)) return true;
+ if (!previous.similaritySearch.isEqual(actual.similaritySearch))
+ return true;
+
+ return false;
}
}
diff --git a/frontend/v1/domain/entities/record/Records.test.ts b/frontend/v1/domain/entities/record/Records.test.ts
index 3e509113c3..86943ed58a 100644
--- a/frontend/v1/domain/entities/record/Records.test.ts
+++ b/frontend/v1/domain/entities/record/Records.test.ts
@@ -1,3 +1,4 @@
+import { PageCriteria } from "../page/PageCriteria";
import { Record } from "./Record";
import { RecordCriteria } from "./RecordCriteria";
import { Records } from "./Records";
@@ -37,21 +38,33 @@ describe("Records", () => {
describe("existsRecordOn", () => {
test("should return true when the record exists", () => {
+ const page = new PageCriteria();
+ page.client = {
+ page: 1,
+ many: 10,
+ };
+
const records = new Records([
new Record("1", "1", [], [], null, [], 1, 1),
]);
- const exists = records.existsRecordOn(1);
+ const exists = records.existsRecordOn(page);
expect(exists).toBeTruthy();
});
test("should return false when the record not exists in this page", () => {
+ const page = new PageCriteria();
+ page.client = {
+ page: 1,
+ many: 10,
+ };
+
const records = new Records([
new Record("1", "1", [], [], null, [], 1, 2),
]);
- const exists = records.existsRecordOn(1);
+ const exists = records.existsRecordOn(page);
expect(exists).toBeFalsy();
});
@@ -59,21 +72,33 @@ describe("Records", () => {
describe("getRecordOn", () => {
test("should return the record when the record exists", () => {
+ const page = new PageCriteria();
+ page.client = {
+ page: 1,
+ many: 10,
+ };
+
const records = new Records([
new Record("1", "1", [], [], null, [], 1, 1),
]);
- const record = records.getRecordOn(1);
+ const record = records.getRecordOn(page);
expect(record).toEqual(new Record("1", "1", [], [], null, [], 1, 1));
});
test("should return undefined when the record not exists in this page", () => {
+ const page = new PageCriteria();
+ page.client = {
+ page: 1,
+ many: 10,
+ };
+
const records = new Records([
new Record("1", "1", [], [], null, [], 1, 2),
]);
- const record = records.getRecordOn(1);
+ const record = records.getRecordOn(page);
expect(record).toBeUndefined();
});
@@ -98,11 +123,11 @@ describe("Records", () => {
});
});
- describe("getPageToFind", () => {
+ describe("synchronizePagination", () => {
test("the current page should be from 1 to 10 when no have records", () => {
const criteria = new RecordCriteria(
"1",
- 1,
+ "1",
"pending",
"",
"",
@@ -113,15 +138,15 @@ describe("Records", () => {
);
const records = new Records([]);
- const pageToFind = records.getPageToFind(criteria);
+ records.synchronizeQueuePagination(criteria);
- expect(pageToFind).toEqual({ from: 1, many: 10 });
+ expect(criteria.page.server).toEqual({ from: 1, many: 10 });
});
test("the page should be from 10 and many 10 when the user submit one record in current queue and going to forward", () => {
const criteria = new RecordCriteria(
"1",
- 10,
+ "10",
"pending",
"",
"",
@@ -149,17 +174,17 @@ describe("Records", () => {
updatedAt: "2021-01-01",
});
- criteria.page += 1;
+ criteria.nextPage();
- const pageToFind = records.getPageToFind(criteria);
+ records.synchronizeQueuePagination(criteria);
- expect(pageToFind).toEqual({ from: 10, many: 10 });
+ expect(criteria.page.server).toEqual({ from: 10, many: 10 });
});
test("the page should be from 9 and many 10 when the user submit two record in current queue and going to forward", () => {
const criteria = new RecordCriteria(
"1",
- 10,
+ "10",
"pending",
"",
"",
@@ -193,17 +218,17 @@ describe("Records", () => {
updatedAt: "2021-01-01",
});
- criteria.page += 1;
+ criteria.nextPage();
- const pageToFind = records.getPageToFind(criteria);
+ records.synchronizeQueuePagination(criteria);
- expect(pageToFind).toEqual({ from: 9, many: 10 });
+ expect(criteria.page.server).toEqual({ from: 9, many: 10 });
});
test("the page should be from 2 and many 1 when the user start with page 3 and go to backward", () => {
const criteria = new RecordCriteria(
"1",
- 3,
+ "3",
"pending",
"",
"",
@@ -216,17 +241,17 @@ describe("Records", () => {
new Record("1", "1", [], [], null, [], 1, 3),
new Record("2", "1", [], [], null, [], 1, 4),
]);
- criteria.page -= 1;
+ criteria.previousPage();
- const pageToFind = records.getPageToFind(criteria);
+ records.synchronizeQueuePagination(criteria);
- expect(pageToFind).toEqual({ from: 2, many: 1 });
+ expect(criteria.page.server).toEqual({ from: 2, many: 1 });
});
test("the current page should be from 1 and many 50 when the user is filtering by similarity", () => {
const criteria = new RecordCriteria(
"1",
- 3,
+ "3",
"pending",
"",
"",
@@ -237,15 +262,15 @@ describe("Records", () => {
);
const records = new Records([]);
- const pageToFind = records.getPageToFind(criteria);
+ records.synchronizeQueuePagination(criteria);
- expect(pageToFind).toEqual({ from: 1, many: 50 });
+ expect(criteria.page.server).toEqual({ from: 1, many: 50 });
});
test("the current page should be from 1 and many 50 when the user is filtering by similarity but is going to backward", () => {
const criteria = new RecordCriteria(
"1",
- 3,
+ "3",
"pending",
"",
"",
@@ -258,17 +283,17 @@ describe("Records", () => {
new Record("1", "1", [], [], null, [], 1, 3),
new Record("2", "1", [], [], null, [], 1, 4),
]);
- criteria.page -= 1;
+ criteria.previousPage();
- const pageToFind = records.getPageToFind(criteria);
+ records.synchronizeQueuePagination(criteria);
- expect(pageToFind).toEqual({ from: 1, many: 50 });
+ expect(criteria.page.server).toEqual({ from: 1, many: 50 });
});
test("the current page should be from 1 and many 10 when the user is paginating forward from page 8 and the queue has 8 records in draft but status is pending", () => {
const criteria = new RecordCriteria(
"1",
- 9,
+ "9",
"pending",
"",
"",
@@ -360,9 +385,58 @@ describe("Records", () => {
),
]);
- const pageToFind = records.getPageToFind(criteria);
+ records.synchronizeQueuePagination(criteria);
+
+ expect(criteria.page.server).toEqual({ from: 1, many: 10 });
+ });
+
+ test("when the user is in bulk mode but was in focus mode the page should be from 1 and many 10", () => {
+ const criteria = new RecordCriteria(
+ "1",
+ "5",
+ "pending",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ );
+ const records = new Records([
+ new Record("1", "1", [], [], null, [], 1, 5),
+ ]);
+
+ criteria.page.bulkMode();
+
+ records.synchronizeQueuePagination(criteria);
+
+ expect(criteria.page.server).toEqual({ from: 1, many: 10 });
+ });
+
+ test("when the user was in bulk mode but is in focus mode the page should be from 1 and many 10", () => {
+ const criteria = new RecordCriteria(
+ "1",
+ "55",
+ "pending",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ );
+ const records = new Records([
+ new Record("1", "1", [], [], null, [], 1, 55),
+ ]);
+
+ criteria.page.bulkMode();
+ criteria.commit();
+
+ criteria.page.focusMode();
+
+ records.synchronizeQueuePagination(criteria);
- expect(pageToFind).toEqual({ from: 1, many: 10 });
+ expect(criteria.page.server).toEqual({ from: 1, many: 10 });
});
});
diff --git a/frontend/v1/domain/entities/record/Records.ts b/frontend/v1/domain/entities/record/Records.ts
index 9fc53e6294..f0e69f6b19 100644
--- a/frontend/v1/domain/entities/record/Records.ts
+++ b/frontend/v1/domain/entities/record/Records.ts
@@ -1,10 +1,8 @@
-import { Pagination } from "../Pagination";
+import { PageCriteria } from "../page/PageCriteria";
import { Record } from "./Record";
import { RecordStatus } from "./RecordAnswer";
import { RecordCriteria } from "./RecordCriteria";
-const NEXT_RECORDS_TO_FETCH = 10;
-
export class Records {
constructor(
public records: Record[] = [],
@@ -17,48 +15,64 @@ export class Records {
return this.records.length > 0;
}
- existsRecordOn(page: number) {
- return !!this.getRecordOn(page);
+ existsRecordOn(criteria: PageCriteria) {
+ return !!this.getRecordOn(criteria);
+ }
+
+ getRecordOn(criteria: PageCriteria) {
+ return this.records.find((record) => record.page === criteria.client.page);
}
- getRecordOn(page: number) {
- return this.records.find((record) => record.page === page);
+ getRecordsOn(criteria: PageCriteria): Record[] {
+ return this.records
+ .filter((record) => record.page >= criteria.client.page)
+ .splice(0, criteria.client.many);
}
getById(recordId: string): Record {
return this.records.find((record) => record.id === recordId);
}
- getPageToFind(criteria: RecordCriteria): Pagination {
- const { page, status, isFilteringBySimilarity, similaritySearch } =
- criteria;
-
- if (isFilteringBySimilarity)
- return { from: 1, many: similaritySearch.limit };
-
- const currentPage: Pagination = {
- from: page,
- many: NEXT_RECORDS_TO_FETCH,
- };
-
- if (!this.hasRecordsToAnnotate) return currentPage;
-
- const isMovingToNext = page > this.lastRecord.page;
-
- if (isMovingToNext) {
- const recordsAnnotated = this.recordsAnnotatedOnQueue(status);
-
- return {
- from: this.lastRecord.page + 1 - recordsAnnotated,
- many: NEXT_RECORDS_TO_FETCH,
- };
- } else if (this.firstRecord.page > page)
- return {
- from: this.firstRecord.page - 1,
- many: 1,
- };
-
- return currentPage;
+ synchronizeQueuePagination(criteria: RecordCriteria): void {
+ const {
+ page,
+ status,
+ isFilteringBySimilarity,
+ similaritySearch,
+ committed,
+ } = criteria;
+
+ if (page.isBulkMode && committed.page.isFocusMode) return;
+ if (page.isFocusMode && committed.page.isBulkMode) return;
+
+ if (isFilteringBySimilarity) {
+ return page.synchronizePagination({
+ from: 1,
+ many: similaritySearch.limit,
+ });
+ }
+
+ if (this.hasRecordsToAnnotate) {
+ const isMovingForward = page.client.page > this.lastRecord.page;
+
+ if (isMovingForward) {
+ const recordsAnnotated = this.recordsAnnotatedOnQueue(status);
+
+ return page.synchronizePagination({
+ from: this.lastRecord.page + 1 - recordsAnnotated,
+ many: page.client.many,
+ });
+ } else if (this.firstRecord.page > page.client.page)
+ return page.synchronizePagination({
+ from: this.firstRecord.page - 1,
+ many: 1,
+ });
+ }
+
+ page.synchronizePagination({
+ from: page.client.page,
+ many: page.client.many,
+ });
}
append(newRecords: Records) {
diff --git a/frontend/v1/domain/entities/response/ResponseCriteria.ts b/frontend/v1/domain/entities/response/ResponseCriteria.ts
index f98c3c415c..2c28739323 100644
--- a/frontend/v1/domain/entities/response/ResponseCriteria.ts
+++ b/frontend/v1/domain/entities/response/ResponseCriteria.ts
@@ -49,8 +49,8 @@ export class ResponseCriteria extends Criteria {
});
}
- withValue(value: ResponseSearch[]) {
- this.value = value.map((v) => {
+ withValue(responseCriteria: ResponseCriteria) {
+ this.value = responseCriteria.value.map((v) => {
return {
name: v.name,
value: v.value,
diff --git a/frontend/v1/domain/entities/similarity/SimilarityCriteria.test.ts b/frontend/v1/domain/entities/similarity/SimilarityCriteria.test.ts
index c4af1c6831..dac92d4b47 100644
--- a/frontend/v1/domain/entities/similarity/SimilarityCriteria.test.ts
+++ b/frontend/v1/domain/entities/similarity/SimilarityCriteria.test.ts
@@ -4,7 +4,10 @@ describe("SimilarityCriteria ", () => {
describe("reset should", () => {
test("set default criteria values", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue("recordId", "vectorName", 50, "least");
+ criteria.recordId = "recordId";
+ criteria.vectorName = "vectorName";
+ criteria.limit = 50;
+ criteria.order = "least";
criteria.reset();
@@ -18,21 +21,30 @@ describe("SimilarityCriteria ", () => {
describe("isCompleted should", () => {
test("return true when all criteria are defined", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue("recordId", "vectorName", 50, "least");
+ criteria.recordId = "recordId";
+ criteria.vectorName = "vectorName";
+ criteria.limit = 50;
+ criteria.order = "least";
expect(criteria.isCompleted).toBe(true);
});
test("return false when recordId is undefined", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue(undefined, "vectorName", 50, "least");
+ criteria.recordId = undefined;
+ criteria.vectorName = "vectorName";
+ criteria.limit = 50;
+ criteria.order = "least";
expect(criteria.isCompleted).toBe(false);
});
test("return false when vectorName is undefined", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue("recordId", undefined, 50, "least");
+ criteria.recordId = "recordId";
+ criteria.vectorName = undefined;
+ criteria.limit = 50;
+ criteria.order = "least";
expect(criteria.isCompleted).toBe(false);
});
@@ -41,45 +53,80 @@ describe("SimilarityCriteria ", () => {
describe("isEqual should", () => {
test("return true when all criteria are equal", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue("recordId", "vectorName", 50, "least");
+ criteria.recordId = "recordId";
+ criteria.vectorName = "vectorName";
+ criteria.limit = 50;
+ criteria.order = "least";
+
const other = new SimilarityCriteria();
- other.withValue("recordId", "vectorName", 50, "least");
+ other.recordId = "recordId";
+ other.vectorName = "vectorName";
+ other.limit = 50;
+ other.order = "least";
expect(criteria.isEqual(other)).toBe(true);
});
test("return false when recordId is different", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue("recordId", "vectorName", 50, "least");
+ criteria.recordId = "recordId";
+ criteria.vectorName = "vectorName";
+ criteria.limit = 50;
+ criteria.order = "least";
+
const other = new SimilarityCriteria();
- other.withValue("otherRecordId", "vectorName", 50, "least");
+ other.recordId = "otherRecordId";
+ other.vectorName = "vectorName";
+ other.limit = 50;
+ other.order = "least";
expect(criteria.isEqual(other)).toBe(false);
});
test("return false when vectorName is different", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue("recordId", "vectorName", 50, "least");
+ criteria.recordId = "recordId";
+ criteria.vectorName = "vectorName";
+ criteria.limit = 50;
+ criteria.order = "least";
+
const other = new SimilarityCriteria();
- other.withValue("recordId", "othervectorName", 50, "least");
+ other.recordId = "recordId";
+ other.vectorName = "otherVectorName";
+ other.limit = 50;
+ other.order = "least";
expect(criteria.isEqual(other)).toBe(false);
});
test("return false when limit is different", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue("recordId", "vectorName", 50, "least");
+ criteria.recordId = "recordId";
+ criteria.vectorName = "vectorName";
+ criteria.limit = 50;
+ criteria.order = "least";
+
const other = new SimilarityCriteria();
- other.withValue("recordId", "vectorName", 100, "least");
+ other.recordId = "recordId";
+ other.vectorName = "vectorName";
+ other.limit = 100;
+ other.order = "least";
expect(criteria.isEqual(other)).toBe(false);
});
test("return false when order is different", () => {
const criteria = new SimilarityCriteria();
- criteria.withValue("recordId", "vectorName", 50, "least");
+ criteria.recordId = "recordId";
+ criteria.vectorName = "vectorName";
+ criteria.limit = 50;
+ criteria.order = "least";
+
const other = new SimilarityCriteria();
- other.withValue("recordId", "vectorName", 50, "most");
+ other.recordId = "recordId";
+ other.vectorName = "vectorName";
+ other.limit = 50;
+ other.order = "most";
expect(criteria.isEqual(other)).toBe(false);
});
diff --git a/frontend/v1/domain/entities/similarity/SimilarityCriteria.ts b/frontend/v1/domain/entities/similarity/SimilarityCriteria.ts
index ed7927e998..2ab3ef50a4 100644
--- a/frontend/v1/domain/entities/similarity/SimilarityCriteria.ts
+++ b/frontend/v1/domain/entities/similarity/SimilarityCriteria.ts
@@ -27,12 +27,9 @@ export class SimilarityCriteria extends Criteria {
this.order = params[7] as SimilarityOrder;
}
- withValue(
- recordId: string,
- vectorName: string,
- limit: number,
- order: SimilarityOrder
- ) {
+ withValue(similarityCriteria: SimilarityCriteria) {
+ const { recordId, vectorName, limit, order } = similarityCriteria;
+
if (!recordId && !vectorName && !limit && !order) return;
this.recordId = recordId;
diff --git a/frontend/v1/domain/entities/sort/SortCriteria.ts b/frontend/v1/domain/entities/sort/SortCriteria.ts
index 25a57dd9ce..11d01b7b6e 100644
--- a/frontend/v1/domain/entities/sort/SortCriteria.ts
+++ b/frontend/v1/domain/entities/sort/SortCriteria.ts
@@ -14,8 +14,8 @@ export class SortCriteria extends Criteria {
});
}
- witValue(value: SortSearch[]) {
- this.value = value.map((v) => ({ ...v }));
+ withValue(sortCriteria: SortCriteria) {
+ this.value = sortCriteria.value.map((v) => ({ ...v }));
}
reset() {
diff --git a/frontend/v1/domain/entities/suggestion/SuggestionCriteria.ts b/frontend/v1/domain/entities/suggestion/SuggestionCriteria.ts
index f1b3ef1116..a0bbb1f60a 100644
--- a/frontend/v1/domain/entities/suggestion/SuggestionCriteria.ts
+++ b/frontend/v1/domain/entities/suggestion/SuggestionCriteria.ts
@@ -70,8 +70,8 @@ export class SuggestionCriteria extends Criteria {
});
}
- withValue(value: SuggestionSearch[]) {
- this.value = value.map((v) => {
+ withValue(suggestionCriteria: SuggestionCriteria) {
+ this.value = suggestionCriteria.value.map((v) => {
return {
name: v.name,
value: v.value,
diff --git a/frontend/v1/domain/usecases/clear-record-use-case.ts b/frontend/v1/domain/usecases/clear-record-use-case.ts
deleted file mode 100644
index 2ce515e839..0000000000
--- a/frontend/v1/domain/usecases/clear-record-use-case.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { IEventDispatcher } from "@codescouts/events";
-import { RecordResponseUpdatedEvent } from "../events/RecordResponseUpdatedEvent";
-import { Record } from "@/v1/domain/entities/record/Record";
-import { RecordRepository } from "@/v1/infrastructure/repositories";
-
-export class ClearRecordUseCase {
- constructor(
- private readonly recordRepository: RecordRepository,
- private readonly eventDispatcher: IEventDispatcher
- ) {}
-
- async execute(record: Record) {
- await this.recordRepository.deleteRecordResponse(record);
-
- if (record.answer) {
- this.eventDispatcher.dispatch(new RecordResponseUpdatedEvent(record));
- }
-
- record.clear();
- }
-}
diff --git a/frontend/v1/domain/usecases/discard-bulk-annotation-use-case.ts b/frontend/v1/domain/usecases/discard-bulk-annotation-use-case.ts
new file mode 100644
index 0000000000..10b64ca92e
--- /dev/null
+++ b/frontend/v1/domain/usecases/discard-bulk-annotation-use-case.ts
@@ -0,0 +1,33 @@
+import { IEventDispatcher } from "@codescouts/events";
+import { Record } from "../entities/record/Record";
+import { RecordResponseUpdatedEvent } from "../events/RecordResponseUpdatedEvent";
+import { RecordRepository } from "~/v1/infrastructure/repositories";
+
+export class DiscardBulkAnnotationUseCase {
+ constructor(
+ private readonly recordRepository: RecordRepository,
+ private readonly eventDispatcher: IEventDispatcher
+ ) {}
+
+ async execute(records: Record[], recordReference: Record) {
+ records.forEach((record) => record.answerWith(recordReference));
+
+ const responses = await this.recordRepository.discardBulkRecordResponse(
+ records
+ );
+
+ responses
+ .filter((r) => r.success)
+ .forEach(({ recordId, response }) => {
+ const record = records.find((r) => r.id === recordId);
+
+ record.discard(response);
+ });
+
+ this.eventDispatcher.dispatch(
+ new RecordResponseUpdatedEvent(recordReference)
+ );
+
+ return responses.every((r) => r.success);
+ }
+}
diff --git a/frontend/v1/domain/usecases/load-records-to-annotate-use-case.ts b/frontend/v1/domain/usecases/load-records-to-annotate-use-case.ts
index 6dc035df25..922f0d4eaa 100644
--- a/frontend/v1/domain/usecases/load-records-to-annotate-use-case.ts
+++ b/frontend/v1/domain/usecases/load-records-to-annotate-use-case.ts
@@ -26,8 +26,8 @@ export class LoadRecordsToAnnotateUseCase {
let newRecords = await this.loadRecords(criteria);
let isRecordExistForCurrentPage = newRecords.existsRecordOn(page);
- if (!isRecordExistForCurrentPage && page !== 1) {
- criteria.page = 1;
+ if (!isRecordExistForCurrentPage && !page.isFirstPage()) {
+ criteria.page.goToFirst();
newRecords = await this.loadRecords(criteria);
@@ -74,11 +74,11 @@ export class LoadRecordsToAnnotateUseCase {
}
private async loadRecords(criteria: RecordCriteria) {
- const { datasetId, page } = criteria;
+ const { datasetId } = criteria;
const savedRecords = this.recordsStorage.get();
- const pagination = savedRecords.getPageToFind(criteria);
+ savedRecords.synchronizeQueuePagination(criteria);
- const getRecords = this.recordRepository.getRecords(criteria, pagination);
+ const getRecords = this.recordRepository.getRecords(criteria);
const getQuestions = this.questionRepository.getQuestions(datasetId);
const getFields = this.fieldRepository.getFields(datasetId);
@@ -87,9 +87,7 @@ export class LoadRecordsToAnnotateUseCase {
const recordsToAnnotate = recordsFromBackend.records.map(
(record, index) => {
- const recordPage = criteria.isFilteringBySimilarity
- ? index + pagination.from
- : index + page;
+ const recordPage = index + criteria.queuePage;
const fields = fieldsFromBackend
.filter((f) => record.fields[f.name])
@@ -127,15 +125,17 @@ export class LoadRecordsToAnnotateUseCase {
)
: null;
- const suggestions = record.suggestions.map((suggestion) => {
- return new Suggestion(
- suggestion.id,
- suggestion.question_id,
- suggestion.value,
- suggestion.score,
- suggestion.agent
- );
- });
+ const suggestions = !criteria.page.isBulkMode
+ ? record.suggestions.map((suggestion) => {
+ return new Suggestion(
+ suggestion.id,
+ suggestion.question_id,
+ suggestion.value,
+ suggestion.score,
+ suggestion.agent
+ );
+ })
+ : [];
return new Record(
record.id,
diff --git a/frontend/v1/domain/usecases/save-draft-bulk-annotation-use-case.ts b/frontend/v1/domain/usecases/save-draft-bulk-annotation-use-case.ts
new file mode 100644
index 0000000000..883b98cc51
--- /dev/null
+++ b/frontend/v1/domain/usecases/save-draft-bulk-annotation-use-case.ts
@@ -0,0 +1,31 @@
+import { IEventDispatcher } from "@codescouts/events";
+import { Record } from "../entities/record/Record";
+import { RecordResponseUpdatedEvent } from "../events/RecordResponseUpdatedEvent";
+import { RecordRepository } from "~/v1/infrastructure/repositories";
+
+export class SaveDraftBulkAnnotationUseCase {
+ constructor(
+ private readonly recordRepository: RecordRepository,
+ private readonly eventDispatcher: IEventDispatcher
+ ) {}
+
+ async execute(records: Record[], recordReference: Record): Promise {
+ records.forEach((record) => record.answerWith(recordReference));
+
+ const responses = await this.recordRepository.saveDraftBulkRecordResponse(
+ records
+ );
+
+ responses
+ .filter((r) => r.success)
+ .forEach(({ recordId, response }) => {
+ const record = records.find((r) => r.id === recordId);
+
+ record.submit(response);
+ });
+
+ this.eventDispatcher.dispatch(
+ new RecordResponseUpdatedEvent(recordReference)
+ );
+ }
+}
diff --git a/frontend/v1/domain/usecases/save-draft-use-case.ts b/frontend/v1/domain/usecases/save-draft-use-case.ts
index 71c482cd37..4af6ce86ab 100644
--- a/frontend/v1/domain/usecases/save-draft-use-case.ts
+++ b/frontend/v1/domain/usecases/save-draft-use-case.ts
@@ -3,7 +3,7 @@ import { RecordResponseUpdatedEvent } from "../events/RecordResponseUpdatedEvent
import { Record } from "../entities/record/Record";
import { RecordRepository } from "@/v1/infrastructure/repositories";
-export class SaveDraftRecord {
+export class SaveDraftUseCase {
constructor(
private readonly recordRepository: RecordRepository,
private readonly eventDispatcher: IEventDispatcher
diff --git a/frontend/v1/domain/usecases/submit-bulk-annotation-use-case.ts b/frontend/v1/domain/usecases/submit-bulk-annotation-use-case.ts
new file mode 100644
index 0000000000..86bd43661f
--- /dev/null
+++ b/frontend/v1/domain/usecases/submit-bulk-annotation-use-case.ts
@@ -0,0 +1,33 @@
+import { IEventDispatcher } from "@codescouts/events";
+import { Record } from "../entities/record/Record";
+import { RecordResponseUpdatedEvent } from "../events/RecordResponseUpdatedEvent";
+import { RecordRepository } from "~/v1/infrastructure/repositories";
+
+export class SubmitBulkAnnotationUseCase {
+ constructor(
+ private readonly recordRepository: RecordRepository,
+ private readonly eventDispatcher: IEventDispatcher
+ ) {}
+
+ async execute(records: Record[], recordReference: Record) {
+ records.forEach((record) => record.answerWith(recordReference));
+
+ const responses = await this.recordRepository.submitBulkRecordResponse(
+ records
+ );
+
+ responses
+ .filter((r) => r.success)
+ .forEach(({ recordId, response }) => {
+ const record = records.find((r) => r.id === recordId);
+
+ record.submit(response);
+ });
+
+ this.eventDispatcher.dispatch(
+ new RecordResponseUpdatedEvent(recordReference)
+ );
+
+ return responses.every((r) => r.success);
+ }
+}
diff --git a/frontend/v1/domain/usecases/submit-record-use-case.ts b/frontend/v1/domain/usecases/submit-record-use-case.ts
index f9ed81014d..1f3cabeda7 100644
--- a/frontend/v1/domain/usecases/submit-record-use-case.ts
+++ b/frontend/v1/domain/usecases/submit-record-use-case.ts
@@ -10,9 +10,7 @@ export class SubmitRecordUseCase {
) {}
async execute(record: Record) {
- const response = await this.recordRepository.submitNewRecordResponse(
- record
- );
+ const response = await this.recordRepository.submitRecordResponse(record);
record.submit(response);
diff --git a/frontend/v1/infrastructure/repositories/MetadataMetricsRepository.ts b/frontend/v1/infrastructure/repositories/MetadataMetricsRepository.ts
index e458f5cb87..c52c554c1f 100644
--- a/frontend/v1/infrastructure/repositories/MetadataMetricsRepository.ts
+++ b/frontend/v1/infrastructure/repositories/MetadataMetricsRepository.ts
@@ -10,9 +10,10 @@ export class MetadataMetricsRepository {
async getMetric(metadataId: string): Promise {
try {
- const url = `/v1/metadata-properties/${metadataId}/metrics`;
-
- const { data } = await this.axios.get(url);
+ const { data } = await this.axios.get(
+ `/v1/metadata-properties/${metadataId}/metrics`,
+ { headers: { "cache-control": "max-age=120" } }
+ );
return {
id: metadataId,
diff --git a/frontend/v1/infrastructure/repositories/MetadataRepository.ts b/frontend/v1/infrastructure/repositories/MetadataRepository.ts
index da426d12ab..2fd5750a7c 100644
--- a/frontend/v1/infrastructure/repositories/MetadataRepository.ts
+++ b/frontend/v1/infrastructure/repositories/MetadataRepository.ts
@@ -16,11 +16,9 @@ export class MetadataRepository {
async getMetadataFilters(datasetId: string) {
try {
- const url = `/v1/me/datasets/${datasetId}/metadata-properties`;
+ const items = await this.getMetadataProperties(datasetId);
- const { data } = await this.axios.get>(url);
-
- return this.completeEmptyMetadataFilters(data.items);
+ return this.completeEmptyMetadataFilters(items);
} catch (err) {
throw {
response: METADATA_API_ERRORS.ERROR_FETCHING_METADATA,
@@ -31,8 +29,10 @@ export class MetadataRepository {
async getMetadataProperties(datasetId: string) {
try {
// TODO: Review this endpoint, for admin should be /v1/datasets/${datasetId}/metadata-properties without ME.
- const url = `/v1/me/datasets/${datasetId}/metadata-properties`;
- const { data } = await this.axios.get>(url);
+ const { data } = await this.axios.get>(
+ `/v1/me/datasets/${datasetId}/metadata-properties`,
+ { headers: { "cache-control": "max-age=120" } }
+ );
return data.items;
} catch (err) {
diff --git a/frontend/v1/infrastructure/repositories/RecordRepository.ts b/frontend/v1/infrastructure/repositories/RecordRepository.ts
index 4478f7e131..7dc0d4c8df 100644
--- a/frontend/v1/infrastructure/repositories/RecordRepository.ts
+++ b/frontend/v1/infrastructure/repositories/RecordRepository.ts
@@ -2,7 +2,7 @@ import { type NuxtAxiosInstance } from "@nuxtjs/axios";
import {
BackedRecord,
BackendAnswerCombinations,
- BackendResponse,
+ BackendResponseResponse,
BackendSearchRecords,
BackendAdvanceSearchQuery,
ResponseWithTotal,
@@ -10,12 +10,14 @@ import {
BackendRecordStatus,
BackendSimilaritySearchOrder,
BackendSort,
+ BackendResponseBulkRequest,
+ BackendResponseRequest,
+ BackendResponseBulkResponse,
} from "../types";
import { RecordAnswer } from "@/v1/domain/entities/record/RecordAnswer";
import { Record } from "@/v1/domain/entities/record/Record";
import { Question } from "@/v1/domain/entities/question/Question";
import { RecordCriteria } from "@/v1/domain/entities/record/RecordCriteria";
-import { Pagination } from "@/v1/domain/entities/Pagination";
import { SimilarityOrder } from "@/v1/domain/entities/similarity/SimilarityCriteria";
import { RangeValue, ValuesOption } from "~/v1/domain/entities/common/Filter";
@@ -25,6 +27,7 @@ const RECORD_API_ERRORS = {
ERROR_DELETING_RECORD_RESPONSE: "ERROR_DELETING_RECORD_RESPONSE",
ERROR_UPDATING_RECORD_RESPONSE: "ERROR_UPDATING_RECORD_RESPONSE",
ERROR_CREATING_RECORD_RESPONSE: "ERROR_CREATING_RECORD_RESPONSE",
+ ERROR_CREATING_RECORD_RESPONSE_BULK: "ERROR_CREATING_RECORD_RESPONSE_BULK",
};
const BACKEND_ORDER: {
@@ -37,14 +40,11 @@ const BACKEND_ORDER: {
export class RecordRepository {
constructor(private readonly axios: NuxtAxiosInstance) {}
- getRecords(
- criteria: RecordCriteria,
- pagination: Pagination
- ): Promise {
+ getRecords(criteria: RecordCriteria): Promise {
if (criteria.isFilteringByAdvanceSearch)
- return this.getRecordsByAdvanceSearch(criteria, pagination);
+ return this.getRecordsByAdvanceSearch(criteria);
- return this.getRecordsByDatasetId(criteria, pagination);
+ return this.getRecordsByDatasetId(criteria);
}
async getRecord(recordId: string): Promise {
@@ -79,12 +79,24 @@ export class RecordRepository {
return this.createRecordResponse(record, "discarded");
}
- submitNewRecordResponse(record: Record): Promise {
+ submitRecordResponse(record: Record): Promise {
if (record.answer) return this.updateRecordResponse(record, "submitted");
return this.createRecordResponse(record, "submitted");
}
+ discardBulkRecordResponse(records: Record[]) {
+ return this.createBulkRecord(records, "discarded");
+ }
+
+ submitBulkRecordResponse(records: Record[]) {
+ return this.createBulkRecord(records, "submitted");
+ }
+
+ saveDraftBulkRecordResponse(records: Record[]) {
+ return this.createBulkRecord(records, "draft");
+ }
+
saveDraft(record: Record): Promise {
if (record.answer) return this.updateRecordResponse(record, "draft");
@@ -98,7 +110,7 @@ export class RecordRepository {
try {
const request = this.createRequest(status, record.questions);
- const { data } = await this.axios.put(
+ const { data } = await this.axios.put(
`/v1/responses/${record.answer.id}`,
request
);
@@ -111,6 +123,43 @@ export class RecordRepository {
}
}
+ private async createBulkRecord(
+ records: Record[],
+ status: BackendRecordStatus
+ ) {
+ try {
+ const request = this.createRequestForBulk(status, records);
+
+ const { data } = await this.axios.post(
+ "/v1/me/responses/bulk",
+ request
+ );
+
+ return data.items.map(({ item, error }) => {
+ if (item) {
+ return {
+ success: true,
+ recordId: item.record_id,
+ response: new RecordAnswer(
+ item.id,
+ item.status,
+ item.values,
+ item.updated_at
+ ),
+ };
+ }
+
+ return {
+ error: error.detail,
+ };
+ });
+ } catch (error) {
+ throw {
+ response: RECORD_API_ERRORS.ERROR_CREATING_RECORD_RESPONSE_BULK,
+ };
+ }
+ }
+
private async createRecordResponse(
record: Record,
status: BackendRecordStatus
@@ -118,7 +167,7 @@ export class RecordRepository {
try {
const request = this.createRequest(status, record.questions);
- const { data } = await this.axios.post(
+ const { data } = await this.axios.post(
`/v1/records/${record.id}/responses`,
request
);
@@ -137,11 +186,10 @@ export class RecordRepository {
}
private async getRecordsByDatasetId(
- criteria: RecordCriteria,
- pagination: Pagination
+ criteria: RecordCriteria
): Promise {
- const { datasetId, status } = criteria;
- const { from, many } = pagination;
+ const { datasetId, status, page } = criteria;
+ const { from, many } = page.server;
try {
const url = `/v1/me/datasets/${datasetId}/records`;
@@ -167,11 +215,11 @@ export class RecordRepository {
}
private async getRecordsByAdvanceSearch(
- criteria: RecordCriteria,
- pagination: Pagination
+ criteria: RecordCriteria
): Promise {
const {
datasetId,
+ page,
status,
searchText,
metadata,
@@ -186,7 +234,7 @@ export class RecordRepository {
isFilteringBySuggestion,
isSortingBy,
} = criteria;
- const { from, many } = pagination;
+ const { from, many } = page.server;
try {
const url = `/v1/me/datasets/${datasetId}/records/search`;
@@ -415,10 +463,28 @@ export class RecordRepository {
}
}
+ private createRequestForBulk(
+ status: BackendRecordStatus,
+ records: Record[]
+ ): BackendResponseBulkRequest {
+ const request: BackendResponseBulkRequest = {
+ items: [],
+ };
+
+ records.forEach(({ id, questions }) => {
+ request.items.push({
+ ...this.createRequest(status, questions),
+ record_id: id,
+ });
+ });
+
+ return request;
+ }
+
private createRequest(
status: BackendRecordStatus,
questions: Question[]
- ): Omit {
+ ): BackendResponseRequest {
const values = {} as BackendAnswerCombinations;
questions
diff --git a/frontend/v1/infrastructure/services/useRoutes.ts b/frontend/v1/infrastructure/services/useRoutes.ts
index b47bda931e..af8ce41d14 100644
--- a/frontend/v1/infrastructure/services/useRoutes.ts
+++ b/frontend/v1/infrastructure/services/useRoutes.ts
@@ -2,14 +2,14 @@ import { useContext, useRoute, useRouter } from "@nuxtjs/composition-api";
import { Dataset } from "@/v1/domain/entities/Dataset";
type KindOfParam =
- | "_status"
- | "_page"
- | "_search"
- | "_metadata"
- | "_sort"
- | "_response"
- | "_suggestion"
- | "_similarity";
+ | "status"
+ | "page"
+ | "search"
+ | "metadata"
+ | "sort"
+ | "response"
+ | "suggestion"
+ | "similarity";
type QueryParam = {
key: KindOfParam;
diff --git a/frontend/v1/infrastructure/types/record.ts b/frontend/v1/infrastructure/types/record.ts
index ef7dc53a84..5507716475 100644
--- a/frontend/v1/infrastructure/types/record.ts
+++ b/frontend/v1/infrastructure/types/record.ts
@@ -15,7 +15,12 @@ interface BackendSuggestion {
}
export type BackendRecordStatus = "submitted" | "discarded" | "draft";
-export interface BackendResponse {
+export interface BackendResponseRequest {
+ status: BackendRecordStatus;
+ values: BackendAnswerCombinations;
+}
+
+export interface BackendResponseResponse {
id: string;
status: BackendRecordStatus;
values: BackendAnswerCombinations;
@@ -25,7 +30,7 @@ export interface BackendResponse {
export interface BackedRecord {
id: string;
suggestions: BackendSuggestion[];
- responses: BackendResponse[];
+ responses: BackendResponseResponse[];
fields: { [key: string]: string };
updated_at: string;
query_score: number;
@@ -80,3 +85,20 @@ export interface BackendAdvanceSearchQuery {
};
sort?: BackendSort[];
}
+
+export interface BackendResponseBulkRequest {
+ items: {
+ status: BackendRecordStatus;
+ values: BackendAnswerCombinations;
+ record_id: string;
+ }[];
+}
+
+export interface BackendResponseBulkResponse {
+ items: {
+ item: BackendResponseResponse & { record_id: string };
+ error: {
+ detail?: string;
+ };
+ }[];
+}
diff --git a/src/argilla/server/apis/v1/handlers/responses.py b/src/argilla/server/apis/v1/handlers/responses.py
index 1e63e3f305..30949d5abd 100644
--- a/src/argilla/server/apis/v1/handlers/responses.py
+++ b/src/argilla/server/apis/v1/handlers/responses.py
@@ -17,23 +17,25 @@
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy.ext.asyncio import AsyncSession
-import argilla.server.errors.future as errors
from argilla.server.contexts import datasets
from argilla.server.database import get_async_db
-from argilla.server.models import Record, Response, User
-from argilla.server.policies import RecordPolicyV1, ResponsePolicyV1, authorize
+from argilla.server.errors.future import NotFoundError
+from argilla.server.models import Response, User
+from argilla.server.policies import ResponsePolicyV1, authorize
from argilla.server.schemas.v1.responses import (
Response as ResponseSchema,
)
from argilla.server.schemas.v1.responses import (
- ResponseBulk,
- ResponseBulkError,
ResponsesBulk,
ResponsesBulkCreate,
ResponseUpdate,
)
from argilla.server.search_engine import SearchEngine, get_search_engine
from argilla.server.security import auth
+from argilla.server.use_cases.responses.upsert_responses_in_bulk import (
+ UpsertResponsesInBulkUseCase,
+ UpsertResponsesInBulkUseCaseFactory,
+)
router = APIRouter(tags=["responses"])
@@ -49,36 +51,19 @@ async def _get_response(db: AsyncSession, response_id: UUID) -> Response:
return response
-async def _get_record(db: AsyncSession, record_id: UUID) -> Record:
- record = await datasets.get_record_by_id(db, record_id, with_dataset=True)
- if record is None:
- raise errors.NotFoundError(f"Record with id `{record_id}` not found")
-
- return record
-
-
@router.post("/me/responses/bulk", response_model=ResponsesBulk)
async def create_current_user_responses_bulk(
*,
- db: AsyncSession = Depends(get_async_db),
- search_engine: SearchEngine = Depends(get_search_engine),
body: ResponsesBulkCreate,
current_user: User = Security(auth.get_current_user),
+ use_case: UpsertResponsesInBulkUseCase = Depends(UpsertResponsesInBulkUseCaseFactory()),
):
- responses_bulk_items = []
- for item in body.items:
- try:
- record = await _get_record(db, item.record_id)
-
- await authorize(current_user, RecordPolicyV1.create_response(record))
-
- response = await datasets.upsert_response(db, search_engine, record, current_user, item)
- except Exception as err:
- responses_bulk_items.append(ResponseBulk(item=None, error=ResponseBulkError(detail=str(err))))
- else:
- responses_bulk_items.append(ResponseBulk(item=ResponseSchema.from_orm(response), error=None))
-
- return ResponsesBulk(items=responses_bulk_items)
+ try:
+ responses_bulk_items = await use_case.execute(body.items, user=current_user)
+ except NotFoundError as err:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(err))
+ else:
+ return ResponsesBulk(items=responses_bulk_items)
@router.put("/responses/{response_id}", response_model=ResponseSchema)
diff --git a/src/argilla/server/contexts/datasets.py b/src/argilla/server/contexts/datasets.py
index 854216a4b1..4bb2235d0b 100644
--- a/src/argilla/server/contexts/datasets.py
+++ b/src/argilla/server/contexts/datasets.py
@@ -421,12 +421,17 @@ async def get_record_by_id(
async def get_records_by_ids(
db: "AsyncSession",
- dataset_id: UUID,
records_ids: Iterable[UUID],
+ dataset_id: Optional[UUID] = None,
include: Optional["RecordIncludeParam"] = None,
user_id: Optional[UUID] = None,
-) -> List[Record]:
- query = select(Record).filter(Record.dataset_id == dataset_id, Record.id.in_(records_ids))
+) -> List[Union[Record, None]]:
+ query = select(Record)
+
+ if dataset_id:
+ query.filter(Record.dataset_id == dataset_id)
+
+ query = query.filter(Record.id.in_(records_ids))
if include and include.with_responses:
if not user_id:
@@ -443,7 +448,7 @@ async def get_records_by_ids(
# Preserve the order of the `record_ids` list
record_order_map = {record.id: record for record in records}
- ordered_records = [record_order_map[record_id] for record_id in records_ids]
+ ordered_records = [record_order_map.get(record_id, None) for record_id in records_ids]
return ordered_records
@@ -822,6 +827,16 @@ async def _preload_record_relationships_before_index(db: "AsyncSession", record:
)
+async def preload_records_relationships_before_validate(db: "AsyncSession", records: List[Record]) -> None:
+ await db.execute(
+ select(Record)
+ .filter(Record.id.in_([record.id for record in records]))
+ .options(
+ selectinload(Record.dataset).selectinload(Dataset.questions),
+ )
+ )
+
+
async def update_records(
db: "AsyncSession", search_engine: "SearchEngine", dataset: Dataset, records_update: "RecordsUpdate"
) -> None:
diff --git a/src/argilla/server/search_engine/commons.py b/src/argilla/server/search_engine/commons.py
index 3ab7e76c29..f88f78a807 100644
--- a/src/argilla/server/search_engine/commons.py
+++ b/src/argilla/server/search_engine/commons.py
@@ -270,7 +270,7 @@ async def create_index(self, dataset: Dataset):
async def configure_metadata_property(self, dataset: Dataset, metadata_property: MetadataProperty):
mapping = es_mapping_for_metadata_property(metadata_property)
- index_name = await self._get_index_or_raise(dataset)
+ index_name = await self._get_dataset_index(dataset)
await self.put_index_mapping_request(index_name, mapping)
@@ -280,7 +280,7 @@ async def delete_index(self, dataset: Dataset):
await self._delete_index_request(index_name)
async def index_records(self, dataset: Dataset, records: Iterable[Record]):
- index_name = await self._get_index_or_raise(dataset)
+ index_name = await self._get_dataset_index(dataset)
bulk_actions = [
{
@@ -297,7 +297,7 @@ async def index_records(self, dataset: Dataset, records: Iterable[Record]):
await self._refresh_index_request(index_name)
async def delete_records(self, dataset: Dataset, records: Iterable[Record]):
- index_name = await self._get_index_or_raise(dataset)
+ index_name = await self._get_dataset_index(dataset)
bulk_actions = [{"_op_type": "delete", "_id": record.id, "_index": index_name} for record in records]
@@ -305,7 +305,7 @@ async def delete_records(self, dataset: Dataset, records: Iterable[Record]):
async def update_record_response(self, response: Response):
record = response.record
- index_name = await self._get_index_or_raise(record.dataset)
+ index_name = await self._get_dataset_index(record.dataset)
es_responses = self._map_record_responses_to_es([response])
@@ -313,14 +313,14 @@ async def update_record_response(self, response: Response):
async def delete_record_response(self, response: Response):
record = response.record
- index_name = await self._get_index_or_raise(record.dataset)
+ index_name = await self._get_dataset_index(record.dataset)
await self._update_document_request(
index_name, id=record.id, body={"script": f'ctx._source["responses"].remove("{response.user.username}")'}
)
async def update_record_suggestion(self, suggestion: Suggestion):
- index_name = await self._get_index_or_raise(suggestion.record.dataset)
+ index_name = await self._get_dataset_index(suggestion.record.dataset)
es_suggestions = self._map_record_suggestions_to_es([suggestion])
@@ -331,7 +331,7 @@ async def update_record_suggestion(self, suggestion: Suggestion):
)
async def delete_record_suggestion(self, suggestion: Suggestion):
- index_name = await self._get_index_or_raise(suggestion.record.dataset)
+ index_name = await self._get_dataset_index(suggestion.record.dataset)
await self._update_document_request(
index_name,
@@ -340,7 +340,7 @@ async def delete_record_suggestion(self, suggestion: Suggestion):
)
async def set_records_vectors(self, dataset: Dataset, vectors: Iterable[Vector]):
- index_name = await self._get_index_or_raise(dataset)
+ index_name = await self._get_dataset_index(dataset)
bulk_actions = [
{
@@ -399,7 +399,7 @@ async def similarity_search(
# Wrapping filter in a list to use easily on each engine implementation
query_filters = [self.build_elasticsearch_filter(filter)]
- index = await self._get_index_or_raise(dataset)
+ index = await self._get_dataset_index(dataset)
response = await self._request_similarity_search(
index=index,
vector_settings=vector_settings,
@@ -545,7 +545,7 @@ def _map_record_metadata_to_es(
return search_engine_metadata
async def configure_index_vectors(self, vector_settings: VectorSettings) -> None:
- index = await self._get_index_or_raise(vector_settings.dataset)
+ index = await self._get_dataset_index(vector_settings.dataset)
mappings = self._mapping_for_vector_settings(vector_settings)
await self.put_index_mapping_request(index, mappings)
@@ -583,7 +583,7 @@ async def search(
bool_query["filter"] = self.build_elasticsearch_filter(filter)
es_query = {"bool": bool_query}
- index = await self._get_index_or_raise(dataset)
+ index = await self._get_dataset_index(dataset)
es_sort = self.build_elasticsearch_sort(sort) if sort else None
response = await self._index_search_request(index, query=es_query, size=limit, from_=offset, sort=es_sort)
@@ -591,7 +591,7 @@ async def search(
return await self._process_search_response(response)
async def compute_metrics_for(self, metadata_property: MetadataProperty) -> MetadataMetrics:
- index_name = await self._get_index_or_raise(metadata_property.dataset)
+ index_name = await self._get_dataset_index(metadata_property.dataset)
if metadata_property.type == MetadataPropertyType.terms:
return await self._metrics_for_terms_property(index_name, metadata_property)
@@ -727,10 +727,8 @@ def _dynamic_templates_for_question_responses(self, questions: List[Question]) -
],
]
- async def _get_index_or_raise(self, dataset: Dataset):
+ async def _get_dataset_index(self, dataset: Dataset):
index_name = es_index_name_for_dataset(dataset)
- if not await self._index_exists_request(index_name):
- raise ValueError(f"Cannot access to index for dataset {dataset.id}: the specified index does not exist")
return index_name
diff --git a/src/argilla/server/use_cases/__init__.py b/src/argilla/server/use_cases/__init__.py
new file mode 100644
index 0000000000..55be41799b
--- /dev/null
+++ b/src/argilla/server/use_cases/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2021-present, the Recognai S.L. team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/src/argilla/server/use_cases/responses/__init__.py b/src/argilla/server/use_cases/responses/__init__.py
new file mode 100644
index 0000000000..55be41799b
--- /dev/null
+++ b/src/argilla/server/use_cases/responses/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2021-present, the Recognai S.L. team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/src/argilla/server/use_cases/responses/upsert_responses_in_bulk.py b/src/argilla/server/use_cases/responses/upsert_responses_in_bulk.py
new file mode 100644
index 0000000000..7385fcc3dd
--- /dev/null
+++ b/src/argilla/server/use_cases/responses/upsert_responses_in_bulk.py
@@ -0,0 +1,60 @@
+# Copyright 2021-present, the Recognai S.L. team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import List
+
+from fastapi import Depends
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from argilla.server.contexts import datasets
+from argilla.server.database import get_async_db
+from argilla.server.errors import future as errors
+from argilla.server.models import User
+from argilla.server.policies import RecordPolicyV1, authorize
+from argilla.server.schemas.v1.responses import Response, ResponseBulk, ResponseBulkError, ResponseUpsert
+from argilla.server.search_engine import SearchEngine, get_search_engine
+
+
+class UpsertResponsesInBulkUseCase:
+ def __init__(self, db: AsyncSession, search_engine: SearchEngine):
+ self.db = db
+ self.search_engine = search_engine
+
+ async def execute(self, responses: List[ResponseUpsert], user: User) -> List[ResponseBulk]:
+ responses_bulk_items = []
+
+ all_records = await datasets.get_records_by_ids(self.db, [item.record_id for item in responses])
+ non_empty_records = [r for r in all_records if r is not None]
+
+ await datasets.preload_records_relationships_before_validate(self.db, non_empty_records)
+ for item, record in zip(responses, all_records):
+ try:
+ if record is None:
+ raise errors.NotFoundError(f"Record with id `{item.record_id}` not found")
+
+ await authorize(user, RecordPolicyV1.create_response(record))
+ response = await datasets.upsert_response(self.db, self.search_engine, record, user, item)
+ except Exception as err:
+ responses_bulk_items.append(ResponseBulk(item=None, error=ResponseBulkError(detail=str(err))))
+ else:
+ responses_bulk_items.append(ResponseBulk(item=Response.from_orm(response), error=None))
+
+ return responses_bulk_items
+
+
+class UpsertResponsesInBulkUseCaseFactory:
+ def __call__(
+ self, db: AsyncSession = Depends(get_async_db), search_engine: SearchEngine = Depends(get_search_engine)
+ ):
+ return UpsertResponsesInBulkUseCase(db, search_engine)
diff --git a/tests/unit/server/api/v1/responses/test_create_current_user_responses_bulk.py b/tests/unit/server/api/v1/responses/test_create_current_user_responses_bulk.py
index 7713923460..d5aaf743fe 100644
--- a/tests/unit/server/api/v1/responses/test_create_current_user_responses_bulk.py
+++ b/tests/unit/server/api/v1/responses/test_create_current_user_responses_bulk.py
@@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-
+import os
from datetime import datetime
from unittest.mock import call
from uuid import UUID, uuid4
@@ -21,6 +21,7 @@
from argilla.server.enums import ResponseStatus
from argilla.server.models import Response, User
from argilla.server.search_engine import SearchEngine
+from argilla.server.use_cases.responses.upsert_responses_in_bulk import UpsertResponsesInBulkUseCase
from httpx import AsyncClient
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -386,3 +387,59 @@ async def test_too_many_responses(self, async_client: AsyncClient, owner_auth_he
)
assert resp.status_code == 422
+
+ @pytest.mark.skipif(reason="Profiling is not active", condition=not bool(os.getenv("TEST_PROFILING", None)))
+ async def test_create_responses_in_bulk_profiling(self, db: "AsyncSession", elasticsearch_config: dict):
+ from argilla.server.schemas.v1.responses import DraftResponseUpsert
+ from argilla.server.search_engine import ElasticSearchEngine
+ from pyinstrument import Profiler
+
+ from tests.factories import OwnerFactory, TextFieldFactory
+
+ async def refresh_dataset(dataset):
+ await dataset.awaitable_attrs.fields
+ await dataset.awaitable_attrs.questions
+ await dataset.awaitable_attrs.metadata_properties
+ await dataset.awaitable_attrs.vectors_settings
+
+ async def refresh_records(records):
+ for record in records:
+ await record.awaitable_attrs.suggestions
+ await record.awaitable_attrs.responses
+ await record.awaitable_attrs.vectors
+
+ dataset = await DatasetFactory.create()
+ user = await OwnerFactory.create()
+
+ await RatingQuestionFactory.create(name="prompt-quality", required=True, dataset=dataset)
+ await TextFieldFactory.create(name="text", required=True, dataset=dataset)
+ await TextFieldFactory.create(name="sentiment", required=True, dataset=dataset)
+
+ records = await RecordFactory.create_batch(dataset=dataset, size=500)
+
+ engine = ElasticSearchEngine(config=elasticsearch_config, number_of_replicas=0, number_of_shards=1)
+
+ await refresh_dataset(dataset)
+ await refresh_records(records)
+
+ await engine.create_index(dataset)
+ await engine.index_records(dataset, records)
+
+ profiler = Profiler()
+
+ responses = [
+ DraftResponseUpsert.parse_obj(
+ {
+ "values": {"prompt-quality": {"value": 10}},
+ "record_id": record.id,
+ "status": "draft",
+ }
+ )
+ for record in records
+ ]
+ use_case = UpsertResponsesInBulkUseCase(db, engine)
+ with profiler:
+ bulk_items = await use_case.execute(responses, user)
+ await use_case.execute([bulk_item.item for bulk_item in bulk_items], user)
+
+ profiler.open_in_browser()
diff --git a/tests/unit/server/search_engine/test_commons.py b/tests/unit/server/search_engine/test_commons.py
index 3e47616a21..0dab12256a 100644
--- a/tests/unit/server/search_engine/test_commons.py
+++ b/tests/unit/server/search_engine/test_commons.py
@@ -284,14 +284,6 @@ class TestBaseElasticAndOpenSearchEngine:
"""
- # TODO: Use other public method to detect the error
- async def test_get_index_or_raise(self, search_engine: BaseElasticAndOpenSearchEngine):
- dataset = await DatasetFactory.create()
- with pytest.raises(
- ValueError, match=f"Cannot access to index for dataset {dataset.id}: the specified index does not exist"
- ):
- await search_engine._get_index_or_raise(dataset)
-
async def test_create_index_for_dataset(
self, search_engine: BaseElasticAndOpenSearchEngine, db: "AsyncSession", opensearch: OpenSearch
):