diff options
Diffstat (limited to 'app/assets/javascripts/set_status_modal')
5 files changed, 377 insertions, 192 deletions
diff --git a/app/assets/javascripts/set_status_modal/constants.js b/app/assets/javascripts/set_status_modal/constants.js new file mode 100644 index 00000000000..53e64db1497 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/constants.js @@ -0,0 +1,14 @@ +import { timeRanges } from '~/vue_shared/constants'; +import { __ } from '~/locale'; + +export const NEVER_TIME_RANGE = { + label: __('Never'), + name: 'never', +}; + +export const TIME_RANGES_WITH_NEVER = [NEVER_TIME_RANGE, ...timeRanges]; + +export const AVAILABILITY_STATUS = { + BUSY: 'busy', + NOT_SET: 'not_set', +}; diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue new file mode 100644 index 00000000000..7f9a30b7ff1 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/set_status_form.vue @@ -0,0 +1,231 @@ +<script> +import { + GlButton, + GlTooltipDirective, + GlIcon, + GlFormCheckbox, + GlFormInput, + GlFormInputGroup, + GlDropdown, + GlDropdownItem, + GlSprintf, + GlFormGroup, + GlSafeHtmlDirective, +} from '@gitlab/ui'; +import $ from 'jquery'; +import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; +import * as Emoji from '~/emoji'; +import { s__ } from '~/locale'; +import { TIME_RANGES_WITH_NEVER, AVAILABILITY_STATUS } from './constants'; + +export default { + components: { + GlButton, + GlIcon, + GlFormCheckbox, + GlFormInput, + GlFormInputGroup, + GlDropdown, + GlDropdownItem, + GlSprintf, + GlFormGroup, + EmojiPicker: () => import('~/emoji/components/picker.vue'), + }, + directives: { + GlTooltip: GlTooltipDirective, + SafeHtml: GlSafeHtmlDirective, + }, + props: { + defaultEmoji: { + type: String, + required: false, + default: '', + }, + emoji: { + type: String, + required: true, + }, + message: { + type: String, + required: true, + }, + availability: { + type: Boolean, + required: true, + }, + clearStatusAfter: { + type: Object, + required: false, + default: () => ({}), + }, + currentClearStatusAfter: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + defaultEmojiTag: '', + emojiTag: '', + }; + }, + computed: { + isCustomEmoji() { + return this.emoji !== this.defaultEmoji; + }, + isDirty() { + return Boolean(this.message.length || this.isCustomEmoji); + }, + noEmoji() { + return this.emojiTag === ''; + }, + }, + mounted() { + this.setupEmojiListAndAutocomplete(); + }, + methods: { + async setupEmojiListAndAutocomplete() { + const emojiAutocomplete = new GfmAutoComplete(); + emojiAutocomplete.setup($(this.$refs.statusMessageField.$el), { emojis: true }); + + if (this.emoji) { + this.emojiTag = Emoji.glEmojiTag(this.emoji); + } + this.defaultEmojiTag = Emoji.glEmojiTag(this.defaultEmoji); + + this.setDefaultEmoji(); + }, + setDefaultEmoji() { + const { emojiTag } = this; + const hasStatusMessage = Boolean(this.message.length); + if (hasStatusMessage && emojiTag) { + return; + } + + if (hasStatusMessage) { + this.emojiTag = this.defaultEmojiTag; + } else if (emojiTag === this.defaultEmojiTag) { + this.clearEmoji(); + } + }, + handleEmojiClick(emoji) { + this.$emit('emoji-click', emoji); + + this.emojiTag = Emoji.glEmojiTag(emoji); + }, + clearEmoji() { + if (this.emojiTag) { + this.emojiTag = ''; + } + }, + clearStatusInputs() { + this.$emit('emoji-click', ''); + this.$emit('message-input', ''); + this.clearEmoji(); + }, + }, + TIME_RANGES_WITH_NEVER, + AVAILABILITY_STATUS, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, + i18n: { + statusMessagePlaceholder: s__(`SetStatusModal|What's your status?`), + clearStatusButtonLabel: s__('SetStatusModal|Clear status'), + availabilityCheckboxLabel: s__('SetStatusModal|Busy'), + availabilityCheckboxHelpText: s__( + 'SetStatusModal|An indicator appears next to your name and avatar', + ), + clearStatusAfterDropdownLabel: s__('SetStatusModal|Clear status after'), + clearStatusAfterMessage: s__('SetStatusModal|Your status resets on %{date}.'), + }, +}; +</script> + +<template> + <div> + <gl-form-input-group class="gl-mb-5"> + <gl-form-input + ref="statusMessageField" + :value="message" + :placeholder="$options.i18n.statusMessagePlaceholder" + @keyup="setDefaultEmoji" + @input="$emit('message-input', $event)" + @keyup.enter.prevent + /> + <template #prepend> + <emoji-picker + dropdown-class="gl-h-full" + toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none!" + boundary="viewport" + :right="false" + @click="handleEmojiClick" + > + <template #button-content> + <span + v-if="noEmoji" + class="no-emoji-placeholder position-relative" + data-testid="no-emoji-placeholder" + > + <gl-icon name="slight-smile" class="award-control-icon-neutral" /> + <gl-icon name="smiley" class="award-control-icon-positive" /> + <gl-icon name="smile" class="award-control-icon-super-positive" /> + </span> + <span v-else> + <span + v-safe-html:[$options.safeHtmlConfig]="emojiTag" + data-testid="selected-emoji" + ></span> + </span> + </template> + </emoji-picker> + </template> + <template v-if="isDirty" #append> + <gl-button + v-gl-tooltip.bottom + :title="$options.i18n.clearStatusButtonLabel" + :aria-label="$options.i18n.clearStatusButtonLabel" + icon="close" + class="js-clear-user-status-button" + @click="clearStatusInputs" + /> + </template> + </gl-form-input-group> + + <gl-form-checkbox + :checked="availability" + class="gl-mb-5" + data-testid="user-availability-checkbox" + @input="$emit('availability-input', $event)" + > + {{ $options.i18n.availabilityCheckboxLabel }} + <template #help> + {{ $options.i18n.availabilityCheckboxHelpText }} + </template> + </gl-form-checkbox> + + <gl-form-group :label="$options.i18n.clearStatusAfterDropdownLabel" class="gl-mb-0"> + <gl-dropdown + block + :text="clearStatusAfter.label" + data-testid="clear-status-at-dropdown" + toggle-class="gl-mb-0 gl-form-input-md" + > + <gl-dropdown-item + v-for="after in $options.TIME_RANGES_WITH_NEVER" + :key="after.name" + :data-testid="after.name" + @click="$emit('clear-status-after-click', after)" + >{{ after.label }}</gl-dropdown-item + > + </gl-dropdown> + + <template v-if="currentClearStatusAfter.length" #description> + <span data-testid="clear-status-at-message"> + <gl-sprintf :message="$options.i18n.clearStatusAfterMessage"> + <template #date>{{ currentClearStatusAfter }}</template> + </gl-sprintf> + </span> + </template> + </gl-form-group> + </div> +</template> diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index 2cdec8fc481..80b1cb8c4d5 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -1,55 +1,21 @@ <script> -import { - GlButton, - GlToast, - GlModal, - GlTooltipDirective, - GlIcon, - GlFormCheckbox, - GlFormInput, - GlFormInputGroup, - GlDropdown, - GlDropdownItem, - GlSafeHtmlDirective, -} from '@gitlab/ui'; -import $ from 'jquery'; +import { GlToast, GlTooltipDirective, GlSafeHtmlDirective, GlModal } from '@gitlab/ui'; import Vue from 'vue'; -import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; -import * as Emoji from '~/emoji'; import createFlash from '~/flash'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; -import { __, s__, sprintf } from '~/locale'; +import { s__ } from '~/locale'; import { updateUserStatus } from '~/rest_api'; -import { timeRanges } from '~/vue_shared/constants'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { isUserBusy } from './utils'; - -export const AVAILABILITY_STATUS = { - BUSY: 'busy', - NOT_SET: 'not_set', -}; +import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants'; +import SetStatusForm from './set_status_form.vue'; Vue.use(GlToast); -const statusTimeRanges = [ - { - label: __('Never'), - name: 'never', - }, - ...timeRanges, -]; - export default { components: { - GlButton, - GlIcon, GlModal, - GlFormCheckbox, - GlFormInput, - GlFormInputGroup, - GlDropdown, - GlDropdownItem, - EmojiPicker: () => import('~/emoji/components/picker.vue'), + SetStatusForm, }, directives: { GlTooltip: GlTooltipDirective, @@ -85,26 +51,12 @@ export default { return { defaultEmojiTag: '', emoji: this.currentEmoji, - emojiMenu: null, - emojiTag: '', message: this.currentMessage, modalId: 'set-user-status-modal', - noEmoji: true, availability: isUserBusy(this.currentAvailability), - clearStatusAfter: statusTimeRanges[0], - clearStatusAfterMessage: sprintf(s__('SetStatusModal|Your status resets on %{date}.'), { - date: this.currentClearStatusAfter, - }), + clearStatusAfter: NEVER_TIME_RANGE, }; }, - computed: { - isCustomEmoji() { - return this.emoji !== this.defaultEmoji; - }, - isDirty() { - return Boolean(this.message.length || this.isCustomEmoji); - }, - }, mounted() { this.$root.$emit(BV_SHOW_MODAL, this.modalId); }, @@ -112,62 +64,10 @@ export default { closeModal() { this.$root.$emit(BV_HIDE_MODAL, this.modalId); }, - setupEmojiListAndAutocomplete() { - const emojiAutocomplete = new GfmAutoComplete(); - emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true }); - - Emoji.initEmojiMap() - .then(() => { - if (this.emoji) { - this.emojiTag = Emoji.glEmojiTag(this.emoji); - } - this.noEmoji = this.emoji === ''; - this.defaultEmojiTag = Emoji.glEmojiTag(this.defaultEmoji); - - this.setDefaultEmoji(); - }) - .catch(() => - createFlash({ - message: __('Failed to load emoji list.'), - }), - ); - }, - setDefaultEmoji() { - const { emojiTag } = this; - const hasStatusMessage = Boolean(this.message.length); - if (hasStatusMessage && emojiTag) { - return; - } - - if (hasStatusMessage) { - this.noEmoji = false; - this.emojiTag = this.defaultEmojiTag; - } else if (emojiTag === this.defaultEmojiTag) { - this.noEmoji = true; - this.clearEmoji(); - } - }, - setEmoji(emoji) { - this.emoji = emoji; - this.noEmoji = false; - this.clearEmoji(); - - this.emojiTag = Emoji.glEmojiTag(this.emoji); - }, - clearEmoji() { - if (this.emojiTag) { - this.emojiTag = ''; - } - }, - clearStatusInputs() { - this.emoji = ''; - this.message = ''; - this.noEmoji = true; - this.clearEmoji(); - }, removeStatus() { this.availability = false; - this.clearStatusInputs(); + this.emoji = ''; + this.message = ''; this.setStatus(); }, setStatus() { @@ -178,7 +78,7 @@ export default { message, availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET, clearStatusAfter: - clearStatusAfter.label === statusTimeRanges[0].label ? null : clearStatusAfter.shortcut, + clearStatusAfter.label === NEVER_TIME_RANGE.label ? null : clearStatusAfter.shortcut, }) .then(this.onUpdateSuccess) .catch(this.onUpdateFail); @@ -197,11 +97,19 @@ export default { this.closeModal(); }, - setClearStatusAfter(after) { + handleMessageInput(value) { + this.message = value; + }, + handleEmojiClick(emoji) { + this.emoji = emoji; + }, + handleClearStatusAfterClick(after) { this.clearStatusAfter = after; }, + handleAvailabilityInput(value) { + this.availability = value; + }, }, - statusTimeRanges, safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, actionPrimary: { text: s__('SetStatusModal|Set status') }, actionSecondary: { text: s__('SetStatusModal|Remove status') }, @@ -215,85 +123,20 @@ export default { :action-primary="$options.actionPrimary" :action-secondary="$options.actionSecondary" modal-class="set-user-status-modal" - @shown="setupEmojiListAndAutocomplete" @primary="setStatus" @secondary="removeStatus" > - <input v-model="emoji" class="js-status-emoji-field" type="hidden" name="user[status][emoji]" /> - <gl-form-input-group class="gl-mb-5"> - <gl-form-input - ref="statusMessageField" - v-model="message" - :placeholder="s__(`SetStatusModal|What's your status?`)" - class="js-status-message-field" - name="user[status][message]" - @keyup="setDefaultEmoji" - @keyup.enter.prevent - /> - <template #prepend> - <emoji-picker - dropdown-class="gl-h-full" - toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none!" - boundary="viewport" - :right="false" - @click="setEmoji" - > - <template #button-content> - <span v-safe-html:[$options.safeHtmlConfig]="emojiTag"></span> - <span - v-show="noEmoji" - class="js-no-emoji-placeholder no-emoji-placeholder position-relative" - > - <gl-icon name="slight-smile" class="award-control-icon-neutral" /> - <gl-icon name="smiley" class="award-control-icon-positive" /> - <gl-icon name="smile" class="award-control-icon-super-positive" /> - </span> - </template> - </emoji-picker> - </template> - <template v-if="isDirty" #append> - <gl-button - v-gl-tooltip.bottom - :title="s__('SetStatusModal|Clear status')" - :aria-label="s__('SetStatusModal|Clear status')" - icon="close" - class="js-clear-user-status-button" - @click="clearStatusInputs" - /> - </template> - </gl-form-input-group> - - <gl-form-checkbox - v-model="availability" - class="gl-mb-5" - data-testid="user-availability-checkbox" - > - {{ s__('SetStatusModal|Busy') }} - <template #help> - {{ s__('SetStatusModal|An indicator appears next to your name and avatar') }} - </template> - </gl-form-checkbox> - - <div class="form-group"> - <div class="gl-display-flex gl-align-items-baseline"> - <span class="gl-mr-3">{{ s__('SetStatusModal|Clear status after') }}</span> - <gl-dropdown :text="clearStatusAfter.label" data-testid="clear-status-at-dropdown"> - <gl-dropdown-item - v-for="after in $options.statusTimeRanges" - :key="after.name" - :data-testid="after.name" - @click="setClearStatusAfter(after)" - >{{ after.label }}</gl-dropdown-item - > - </gl-dropdown> - </div> - <div - v-if="currentClearStatusAfter.length" - class="gl-mt-3 gl-text-gray-400 gl-font-sm" - data-testid="clear-status-at-message" - > - {{ clearStatusAfterMessage }} - </div> - </div> + <set-status-form + :default-emoji="defaultEmoji" + :emoji="emoji" + :message="message" + :availability="availability" + :clear-status-after="clearStatusAfter" + :current-clear-status-after="currentClearStatusAfter" + @message-input="handleMessageInput" + @emoji-click="handleEmojiClick" + @clear-status-after-click="handleClearStatusAfterClick" + @availability-input="handleAvailabilityInput" + /> </gl-modal> </template> diff --git a/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue b/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue new file mode 100644 index 00000000000..c709611e13d --- /dev/null +++ b/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue @@ -0,0 +1,100 @@ +<script> +import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; +import dateFormat from '~/lib/dateformat'; +import SetStatusForm from './set_status_form.vue'; +import { isUserBusy } from './utils'; +import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants'; + +export default { + components: { SetStatusForm }, + inject: ['fields'], + data() { + return { + emoji: this.fields.emoji.value, + message: this.fields.message.value, + availability: isUserBusy(this.fields.availability.value), + clearStatusAfter: NEVER_TIME_RANGE, + currentClearStatusAfter: this.fields.clearStatusAfter.value, + }; + }, + computed: { + clearStatusAfterInputValue() { + return this.clearStatusAfter.label === NEVER_TIME_RANGE.label + ? null + : this.clearStatusAfter.shortcut; + }, + availabilityInputValue() { + return this.availability + ? this.$options.AVAILABILITY_STATUS.BUSY + : this.$options.AVAILABILITY_STATUS.NOT_SET; + }, + }, + mounted() { + this.$options.formEl = document.querySelector('form.js-edit-user'); + + if (!this.$options.formEl) return; + + this.$options.formEl.addEventListener('ajax:success', this.handleFormSuccess); + }, + beforeDestroy() { + if (!this.$options.formEl) return; + + this.$options.formEl.removeEventListener('ajax:success', this.handleFormSuccess); + }, + methods: { + handleMessageInput(value) { + this.message = value; + }, + handleEmojiClick(emoji) { + this.emoji = emoji; + }, + handleClearStatusAfterClick(after) { + this.clearStatusAfter = after; + }, + handleAvailabilityInput(value) { + this.availability = value; + }, + handleFormSuccess() { + if (!this.clearStatusAfter?.duration?.seconds) { + this.currentClearStatusAfter = ''; + + return; + } + + const now = new Date(); + const currentClearStatusAfterDate = new Date( + now.getTime() + secondsToMilliseconds(this.clearStatusAfter.duration.seconds), + ); + + this.currentClearStatusAfter = dateFormat( + currentClearStatusAfterDate, + "UTC:yyyy-mm-dd HH:MM:ss 'UTC'", + ); + this.clearStatusAfter = NEVER_TIME_RANGE; + }, + }, + AVAILABILITY_STATUS, + formEl: null, +}; +</script> + +<template> + <div> + <input :value="emoji" type="hidden" :name="fields.emoji.name" /> + <input :value="message" type="hidden" :name="fields.message.name" /> + <input :value="availabilityInputValue" type="hidden" :name="fields.availability.name" /> + <input :value="clearStatusAfterInputValue" type="hidden" :name="fields.clearStatusAfter.name" /> + <set-status-form + default-emoji="speech_balloon" + :emoji="emoji" + :message="message" + :availability="availability" + :clear-status-after="clearStatusAfter" + :current-clear-status-after="currentClearStatusAfter" + @message-input="handleMessageInput" + @emoji-click="handleEmojiClick" + @clear-status-after-click="handleClearStatusAfterClick" + @availability-input="handleAvailabilityInput" + /> + </div> +</template> diff --git a/app/assets/javascripts/set_status_modal/utils.js b/app/assets/javascripts/set_status_modal/utils.js index e17d95adb25..950091195d2 100644 --- a/app/assets/javascripts/set_status_modal/utils.js +++ b/app/assets/javascripts/set_status_modal/utils.js @@ -1,7 +1,4 @@ -export const AVAILABILITY_STATUS = { - BUSY: 'busy', - NOT_SET: 'not_set', -}; +import { AVAILABILITY_STATUS } from './constants'; export const isUserBusy = (status = '') => Boolean(status.length && status.toLowerCase().trim() === AVAILABILITY_STATUS.BUSY); |