diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-14 21:08:53 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-14 21:08:53 +0300 |
commit | 5b62f8e3ee531f63ce3c49cae03e2a618ba51615 (patch) | |
tree | 2d2553232fe0663957ee4d1054211cc71cb07679 /app | |
parent | cdb41961fd2bc233d36c5b30f89d087c2efa9818 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
46 files changed, 575 insertions, 182 deletions
diff --git a/app/assets/javascripts/access_level/constants.js b/app/assets/javascripts/access_level/constants.js new file mode 100644 index 00000000000..02a4a3c2f15 --- /dev/null +++ b/app/assets/javascripts/access_level/constants.js @@ -0,0 +1,20 @@ +import { __ } from '~/locale'; + +// Matches `lib/gitlab/access.rb` +export const ACCESS_LEVEL_NO_ACCESS_INTEGER = 0; +export const ACCESS_LEVEL_MINIMAL_ACCESS_INTEGER = 5; +export const ACCESS_LEVEL_GUEST_INTEGER = 10; +export const ACCESS_LEVEL_REPORTER_INTEGER = 20; +export const ACCESS_LEVEL_DEVELOPER_INTEGER = 30; +export const ACCESS_LEVEL_MAINTAINER_INTEGER = 40; +export const ACCESS_LEVEL_OWNER_INTEGER = 50; + +export const ACCESS_LEVEL_LABELS = { + [ACCESS_LEVEL_NO_ACCESS_INTEGER]: __('No access'), + [ACCESS_LEVEL_MINIMAL_ACCESS_INTEGER]: __('Minimal Access'), + [ACCESS_LEVEL_GUEST_INTEGER]: __('Guest'), + [ACCESS_LEVEL_REPORTER_INTEGER]: __('Reporter'), + [ACCESS_LEVEL_DEVELOPER_INTEGER]: __('Developer'), + [ACCESS_LEVEL_MAINTAINER_INTEGER]: __('Maintainer'), + [ACCESS_LEVEL_OWNER_INTEGER]: __('Owner'), +}; diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js index bcb0f079d3d..3ebb07807d2 100644 --- a/app/assets/javascripts/api/user_api.js +++ b/app/assets/javascripts/api/user_api.js @@ -1,6 +1,4 @@ import { DEFAULT_PER_PAGE } from '~/api'; -import { createAlert } from '~/alert'; -import { __ } from '~/locale'; import axios from '../lib/utils/axios_utils'; import { buildApiUrl } from './api_utils'; @@ -44,22 +42,12 @@ export function getUserStatus(id, options) { }); } -export function getUserProjects(userId, query, options, callback) { +export function getUserProjects(userId, options) { const url = buildApiUrl(USER_PROJECTS_PATH).replace(':id', userId); - const defaults = { - search: query, - per_page: DEFAULT_PER_PAGE, - }; - return axios - .get(url, { - params: { ...defaults, ...options }, - }) - .then(({ data }) => callback(data)) - .catch(() => - createAlert({ - message: __('Something went wrong while fetching projects'), - }), - ); + + return axios.get(url, { + params: options, + }); } export function updateUserStatus({ emoji, message, availability, clearStatusAfter }) { diff --git a/app/assets/javascripts/content_editor/extensions/paste_markdown.js b/app/assets/javascripts/content_editor/extensions/paste_markdown.js index 44a094d8bf7..82fa5ce6c1d 100644 --- a/app/assets/javascripts/content_editor/extensions/paste_markdown.js +++ b/app/assets/javascripts/content_editor/extensions/paste_markdown.js @@ -63,13 +63,29 @@ export default Extension.create({ }; }, addProseMirrorPlugins() { + let pasteRaw = false; + return [ new Plugin({ key: new PluginKey('pasteMarkdown'), props: { - handlePaste: (_, event) => { + handleKeyDown: (_, event) => { + pasteRaw = event.key === 'v' && (event.metaKey || event.ctrlKey) && event.shiftKey; + }, + + handlePaste: (view, event) => { const { clipboardData } = event; const content = clipboardData.getData(TEXT_FORMAT); + const { state } = view; + const { tr, selection } = state; + const { from, to } = selection; + + if (pasteRaw) { + tr.insertText(content.replace(/^\s+|\s+$/gm, ''), from, to); + view.dispatch(tr); + return true; + } + const hasHTML = clipboardData.types.some((type) => type === HTML_FORMAT); const hasVsCode = clipboardData.types.some((type) => type === VS_CODE_FORMAT); const vsCodeMeta = hasVsCode ? JSON.parse(clipboardData.getData(VS_CODE_FORMAT)) : {}; diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js index fe1b32c5b0a..11a11ed43bd 100644 --- a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js +++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js @@ -28,6 +28,8 @@ export const getMarkdownSource = (element) => { const range = getRangeFromSourcePos(element.dataset.sourcepos); let elSource = ''; + if (!source.length) return undefined; + for (let i = range.start.row; i <= range.end.row; i += 1) { if (i === range.start.row) { elSource += source[i].substring(range.start.col); diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index c9097b9384f..94f27dbf048 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -131,7 +131,7 @@ export default { </dl> </div> </div> - <div class="table-section section-30 section-wrap"> + <div class="table-section section-20 section-wrap"> <div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Project usage') }}</div> <div class="table-mobile-content deploy-project-list"> <template v-if="projects.length > 0"> @@ -168,7 +168,7 @@ export default { <span v-else class="text-secondary">{{ __('None') }}</span> </div> </div> - <div class="table-section section-15 text-right"> + <div class="table-section section-15"> <div role="rowheader" class="table-mobile-header">{{ __('Created') }}</div> <div class="table-mobile-content text-secondary key-created-at"> <span v-gl-tooltip :title="tooltipTitle(deployKey.created_at)"> @@ -176,7 +176,23 @@ export default { </span> </div> </div> - <div class="table-section section-15 table-button-footer deploy-key-actions"> + <div class="table-section section-15"> + <div role="rowheader" class="table-mobile-header">{{ __('Expires') }}</div> + <div class="table-mobile-content text-secondary key-expires-at"> + <span + v-if="deployKey.expires_at" + v-gl-tooltip + :title="tooltipTitle(deployKey.expires_at)" + data-testid="expires-at-tooltip" + > + <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.expires_at) }}</span> + </span> + <span v-else> + <span data-testid="expires-never">{{ __('Never') }}</span> + </span> + </div> + </div> + <div class="table-section section-10 table-button-footer deploy-key-actions"> <div class="btn-group table-action-buttons"> <action-btn v-if="!isEnabled" :deploy-key="deployKey" type="enable" category="secondary"> {{ __('Enable') }} diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index 77ec1ef590f..e04cbbe72b9 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -34,10 +34,12 @@ export default { <div role="rowheader" class="table-section section-40"> {{ s__('DeployKeys|Deploy key') }} </div> - <div role="rowheader" class="table-section section-30"> + <div role="rowheader" class="table-section section-20"> {{ s__('DeployKeys|Project usage') }} </div> - <div role="rowheader" class="table-section section-15 text-right">{{ __('Created') }}</div> + <div role="rowheader" class="table-section section-15">{{ __('Created') }}</div> + <div role="rowheader" class="table-section section-15">{{ __('Expires') }}</div> + <!-- leave 10% space for actions ---> </div> <deploy-key v-for="deployKey in keys" diff --git a/app/assets/javascripts/featurable/constants.js b/app/assets/javascripts/featurable/constants.js new file mode 100644 index 00000000000..23f1c5e415d --- /dev/null +++ b/app/assets/javascripts/featurable/constants.js @@ -0,0 +1,6 @@ +// Matches `app/models/concerns/featurable.rb` + +export const FEATURABLE_DISABLED = 'disabled'; +export const FEATURABLE_PRIVATE = 'private'; +export const FEATURABLE_ENABLED = 'enabled'; +export const FEATURABLE_PUBLIC = 'public'; diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index d9781ef9c84..8d202194de7 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -16,8 +16,12 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge. import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; import { __ } from '~/locale'; -import { VISIBILITY_LEVELS_STRING_TO_INTEGER } from '~/visibility_level/constants'; -import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, ITEM_TYPE } from '../constants'; +import { + VISIBILITY_LEVELS_STRING_TO_INTEGER, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, +} from '~/visibility_level/constants'; +import { ITEM_TYPE } from '../constants'; import eventHub from '../event_hub'; diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index a4c163b0a81..5674e28f5da 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -2,12 +2,7 @@ import { GlBadge } from '@gitlab/ui'; import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import { - ITEM_TYPE, - VISIBILITY_TYPE_ICON, - GROUP_VISIBILITY_TYPE, - PROJECT_VISIBILITY_TYPE, -} from '../constants'; +import { ITEM_TYPE } from '../constants'; import ItemStatsValue from './item_stats_value.vue'; export default { @@ -24,15 +19,6 @@ export default { }, }, computed: { - visibilityIcon() { - return VISIBILITY_TYPE_ICON[this.item.visibility]; - }, - visibilityTooltip() { - if (this.item.type === ITEM_TYPE.GROUP) { - return GROUP_VISIBILITY_TYPE[this.item.visibility]; - } - return PROJECT_VISIBILITY_TYPE[this.item.visibility]; - }, isProject() { return this.item.type === ITEM_TYPE.PROJECT; }, diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js index 6f5b03788a8..a5854632040 100644 --- a/app/assets/javascripts/groups/constants.js +++ b/app/assets/javascripts/groups/constants.js @@ -1,9 +1,4 @@ import { __, s__ } from '~/locale'; -import { - VISIBILITY_LEVEL_PRIVATE_STRING, - VISIBILITY_LEVEL_INTERNAL_STRING, - VISIBILITY_LEVEL_PUBLIC_STRING, -} from '~/visibility_level/constants'; export const MAX_CHILDREN_COUNT = 20; @@ -30,36 +25,6 @@ export const ITEM_TYPE = { GROUP: 'group', }; -export const GROUP_VISIBILITY_TYPE = { - [VISIBILITY_LEVEL_PUBLIC_STRING]: __( - 'Public - The group and any public projects can be viewed without any authentication.', - ), - [VISIBILITY_LEVEL_INTERNAL_STRING]: __( - 'Internal - The group and any internal projects can be viewed by any logged in user except external users.', - ), - [VISIBILITY_LEVEL_PRIVATE_STRING]: __( - 'Private - The group and its projects can only be viewed by members.', - ), -}; - -export const PROJECT_VISIBILITY_TYPE = { - [VISIBILITY_LEVEL_PUBLIC_STRING]: __( - 'Public - The project can be accessed without any authentication.', - ), - [VISIBILITY_LEVEL_INTERNAL_STRING]: __( - 'Internal - The project can be accessed by any logged in user except external users.', - ), - [VISIBILITY_LEVEL_PRIVATE_STRING]: __( - 'Private - Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', - ), -}; - -export const VISIBILITY_TYPE_ICON = { - [VISIBILITY_LEVEL_PUBLIC_STRING]: 'earth', - [VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield', - [VISIBILITY_LEVEL_PRIVATE_STRING]: 'lock', -}; - export const OVERVIEW_TABS_SORTING_ITEMS = [ { label: __('Name'), diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index 7ec56b29c88..ec894586803 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -68,6 +68,7 @@ if (viewBlobEl) { originalBranch, resourceId, userId, + explainCodeAvailable, } = viewBlobEl.dataset; // eslint-disable-next-line no-new @@ -81,6 +82,7 @@ if (viewBlobEl) { originalBranch, resourceId, userId, + explainCodeAvailable: parseBoolean(explainCodeAvailable), }, render(createElement) { return createElement(BlobContentViewer, { diff --git a/app/assets/javascripts/profile/components/overview_tab.vue b/app/assets/javascripts/profile/components/overview_tab.vue index 76fb13919df..8eede317344 100644 --- a/app/assets/javascripts/profile/components/overview_tab.vue +++ b/app/assets/javascripts/profile/components/overview_tab.vue @@ -1,18 +1,44 @@ <script> -import { GlTab } from '@gitlab/ui'; +import { GlTab, GlLoadingIcon, GlLink } from '@gitlab/ui'; import { s__ } from '~/locale'; +import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; import ActivityCalendar from './activity_calendar.vue'; export default { i18n: { title: s__('UserProfile|Overview'), + personalProjects: s__('UserProfile|Personal projects'), + viewAll: s__('UserProfile|View all'), + }, + components: { GlTab, GlLoadingIcon, GlLink, ActivityCalendar, ProjectsList }, + props: { + personalProjects: { + type: Array, + required: true, + }, + personalProjectsLoading: { + type: Boolean, + required: true, + }, }, - components: { GlTab, ActivityCalendar }, }; </script> <template> <gl-tab :title="$options.i18n.title"> <activity-calendar /> + <div class="gl-mx-n3 gl-display-flex gl-flex-wrap-wrap"> + <div class="gl-px-3 gl-w-full gl-lg-w-half"></div> + <div class="gl-px-3 gl-w-full gl-lg-w-half" data-testid="personal-projects-section"> + <div + class="gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid" + > + <h4 class="gl-flex-grow-1">{{ $options.i18n.personalProjects }}</h4> + <gl-link href="">{{ $options.i18n.viewAll }}</gl-link> + </div> + <gl-loading-icon v-if="personalProjectsLoading" class="gl-mt-5" size="md" /> + <projects-list v-else :projects="personalProjects" /> + </div> + </div> </gl-tab> </template> diff --git a/app/assets/javascripts/profile/components/profile_tabs.vue b/app/assets/javascripts/profile/components/profile_tabs.vue index b39bfabb832..25b94d7dc7f 100644 --- a/app/assets/javascripts/profile/components/profile_tabs.vue +++ b/app/assets/javascripts/profile/components/profile_tabs.vue @@ -1,6 +1,10 @@ <script> import { GlTabs } from '@gitlab/ui'; +import { getUserProjects } from '~/rest_api'; +import { s__ } from '~/locale'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { createAlert } from '~/alert'; import OverviewTab from './overview_tab.vue'; import ActivityTab from './activity_tab.vue'; import GroupsTab from './groups_tab.vue'; @@ -12,6 +16,11 @@ import FollowersTab from './followers_tab.vue'; import FollowingTab from './following_tab.vue'; export default { + i18n: { + personalProjectsErrorMessage: s__( + 'UserProfile|An error occurred loading the personal projects. Please refresh the page to try again.', + ), + }, components: { GlTabs, OverviewTab, @@ -62,6 +71,22 @@ export default { component: FollowingTab, }, ], + inject: ['userId'], + data() { + return { + personalProjectsLoading: true, + personalProjects: [], + }; + }, + async mounted() { + try { + const response = await getUserProjects(this.userId, { per_page: 10 }); + this.personalProjects = convertObjectPropsToCamelCase(response.data, { deep: true }); + this.personalProjectsLoading = false; + } catch (error) { + createAlert({ message: this.$options.i18n.personalProjectsErrorMessage }); + } + }, }; </script> @@ -72,6 +97,8 @@ export default { v-for="{ key, component } in $options.tabs" :key="key" class="container-fluid container-limited" + :personal-projects="personalProjects" + :personal-projects-loading="personalProjectsLoading" /> </gl-tabs> </template> diff --git a/app/assets/javascripts/profile/index.js b/app/assets/javascripts/profile/index.js index fbe0e3534d8..101e52c873e 100644 --- a/app/assets/javascripts/profile/index.js +++ b/app/assets/javascripts/profile/index.js @@ -13,15 +13,17 @@ export const initProfileTabs = () => { if (!el) return false; - const { followees, followers, userCalendarPath, utcOffset } = el.dataset; + const { followees, followers, userCalendarPath, utcOffset, userId } = el.dataset; return new Vue({ el, + name: 'ProfileRoot', provide: { followees: parseInt(followers, 10), followers: parseInt(followees, 10), userCalendarPath, utcOffset, + userId, }, render(createElement) { return createElement(ProfileTabs); diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 4ce714f7c21..334e7964bc2 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -41,6 +41,7 @@ export default { originalBranch: { default: '', }, + explainCodeAvailable: { default: false }, }, apollo: { projectInfo: { @@ -144,7 +145,7 @@ export default { }, computed: { shouldRenderGenie() { - return this.glFeatures.explainCode && this.glFeatures.explainCodeSnippet && this.isLoggedIn; + return this.explainCodeAvailable; }, isLoggedIn() { return isLoggedIn(); diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index 294c0c13648..5a3958d8e4a 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -32,7 +32,16 @@ Vue.use(PerformancePlugin, { export default function setupVueRepositoryList() { const el = document.getElementById('js-tree-list'); const { dataset } = el; - const { projectPath, projectShortPath, ref, escapedRef, fullName, resourceId, userId } = dataset; + const { + projectPath, + projectShortPath, + ref, + escapedRef, + fullName, + resourceId, + userId, + explainCodeAvailable, + } = dataset; const router = createRouter(projectPath, escapedRef); apolloProvider.clients.defaultClient.cache.writeQuery({ @@ -281,7 +290,7 @@ export default function setupVueRepositoryList() { store: createStore(), router, apolloProvider, - provide: { resourceId, userId }, + provide: { resourceId, userId, explainCodeAvailable: parseBoolean(explainCodeAvailable) }, render(h) { return h(App); }, diff --git a/app/assets/javascripts/visibility_level/constants.js b/app/assets/javascripts/visibility_level/constants.js index 77736fb6ef5..e30982985b3 100644 --- a/app/assets/javascripts/visibility_level/constants.js +++ b/app/assets/javascripts/visibility_level/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export const VISIBILITY_LEVEL_PRIVATE_STRING = 'private'; export const VISIBILITY_LEVEL_INTERNAL_STRING = 'internal'; export const VISIBILITY_LEVEL_PUBLIC_STRING = 'public'; @@ -18,3 +20,33 @@ export const VISIBILITY_LEVELS_INTEGER_TO_STRING = { [VISIBILITY_LEVEL_INTERNAL_INTEGER]: VISIBILITY_LEVEL_INTERNAL_STRING, [VISIBILITY_LEVEL_PUBLIC_INTEGER]: VISIBILITY_LEVEL_PUBLIC_STRING, }; + +export const GROUP_VISIBILITY_TYPE = { + [VISIBILITY_LEVEL_PUBLIC_STRING]: __( + 'Public - The group and any public projects can be viewed without any authentication.', + ), + [VISIBILITY_LEVEL_INTERNAL_STRING]: __( + 'Internal - The group and any internal projects can be viewed by any logged in user except external users.', + ), + [VISIBILITY_LEVEL_PRIVATE_STRING]: __( + 'Private - The group and its projects can only be viewed by members.', + ), +}; + +export const PROJECT_VISIBILITY_TYPE = { + [VISIBILITY_LEVEL_PUBLIC_STRING]: __( + 'Public - The project can be accessed without any authentication.', + ), + [VISIBILITY_LEVEL_INTERNAL_STRING]: __( + 'Internal - The project can be accessed by any logged in user except external users.', + ), + [VISIBILITY_LEVEL_PRIVATE_STRING]: __( + 'Private - Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', + ), +}; + +export const VISIBILITY_TYPE_ICON = { + [VISIBILITY_LEVEL_PUBLIC_STRING]: 'earth', + [VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield', + [VISIBILITY_LEVEL_PRIVATE_STRING]: 'lock', +}; diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue index 9ebf782a1d9..7803d6f53e0 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue @@ -23,7 +23,7 @@ export default { return this.value === 'markdown'; }, text() { - return this.markdownEditorSelected ? __('Viewing markdown') : __('Viewing rich text'); + return this.markdownEditorSelected ? __('Editing markdown') : __('Editing rich text'); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue new file mode 100644 index 00000000000..1ace1c52a68 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue @@ -0,0 +1,37 @@ +<script> +import ProjectsListItem from './projects_list_item.vue'; + +export default { + components: { ProjectsListItem }, + props: { + /** + * Expected format: + * + * { + * id: number | string; + * name: string; + * webUrl: string; + * forksCount?: number; + * avatarUrl: string | null; + * starCount: number; + * visibility: string; + * issuesAccessLevel: string; + * forkingAccessLevel: string; + * openIssuesCount: number; + * permissions: { + * projectAccess: { accessLevel: 50 }; + * }[]; + */ + projects: { + type: Array, + required: true, + }, + }, +}; +</script> + +<template> + <ul class="gl-p-0 gl-list-style-none"> + <projects-list-item v-for="project in projects" :key="project.id" :project="project" /> + </ul> +</template> diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue new file mode 100644 index 00000000000..f77fd029e93 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue @@ -0,0 +1,152 @@ +<script> +import { GlAvatarLabeled, GlIcon, GlLink, GlBadge, GlTooltipDirective } from '@gitlab/ui'; + +import { VISIBILITY_TYPE_ICON, PROJECT_VISIBILITY_TYPE } from '~/visibility_level/constants'; +import { ACCESS_LEVEL_LABELS } from '~/access_level/constants'; +import { FEATURABLE_ENABLED } from '~/featurable/constants'; +import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue'; +import { __ } from '~/locale'; +import { numberToMetricPrefix } from '~/lib/utils/number_utils'; + +export default { + i18n: { + stars: __('Stars'), + forks: __('Forks'), + issues: __('Issues'), + archived: __('Archived'), + }, + components: { + GlAvatarLabeled, + GlIcon, + UserAccessRoleBadge, + GlLink, + GlBadge, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + /** + * Expected format: + * + * { + * id: number | string; + * name: string; + * webUrl: string; + * forksCount?: number; + * avatarUrl: string | null; + * starCount: number; + * visibility: string; + * issuesAccessLevel: string; + * forkingAccessLevel: string; + * openIssuesCount: number; + * permissions: { + * projectAccess: { accessLevel: 50 }; + * }; + */ + project: { + type: Object, + required: true, + }, + }, + computed: { + visibilityIcon() { + return VISIBILITY_TYPE_ICON[this.project.visibility]; + }, + visibilityTooltip() { + return PROJECT_VISIBILITY_TYPE[this.project.visibility]; + }, + accessLevel() { + return this.project.permissions?.projectAccess?.accessLevel; + }, + accessLevelLabel() { + return ACCESS_LEVEL_LABELS[this.accessLevel]; + }, + shouldShowAccessLevel() { + return this.accessLevel !== undefined; + }, + starsHref() { + return `${this.project.webUrl}/-/starrers`; + }, + forksHref() { + return `${this.project.webUrl}/-/forks`; + }, + issuesHref() { + return `${this.project.webUrl}/-/issues`; + }, + isForkingEnabled() { + return ( + this.project.forkingAccessLevel === FEATURABLE_ENABLED && + this.project.forksCount !== undefined + ); + }, + isIssuesEnabled() { + return this.project.issuesAccessLevel === FEATURABLE_ENABLED; + }, + }, + methods: { + numberToMetricPrefix, + }, +}; +</script> + +<template> + <li class="gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b"> + <gl-avatar-labeled + class="gl-flex-grow-1" + :entity-id="project.id" + :entity-name="project.name" + :label="project.name" + :label-link="project.webUrl" + shape="rect" + :size="48" + > + <template #meta> + <gl-icon + v-gl-tooltip="visibilityTooltip" + :name="visibilityIcon" + class="gl-text-secondary gl-ml-3" + /> + <user-access-role-badge v-if="shouldShowAccessLevel" class="gl-ml-3">{{ + accessLevelLabel + }}</user-access-role-badge> + </template> + </gl-avatar-labeled> + <div + class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-mt-0" + > + <div class="gl-display-flex gl-align-items-center gl-gap-x-3"> + <gl-badge v-if="project.archived" variant="warning">{{ $options.i18n.archived }}</gl-badge> + <gl-link + v-gl-tooltip="$options.i18n.stars" + :href="starsHref" + :aria-label="$options.i18n.stars" + class="gl-text-secondary" + > + <gl-icon name="star-o" /> + <span>{{ numberToMetricPrefix(project.starCount) }}</span> + </gl-link> + <gl-link + v-if="isForkingEnabled" + v-gl-tooltip="$options.i18n.forks" + :href="forksHref" + :aria-label="$options.i18n.forks" + class="gl-text-secondary" + > + <gl-icon name="fork" /> + <span>{{ numberToMetricPrefix(project.forksCount) }}</span> + </gl-link> + <gl-link + v-if="isIssuesEnabled" + v-gl-tooltip="$options.i18n.issues" + :href="issuesHref" + :aria-label="$options.i18n.issues" + class="gl-text-secondary" + > + <gl-icon name="issues" /> + <span>{{ numberToMetricPrefix(project.openIssuesCount) }}</span> + </gl-link> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue index 3c56b627673..5dfae18b698 100644 --- a/app/assets/javascripts/work_items/components/work_item_actions.vue +++ b/app/assets/javascripts/work_items/components/work_item_actions.vue @@ -2,33 +2,53 @@ import { GlDropdown, GlDropdownItem, + GlDropdownForm, GlDropdownDivider, GlModal, GlModalDirective, + GlToggle, } from '@gitlab/ui'; import { s__ } from '~/locale'; import Tracking from '~/tracking'; +import toast from '~/vue_shared/plugins/global_toast'; +import { isLoggedIn } from '~/lib/utils/common_utils'; import { sprintfWorkItem, I18N_WORK_ITEM_DELETE, I18N_WORK_ITEM_ARE_YOU_SURE_DELETE, + TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION, + TEST_ID_NOTIFICATIONS_TOGGLE_ACTION, + TEST_ID_NOTIFICATIONS_TOGGLE_FORM, + TEST_ID_DELETE_ACTION, + WIDGET_TYPE_NOTIFICATIONS, } from '../constants'; +import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql'; export default { i18n: { enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'), disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'), + notifications: s__('WorkItem|Notifications'), + notificationOn: s__('WorkItem|Notifications turned on.'), + notificationOff: s__('WorkItem|Notifications turned off.'), }, components: { GlDropdown, GlDropdownItem, + GlDropdownForm, GlDropdownDivider, GlModal, + GlToggle, }, directives: { GlModal: GlModalDirective, }, mixins: [Tracking.mixin({ label: 'actions_menu' })], + isLoggedIn: isLoggedIn(), + notificationsToggleTestId: TEST_ID_NOTIFICATIONS_TOGGLE_ACTION, + notificationsToggleFormTestId: TEST_ID_NOTIFICATIONS_TOGGLE_FORM, + confidentialityTestId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION, + deleteActionTestId: TEST_ID_DELETE_ACTION, props: { workItemId: { type: String, @@ -60,8 +80,12 @@ export default { required: false, default: false, }, + subscribedToNotifications: { + type: Boolean, + required: false, + default: false, + }, }, - emits: ['deleteWorkItem', 'toggleWorkItemConfidentiality'], computed: { i18n() { return { @@ -84,6 +108,56 @@ export default { this.track('cancel_delete_work_item'); } }, + toggleNotifications(subscribed) { + const inputVariables = { + id: this.workItemId, + notificationsWidget: { + subscribed, + }, + }; + this.$apollo + .mutate({ + mutation: updateWorkItemNotificationsMutation, + variables: { + input: inputVariables, + }, + optimisticResponse: { + workItemUpdate: { + errors: [], + workItem: { + id: this.workItemId, + widgets: [ + { + type: WIDGET_TYPE_NOTIFICATIONS, + subscribed, + __typename: 'WorkItemWidgetNotifications', + }, + ], + __typename: 'WorkItem', + }, + __typename: 'WorkItemUpdatePayload', + }, + }, + }) + .then( + ({ + data: { + workItemUpdate: { errors }, + }, + }) => { + if (errors?.length) { + throw new Error(errors[0]); + } + toast( + subscribed ? this.$options.i18n.notificationOn : this.$options.i18n.notificationOff, + ); + }, + ) + .catch((error) => { + this.updateError = error.message; + this.$emit('error', error.message); + }); + }, }, }; </script> @@ -99,9 +173,27 @@ export default { no-caret right > + <template v-if="$options.isLoggedIn"> + <gl-dropdown-form + class="work-item-notifications-form" + :data-testid="$options.notificationsToggleFormTestId" + > + <div class="gl-px-5 gl-pb-2 gl-pt-1"> + <gl-toggle + :value="subscribedToNotifications" + :label="$options.i18n.notifications" + :data-testid="$options.notificationsToggleTestId" + label-position="left" + label-id="notifications-toggle" + @change="toggleNotifications($event)" + /> + </div> + </gl-dropdown-form> + <gl-dropdown-divider /> + </template> <template v-if="canUpdate && !isParentConfidential"> <gl-dropdown-item - data-testid="confidentiality-toggle-action" + :data-testid="$options.confidentialityTestId" @click="handleToggleWorkItemConfidentiality" >{{ isConfidential @@ -114,7 +206,7 @@ export default { <gl-dropdown-item v-if="canDelete" v-gl-modal="'work-item-confirm-delete'" - data-testid="delete-action" + :data-testid="$options.deleteActionTestId" variant="danger" >{{ i18n.deleteWorkItem }}</gl-dropdown-item > diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 738305ad670..06e8a65ecf7 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -26,6 +26,7 @@ import { i18n, WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_LABELS, + WIDGET_TYPE_NOTIFICATIONS, WIDGET_TYPE_DESCRIPTION, WIDGET_TYPE_START_AND_DUE_DATE, WIDGET_TYPE_WEIGHT, @@ -271,6 +272,9 @@ export default { hasDescriptionWidget() { return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION); }, + workItemNotificationsSubscribed() { + return Boolean(this.isWidgetPresent(WIDGET_TYPE_NOTIFICATIONS)?.subscribed); + }, workItemAssignees() { return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES); }, @@ -557,6 +561,7 @@ export default { <work-item-actions v-if="canUpdate || canDelete" :work-item-id="workItem.id" + :subscribed-to-notifications="workItemNotificationsSubscribed" :work-item-type="workItemType" :can-delete="canDelete" :can-update="canUpdate" diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index bbcf78e23aa..6af4f0fe790 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -14,6 +14,7 @@ export const TASK_TYPE_NAME = 'Task'; export const WIDGET_TYPE_ASSIGNEES = 'ASSIGNEES'; export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION'; +export const WIDGET_TYPE_NOTIFICATIONS = 'NOTIFICATIONS'; export const WIDGET_TYPE_LABELS = 'LABELS'; export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE'; export const WIDGET_TYPE_WEIGHT = 'WEIGHT'; @@ -205,3 +206,8 @@ export const WORK_ITEM_ACTIVITY_SORT_OPTIONS = [ { key: DESC, text: __('Newest first'), testid: 'newest-first' }, { key: ASC, text: __('Oldest first') }, ]; + +export const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action'; +export const TEST_ID_NOTIFICATIONS_TOGGLE_ACTION = 'notifications-toggle-action'; +export const TEST_ID_NOTIFICATIONS_TOGGLE_FORM = 'notifications-toggle-form'; +export const TEST_ID_DELETE_ACTION = 'delete-action'; diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql new file mode 100644 index 00000000000..f8952b62f28 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql @@ -0,0 +1,13 @@ +mutation updateWorkItemNotificationsWidget($input: WorkItemUpdateInput!) { + workItemUpdate(input: $input) { + workItem { + id + widgets { + ... on WorkItemWidgetNotifications { + type + subscribed + } + } + } + } +} diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql index b5d27231bef..44fda3ee894 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql @@ -36,4 +36,9 @@ fragment WorkItemMetadataWidgets on WorkItemWidget { } } } + + ... on WorkItemWidgetNotifications { + type + subscribed + } } diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql index bf8eafe3211..8039ef53f98 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql @@ -85,4 +85,8 @@ fragment WorkItemWidgets on WorkItemWidget { ... on WorkItemWidgetNotes { type } + ... on WorkItemWidgetNotifications { + type + subscribed + } } diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index cd626f449d9..483c4dc226b 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -1,6 +1,5 @@ @import './pages/colors'; @import './pages/commits'; -@import './pages/detail_page'; @import './pages/events'; @import './pages/groups'; @import './pages/hierarchy'; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss index de8142924f9..de8142924f9 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/components/detail_page.scss diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index 5f6883623b2..ecbb872e1df 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -93,3 +93,14 @@ top: -8px; } } + + +.work-item-notifications-form { + .gl-toggle { + @include gl-ml-auto; + } + + .gl-toggle-label { + @include gl-font-weight-normal; + } +} diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index a0c82998108..bd6d9b835c3 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -32,8 +32,6 @@ class GroupsController < Groups::ApplicationController before_action :check_export_rate_limit!, only: [:export, :download_export] - before_action :track_experiment_event, only: [:new] - before_action only: :issues do push_frontend_feature_flag(:or_issuable_queries, group) push_frontend_feature_flag(:frontend_caching, group) @@ -402,12 +400,6 @@ class GroupsController < Groups::ApplicationController captcha_enabled? && !params[:parent_id] end - def track_experiment_event - return if params[:parent_id] - - experiment(:require_verification_for_namespace_creation, user: current_user).track(:start_create_group) - end - def group_feature_attributes [] end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 5173abfbfd5..53c6676b62b 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -49,8 +49,6 @@ class Projects::BlobController < Projects::ApplicationController before_action do push_frontend_feature_flag(:highlight_js, @project) - push_frontend_feature_flag(:explain_code_snippet, current_user) - push_licensed_feature(:explain_code, @project) if @project.licensed_feature_available?(:explain_code) push_frontend_feature_flag(:synchronize_fork, @project&.fork_source) push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks) end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 22a42d22914..9cdbd2a30f6 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -82,7 +82,7 @@ class Projects::DeployKeysController < Projects::ApplicationController def create_params create_params = params.require(:deploy_key) - .permit(:key, :title, deploy_keys_projects_attributes: [:can_push]) + .permit(:key, :title, :expires_at, deploy_keys_projects_attributes: [:can_push]) create_params.dig(:deploy_keys_projects_attributes, '0')&.merge!(project_id: @project.id) create_params end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 367417ba840..0631c02355e 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -19,8 +19,6 @@ class Projects::TreeController < Projects::ApplicationController before_action do push_frontend_feature_flag(:highlight_js, @project) push_frontend_feature_flag(:synchronize_fork, @project.fork_source) - push_frontend_feature_flag(:explain_code_snippet, current_user) - push_licensed_feature(:explain_code, @project) if @project.licensed_feature_available?(:explain_code) push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index be30255ec4e..a6bc754d09e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -39,10 +39,8 @@ class ProjectsController < Projects::ApplicationController before_action do push_frontend_feature_flag(:highlight_js, @project) push_frontend_feature_flag(:synchronize_fork, @project&.fork_source) - push_frontend_feature_flag(:explain_code_snippet, current_user) push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks) push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies) - push_licensed_feature(:explain_code, @project) if @project.present? && @project.licensed_feature_available?(:explain_code) push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?) push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?) push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?) diff --git a/app/experiments/require_verification_for_namespace_creation_experiment.rb b/app/experiments/require_verification_for_namespace_creation_experiment.rb deleted file mode 100644 index 914c5c4a29e..00000000000 --- a/app/experiments/require_verification_for_namespace_creation_experiment.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment - control { false } - candidate { true } - - exclude :existing_user - - EXPERIMENT_START_DATE = Date.new(2022, 1, 31) - - def candidate? - run - end - - private - - def existing_user - return false unless user_or_actor - - user_or_actor.created_at < EXPERIMENT_START_DATE - end -end diff --git a/app/graphql/types/work_items/available_export_fields_enum.rb b/app/graphql/types/work_items/available_export_fields_enum.rb index 59dd7ba89b1..f5b26d9818d 100644 --- a/app/graphql/types/work_items/available_export_fields_enum.rb +++ b/app/graphql/types/work_items/available_export_fields_enum.rb @@ -8,6 +8,7 @@ module Types value 'ID', value: 'id', description: 'Unique identifier.' value 'TITLE', value: 'title', description: 'Title.' + value 'DESCRIPTION', value: 'description', description: 'Description.' value 'TYPE', value: 'type', description: 'Type of the work item.' value 'AUTHOR', value: 'author', description: 'Author name.' value 'AUTHOR_USERNAME', value: 'author username', description: 'Author username.' diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 66e710485af..0dfc832c457 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -132,16 +132,6 @@ module GroupsHelper } end - def verification_for_group_creation_data - # overridden in EE - {} - end - - def require_verification_for_namespace_creation_enabled? - # overridden in EE - false - end - def group_name_and_path_app_data { base_path: root_url, diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index cf6c251aa3f..a137ff4d6f2 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -191,7 +191,8 @@ module UsersHelper followees: user.followees.count, followers: user.followers.count, user_calendar_path: user_calendar_path(user, :json), - utc_offset: local_timezone_instance(user.timezone).now.utc_offset + utc_offset: local_timezone_instance(user.timezone).now.utc_offset, + user_id: user.id } end diff --git a/app/serializers/deploy_keys/basic_deploy_key_entity.rb b/app/serializers/deploy_keys/basic_deploy_key_entity.rb index 9184bc5f0ce..4a3dd3c8f08 100644 --- a/app/serializers/deploy_keys/basic_deploy_key_entity.rb +++ b/app/serializers/deploy_keys/basic_deploy_key_entity.rb @@ -10,6 +10,7 @@ module DeployKeys expose :destroyed_when_orphaned?, as: :destroyed_when_orphaned expose :almost_orphaned?, as: :almost_orphaned expose :created_at + expose :expires_at expose :updated_at expose :can_edit expose :user, as: :owner, using: ::API::Entities::UserBasic, if: -> (_, opts) { can_read_owner?(opts) } diff --git a/app/serializers/group_deploy_key_entity.rb b/app/serializers/group_deploy_key_entity.rb index c0bb0448a51..9e7be6de35d 100644 --- a/app/serializers/group_deploy_key_entity.rb +++ b/app/serializers/group_deploy_key_entity.rb @@ -7,6 +7,7 @@ class GroupDeployKeyEntity < Grape::Entity expose :fingerprint expose :fingerprint_sha256 expose :created_at + expose :expires_at expose :updated_at expose :group_deploy_keys_groups, using: GroupDeployKeysGroupEntity do |group_deploy_key| group_deploy_key.group_deploy_keys_groups_for_user(options[:user]) diff --git a/app/services/branches/validate_new_service.rb b/app/services/branches/validate_new_service.rb index e45183d160f..0bee7ffaa66 100644 --- a/app/services/branches/validate_new_service.rb +++ b/app/services/branches/validate_new_service.rb @@ -29,3 +29,5 @@ module Branches end end end + +Branches::ValidateNewService.prepend_mod diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 4c3ea0f292e..1d306d4d3b8 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -6,8 +6,9 @@ .group-edit-container - .js-new-group-creation{ data: { has_errors: @group.errors.any?.to_s, root_path: root_path, groups_url: dashboard_groups_url }.merge(subgroup_creation_data(@group), - verification_for_group_creation_data) } + .js-new-group-creation{ data: { has_errors: @group.errors.any?.to_s, + root_path: root_path, + groups_url: dashboard_groups_url }.merge(subgroup_creation_data(@group)) } .row{ 'v-cloak': true } #create-group-pane.tab-pane diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index df7499f5f0f..9cb5ec39de2 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,7 +1,6 @@ - empty_repo = @project.empty_repo? - show_auto_devops_callout = show_auto_devops_callout?(@project) - emails_disabled = @project.emails_disabled? -- cache_enabled = Feature.enabled?(:cache_home_panel, @project, type: :development) .project-home-panel.js-show-on-project-root.gl-mt-4.gl-mb-5{ class: [("empty-project" if empty_repo)] } .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3.gl-gap-5 @@ -25,28 +24,26 @@ %span.gl-ml-3.gl-mb-3 = render 'shared/members/access_request_links', source: @project - = cache_if(cache_enabled, [@project, @project.star_count, @project.forks_count, :buttons, current_user, @notification_setting], expires_in: 1.day) do - .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-center.gl-flex-wrap.gl-gap-3 - - if current_user - - if current_user.admin? - = link_to [:admin, @project], class: 'btn btn-default gl-button btn-icon', title: _('View project in admin area'), - data: {toggle: 'tooltip', placement: 'top', container: 'body'} do - = sprite_icon('admin') - - if @notification_setting - .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, no_flip: 'true' } } + .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-center.gl-flex-wrap.gl-gap-3 + - if current_user + - if current_user.admin? + = link_to [:admin, @project], class: 'btn btn-default gl-button btn-icon', title: _('View project in admin area'), + data: {toggle: 'tooltip', placement: 'top', container: 'body'} do + = sprite_icon('admin') + - if @notification_setting + .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, no_flip: 'true' } } - = render 'projects/buttons/star' - = render 'projects/buttons/fork' + = render 'projects/buttons/star' + = render 'projects/buttons/fork' - if can?(current_user, :read_code, @project) - = cache_if(cache_enabled, [@project, :read_code], expires_in: 1.minute) do - %nav.project-stats - - if @project.empty_repo? - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors - - else - = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) + %nav.project-stats + - if @project.empty_repo? + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors + - else + = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) .gl-my-3 - = render "shared/projects/topics", project: @project, cache_enabled: cache_enabled + = render "shared/projects/topics", project: @project .home-panel-home-desc.mt-1 - if @project.description.present? .home-panel-description.text-break diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml index 93f31629ca7..584d0758c76 100644 --- a/app/views/shared/deploy_keys/_form.html.haml +++ b/app/views/shared/deploy_keys/_form.html.haml @@ -27,6 +27,11 @@ .col-sm-10 = form.text_field :fingerprint, class: 'form-control gl-form-input', readonly: 'readonly' +.form-group + .col-sm-10 + = form.label :expires_at, _('Expiration date (optional)'), class: 'label-bold' + = form.text_field :expires_at, class: 'form-control gl-form-input', readonly: 'readonly' + - if deploy_keys_project.present? = form.fields_for :deploy_keys_projects, deploy_keys_project do |deploy_keys_project_form| .form-group diff --git a/app/views/shared/deploy_keys/_project_group_form.html.haml b/app/views/shared/deploy_keys/_project_group_form.html.haml index 11fa44fe282..c9e17b18264 100644 --- a/app/views/shared/deploy_keys/_project_group_form.html.haml +++ b/app/views/shared/deploy_keys/_project_group_form.html.haml @@ -15,6 +15,10 @@ .form-group.row = deploy_keys_project_form.gitlab_ui_checkbox_component :can_push, _('Grant write permissions to this key'), help_text: _('Allow this key to push to this repository') + .form-group.row + = f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold' + = f.gitlab_ui_datepicker :expires_at, data: { qa_selector: 'deploy_key_expires_at_field' }, value: f.object.expires_at + %p.form-text.text-muted= ssh_key_expires_field_description .form-group.row = f.submit _("Add key"), data: { qa_selector: "add_deploy_key_button"}, pajamas_button: true diff --git a/app/views/shared/projects/_topics.html.haml b/app/views/shared/projects/_topics.html.haml index be513af4e3f..12246d1dcfa 100644 --- a/app/views/shared/projects/_topics.html.haml +++ b/app/views/shared/projects/_topics.html.haml @@ -1,31 +1,29 @@ -- cache_enabled = false unless local_assigns[:cache_enabled] == true - max_project_topic_length = 15 - if project.topics.present? - = cache_if(cache_enabled, [project, :topic_list], expires_in: 1.day) do - .gl-w-full.gl-display-inline-flex.gl-flex-wrap.gl-font-base.gl-font-weight-normal.gl-align-items-center.gl-mx-n2.gl-my-n2{ 'data-testid': 'project_topic_list' } - %span.gl-p-2.gl-text-gray-500 - = _('Topics') + ':' - - project.topics_to_show.each do |topic| - - explore_project_topic_path = topic_explore_projects_path(topic_name: topic[:name]) - - if topic[:title].length > max_project_topic_length - %a.gl-p-2.has-tooltip{ data: { container: "body" }, title: topic[:title], href: explore_project_topic_path, itemprop: 'keywords' } - = gl_badge_tag truncate(topic[:title], length: max_project_topic_length) - - else - %a.gl-p-2{ href: explore_project_topic_path, itemprop: 'keywords' } - = gl_badge_tag topic[:title] + .gl-w-full.gl-display-inline-flex.gl-flex-wrap.gl-font-base.gl-font-weight-normal.gl-align-items-center.gl-mx-n2.gl-my-n2{ 'data-testid': 'project_topic_list' } + %span.gl-p-2.gl-text-gray-500 + = _('Topics') + ':' + - project.topics_to_show.each do |topic| + - explore_project_topic_path = topic_explore_projects_path(topic_name: topic[:name]) + - if topic[:title].length > max_project_topic_length + %a.gl-p-2.has-tooltip{ data: { container: "body" }, title: topic[:title], href: explore_project_topic_path, itemprop: 'keywords' } + = gl_badge_tag truncate(topic[:title], length: max_project_topic_length) + - else + %a.gl-p-2{ href: explore_project_topic_path, itemprop: 'keywords' } + = gl_badge_tag topic[:title] - - if project.has_extra_topics? - - title = _('More topics') - - content = capture do - %span.gl-display-inline-flex.gl-flex-wrap - - project.topics_not_shown.each do |topic| - - explore_project_topic_path = topic_explore_projects_path(topic_name: topic[:name]) - - if topic[:title].length > max_project_topic_length - %a.gl-mr-3.gl-mb-3.has-tooltip{ data: { container: "body" }, title: topic[:title], href: explore_project_topic_path, itemprop: 'keywords' } - = gl_badge_tag truncate(topic[:title], length: max_project_topic_length) - - else - %a.gl-mr-3.gl-mb-3{ href: explore_project_topic_path, itemprop: 'keywords' } - = gl_badge_tag topic[:title] - .text-nowrap.gl-p-2{ role: 'button', tabindex: 0, data: { toggle: 'popover', triggers: 'focus hover', html: 'true', placement: 'top', title: title, content: content } } - = _("+ %{count} more") % { count: project.count_of_extra_topics_not_shown } + - if project.has_extra_topics? + - title = _('More topics') + - content = capture do + %span.gl-display-inline-flex.gl-flex-wrap + - project.topics_not_shown.each do |topic| + - explore_project_topic_path = topic_explore_projects_path(topic_name: topic[:name]) + - if topic[:title].length > max_project_topic_length + %a.gl-mr-3.gl-mb-3.has-tooltip{ data: { container: "body" }, title: topic[:title], href: explore_project_topic_path, itemprop: 'keywords' } + = gl_badge_tag truncate(topic[:title], length: max_project_topic_length) + - else + %a.gl-mr-3.gl-mb-3{ href: explore_project_topic_path, itemprop: 'keywords' } + = gl_badge_tag topic[:title] + .text-nowrap.gl-p-2{ role: 'button', tabindex: 0, data: { toggle: 'popover', triggers: 'focus hover', html: 'true', placement: 'top', title: title, content: content } } + = _("+ %{count} more") % { count: project.count_of_extra_topics_not_shown } |