diff options
Diffstat (limited to 'app/assets/javascripts/profile')
10 files changed, 182 insertions, 39 deletions
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index c64fbc91d12..915f6578ac3 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,5 +1,6 @@ <script> import { GlModal, GlSprintf } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import csrf from '~/lib/utils/csrf'; import { __, s__ } from '~/locale'; @@ -8,6 +9,7 @@ export default { GlModal, GlSprintf, }, + mixins: [glFeatureFlagMixin()], props: { actionUrl: { type: String, @@ -67,6 +69,9 @@ export default { }, }, i18n: { + textdelay: s__(`Profiles| +You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. +Once you confirm %{deleteAccount}, it cannot be undone or recovered. You might have to wait seven days before creating a new account with the same username or email.`), text: s__(`Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), @@ -85,7 +90,16 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), @primary="onSubmit" > <p> - <gl-sprintf :message="$options.i18n.text"> + <gl-sprintf v-if="glFeatures.delayDeleteOwnUser" :message="$options.i18n.textdelay"> + <template #yourAccount> + <strong>{{ s__('Profiles|your account') }}</strong> + </template> + + <template #deleteAccount> + <strong>{{ s__('Profiles|Delete account') }}</strong> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="$options.i18n.text"> <template #yourAccount> <strong>{{ s__('Profiles|your account') }}</strong> </template> diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index d96b5748abc..ae017d2a299 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -115,7 +115,7 @@ Please update your Git repository remotes as soon as possible.`), v-model="newUsername" data-testid="new-username-input" :disabled="isRequestPending" - class="form-control" + class="form-control gl-md-form-input-lg" required="required" /> </div> diff --git a/app/assets/javascripts/profile/components/follow.vue b/app/assets/javascripts/profile/components/follow.vue index 7bab8a1c30d..2673ab6fbf4 100644 --- a/app/assets/javascripts/profile/components/follow.vue +++ b/app/assets/javascripts/profile/components/follow.vue @@ -1,7 +1,14 @@ <script> -import { GlAvatarLabeled, GlAvatarLink, GlLoadingIcon, GlPagination } from '@gitlab/ui'; +import { + GlAvatarLabeled, + GlAvatarLink, + GlLoadingIcon, + GlPagination, + GlEmptyState, +} from '@gitlab/ui'; import { DEFAULT_PER_PAGE } from '~/api'; import { NEXT, PREV } from '~/vue_shared/components/pagination/constants'; +import { isCurrentUser } from '~/lib/utils/common_utils'; export default { i18n: { @@ -13,7 +20,9 @@ export default { GlAvatarLink, GlLoadingIcon, GlPagination, + GlEmptyState, }, + inject: ['followEmptyState', 'userId'], props: { /** * Expected format: @@ -48,12 +57,34 @@ export default { required: false, default: DEFAULT_PER_PAGE, }, + currentUserEmptyStateTitle: { + type: String, + required: true, + }, + visitorEmptyStateTitle: { + type: String, + required: true, + }, + }, + computed: { + emptyStateTitle() { + return isCurrentUser(this.userId) + ? this.currentUserEmptyStateTitle + : this.visitorEmptyStateTitle; + }, }, }; </script> <template> <gl-loading-icon v-if="loading" class="gl-mt-5" size="md" /> + <gl-empty-state + v-else-if="!users.length" + class="gl-mt-5" + :svg-path="followEmptyState" + :svg-height="144" + :title="emptyStateTitle" + /> <div v-else> <div class="gl-my-n3 gl-mx-n3 gl-display-flex gl-flex-wrap"> <div v-for="user in users" :key="user.id" class="gl-p-3 gl-w-full gl-md-w-half gl-lg-w-25p"> diff --git a/app/assets/javascripts/profile/components/followers_tab.vue b/app/assets/javascripts/profile/components/followers_tab.vue index 1fa579bc611..927424d6c3f 100644 --- a/app/assets/javascripts/profile/components/followers_tab.vue +++ b/app/assets/javascripts/profile/components/followers_tab.vue @@ -12,6 +12,8 @@ export default { errorMessage: s__( 'UserProfile|An error occurred loading the followers. Please refresh the page to try again.', ), + currentUserEmptyStateTitle: s__('UserProfile|You do not have any followers'), + visitorEmptyStateTitle: s__("UserProfile|This user doesn't have any followers"), }, components: { GlBadge, @@ -68,6 +70,8 @@ export default { :loading="loading" :page="page" :total-items="totalItems" + :current-user-empty-state-title="$options.i18n.currentUserEmptyStateTitle" + :visitor-empty-state-title="$options.i18n.visitorEmptyStateTitle" @pagination-input="onPaginationInput" /> </gl-tab> diff --git a/app/assets/javascripts/profile/components/following_tab.vue b/app/assets/javascripts/profile/components/following_tab.vue index 8ee878e3dcc..66c7ee42a3f 100644 --- a/app/assets/javascripts/profile/components/following_tab.vue +++ b/app/assets/javascripts/profile/components/following_tab.vue @@ -1,16 +1,62 @@ <script> import { GlBadge, GlTab } from '@gitlab/ui'; import { s__ } from '~/locale'; +import { getUserFollowing } from '~/rest_api'; +import { createAlert } from '~/alert'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import Follow from './follow.vue'; export default { i18n: { title: s__('UserProfile|Following'), + errorMessage: s__( + 'UserProfile|An error occurred loading the following. Please refresh the page to try again.', + ), + currentUserEmptyStateTitle: s__('UserProfile|You are not following other users'), + visitorEmptyStateTitle: s__("UserProfile|This user isn't following other users"), }, components: { GlBadge, GlTab, + Follow, + }, + inject: ['followeesCount', 'userId'], + data() { + return { + following: [], + loading: true, + totalItems: 0, + page: 1, + }; + }, + watch: { + page: { + async handler() { + this.loading = true; + + try { + const { data: following, headers } = await getUserFollowing(this.userId, { + page: this.page, + }); + + const { total } = parseIntPagination(normalizeHeaders(headers)); + + this.following = following; + this.totalItems = total; + } catch (error) { + createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); + } finally { + this.loading = false; + } + }, + immediate: true, + }, + }, + methods: { + onPaginationInput(page) { + this.page = page; + }, }, - inject: ['followeesCount'], }; </script> @@ -20,5 +66,14 @@ export default { <span>{{ $options.i18n.title }}</span> <gl-badge size="sm" class="gl-ml-2">{{ followeesCount }}</gl-badge> </template> + <follow + :users="following" + :loading="loading" + :page="page" + :total-items="totalItems" + :current-user-empty-state-title="$options.i18n.currentUserEmptyStateTitle" + :visitor-empty-state-title="$options.i18n.visitorEmptyStateTitle" + @pagination-input="onPaginationInput" + /> </gl-tab> </template> diff --git a/app/assets/javascripts/profile/components/profile_tabs.vue b/app/assets/javascripts/profile/components/profile_tabs.vue index 3a30c3bdc9b..e24167eb4fa 100644 --- a/app/assets/javascripts/profile/components/profile_tabs.vue +++ b/app/assets/javascripts/profile/components/profile_tabs.vue @@ -5,6 +5,7 @@ import { getUserProjects } from '~/rest_api'; import { s__ } from '~/locale'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { createAlert } from '~/alert'; +import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants'; import OverviewTab from './overview_tab.vue'; import ActivityTab from './activity_tab.vue'; import GroupsTab from './groups_tab.vue'; @@ -81,7 +82,21 @@ export default { async mounted() { try { const response = await getUserProjects(this.userId, { per_page: 10 }); - this.personalProjects = convertObjectPropsToCamelCase(response.data, { deep: true }); + this.personalProjects = convertObjectPropsToCamelCase(response.data, { deep: true }).map( + (project) => { + // This API does not return the `visibility` key if user is signed out. + // Because this API only returns public projects when signed out, in this case, we can assume + // the `visibility` attribute is `public` if it is missing. + if (!project.visibility) { + return { + ...project, + visibility: VISIBILITY_LEVEL_PUBLIC_STRING, + }; + } + + return project; + }, + ); this.personalProjectsLoading = false; } catch (error) { createAlert({ message: this.$options.i18n.personalProjectsErrorMessage }); diff --git a/app/assets/javascripts/profile/components/snippets/snippets_tab.vue b/app/assets/javascripts/profile/components/snippets/snippets_tab.vue index fce5e2f5e78..95649f9645b 100644 --- a/app/assets/javascripts/profile/components/snippets/snippets_tab.vue +++ b/app/assets/javascripts/profile/components/snippets/snippets_tab.vue @@ -1,9 +1,11 @@ <script> import { GlTab, GlKeysetPagination, GlEmptyState } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_USER } from '~/graphql_shared/constants'; import { SNIPPET_MAX_LIST_COUNT } from '~/profile/constants'; +import { isCurrentUser } from '~/lib/utils/common_utils'; +import { helpPagePath } from '~/helpers/help_page_helper'; import getUserSnippets from '../graphql/get_user_snippets.query.graphql'; import SnippetRow from './snippet_row.vue'; @@ -11,7 +13,11 @@ export default { name: 'SnippetsTab', i18n: { title: s__('UserProfile|Snippets'), - noSnippets: s__('UserProfiles|No snippets found.'), + currentUserEmptyStateTitle: s__('UserProfile|Get started with snippets'), + visitorEmptyStateTitle: s__("UserProfile|This user doesn't have any snippets"), + emptyStateDescription: s__('UserProfile|Store, share, and embed bits of code and text.'), + newSnippet: __('New snippet'), + learnMore: __('Learn more'), }, components: { GlTab, @@ -19,7 +25,7 @@ export default { GlEmptyState, SnippetRow, }, - inject: ['userId', 'snippetsEmptyState'], + inject: ['userId', 'snippetsEmptyState', 'newSnippetPath'], data() { return { userInfo: {}, @@ -57,6 +63,14 @@ export default { hasSnippets() { return this.userSnippets?.length; }, + emptyStateTitle() { + return isCurrentUser(this.userId) + ? this.$options.i18n.currentUserEmptyStateTitle + : this.$options.i18n.visitorEmptyStateTitle; + }, + emptyStateDescription() { + return isCurrentUser(this.userId) ? this.$options.i18n.emptyStateDescription : null; + }, }, methods: { isLastSnippet(index) { @@ -76,6 +90,7 @@ export default { beforeToken: this.pageInfo.startCursor, }; }, + helpPagePath, }, }; </script> @@ -100,11 +115,17 @@ export default { </div> </template> <template v-if="!hasSnippets"> - <gl-empty-state class="gl-mt-5" :svg-height="75" :svg-path="snippetsEmptyState"> - <template #title> - <p class="gl-font-weight-bold gl-mt-n5">{{ $options.i18n.noSnippets }}</p> - </template> - </gl-empty-state> + <gl-empty-state + class="gl-mt-5" + :svg-path="snippetsEmptyState" + :svg-height="144" + :title="emptyStateTitle" + :description="emptyStateDescription" + :primary-button-link="newSnippetPath" + :primary-button-text="$options.i18n.newSnippet" + :secondary-button-text="$options.i18n.learnMore" + :secondary-button-link="helpPagePath('user/snippets')" + /> </template> </gl-tab> </template> diff --git a/app/assets/javascripts/profile/components/user_achievements.vue b/app/assets/javascripts/profile/components/user_achievements.vue index 13a1b797a83..7ce6b61c4ac 100644 --- a/app/assets/javascripts/profile/components/user_achievements.vue +++ b/app/assets/javascripts/profile/components/user_achievements.vue @@ -85,7 +85,7 @@ export default { :size="32" tabindex="0" shape="rect" - class="gl-mx-2" + class="gl-mx-2 gl-p-1 gl-border-none" /> <br /> <gl-badge v-if="showCountBadge(userAchievement.count)" variant="info" size="sm">{{ diff --git a/app/assets/javascripts/profile/index.js b/app/assets/javascripts/profile/index.js index 198ffdb434b..76430d7b34d 100644 --- a/app/assets/javascripts/profile/index.js +++ b/app/assets/javascripts/profile/index.js @@ -21,6 +21,8 @@ export const initProfileTabs = () => { utcOffset, userId, snippetsEmptyState, + newSnippetPath, + followEmptyState, } = el.dataset; const apolloProvider = new VueApollo({ @@ -39,6 +41,8 @@ export const initProfileTabs = () => { utcOffset, userId, snippetsEmptyState, + newSnippetPath, + followEmptyState, }, render(createElement) { return createElement(ProfileTabs); diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue index 164ec46cdb9..aa30192b74b 100644 --- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue +++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue @@ -110,34 +110,33 @@ export default { </script> <template> - <div class="row gl-mt-3 js-preferences-form js-search-settings-section"> - <div v-if="integrationViews.length" class="col-sm-12"> - <hr data-testid="profile-preferences-integrations-rule" /> - </div> - <div v-if="integrationViews.length" class="col-lg-4 profile-settings-sidebar"> - <h4 class="gl-mt-0" data-testid="profile-preferences-integrations-heading"> - {{ $options.i18n.integrations }} - </h4> - <p> + <div class="gl-display-contents js-preferences-form"> + <div + v-if="integrationViews.length" + class="settings-section gl-border-t gl-pt-6! js-search-settings-section" + > + <div class="settings-sticky-header"> + <div class="settings-sticky-header-inner"> + <h4 class="gl-my-0" data-testid="profile-preferences-integrations-heading"> + {{ $options.i18n.integrations }} + </h4> + </div> + </div> + <p class="gl-text-secondary"> {{ $options.i18n.integrationsDescription }} </p> + <div> + <integration-view + v-for="view in integrationViews" + :key="view.name" + :help-link="view.help_link" + :message="view.message" + :message-url="view.message_url" + :config="$options.integrationViewConfigs[view.name]" + /> + </div> </div> - <div v-if="integrationViews.length" class="col-lg-8"> - <integration-view - v-for="view in integrationViews" - :key="view.name" - :help-link="view.help_link" - :message="view.message" - :message-url="view.message_url" - :config="$options.integrationViewConfigs[view.name]" - /> - </div> - - <div class="col-lg-4"></div> - <div class="col-lg-8"> - <hr /> - </div> - <div class="col-sm-12 js-hide-when-nothing-matches-search"> + <div class="settings-sticky-footer js-hide-when-nothing-matches-search"> <gl-button category="primary" variant="confirm" |