diff --git a/requirements-base.txt b/requirements-base.txt index f0d0389d530e..97db3619e514 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -114,7 +114,7 @@ deprecation==2.1.0 # via pdpyras dnspython==2.4.2 # via email-validator -duo-client==5.1.0 +duo-client==5.2.0 # via -r requirements-base.in ecdsa==0.18.0 # via python-jose @@ -212,7 +212,7 @@ lxml==4.9.3 # premailer mako==1.2.4 # via alembic -markdown==3.5 +markdown==3.5.1 # via -r requirements-base.in markupsafe==2.1.3 # via @@ -278,7 +278,7 @@ preshed==3.0.8 # via # spacy # thinc -protobuf==4.24.4 +protobuf==4.25.0 # via # -r requirements-base.in # google-api-core @@ -382,7 +382,7 @@ scipy==1.11.2 # via statsmodels sentry-asgi==0.2.0 # via -r requirements-base.in -sentry-sdk==1.32.0 +sentry-sdk==1.34.0 # via # -r requirements-base.in # sentry-asgi diff --git a/requirements-dev.txt b/requirements-dev.txt index 3e20adc3d858..c5c9820e99c2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,8 +10,6 @@ asttokens==2.2.1 # stack-data attrs==22.1.0 # via -r requirements-dev.in -backcall==0.2.0 - # via ipython black==23.10.1 # via -r requirements-dev.in cfgv==3.4.0 @@ -44,7 +42,7 @@ identify==2.5.27 # via pre-commit iniconfig==2.0.0 # via pytest -ipython==8.16.1 +ipython==8.17.2 # via -r requirements-dev.in jedi==0.19.0 # via ipython @@ -64,8 +62,6 @@ pathspec==0.11.2 # via black pexpect==4.8.0 # via ipython -pickleshare==0.7.5 - # via ipython platformdirs==3.10.0 # via # black diff --git a/src/dispatch/static/dispatch/components.d.ts b/src/dispatch/static/dispatch/components.d.ts index 938f0eda76fa..20b7ce8714af 100644 --- a/src/dispatch/static/dispatch/components.d.ts +++ b/src/dispatch/static/dispatch/components.d.ts @@ -25,6 +25,7 @@ declare module '@vue/runtime-core' { MonacoEditor: typeof import('./src/components/MonacoEditor.vue')['default'] NotificationSnackbarsWrapper: typeof import('./src/components/NotificationSnackbarsWrapper.vue')['default'] PageHeader: typeof import('./src/components/PageHeader.vue')['default'] + ParticipantSelect: typeof import('./src/components/ParticipantSelect.vue')['default'] Refresh: typeof import('./src/components/Refresh.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/src/dispatch/static/dispatch/src/case/DetailsTab.vue b/src/dispatch/static/dispatch/src/case/DetailsTab.vue index 99786988c9d8..0dd2acc99f0d 100644 --- a/src/dispatch/static/dispatch/src/case/DetailsTab.vue +++ b/src/dispatch/static/dispatch/src/case/DetailsTab.vue @@ -131,7 +131,7 @@ import CaseSeveritySelect from "@/case/severity/CaseSeveritySelect.vue" import CaseTypeSelect from "@/case/type/CaseTypeSelect.vue" import DateTimePickerMenu from "@/components/DateTimePickerMenu.vue" import IncidentFilterCombobox from "@/incident/IncidentFilterCombobox.vue" -import ParticipantSelect from "@/incident/ParticipantSelect.vue" +import ParticipantSelect from "@/components/ParticipantSelect.vue" import ProjectSelect from "@/project/ProjectSelect.vue" import TagFilterAutoComplete from "@/tag/TagFilterAutoComplete.vue" diff --git a/src/dispatch/static/dispatch/src/case/HandoffDialog.vue b/src/dispatch/static/dispatch/src/case/HandoffDialog.vue index 05e65c5801ba..b786ff8b54a8 100644 --- a/src/dispatch/static/dispatch/src/case/HandoffDialog.vue +++ b/src/dispatch/static/dispatch/src/case/HandoffDialog.vue @@ -31,7 +31,7 @@ import { mapFields } from "vuex-map-fields" import { mapActions } from "vuex" -import ParticipantSelect from "@/incident/ParticipantSelect.vue" +import ParticipantSelect from "@/components/ParticipantSelect.vue" export default { name: "CaseHandoffDialog", diff --git a/src/dispatch/static/dispatch/src/case/ParticipantSelect.vue b/src/dispatch/static/dispatch/src/case/ParticipantSelect.vue deleted file mode 100644 index 0e8fbc57e67a..000000000000 --- a/src/dispatch/static/dispatch/src/case/ParticipantSelect.vue +++ /dev/null @@ -1,127 +0,0 @@ -<template> - <v-combobox - :items="items" - :label="label" - :loading="loading" - v-model:search="search" - @update:search="getFilteredData()" - chips - clearable - closable-chips - hide-selected - item-title="individual.name" - :item-props="(item) => ({ subtitle: item.individual.email })" - no-filter - return-object - v-model="participant" - > - <template #no-data> - <v-list-item> - <v-list-item-title> - No individuals matching " - <strong>{{ search }}</strong - >". - </v-list-item-title> - </v-list-item> - </template> - <template #append-item> - <v-list-item v-if="more" @click="loadMore()"> - <v-list-item-subtitle> Load More </v-list-item-subtitle> - </v-list-item> - </template> - </v-combobox> -</template> - -<script> -import { cloneDeep, debounce } from "lodash" - -import SearchUtils from "@/search/utils" -import IndividualApi from "@/individual/api" - -export default { - name: "ParticipantSelect", - props: { - modelValue: { - type: Object, - default: function () { - return null - }, - }, - label: { - type: String, - default: function () { - return "Participant" - }, - }, - }, - - data() { - return { - loading: false, - items: [], - more: false, - numItems: 5, - search: null, - } - }, - - computed: { - participant: { - get() { - return cloneDeep(this.modelValue) - }, - set(value) { - this.$emit("update:modelValue", value) - }, - }, - }, - - created() { - this.fetchData() - }, - - methods: { - loadMore() { - this.numItems = this.numItems + 5 - this.fetchData() - }, - fetchData() { - this.loading = "error" - let filterOptions = { - q: this.search, - sortBy: ["name"], - descending: [false], - itemsPerPage: this.numItems, - } - - if (this.project) { - filterOptions = { - ...filterOptions, - filters: { - project: [this.project], - }, - } - filterOptions = SearchUtils.createParametersFromTableOptions({ ...filterOptions }) - } - - IndividualApi.getAll(filterOptions).then((response) => { - this.items = response.data.items.map(function (x) { - return { individual: x } - }) - this.total = response.data.total - - if (this.items.length < this.total) { - this.more = true - } else { - this.more = false - } - - this.loading = false - }) - }, - getFilteredData: debounce(function () { - this.fetchData() - }, 500), - }, -} -</script> diff --git a/src/dispatch/static/dispatch/src/components/ParticipantSelect.vue b/src/dispatch/static/dispatch/src/components/ParticipantSelect.vue new file mode 100644 index 000000000000..f7fc562aae18 --- /dev/null +++ b/src/dispatch/static/dispatch/src/components/ParticipantSelect.vue @@ -0,0 +1,139 @@ +<template> + <v-autocomplete + :items="items" + :label="labelProp" + :loading="loading" + v-model:search="search" + clearable + hide-selected + item-title="individual.name" + item-value="individual.id" + return-object + chips + :hide-no-data="false" + v-model="participant" + @update:modelValue="handleClear" + > + <template #no-data> + <v-list-item v-if="!loading"> + <v-list-item-title> + No individuals matching <strong>"{{ search }}".</strong> + </v-list-item-title> + </v-list-item> + </template> + <template #item="{ props, item }"> + <v-list-item v-bind="props" :subtitle="item.raw.individual.email" /> + </template> + <template #append-item v-if="items.length < total.value"> + <v-list-item @click="loadMore()"> + <v-list-item-subtitle> Load More </v-list-item-subtitle> + </v-list-item> + </template> + <template #chip="data"> + <v-chip v-bind="data.props" pill> + <template #prepend> + <v-avatar color="teal" start> {{ initials(data.item.title) }} </v-avatar> + </template> + {{ data.item.title }} + </v-chip> + </template> + </v-autocomplete> +</template> + +<script> +import { ref, watch, toRefs, onMounted } from "vue" +import { initials } from "@/filters" +import { debounce } from "lodash" + +import IndividualApi from "@/individual/api" + +export default { + name: "ParticipantSelect", + props: { + labelProp: { + // Define the labelProp + type: String, + default: "Participant", + }, + initialValue: { + type: Object, + default: () => ({}), + }, + }, + setup(props) { + const { labelProp } = toRefs(props) // toRefs make props reactive + + let loading = ref(false) + let items = ref([]) + console.log(items) + let numItems = ref(10) + let participant = ref({ ...props.initialValue }) + let currentPage = ref(1) + let total = ref(0) + const search = ref(props.initialValue.name) + + let debouncedGetIndividualData = null + + const getIndividualData = async (searchVal, page = currentPage.value) => { + loading.value = true + let filterOptions = { + q: searchVal, + sortBy: ["name"], + descending: [false], + itemsPerPage: numItems.value * page, + } + + await IndividualApi.getAll(filterOptions).then((response) => { + console.log(response.data.items) + items.value = response.data.items.map(function (x) { + return { individual: x } + }) + total.value = response.data.total + }) + + loading.value = false + } + + onMounted(() => { + debouncedGetIndividualData = debounce(getIndividualData, 300) + debouncedGetIndividualData(search.value) + }) + + const loadMore = async () => { + currentPage.value++ + numItems.value += 10 + await debouncedGetIndividualData(search.value) + } + + const handleClear = (newValue) => { + if (!newValue) { + items.value = [] + search.value = null + participant.value = null + numItems.value = 10 + currentPage.value = 1 + } + } + + watch(search, async (newVal, oldVal) => { + if (oldVal !== newVal) { + numItems.value = 10 + await debouncedGetIndividualData(newVal) + } + }) + + return { + getIndividualData, + handleClear, + initials, + items, + labelProp, + loading, + loadMore, + participant, + search, + total, + } + }, +} +</script> diff --git a/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue b/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue index 7dac62efee00..97875ed28629 100644 --- a/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue +++ b/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue @@ -75,7 +75,7 @@ import ProjectCombobox from "@/project/ProjectCombobox.vue" import RouterUtils from "@/router/utils" import SearchUtils from "@/search/utils" import TagFilterAutoComplete from "@/tag/TagFilterAutoComplete.vue" -import ParticipantSelect from "@/incident/ParticipantSelect.vue" +import ParticipantSelect from "@/components/ParticipantSelect.vue" let today = function () { let now = new Date() diff --git a/src/dispatch/static/dispatch/src/incident/DetailsTab.vue b/src/dispatch/static/dispatch/src/incident/DetailsTab.vue index cb646216cc9e..4a8914fc5561 100644 --- a/src/dispatch/static/dispatch/src/incident/DetailsTab.vue +++ b/src/dispatch/static/dispatch/src/incident/DetailsTab.vue @@ -125,7 +125,7 @@ import IncidentFilterCombobox from "@/incident/IncidentFilterCombobox.vue" import IncidentPrioritySelect from "@/incident/priority/IncidentPrioritySelect.vue" import IncidentSeveritySelect from "@/incident/severity/IncidentSeveritySelect.vue" import IncidentTypeSelect from "@/incident/type/IncidentTypeSelect.vue" -import ParticipantSelect from "@/incident/ParticipantSelect.vue" +import ParticipantSelect from "@/components/ParticipantSelect.vue" import ProjectSelect from "@/project/ProjectSelect.vue" import TagFilterAutoComplete from "@/tag/TagFilterAutoComplete.vue" diff --git a/src/dispatch/static/dispatch/src/incident/HandoffDialog.vue b/src/dispatch/static/dispatch/src/incident/HandoffDialog.vue index 2cc96a6cc948..b7756a990b0d 100644 --- a/src/dispatch/static/dispatch/src/incident/HandoffDialog.vue +++ b/src/dispatch/static/dispatch/src/incident/HandoffDialog.vue @@ -38,7 +38,7 @@ import { mapFields } from "vuex-map-fields" import { mapActions } from "vuex" -import ParticipantSelect from "@/incident/ParticipantSelect.vue" +import ParticipantSelect from "@/components/ParticipantSelect.vue" export default { name: "IncidentHandoffDialog", diff --git a/src/dispatch/static/dispatch/src/incident/ParticipantSelect.vue b/src/dispatch/static/dispatch/src/incident/ParticipantSelect.vue deleted file mode 100644 index ea5dbeaa1393..000000000000 --- a/src/dispatch/static/dispatch/src/incident/ParticipantSelect.vue +++ /dev/null @@ -1,128 +0,0 @@ -<template> - <v-combobox - :items="items" - :label="label" - :loading="loading" - v-model:search="search" - @update:search="getFilteredData()" - chips - clearable - hide-selected - item-title="individual.name" - no-filter - return-object - v-model="participant" - > - <template #no-data> - <v-list-item> - <v-list-item-title> - No individuals matching " - <strong>{{ search }}</strong - >". - </v-list-item-title> - </v-list-item> - </template> - <template #item="{ props, item }"> - <v-list-item v-bind="props" :subtitle="item.raw.individual.email" /> - </template> - <template #append-item> - <v-list-item v-if="more" @click="loadMore()"> - <v-list-item-subtitle> Load More </v-list-item-subtitle> - </v-list-item> - </template> - </v-combobox> -</template> - -<script> -import { cloneDeep, debounce } from "lodash" - -import SearchUtils from "@/search/utils" -import IndividualApi from "@/individual/api" - -export default { - name: "ParticipantSelect", - props: { - modelValue: { - type: Object, - default: function () { - return null - }, - }, - label: { - type: String, - default: function () { - return "Participant" - }, - }, - }, - - data() { - return { - loading: false, - items: [], - more: false, - numItems: 5, - search: null, - } - }, - - computed: { - participant: { - get() { - return cloneDeep(this.modelValue) - }, - set(value) { - this.$emit("update:modelValue", value) - }, - }, - }, - - created() { - this.fetchData() - }, - - methods: { - loadMore() { - this.numItems = this.numItems + 5 - this.fetchData() - }, - fetchData() { - this.loading = "error" - let filterOptions = { - q: this.search, - sortBy: ["name"], - descending: [false], - itemsPerPage: this.numItems, - } - - if (this.project) { - filterOptions = { - ...filterOptions, - filters: { - project: [this.project], - }, - } - filterOptions = SearchUtils.createParametersFromTableOptions({ ...filterOptions }) - } - - IndividualApi.getAll(filterOptions).then((response) => { - this.items = response.data.items.map(function (x) { - return { individual: x } - }) - this.total = response.data.total - - if (this.items.length < this.total) { - this.more = true - } else { - this.more = false - } - - this.loading = false - }) - }, - getFilteredData: debounce(function () { - this.fetchData() - }, 500), - }, -} -</script> diff --git a/src/dispatch/static/dispatch/src/incident/TableFilterDialog.vue b/src/dispatch/static/dispatch/src/incident/TableFilterDialog.vue index 2357d35f3719..6d03d3b392cb 100644 --- a/src/dispatch/static/dispatch/src/incident/TableFilterDialog.vue +++ b/src/dispatch/static/dispatch/src/incident/TableFilterDialog.vue @@ -78,7 +78,7 @@ import IncidentTypeCombobox from "@/incident/type/IncidentTypeCombobox.vue" import ProjectCombobox from "@/project/ProjectCombobox.vue" import TagFilterAutoComplete from "@/tag/TagFilterAutoComplete.vue" import TagTypeFilterCombobox from "@/tag_type/TagTypeFilterCombobox.vue" -import ParticipantSelect from "@/incident/ParticipantSelect.vue" +import ParticipantSelect from "@/components/ParticipantSelect.vue" export default { name: "IncidentTableFilterDialog", diff --git a/src/dispatch/static/dispatch/src/task/NewEditSheet.vue b/src/dispatch/static/dispatch/src/task/NewEditSheet.vue index b67cbb63c5c9..6e39e33928e9 100644 --- a/src/dispatch/static/dispatch/src/task/NewEditSheet.vue +++ b/src/dispatch/static/dispatch/src/task/NewEditSheet.vue @@ -107,7 +107,7 @@ import { mapActions } from "vuex" import ProjectSelect from "@/project/ProjectSelect.vue" import IncidentSelect from "@/incident/IncidentSelect.vue" -import ParticipantSelect from "@/incident/ParticipantSelect.vue" +import ParticipantSelect from "@/components/ParticipantSelect.vue" import AssigneeCombobox from "@/task/AssigneeCombobox.vue" import DateTimePickerMenu from "@/components/DateTimePickerMenu.vue"