diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-13 21:09:27 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-13 21:09:27 +0300 |
commit | 5248c5e2212b8e42b28b23e6839d69db0006829b (patch) | |
tree | f989d4b4cd06fc5dc28c024a5f230b42b0af179b /app/assets/javascripts/admin | |
parent | 0d55697d64b5f053bbd0f69da2962e7478097de3 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/admin')
8 files changed, 407 insertions, 1 deletions
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.vue b/app/assets/javascripts/admin/broadcast_messages/components/base.vue index b7bafe46327..f869d21d55f 100644 --- a/app/assets/javascripts/admin/broadcast_messages/components/base.vue +++ b/app/assets/javascripts/admin/broadcast_messages/components/base.vue @@ -5,14 +5,18 @@ import { buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; import { createAlert, VARIANT_DANGER } from '~/flash'; import { s__ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; +import { NEW_BROADCAST_MESSAGE } from '../constants'; +import MessageForm from './message_form.vue'; import MessagesTable from './messages_table.vue'; const PER_PAGE = 20; export default { name: 'BroadcastMessagesBase', + NEW_BROADCAST_MESSAGE, components: { GlPagination, + MessageForm, MessagesTable, }, @@ -97,6 +101,7 @@ export default { <template> <div> + <message-form :broadcast-message="$options.NEW_BROADCAST_MESSAGE" /> <messages-table v-if="hasVisibleMessages" :messages="visibleMessages" diff --git a/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue new file mode 100644 index 00000000000..07814ef2511 --- /dev/null +++ b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue @@ -0,0 +1,47 @@ +<script> +import { GlDatepicker, GlFormInput } from '@gitlab/ui'; +import { dateToTimeInputValue, timeToHoursMinutes } from '~/lib/utils/datetime/date_format_utility'; + +export default { + name: 'DatetimePicker', + components: { + GlDatepicker, + GlFormInput, + }, + props: { + value: { + type: Date, + required: true, + }, + }, + computed: { + date: { + get() { + return this.value; + }, + set(val) { + const dup = new Date(this.value.getTime()); + dup.setFullYear(val.getFullYear(), val.getMonth(), val.getDate()); + this.$emit('input', dup); + }, + }, + time: { + get() { + return dateToTimeInputValue(this.value); + }, + set(val) { + const dup = new Date(this.value.getTime()); + const { hours, minutes } = timeToHoursMinutes(val); + dup.setHours(hours, minutes); + this.$emit('input', dup); + }, + }, + }, +}; +</script> +<template> + <div class="gl-display-flex gl-gap-3 gl-align-items-center"> + <gl-datepicker v-model="date" /> + <gl-form-input v-model="time" size="sm" type="time" data-testid="time-picker" /> + </div> +</template> diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue new file mode 100644 index 00000000000..36796708e78 --- /dev/null +++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue @@ -0,0 +1,225 @@ +<script> +import { + GlButton, + GlBroadcastMessage, + GlForm, + GlFormCheckbox, + GlFormCheckboxGroup, + GlFormInput, + GlFormSelect, + GlFormText, + GlFormTextarea, +} from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; +import { s__ } from '~/locale'; +import { createAlert, VARIANT_DANGER } from '~/flash'; +import { redirectTo } from '~/lib/utils/url_utility'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { BROADCAST_MESSAGES_PATH, THEMES, TYPES, TYPE_BANNER } from '../constants'; +import MessageFormGroup from './message_form_group.vue'; +import DatetimePicker from './datetime_picker.vue'; + +const FORM_HEADERS = { headers: { 'Content-Type': 'application/json; charset=utf-8' } }; + +export default { + name: 'MessageForm', + components: { + DatetimePicker, + GlButton, + GlBroadcastMessage, + GlForm, + GlFormCheckbox, + GlFormCheckboxGroup, + GlFormInput, + GlFormSelect, + GlFormText, + GlFormTextarea, + MessageFormGroup, + }, + mixins: [glFeatureFlagsMixin()], + inject: ['targetAccessLevelOptions'], + i18n: { + message: s__('BroadcastMessages|Message'), + messagePlaceholder: s__('BroadcastMessages|Your message here'), + type: s__('BroadcastMessages|Type'), + theme: s__('BroadcastMessages|Theme'), + dismissable: s__('BroadcastMessages|Dismissable'), + dismissableDescription: s__('BroadcastMessages|Allow users to dismiss the broadcast message'), + targetRoles: s__('BroadcastMessages|Target roles'), + targetRolesDescription: s__( + 'BroadcastMessages|The broadcast message displays only to users in projects and groups who have these roles.', + ), + targetPath: s__('BroadcastMessages|Target Path'), + targetPathDescription: s__('BroadcastMessages|Paths can contain wildcards, like */welcome'), + startsAt: s__('BroadcastMessages|Starts at'), + endsAt: s__('BroadcastMessages|Ends at'), + add: s__('BroadcastMessages|Add broadcast message'), + addError: s__('BroadcastMessages|There was an error adding broadcast message.'), + update: s__('BroadcastMessages|Update broadcast message'), + updateError: s__('BroadcastMessages|There was an error updating broadcast message.'), + }, + messageThemes: THEMES, + messageTypes: TYPES, + props: { + broadcastMessage: { + type: Object, + required: true, + }, + }, + data() { + return { + loading: false, + message: this.broadcastMessage.message, + type: this.broadcastMessage.broadcastType, + theme: this.broadcastMessage.theme, + dismissable: this.broadcastMessage.dismissable || false, + targetPath: this.broadcastMessage.targetPath, + targetAccessLevels: this.broadcastMessage.targetAccessLevels, + targetAccessLevelOptions: this.targetAccessLevelOptions.map(([text, value]) => ({ + text, + value, + })), + startsAt: new Date(this.broadcastMessage.startsAt.getTime()), + endsAt: new Date(this.broadcastMessage.endsAt.getTime()), + }; + }, + computed: { + isBanner() { + return this.type === TYPE_BANNER; + }, + messageBlank() { + return this.message.trim() === ''; + }, + messagePreview() { + return this.messageBlank ? this.$options.i18n.messagePlaceholder : this.message; + }, + isAddForm() { + return !this.broadcastMessage.id; + }, + formPath() { + return this.isAddForm + ? BROADCAST_MESSAGES_PATH + : `${BROADCAST_MESSAGES_PATH}/${this.broadcastMessage.id}`; + }, + formPayload() { + return JSON.stringify({ + message: this.message, + broadcast_type: this.type, + theme: this.theme, + dismissable: this.dismissable, + target_path: this.targetPath, + target_access_levels: this.targetAccessLevels, + starts_at: this.startsAt.toISOString(), + ends_at: this.endsAt.toISOString(), + }); + }, + }, + methods: { + async onSubmit() { + this.loading = true; + + const success = await this.submitForm(); + if (success) { + redirectTo(BROADCAST_MESSAGES_PATH); + } else { + this.loading = false; + } + }, + + async submitForm() { + const requestMethod = this.isAddForm ? 'post' : 'patch'; + + try { + await axios[requestMethod](this.formPath, this.formPayload, FORM_HEADERS); + } catch (e) { + const message = this.isAddForm + ? this.$options.i18n.addError + : this.$options.i18n.updateError; + createAlert({ message, variant: VARIANT_DANGER }); + return false; + } + return true; + }, + }, +}; +</script> +<template> + <gl-form @submit.prevent="onSubmit"> + <gl-broadcast-message class="gl-my-6" :type="type" :theme="theme" :dismissible="dismissable"> + {{ messagePreview }} + </gl-broadcast-message> + + <message-form-group :label="$options.i18n.message" label-for="message-textarea"> + <gl-form-textarea + id="message-textarea" + v-model="message" + size="sm" + :placeholder="$options.i18n.messagePlaceholder" + /> + </message-form-group> + + <message-form-group :label="$options.i18n.type" label-for="type-select"> + <gl-form-select id="type-select" v-model="type" :options="$options.messageTypes" /> + </message-form-group> + + <template v-if="isBanner"> + <message-form-group :label="$options.i18n.theme" label-for="theme-select"> + <gl-form-select + id="theme-select" + v-model="theme" + :options="$options.messageThemes" + data-testid="theme-select" + /> + </message-form-group> + + <message-form-group :label="$options.i18n.dismissable" label-for="dismissable-checkbox"> + <gl-form-checkbox + id="dismissable-checkbox" + v-model="dismissable" + class="gl-mt-3" + data-testid="dismissable-checkbox" + > + <span>{{ $options.i18n.dismissableDescription }}</span> + </gl-form-checkbox> + </message-form-group> + </template> + + <message-form-group + v-if="glFeatures.roleTargetedBroadcastMessages" + :label="$options.i18n.targetRoles" + data-testid="target-roles-checkboxes" + > + <gl-form-checkbox-group v-model="targetAccessLevels" :options="targetAccessLevelOptions" /> + <gl-form-text> + {{ $options.i18n.targetRolesDescription }} + </gl-form-text> + </message-form-group> + + <message-form-group :label="$options.i18n.targetPath" label-for="target-path-input"> + <gl-form-input id="target-path-input" v-model="targetPath" /> + <gl-form-text> + {{ $options.i18n.targetPathDescription }} + </gl-form-text> + </message-form-group> + + <message-form-group :label="$options.i18n.startsAt"> + <datetime-picker v-model="startsAt" /> + </message-form-group> + + <message-form-group :label="$options.i18n.endsAt"> + <datetime-picker v-model="endsAt" /> + </message-form-group> + + <div class="form-actions gl-mb-3"> + <gl-button + type="submit" + variant="confirm" + :loading="loading" + :disabled="messageBlank" + data-testid="submit-button" + > + {{ isAddForm ? $options.i18n.add : $options.i18n.update }} + </gl-button> + </div> + </gl-form> +</template> diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue new file mode 100644 index 00000000000..eec51c0c28b --- /dev/null +++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue @@ -0,0 +1,34 @@ +<script> +import { GlFormGroup } from '@gitlab/ui'; + +export default { + name: 'MessageFormGroup', + components: { + GlFormGroup, + }, + props: { + label: { + type: String, + required: true, + }, + labelFor: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> +<template> + <div> + <gl-form-group + :label="label" + :label-for="labelFor" + label-cols-sm="2" + label-class="gl-mt-3" + label-align-sm="right" + > + <slot></slot> + </gl-form-group> + </div> +</template> diff --git a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue index 44c6bc72705..a523dd3b391 100644 --- a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue +++ b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue @@ -2,6 +2,7 @@ import { GlButton, GlTableLite } from '@gitlab/ui'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { __ } from '~/locale'; +import { formatDate } from '~/lib/utils/datetime/date_format_utility'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; const DEFAULT_TD_CLASSES = 'gl-vertical-align-middle!'; @@ -78,6 +79,11 @@ export default { safeHtmlConfig: { ADD_TAGS: ['use'], }, + methods: { + formatDate(dateString) { + return formatDate(new Date(dateString)); + }, + }, }; </script> <template> @@ -91,6 +97,14 @@ export default { <div v-safe-html:[$options.safeHtmlConfig]="preview"></div> </template> + <template #cell(starts_at)="{ item: { starts_at } }"> + {{ formatDate(starts_at) }} + </template> + + <template #cell(ends_at)="{ item: { ends_at } }"> + {{ formatDate(ends_at) }} + </template> + <template #cell(buttons)="{ item: { id, edit_path, disable_delete } }"> <gl-button icon="pencil" diff --git a/app/assets/javascripts/admin/broadcast_messages/constants.js b/app/assets/javascripts/admin/broadcast_messages/constants.js new file mode 100644 index 00000000000..6250d5a943d --- /dev/null +++ b/app/assets/javascripts/admin/broadcast_messages/constants.js @@ -0,0 +1,35 @@ +import { s__ } from '~/locale'; + +export const BROADCAST_MESSAGES_PATH = '/admin/broadcast_messages'; + +export const TYPE_BANNER = 'banner'; +export const TYPE_NOTIFICATION = 'notification'; + +export const TYPES = [ + { value: TYPE_BANNER, text: s__('BroadcastMessages|Banner') }, + { value: TYPE_NOTIFICATION, text: s__('BroadcastMessages|Notification') }, +]; + +export const THEMES = [ + { value: 'indigo', text: s__('BroadcastMessages|Indigo') }, + { value: 'light-indigo', text: s__('BroadcastMessages|Light Indigo') }, + { value: 'blue', text: s__('BroadcastMessages|Blue') }, + { value: 'light-blue', text: s__('BroadcastMessages|Light Blue') }, + { value: 'green', text: s__('BroadcastMessages|Green') }, + { value: 'light-green', text: s__('BroadcastMessages|Light Green') }, + { value: 'red', text: s__('BroadcastMessages|Red') }, + { value: 'light-red', text: s__('BroadcastMessages|Light Red') }, + { value: 'dark', text: s__('BroadcastMessages|Dark') }, + { value: 'light', text: s__('BroadcastMessages|Light') }, +]; + +export const NEW_BROADCAST_MESSAGE = { + message: '', + broadcastType: TYPES[0].value, + theme: THEMES[0].value, + dismissable: false, + targetPath: '', + targetAccessLevels: [], + startsAt: new Date(), + endsAt: new Date(), +}; diff --git a/app/assets/javascripts/admin/broadcast_messages/edit.js b/app/assets/javascripts/admin/broadcast_messages/edit.js new file mode 100644 index 00000000000..70a270f7a56 --- /dev/null +++ b/app/assets/javascripts/admin/broadcast_messages/edit.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +import MessageForm from './components/message_form.vue'; + +export default () => { + const el = document.querySelector('#js-broadcast-message'); + const { + id, + message, + broadcastType, + theme, + dismissable, + targetAccessLevels, + targetAccessLevelOptions, + targetPath, + startsAt, + endsAt, + } = el.dataset; + + return new Vue({ + el, + name: 'EditBroadcastMessage', + provide: { + targetAccessLevelOptions: JSON.parse(targetAccessLevelOptions), + }, + render(createElement) { + return createElement(MessageForm, { + props: { + broadcastMessage: { + id: parseInt(id, 10), + message, + broadcastType, + theme, + dismissable: dismissable === 'true', + targetAccessLevels: JSON.parse(targetAccessLevels), + targetPath, + startsAt: new Date(startsAt), + endsAt: new Date(endsAt), + }, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/admin/broadcast_messages/index.js b/app/assets/javascripts/admin/broadcast_messages/index.js index 81952d2033e..fd8b2aad4ec 100644 --- a/app/assets/javascripts/admin/broadcast_messages/index.js +++ b/app/assets/javascripts/admin/broadcast_messages/index.js @@ -3,11 +3,14 @@ import BroadcastMessagesBase from './components/base.vue'; export default () => { const el = document.querySelector('#js-broadcast-messages'); - const { page, messagesCount, messages } = el.dataset; + const { page, targetAccessLevelOptions, messagesCount, messages } = el.dataset; return new Vue({ el, name: 'BroadcastMessages', + provide: { + targetAccessLevelOptions: JSON.parse(targetAccessLevelOptions), + }, render(createElement) { return createElement(BroadcastMessagesBase, { props: { |