diff --git a/cypress/e2e/batch_creation.cy.js b/cypress/e2e/batch_creation.cy.js index f0fe6ed1..6cc12ab4 100644 --- a/cypress/e2e/batch_creation.cy.js +++ b/cypress/e2e/batch_creation.cy.js @@ -27,24 +27,24 @@ describe("Batch Creation", () => { cy.get("input[placeholder='Jane']").type(randomName); cy.get("button").contains("Add").click(); - // Open Settings - cy.get("span").contains("Learning").click(); - cy.get("span").contains("Settings").click(); - - // Add evaluator + // Switch to Evaluators tab cy.get("[data-dismissable-layer]") .find("span") .contains(/^Evaluators$/) .click(); + // Click "New" dropdown and select "New Evaluator" cy.get("[data-dismissable-layer]") .find("button") .contains("New") .click(); - const randomEvaluator = `evaluator${dateNow}@example.com`; + cy.get("span").contains("New Evaluator").click(); + const randomEvaluator = `evaluator${dateNow}@example.com`; cy.get("input[placeholder='jane@doe.com']").type(randomEvaluator); + cy.get("input[placeholder='Jane']").type("Evaluator"); cy.get("button").contains("Add").click(); + cy.wait(500); cy.get("div").contains(randomEvaluator).should("be.visible").click(); cy.visit("/lms/batches"); diff --git a/frontend/components.d.ts b/frontend/components.d.ts index b3658285..f61760a2 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -8,6 +8,7 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AddEvaluatorModal: typeof import('./src/components/Modals/AddEvaluatorModal.vue')['default'] Apps: typeof import('./src/components/Sidebar/Apps.vue')['default'] AppSidebar: typeof import('./src/components/Sidebar/AppSidebar.vue')['default'] AssessmentModal: typeof import('./src/components/Modals/AssessmentModal.vue')['default'] @@ -78,6 +79,7 @@ declare module 'vue' { Members: typeof import('./src/components/Settings/Members.vue')['default'] MobileLayout: typeof import('./src/components/MobileLayout.vue')['default'] MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default'] + NewMemberModal: typeof import('./src/components/Modals/NewMemberModal.vue')['default'] NoPermission: typeof import('./src/components/NoPermission.vue')['default'] NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default'] Notes: typeof import('./src/components/Notes/Notes.vue')['default'] diff --git a/frontend/src/components/Controls/Link.vue b/frontend/src/components/Controls/Link.vue index 6d73dbaf..a797c8b1 100644 --- a/frontend/src/components/Controls/Link.vue +++ b/frontend/src/components/Controls/Link.vue @@ -140,7 +140,7 @@ const options = createResource({ params: { txt: text.value, doctype: props.doctype, - filters: props.filters, + filters: JSON.stringify(props.filters), }, transform: (data) => { return data.map((option) => { @@ -158,7 +158,7 @@ const reload = (val) => { params: { txt: val, doctype: props.doctype, - filters: props.filters, + filters: JSON.stringify(props.filters), }, }) options.reload() diff --git a/frontend/src/components/Controls/MultiSelect.vue b/frontend/src/components/Controls/MultiSelect.vue index ba7261aa..41b9c051 100644 --- a/frontend/src/components/Controls/MultiSelect.vue +++ b/frontend/src/components/Controls/MultiSelect.vue @@ -10,6 +10,7 @@ ref="search" class="form-input w-full focus-visible:!ring-0" type="text" + :displayValue="() => query" @change=" (e) => { query = e.target.value @@ -106,7 +107,7 @@ import { ComboboxOptions, ComboboxOption, } from '@headlessui/vue' -import { createResource, Button } from 'frappe-ui' +import { createResource, Button, toast } from 'frappe-ui' import { ref, computed, useAttrs, watch } from 'vue' import { watchDebounced } from '@vueuse/core' import { X, Plus } from 'lucide-vue-next' @@ -115,7 +116,9 @@ const props = defineProps({ label: String, size: { type: String, default: 'sm' }, doctype: { type: String, required: true }, - filters: { type: Object, default: () => ({}) }, + filters: { type: [Object, Array], default: () => ({}) }, + url: { type: String, default: 'frappe.desk.search.search_link' }, + searchParams: { type: Object, default: () => ({}) }, validate: Function, errorMessage: { type: Function, @@ -124,22 +127,18 @@ const props = defineProps({ required: Boolean, }) -const values = defineModel() +const values = defineModel({ default: () => [] }) const attrs = useAttrs() const trigger = ref(null) const query = ref('') const text = ref('') const selectedValue = ref(null) -const error = ref(null) - -const emit = defineEmits(['update:modelValue']) watch(selectedValue, (val) => { if (!val?.value) return query.value = '' addValue(val.value) selectedValue.value = null - emit('update:modelValue', values.value) }) watchDebounced( @@ -153,14 +152,27 @@ watchDebounced( { debounce: 300, immediate: true } ) -const filterOptions = createResource({ - url: 'frappe.desk.search.search_link', - method: 'POST', - auto: true, - params: { - txt: text.value, - doctype: props.doctype, +// Refetch when filters or searchParams change +watch( + () => [props.filters, props.searchParams], + () => { + reload(text.value) }, + { deep: true } +) + +function getParams(txt) { + return { + txt, + doctype: props.doctype, + filters: JSON.stringify(props.filters), + ...props.searchParams, + } +} + +const filterOptions = createResource({ + url: props.url, + method: 'POST', }) const options = computed(() => { @@ -170,10 +182,7 @@ const options = computed(() => { function reload(val) { filterOptions.update({ - params: { - txt: val, - doctype: props.doctype, - }, + params: getParams(val), }) filterOptions.reload() } @@ -186,34 +195,30 @@ function onFocus() { } function addValue(value) { - error.value = null - if (!value) return const splitValues = value.split(',') + let newValues = [...(values.value || [])] splitValues.forEach((val) => { val = val.trim() if (!val) return - if (values.value?.includes(val)) return + if (newValues.includes(val)) return if (props.validate && !props.validate(val)) { - error.value = props.errorMessage(val) + toast.error(props.errorMessage(val)) return } - if (!values.value) values.value = [val] - else values.value.push(val) + newValues.push(val) }) + + values.value = newValues } function removeValue(value) { - let indexToRemove = values.value.indexOf(value) - if (indexToRemove > -1) { - values.value.splice(indexToRemove, 1) - } - emit('update:modelValue', values.value) + values.value = values.value.filter((v) => v !== value) } const labelClasses = computed(() => [ diff --git a/frontend/src/components/Modals/AddEvaluatorModal.vue b/frontend/src/components/Modals/AddEvaluatorModal.vue new file mode 100644 index 00000000..4844b364 --- /dev/null +++ b/frontend/src/components/Modals/AddEvaluatorModal.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/frontend/src/components/Modals/NewMemberModal.vue b/frontend/src/components/Modals/NewMemberModal.vue new file mode 100644 index 00000000..ab46d343 --- /dev/null +++ b/frontend/src/components/Modals/NewMemberModal.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/frontend/src/components/Settings/Evaluators.vue b/frontend/src/components/Settings/Evaluators.vue index f3f01cc7..8e4ded97 100644 --- a/frontend/src/components/Settings/Evaluators.vue +++ b/frontend/src/components/Settings/Evaluators.vue @@ -10,12 +10,43 @@