|
11 | 11 | :text="`Email ${$formatNumber(count)} ${count === 1 ? 'user' : 'users'}`" |
12 | 12 | @click="showMassEmailDialog = true" |
13 | 13 | /> |
| 14 | + <IconButton |
| 15 | + icon="download" |
| 16 | + class="ma-0" |
| 17 | + :color="$themeTokens.primary" |
| 18 | + text="Download CSV" |
| 19 | + data-test="csv" |
| 20 | + :disabled="!count" |
| 21 | + @click="onDownloadCSV" |
| 22 | + /> |
14 | 23 | </h1> |
15 | 24 | <EmailUsersDialog |
16 | 25 | v-model="showMassEmailDialog" |
|
72 | 81 | /> |
73 | 82 | </VFlex> |
74 | 83 | </VLayout> |
| 84 | + <VLayout |
| 85 | + wrap |
| 86 | + class="mb-2" |
| 87 | + > |
| 88 | + <VFlex |
| 89 | + xs12 |
| 90 | + sm6 |
| 91 | + md3 |
| 92 | + class="px-3" |
| 93 | + > |
| 94 | + <VSelect |
| 95 | + v-model="joinedWithinFilter" |
| 96 | + :items="joinedWithinOptions" |
| 97 | + item-text="label" |
| 98 | + item-value="value" |
| 99 | + label="Joined within" |
| 100 | + box |
| 101 | + :menu-props="{ offsetY: true }" |
| 102 | + /> |
| 103 | + </VFlex> |
| 104 | + <VFlex |
| 105 | + xs12 |
| 106 | + sm6 |
| 107 | + md3 |
| 108 | + class="px-3" |
| 109 | + > |
| 110 | + <VSelect |
| 111 | + v-model="activeWithinFilter" |
| 112 | + :items="activeWithinOptions" |
| 113 | + item-text="label" |
| 114 | + item-value="value" |
| 115 | + label="Active within" |
| 116 | + box |
| 117 | + :menu-props="{ offsetY: true }" |
| 118 | + /> |
| 119 | + </VFlex> |
| 120 | + <VFlex |
| 121 | + xs12 |
| 122 | + sm6 |
| 123 | + md3 |
| 124 | + class="align-center d-flex px-3" |
| 125 | + > |
| 126 | + <Checkbox |
| 127 | + v-model="hasPublishedFilter" |
| 128 | + label="Has published a channel" |
| 129 | + /> |
| 130 | + </VFlex> |
| 131 | + <VFlex |
| 132 | + xs12 |
| 133 | + sm6 |
| 134 | + md3 |
| 135 | + class="align-center d-flex px-3" |
| 136 | + > |
| 137 | + <Checkbox |
| 138 | + v-model="hasEditsFilter" |
| 139 | + label="Has Studio edits" |
| 140 | + /> |
| 141 | + </VFlex> |
| 142 | + </VLayout> |
75 | 143 | <VDataTable |
76 | 144 | v-model="selected" |
77 | 145 | :headers="headers" |
|
141 | 209 | import { ref, onMounted, computed, getCurrentInstance } from 'vue'; |
142 | 210 | import { mapGetters } from 'vuex'; |
143 | 211 | import transform from 'lodash/transform'; |
| 212 | + import { saveAs } from 'file-saver'; |
144 | 213 | import { useTable } from '../../composables/useTable'; |
145 | 214 | import { RouteNames, rowsPerPageItems } from '../../constants'; |
146 | 215 | import EmailUsersDialog from './EmailUsersDialog'; |
147 | 216 | import UserItem from './UserItem'; |
| 217 | + import client from 'shared/client'; |
148 | 218 | import { useFilter } from 'shared/composables/useFilter'; |
149 | 219 | import { useKeywordSearch } from 'shared/composables/useKeywordSearch'; |
150 | 220 | import { routerMixin } from 'shared/mixins'; |
|
160 | 230 | sushichef: { label: 'Sushi chef', params: { chef: true } }, |
161 | 231 | }; |
162 | 232 |
|
| 233 | + const DATE_WINDOWS = [ |
| 234 | + { key: 'any', label: 'Any time', months: null }, |
| 235 | + { key: '1mo', label: 'Last month', months: 1 }, |
| 236 | + { key: '3mo', label: 'Last 3 months', months: 3 }, |
| 237 | + { key: '6mo', label: 'Last 6 months', months: 6 }, |
| 238 | + { key: '1yr', label: 'Last year', months: 12 }, |
| 239 | + ]; |
| 240 | +
|
| 241 | + function buildDateWindowFilterMap(paramName) { |
| 242 | + const map = {}; |
| 243 | + for (const window of DATE_WINDOWS) { |
| 244 | + if (window.months === null) { |
| 245 | + map[window.key] = { label: window.label, params: {} }; |
| 246 | + } else { |
| 247 | + const cutoff = new Date(); |
| 248 | + cutoff.setMonth(cutoff.getMonth() - window.months); |
| 249 | + const iso = cutoff.toISOString().slice(0, 10); |
| 250 | + map[window.key] = { label: window.label, params: { [paramName]: iso } }; |
| 251 | + } |
| 252 | + } |
| 253 | + return map; |
| 254 | + } |
| 255 | +
|
| 256 | + function useDateWindowFilter({ name, paramName }) { |
| 257 | + const { filter, options, fetchQueryParams } = useFilter({ |
| 258 | + name, |
| 259 | + filterMap: buildDateWindowFilterMap(paramName), |
| 260 | + defaultValue: 'any', |
| 261 | + }); |
| 262 | + const wrapped = computed({ |
| 263 | + get: () => filter.value.value || 'any', |
| 264 | + set: value => { |
| 265 | + filter.value = options.value.find(o => o.value === value) || {}; |
| 266 | + }, |
| 267 | + }); |
| 268 | + return { filter: wrapped, options, fetchQueryParams }; |
| 269 | + } |
| 270 | +
|
| 271 | + function useBooleanFilter({ name, label, paramName }) { |
| 272 | + const filterMap = { |
| 273 | + no: { label: 'Any', params: {} }, |
| 274 | + yes: { label, params: { [paramName]: true } }, |
| 275 | + }; |
| 276 | + const { filter, options, fetchQueryParams } = useFilter({ |
| 277 | + name, |
| 278 | + filterMap, |
| 279 | + defaultValue: 'no', |
| 280 | + }); |
| 281 | + const wrapped = computed({ |
| 282 | + get: () => filter.value.value === 'yes', |
| 283 | + set: value => { |
| 284 | + const targetKey = value ? 'yes' : 'no'; |
| 285 | + filter.value = options.value.find(o => o.value === targetKey) || {}; |
| 286 | + }, |
| 287 | + }); |
| 288 | + return { filter: wrapped, fetchQueryParams }; |
| 289 | + } |
| 290 | +
|
163 | 291 | export default { |
164 | 292 | name: 'UserTable', |
165 | 293 | components: { |
|
218 | 346 | }, |
219 | 347 | }); |
220 | 348 |
|
| 349 | + const { |
| 350 | + filter: joinedWithinFilter, |
| 351 | + options: joinedWithinOptions, |
| 352 | + fetchQueryParams: joinedWithinFetchQueryParams, |
| 353 | + } = useDateWindowFilter({ name: 'joinedWithin', paramName: 'joined_since' }); |
| 354 | +
|
| 355 | + const { |
| 356 | + filter: activeWithinFilter, |
| 357 | + options: activeWithinOptions, |
| 358 | + fetchQueryParams: activeWithinFetchQueryParams, |
| 359 | + } = useDateWindowFilter({ name: 'activeWithin', paramName: 'active_since' }); |
| 360 | +
|
| 361 | + const { filter: hasPublishedFilter, fetchQueryParams: hasPublishedFetchQueryParams } = |
| 362 | + useBooleanFilter({ |
| 363 | + name: 'hasPublished', |
| 364 | + label: 'Has published a channel', |
| 365 | + paramName: 'published_channel', |
| 366 | + }); |
| 367 | +
|
| 368 | + const { filter: hasEditsFilter, fetchQueryParams: hasEditsFetchQueryParams } = |
| 369 | + useBooleanFilter({ |
| 370 | + name: 'hasEdits', |
| 371 | + label: 'Has Studio edits', |
| 372 | + paramName: 'has_edits', |
| 373 | + }); |
| 374 | +
|
221 | 375 | onMounted(() => { |
222 | 376 | // The locationFilterMap is built from the options in the CountryField component, |
223 | 377 | // so we need to wait until it's mounted to access them. |
|
240 | 394 | ...userTypeFetchQueryParams.value, |
241 | 395 | ...locationFetchQueryParams.value, |
242 | 396 | ...keywordSearchFetchQueryParams.value, |
| 397 | + ...joinedWithinFetchQueryParams.value, |
| 398 | + ...activeWithinFetchQueryParams.value, |
| 399 | + ...hasPublishedFetchQueryParams.value, |
| 400 | + ...hasEditsFetchQueryParams.value, |
243 | 401 | }; |
244 | 402 | }); |
245 | 403 |
|
|
260 | 418 | keywordInput, |
261 | 419 | setKeywords, |
262 | 420 | clearSearch, |
| 421 | + joinedWithinFilter, |
| 422 | + joinedWithinOptions, |
| 423 | + activeWithinFilter, |
| 424 | + activeWithinOptions, |
| 425 | + hasPublishedFilter, |
| 426 | + hasEditsFilter, |
263 | 427 | pagination, |
264 | 428 | loading, |
265 | 429 | loadItems, |
|
333 | 497 | mounted() { |
334 | 498 | this.updateTabTitle('Users - Administration'); |
335 | 499 | }, |
| 500 | + methods: { |
| 501 | + async onDownloadCSV() { |
| 502 | + this.$store.dispatch('showSnackbarSimple', 'Generating CSV...'); |
| 503 | + try { |
| 504 | + const response = await client.get(window.Urls.admin_users_download_csv(), { |
| 505 | + params: this.filterFetchQueryParams, |
| 506 | + responseType: 'blob', |
| 507 | + }); |
| 508 | + const filename = `studio_users_${new Date().toISOString().slice(0, 10)}.csv`; |
| 509 | + saveAs(response.data, filename); |
| 510 | + } catch (error) { |
| 511 | + const status = error.response && error.response.status; |
| 512 | + if (status === 412) { |
| 513 | + this.$store.dispatch( |
| 514 | + 'showSnackbarSimple', |
| 515 | + 'No filters applied. Pick at least one filter and try again.', |
| 516 | + ); |
| 517 | + } else { |
| 518 | + this.$store.dispatch('showSnackbarSimple', 'CSV download failed. Try again.'); |
| 519 | + } |
| 520 | + } |
| 521 | + }, |
| 522 | + }, |
336 | 523 | }; |
337 | 524 |
|
338 | 525 | </script> |
|
0 commit comments