diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
commit | 36a59d088eca61b834191dacea009677a96c052f (patch) | |
tree | e4f33972dab5d8ef79e3944a9f403035fceea43f /app/assets/javascripts/invite_members | |
parent | a1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff) |
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/invite_members')
7 files changed, 218 insertions, 101 deletions
diff --git a/app/assets/javascripts/invite_members/components/import_a_project_modal.vue b/app/assets/javascripts/invite_members/components/import_a_project_modal.vue index d71468284ca..fb6c376cfe6 100644 --- a/app/assets/javascripts/invite_members/components/import_a_project_modal.vue +++ b/app/assets/javascripts/invite_members/components/import_a_project_modal.vue @@ -145,7 +145,7 @@ export default { <gl-button :disabled="importDisabled" :loading="isLoading" - variant="success" + variant="confirm" data-testid="import-button" @click="submitImport" >{{ $options.i18n.modalPrimaryButton }}</gl-button diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index a9aa0e9b760..7857b9d86d2 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -88,6 +88,11 @@ export default { type: Array, required: true, }, + usersLimitDataset: { + type: Object, + required: false, + default: () => ({}), + }, }, data() { return { @@ -146,6 +151,18 @@ export default { isOnLearnGitlab() { return this.source === LEARN_GITLAB; }, + reachedLimit() { + if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) { + return this.usersLimitDataset.membersCount >= this.usersLimitDataset.freeUsersLimit; + } + + return false; + }, + formGroupDescription() { + return this.reachedLimit + ? this.$options.labels.placeHolderDisabled + : this.$options.labels.placeHolder; + }, }, mounted() { eventHub.$on('openModal', (options) => { @@ -274,12 +291,14 @@ export default { :help-link="helpLink" :label-intro-text="labelIntroText" :label-search-field="$options.labels.searchField" - :form-group-description="$options.labels.placeHolder" + :form-group-description="formGroupDescription" :submit-disabled="inviteDisabled" :invalid-feedback-message="invalidFeedbackMessage" :is-loading="isLoading" :new-users-to-invite="newUsersToInvite" :root-group-id="rootId" + :reached-limit="reachedLimit" + :users-limit-dataset="usersLimitDataset" @reset="resetFields" @submit="sendInvite" @access-level="onAccessLevelUpdate" @@ -294,7 +313,10 @@ export default { </template> <template #user-limit-notification> - <user-limit-notification /> + <user-limit-notification + :reached-limit="reachedLimit" + :users-limit-dataset="usersLimitDataset" + /> </template> <template #select="{ validationState, labelId }"> diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue index d9297614a7e..33d37b809c2 100644 --- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue +++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue @@ -8,7 +8,9 @@ import { GlLink, GlSprintf, GlFormInput, + GlIcon, } from '@gitlab/ui'; +import Tracking from '~/tracking'; import { sprintf } from '~/locale'; import ContentTransition from '~/vue_shared/components/content_transition.vue'; import { @@ -16,8 +18,13 @@ import { ACCESS_EXPIRE_DATE, READ_MORE_TEXT, INVITE_BUTTON_TEXT, + INVITE_BUTTON_TEXT_DISABLED, CANCEL_BUTTON_TEXT, + CANCEL_BUTTON_TEXT_DISABLED, HEADER_CLOSE_LABEL, + ON_SHOW_TRACK_LABEL, + ON_CLOSE_TRACK_LABEL, + ON_SUBMIT_TRACK_LABEL, } from '../constants'; const DEFAULT_SLOT = 'default'; @@ -41,8 +48,10 @@ export default { GlDropdownItem, GlSprintf, GlFormInput, + GlIcon, ContentTransition, }, + mixins: [Tracking.mixin()], inheritAttrs: false, props: { modalTitle: { @@ -122,6 +131,16 @@ export default { required: false, default: false, }, + reachedLimit: { + type: Boolean, + required: false, + default: false, + }, + usersLimitDataset: { + type: Object, + required: false, + default: () => ({}), + }, }, data() { // Be sure to check out reset! @@ -151,20 +170,27 @@ export default { }, actionPrimary() { return { - text: this.submitButtonText, + text: this.reachedLimit ? INVITE_BUTTON_TEXT_DISABLED : this.submitButtonText, attributes: { variant: 'confirm', - disabled: this.submitDisabled, - loading: this.isLoading, + disabled: this.reachedLimit ? false : this.submitDisabled, + loading: this.reachedLimit ? false : this.isLoading, 'data-qa-selector': 'invite_button', + ...(this.reachedLimit && { href: this.usersLimitDataset.membersPath }), }, }; }, actionCancel() { + if (this.reachedLimit && this.usersLimitDataset.userNamespace) return undefined; + return { - text: this.cancelButtonText, + text: this.reachedLimit ? CANCEL_BUTTON_TEXT_DISABLED : this.cancelButtonText, + ...(this.reachedLimit && { attributes: { href: this.usersLimitDataset.purchasePath } }), }; }, + selectLabelClass() { + return `col-form-label ${this.reachedLimit ? 'gl-text-gray-500' : ''}`; + }, }, watch: { selectedAccessLevel: { @@ -183,15 +209,24 @@ export default { this.$emit('reset'); }, + onShowModal() { + if (this.reachedLimit) { + this.track('render', { category: 'default', label: ON_SHOW_TRACK_LABEL }); + } + }, onCloseModal(e) { - if (this.preventCancelDefault) { + if (this.preventCancelDefault || this.reachedLimit) { e.preventDefault(); } else { this.onReset(); this.$refs.modal.hide(); } - this.$emit('cancel'); + if (this.reachedLimit) { + this.track('click_button', { category: 'default', label: ON_CLOSE_TRACK_LABEL }); + } else { + this.$emit('cancel'); + } }, changeSelectedItem(item) { this.selectedAccessLevel = item; @@ -200,10 +235,14 @@ export default { // We never want to hide when submitting e.preventDefault(); - this.$emit('submit', { - accessLevel: this.selectedAccessLevel, - expiresAt: this.selectedDate, - }); + if (this.reachedLimit) { + this.track('click_button', { category: 'default', label: ON_SUBMIT_TRACK_LABEL }); + } else { + this.$emit('submit', { + accessLevel: this.selectedAccessLevel, + expiresAt: this.selectedDate, + }); + } }, }, HEADER_CLOSE_LABEL, @@ -227,6 +266,7 @@ export default { :header-close-label="$options.HEADER_CLOSE_LABEL" :action-primary="actionPrimary" :action-cancel="actionCancel" + @shown="onShowModal" @primary="onSubmit" @cancel="onCloseModal" @hidden="onReset" @@ -255,64 +295,73 @@ export default { <gl-form-group :invalid-feedback="invalidFeedbackMessage" :state="validationState" - :description="formGroupDescription" data-testid="members-form-group" > - <label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label> - <slot name="select" v-bind="{ validationState, labelId: selectLabelId }"></slot> + <template #description> + <gl-icon v-if="reachedLimit" name="lock" /> + {{ formGroupDescription }} + </template> + + <label :id="selectLabelId" :class="selectLabelClass">{{ labelSearchField }}</label> + <gl-form-input v-if="reachedLimit" data-testid="disabled-input" disabled /> + <slot v-else name="select" v-bind="{ validationState, labelId: selectLabelId }"></slot> </gl-form-group> - <label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label> - <div class="gl-mt-2 gl-w-half gl-xs-w-full"> - <gl-dropdown - class="gl-shadow-none gl-w-full" - data-qa-selector="access_level_dropdown" - v-bind="$attrs" - :text="selectedRoleName" - > - <template v-for="(key, item) in accessLevels"> - <gl-dropdown-item - :key="key" - active-class="is-active" - is-check-item - :is-checked="key === selectedAccessLevel" - @click="changeSelectedItem(key)" - > - <div>{{ item }}</div> - </gl-dropdown-item> - </template> - </gl-dropdown> - </div> + <template v-if="!reachedLimit"> + <label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label> - <div class="gl-mt-2 gl-w-half gl-xs-w-full"> - <gl-sprintf :message="$options.READ_MORE_TEXT"> - <template #link="{ content }"> - <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </div> + <div class="gl-mt-2 gl-w-half gl-xs-w-full"> + <gl-dropdown + class="gl-shadow-none gl-w-full" + data-qa-selector="access_level_dropdown" + v-bind="$attrs" + :text="selectedRoleName" + > + <template v-for="(key, item) in accessLevels"> + <gl-dropdown-item + :key="key" + active-class="is-active" + is-check-item + :is-checked="key === selectedAccessLevel" + @click="changeSelectedItem(key)" + > + <div>{{ item }}</div> + </gl-dropdown-item> + </template> + </gl-dropdown> + </div> - <label class="gl-mt-5 gl-display-block" for="expires_at">{{ - $options.ACCESS_EXPIRE_DATE - }}</label> - <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"> - <gl-datepicker - v-model="selectedDate" - class="gl-display-inline!" - :min-date="minDate" - :target="null" - > - <template #default="{ formattedDate }"> - <gl-form-input - class="gl-w-full" - :value="formattedDate" - :placeholder="__(`YYYY-MM-DD`)" - /> - </template> - </gl-datepicker> - </div> - <slot name="form-after"></slot> + <div class="gl-mt-2 gl-w-half gl-xs-w-full"> + <gl-sprintf :message="$options.READ_MORE_TEXT"> + <template #link="{ content }"> + <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> + + <label class="gl-mt-5 gl-display-block" for="expires_at">{{ + $options.ACCESS_EXPIRE_DATE + }}</label> + <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"> + <gl-datepicker + v-model="selectedDate" + class="gl-display-inline!" + :min-date="minDate" + :target="null" + > + <template #default="{ formattedDate }"> + <gl-form-input + class="gl-w-full" + :value="formattedDate" + :placeholder="__(`YYYY-MM-DD`)" + /> + </template> + </gl-datepicker> + </div> + <slot name="form-after"></slot> + </template> </template> + <template v-for="{ key } in extraSlots" #[key]> <slot :name="key"></slot> </template> diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 0a191f6d406..30c9294344e 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -134,10 +134,10 @@ export default { :hide-dropdown-with-no-items="hideDropdownWithNoItems" :placeholder="placeholderText" :aria-labelledby="ariaLabelledby" - :text-input-attrs="{ + :text-input-attrs="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ { 'data-testid': 'members-token-select-input', 'data-qa-selector': 'members_token_select_input', - }" + } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" @blur="handleBlur" @text-input="handleTextInput" @input="handleInput" diff --git a/app/assets/javascripts/invite_members/components/user_limit_notification.vue b/app/assets/javascripts/invite_members/components/user_limit_notification.vue index beef1aef8a1..ea5f4317d86 100644 --- a/app/assets/javascripts/invite_members/components/user_limit_notification.vue +++ b/app/assets/javascripts/invite_members/components/user_limit_notification.vue @@ -1,35 +1,51 @@ <script> import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; -import { s__, n__, sprintf } from '~/locale'; +import { n__, sprintf } from '~/locale'; -const CLOSE_TO_LIMIT_COUNT = 2; - -const WARNING_ALERT_TITLE = s__( - 'InviteMembersModal|You only have space for %{count} more %{members} in %{name}', -); - -const DANGER_ALERT_TITLE = s__( - "InviteMembersModal|You've reached your %{count} %{members} limit for %{name}", -); - -const CLOSE_TO_LIMIT_MESSAGE = s__( - 'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.', -); +import { + WARNING_ALERT_TITLE, + DANGER_ALERT_TITLE, + REACHED_LIMIT_MESSAGE, + REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE, + CLOSE_TO_LIMIT_MESSAGE, +} from '../constants'; -const REACHED_LIMIT_MESSAGE = s__( - 'InviteMembersModal|New members will be unable to participate. You can manage your members by removing ones you no longer need.', -).concat(' ', CLOSE_TO_LIMIT_MESSAGE); +const CLOSE_TO_LIMIT_COUNT = 2; export default { name: 'UserLimitNotification', components: { GlAlert, GlSprintf, GlLink }, - inject: ['name', 'newTrialRegistrationPath', 'purchasePath', 'freeUsersLimit', 'membersCount'], + inject: ['name'], + props: { + reachedLimit: { + type: Boolean, + required: true, + }, + usersLimitDataset: { + type: Object, + required: false, + default: () => ({}), + }, + }, computed: { - reachedLimit() { - return this.isLimit(); + freeUsersLimit() { + return this.usersLimitDataset.freeUsersLimit; + }, + membersCount() { + return this.usersLimitDataset.membersCount; + }, + newTrialRegistrationPath() { + return this.usersLimitDataset.newTrialRegistrationPath; + }, + purchasePath() { + return this.usersLimitDataset.purchasePath; }, closeToLimit() { - return this.isLimit(CLOSE_TO_LIMIT_COUNT); + if (this.freeUsersLimit && this.membersCount) { + return this.membersCount >= this.freeUsersLimit - CLOSE_TO_LIMIT_COUNT; + } + + return false; }, warningAlertTitle() { return sprintf(WARNING_ALERT_TITLE, { @@ -51,28 +67,29 @@ export default { title() { return this.reachedLimit ? this.dangerAlertTitle : this.warningAlertTitle; }, + reachedLimitMessage() { + if (this.usersLimitDataset.userNamespace) { + return this.$options.i18n.reachedLimitMessage; + } + + return this.$options.i18n.reachedLimitUpgradeSuggestionMessage; + }, message() { if (this.reachedLimit) { - return this.$options.i18n.reachedLimitMessage; + return this.reachedLimitMessage; } return this.$options.i18n.closeToLimitMessage; }, }, methods: { - isLimit(deviation = 0) { - if (this.freeUsersLimit && this.membersCount) { - return this.membersCount >= this.freeUsersLimit - deviation; - } - - return false; - }, pluralMembers(count) { return n__('member', 'members', count); }, }, i18n: { reachedLimitMessage: REACHED_LIMIT_MESSAGE, + reachedLimitUpgradeSuggestionMessage: REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE, closeToLimitMessage: CLOSE_TO_LIMIT_MESSAGE, }, }; diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 3cd0bfc0181..928f79f1c8d 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -35,8 +35,11 @@ export const MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT = s__( export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__( "InviteMembersModal|Congratulations on creating your project, you're almost there!", ); -export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|GitLab member or email address'); +export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|Username or email address'); export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses'); +export const MEMBERS_PLACEHOLDER_DISABLED = s__( + 'InviteMembersModal|This feature is disabled until this group has space for more members.', +); export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__( 'InviteMembersModal|Create issues for your new team member to work on (optional)', ); @@ -66,7 +69,9 @@ export const READ_MORE_TEXT = s__( `InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`, ); export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite'); +export const INVITE_BUTTON_TEXT_DISABLED = s__('InviteMembersModal|Manage members'); export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel'); +export const CANCEL_BUTTON_TEXT_DISABLED = s__('InviteMembersModal|Explore paid plans'); export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members'); export const MEMBER_MODAL_LABELS = { @@ -94,6 +99,7 @@ export const MEMBER_MODAL_LABELS = { }, searchField: MEMBERS_SEARCH_FIELD, placeHolder: MEMBERS_PLACEHOLDER, + placeHolderDisabled: MEMBERS_PLACEHOLDER_DISABLED, tasksToBeDone: { title: MEMBERS_TASKS_TO_BE_DONE_TITLE, noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS, @@ -118,3 +124,27 @@ export const GROUP_MODAL_LABELS = { }; export const LEARN_GITLAB = 'learn_gitlab'; +export const ON_SHOW_TRACK_LABEL = 'locked_modal_viewed'; +export const ON_CLOSE_TRACK_LABEL = 'explore_paid_plans_clicked'; +export const ON_SUBMIT_TRACK_LABEL = 'manage_members_clicked'; + +export const WARNING_ALERT_TITLE = s__( + 'InviteMembersModal|You only have space for %{count} more %{members} in %{name}', +); +export const DANGER_ALERT_TITLE = s__( + "InviteMembersModal|You've reached your %{count} %{members} limit for %{name}", +); + +export const REACHED_LIMIT_MESSAGE = s__( + 'InviteMembersModal|You cannot add more members, but you can remove members who no longer need access.', +); + +export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.concat( + s__( + 'InviteMembersModal| To get more members and access to additional paid features, an owner of this namespace can start a trial or upgrade to a paid tier.', + ), +); + +export const CLOSE_TO_LIMIT_MESSAGE = s__( + 'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.', +); diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index 958121ad735..a4be3f205a3 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -1,7 +1,7 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; Vue.use(GlToast); @@ -26,10 +26,6 @@ export default (function initInviteMembersModal() { provide: { name: el.dataset.name, newProjectPath: el.dataset.newProjectPath, - newTrialRegistrationPath: el.dataset.newTrialRegistrationPath, - purchasePath: el.dataset.purchasePath, - freeUsersLimit: el.dataset.freeUsersLimit && parseInt(el.dataset.freeUsersLimit, 10), - membersCount: el.dataset.membersCount && parseInt(el.dataset.membersCount, 10), }, render: (createElement) => createElement(InviteMembersModal, { @@ -42,6 +38,9 @@ export default (function initInviteMembersModal() { projects: JSON.parse(el.dataset.projects || '[]'), usersFilter: el.dataset.usersFilter, filterId: parseInt(el.dataset.filterId, 10), + usersLimitDataset: convertObjectPropsToCamelCase( + JSON.parse(el.dataset.usersLimitDataset || '{}'), + ), }, }), }); |