diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-19 15:10:37 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-19 15:10:37 +0300 |
commit | a4db97517ad095914c0652a07486ac607d99dab4 (patch) | |
tree | 58f57b42c52b1b4231cab44ef3934cbe55991d25 /app | |
parent | 17295c75a1a28df78f719e0098dd31fe45ce0446 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
19 files changed, 241 insertions, 157 deletions
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index 7f76b7ca1ac..5d9663abaf2 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -94,7 +94,6 @@ export default { <gl-dropdown-item v-else-if="isDropdownWithEmojiTrigger" v-bind="componentAttributes" - button-class="top-nav-menu-item" @click="openModal" > {{ displayText }} diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue index f7f7b79d52b..ee50afd3613 100644 --- a/app/assets/javascripts/organizations/new/components/app.vue +++ b/app/assets/javascripts/organizations/new/components/app.vue @@ -42,7 +42,10 @@ export default { } = await this.$apollo.mutate({ mutation: organizationCreateMutation, variables: { - input: { name: formValues.name, path: formValues.path }, + input: { name: formValues.name, path: formValues.path, avatar: formValues.avatar }, + }, + context: { + hasUpload: formValues.avatar instanceof File, }, }); diff --git a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue index 1acc4c54f75..283a652f90e 100644 --- a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue +++ b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue @@ -3,7 +3,11 @@ import { s__, __ } from '~/locale'; import { createAlert } from '~/alert'; import { visitUrlWithAlerts } from '~/lib/utils/url_utility'; import NewEditForm from '~/organizations/shared/components/new_edit_form.vue'; -import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants'; +import { + FORM_FIELD_NAME, + FORM_FIELD_ID, + FORM_FIELD_AVATAR, +} from '~/organizations/shared/constants'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPE_ORGANIZATION } from '~/graphql_shared/constants'; @@ -25,7 +29,7 @@ export default { ), successMessage: s__('Organization|Organization was successfully updated.'), }, - fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID], + fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_AVATAR], data() { return { loading: false, @@ -33,9 +37,24 @@ export default { }; }, methods: { + avatarInput(formValues) { + // Organization has an avatar and it is been explicitly removed. + if (this.organization.avatar && formValues.avatar === null) { + return { avatar: null }; + } + + // Avatar has been set or changed. + if (formValues.avatar instanceof File) { + return { avatar: formValues.avatar }; + } + + // Avatar has not been changed at all, do not include the `avatar` key in input. + return {}; + }, async onSubmit(formValues) { this.errors = []; this.loading = true; + try { const { data: { @@ -47,8 +66,12 @@ export default { input: { id: convertToGraphQLId(TYPE_ORGANIZATION, this.organization.id), name: formValues.name, + ...this.avatarInput(formValues), }, }, + context: { + hasUpload: formValues.avatar instanceof File, + }, }); if (errors.length) { diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue index c5bb16b944a..3567fa490ea 100644 --- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue +++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue @@ -3,10 +3,12 @@ import { GlForm, GlFormFields, GlButton } from '@gitlab/ui'; import { formValidators } from '@gitlab/ui/dist/utils'; import { s__, __ } from '~/locale'; import { slugify } from '~/lib/utils/text_utility'; +import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue'; import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH, + FORM_FIELD_AVATAR, FORM_FIELD_PATH_VALIDATORS, } from '../constants'; import OrganizationUrlField from './organization_url_field.vue'; @@ -18,6 +20,7 @@ export default { GlFormFields, GlButton, OrganizationUrlField, + AvatarUploadDropzone, }, i18n: { cancel: __('Cancel'), @@ -36,6 +39,7 @@ export default { return { [FORM_FIELD_NAME]: '', [FORM_FIELD_PATH]: '', + [FORM_FIELD_AVATAR]: null, }; }, }, @@ -43,7 +47,7 @@ export default { type: Array, required: false, default() { - return [FORM_FIELD_NAME, FORM_FIELD_PATH]; + return [FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_AVATAR]; }, }, submitButtonText: { @@ -98,6 +102,13 @@ export default { class: 'gl-w-full', }, }, + [FORM_FIELD_AVATAR]: { + label: s__('Organization|Organization avatar'), + groupAttrs: { + class: 'gl-w-full', + labelSrOnly: true, + }, + }, }; return Object.entries(fields).reduce((accumulator, [fieldKey, fieldDefinition]) => { @@ -148,6 +159,14 @@ export default { @blur="blur" /> </template> + <template #input(avatar)="{ input, value }"> + <avatar-upload-dropzone + :value="value" + :entity="formValues" + :label="fields.avatar.label" + @input="input" + /> + </template> </gl-form-fields> <div class="gl-display-flex gl-gap-3"> <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{ diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js index 7287d84f99f..9f3d066f984 100644 --- a/app/assets/javascripts/organizations/shared/constants.js +++ b/app/assets/javascripts/organizations/shared/constants.js @@ -4,6 +4,7 @@ import { s__ } from '~/locale'; export const FORM_FIELD_NAME = 'name'; export const FORM_FIELD_ID = 'id'; export const FORM_FIELD_PATH = 'path'; +export const FORM_FIELD_AVATAR = 'avatar'; export const FORM_FIELD_PATH_VALIDATORS = [ formValidators.required(s__('Organization|Organization URL is required.')), diff --git a/app/assets/javascripts/organizations/show/components/organization_avatar.vue b/app/assets/javascripts/organizations/show/components/organization_avatar.vue index c57ee0ea5b5..d569af3e9b4 100644 --- a/app/assets/javascripts/organizations/show/components/organization_avatar.vue +++ b/app/assets/javascripts/organizations/show/components/organization_avatar.vue @@ -44,6 +44,7 @@ export default { :entity-name="organization.name" :shape="$options.AVATAR_SHAPE_OPTION_RECT" :size="64" + :src="organization.avatar_url" /> <div class="gl-ml-3"> <div class="gl-display-flex gl-align-items-center"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 1516b63f96d..2607d8e4bb7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -119,7 +119,11 @@ export default { }, ) { if (mergeRequestMergeStatusUpdated) { - this.state = mergeRequestMergeStatusUpdated; + this.state = { + ...mergeRequestMergeStatusUpdated, + mergeRequestsFfOnlyEnabled: this.state.mergeRequestsFfOnlyEnabled, + onlyAllowMergeIfPipelineSucceeds: this.state.onlyAllowMergeIfPipelineSucceeds, + }; if (!this.commitMessageIsTouched) { this.commitMessage = mergeRequestMergeStatusUpdated.defaultMergeCommitMessage; diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue new file mode 100644 index 00000000000..944a48df279 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue @@ -0,0 +1,112 @@ +<script> +import { GlButton, GlAvatar, GlSprintf, GlTruncate } from '@gitlab/ui'; +import { __ } from '~/locale'; +import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; +import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; + +export default { + i18n: { + uploadText: __('Drop or %{linkStart}upload%{linkEnd} an avatar.'), + maxFileSize: __('Max file size is 200 KiB.'), + removeAvatar: __('Remove avatar'), + }, + AVATAR_SHAPE_OPTION_RECT, + components: { GlButton, GlAvatar, GlSprintf, GlTruncate, UploadDropzone }, + props: { + entity: { + type: Object, + required: false, + default: () => ({}), + }, + value: { + type: [String, File], + required: false, + default: '', + }, + label: { + type: String, + required: true, + }, + }, + data() { + return { + avatarObjectUrl: null, + }; + }, + computed: { + avatarSrc() { + if (this.avatarObjectUrl) { + return this.avatarObjectUrl; + } + + if (this.isValueAFile) { + return null; + } + + return this.value; + }, + isValueAFile() { + return this.value instanceof File; + }, + }, + watch: { + value(newValue) { + this.revokeAvatarObjectUrl(); + + if (newValue instanceof File) { + this.avatarObjectUrl = URL.createObjectURL(newValue); + } else { + this.avatarObjectUrl = null; + } + }, + }, + beforeDestroy() { + this.revokeAvatarObjectUrl(); + }, + methods: { + revokeAvatarObjectUrl() { + if (this.avatarObjectUrl === null) { + return; + } + + URL.revokeObjectURL(this.avatarObjectUrl); + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-column-gap-5"> + <gl-avatar + :entity-id="entity.id || null" + :entity-name="entity.name || 'organization'" + :shape="$options.AVATAR_SHAPE_OPTION_RECT" + :size="96" + :src="avatarSrc" + /> + <div class="gl-min-w-0"> + <p class="gl-font-weight-bold gl-line-height-1 gl-mb-3"> + {{ label }} + </p> + <div v-if="value" class="gl-display-flex gl-align-items-center gl-column-gap-3"> + <gl-button @click="$emit('input', null)">{{ $options.i18n.removeAvatar }}</gl-button> + <gl-truncate + v-if="isValueAFile" + class="gl-text-secondary gl-max-w-48 gl-min-w-0" + position="middle" + :text="value.name" + /> + </div> + <upload-dropzone v-else single-file-selection @change="$emit('input', $event)"> + <template #upload-text> + <gl-sprintf :message="$options.i18n.uploadText"> + <template #link="{ content }"> + <span class="gl-link gl-hover-text-decoration-underline">{{ content }}</span> + </template> + </gl-sprintf> + </template> + </upload-dropzone> + <p class="gl-mb-0 gl-mt-3 gl-text-secondary">{{ $options.i18n.maxFileSize }}</p> + </div> + </div> +</template> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 6f4f7a29334..19eaac9e908 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -10,6 +10,7 @@ @import 'framework/animations'; @import 'framework/vue_transitions'; @import 'framework/blocks'; +@import 'framework/breadcrumbs'; @import 'framework/buttons'; @import 'framework/badges'; @import 'framework/calendar'; @@ -23,6 +24,7 @@ @import 'framework/gfm'; @import 'framework/kbd'; @import 'framework/header'; +@import 'framework/top_bar'; @import 'framework/highlight'; @import 'framework/lists'; @import 'framework/logo'; diff --git a/app/assets/stylesheets/framework/breadcrumbs.scss b/app/assets/stylesheets/framework/breadcrumbs.scss new file mode 100644 index 00000000000..2ed611f7ba9 --- /dev/null +++ b/app/assets/stylesheets/framework/breadcrumbs.scss @@ -0,0 +1,21 @@ +.breadcrumbs { + flex: 1; + min-width: 0; + align-self: center; + color: $gl-text-color-secondary; + + .avatar-tile { + margin-right: 4px; + border: 1px solid $border-color; + border-radius: 50%; + vertical-align: sub; + } +} + +.breadcrumb-item-text { + text-decoration: inherit; + + @include media-breakpoint-down(xs) { + @include str-truncated(128px); + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e791a0dbbbd..8282f6143c2 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -806,28 +806,6 @@ } } -@include media-breakpoint-down(xs) { - .navbar-gitlab { - li.dropdown { - position: static; - } - } - - header.navbar-gitlab .dropdown { - .dropdown-menu { - width: 100%; - min-width: 100%; - } - } - - header.navbar-gitlab-new .header-content .dropdown { - .dropdown-menu { - left: 0; - min-width: 100%; - } - } -} - .dropdown-content-faded-mask { position: relative; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 23f40dfe4bf..84e69e40bc2 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -9,119 +9,6 @@ left: 0; right: 0; border-radius: 0; - - .close-icon { - display: none; - } - - .header-content { - width: 100%; - display: flex; - justify-content: space-between; - position: relative; - min-height: var(--header-height); - padding-left: 0; - - .title { - padding-right: 0; - color: currentColor; - display: flex; - position: relative; - margin: 0; - font-size: 18px; - vertical-align: top; - white-space: nowrap; - - img { - height: 24px; - - + .logo-text { - margin-left: 8px; - } - } - - &.wrap { - white-space: normal; - } - - &.initializing { - opacity: 0; - } - - a:not(.canary-badge) { - display: flex; - align-items: center; - padding: 2px 8px; - margin: 4px 2px 4px -8px; - border-radius: $border-radius-default; - - &:active, - &:focus { - @include gl-focus($focus-ring: $focus-ring-dark); - } - } - } - - .dropdown.open { - > a { - border-bottom-color: $white; - } - } - } - - .container-fluid { - padding: 0; - - .nav > li { - > a { - will-change: color; - margin: 4px 0; - padding: 6px 8px; - height: 32px; - } - } - } -} - -.top-bar-container { - min-height: $top-bar-height; -} - -.top-bar-fixed { - @include gl-inset-border-b-1-gray-100; - background-color: $body-bg; - left: var(--application-bar-left); - position: fixed; - right: var(--application-bar-right); - top: $calc-application-bars-height; - width: auto; - z-index: $top-bar-z-index; - - @media (prefers-reduced-motion: no-preference) { - transition: left $gl-transition-duration-medium, right $gl-transition-duration-medium; - } -} - -.breadcrumbs { - flex: 1; - min-width: 0; - align-self: center; - color: $gl-text-color-secondary; - - .avatar-tile { - margin-right: 4px; - border: 1px solid $border-color; - border-radius: 50%; - vertical-align: sub; - } -} - -.breadcrumb-item-text { - text-decoration: inherit; - - @include media-breakpoint-down(xs) { - @include str-truncated(128px); - } } .navbar-empty { @@ -173,17 +60,6 @@ @include media-breakpoint-down(xs) { margin-right: 3px; } } -.top-nav-menu-item { - &.active, - &:hover { - background-color: $nav-active-bg !important; - } - - .gl-icon { - color: inherit !important; - } -} - .header-logged-out { z-index: $header-zindex; min-height: var(--header-height); diff --git a/app/assets/stylesheets/framework/top_bar.scss b/app/assets/stylesheets/framework/top_bar.scss new file mode 100644 index 00000000000..424d4269c52 --- /dev/null +++ b/app/assets/stylesheets/framework/top_bar.scss @@ -0,0 +1,18 @@ +.top-bar-container { + min-height: $top-bar-height; +} + +.top-bar-fixed { + @include gl-inset-border-b-1-gray-100; + background-color: $body-bg; + left: var(--application-bar-left); + position: fixed; + right: var(--application-bar-right); + top: $calc-application-bars-height; + width: auto; + z-index: $top-bar-z-index; + + @media (prefers-reduced-motion: no-preference) { + transition: left $gl-transition-duration-medium, right $gl-transition-duration-medium; + } +} diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 6d3811514d9..94e114e7da8 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -16,6 +16,7 @@ class UploadsController < ApplicationController "projects/topic" => Projects::Topic, 'alert_management_metric_image' => ::AlertManagement::MetricImage, "achievements/achievement" => Achievements::Achievement, + "organizations/organization_detail" => Organizations::OrganizationDetail, "abuse_report" => AbuseReport, nil => PersonalSnippet }.freeze @@ -65,6 +66,8 @@ class UploadsController < ApplicationController can?(current_user, :read_alert_management_metric_image, model.alert) when ::Achievements::Achievement true + when Organizations::OrganizationDetail + can?(current_user, :read_organization, model.organization) else can?(current_user, "read_#{model.class.underscore}".to_sym, model) end @@ -96,7 +99,7 @@ class UploadsController < ApplicationController def cache_settings case model - when User, Appearance, Projects::Topic, Achievements::Achievement + when User, Appearance, Projects::Topic, Achievements::Achievement, Organizations::OrganizationDetail [5.minutes, { public: true, must_revalidate: false }] when Project, Group [5.minutes, { private: true, must_revalidate: true }] diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb index d0dd9dc5aea..b6d39276a03 100644 --- a/app/helpers/organizations/organization_helper.rb +++ b/app/helpers/organizations/organization_helper.rb @@ -4,7 +4,7 @@ module Organizations module OrganizationHelper def organization_show_app_data(organization) { - organization: organization.slice(:id, :name), + organization: organization.slice(:id, :name).merge({ avatar_url: organization.avatar_url(size: 128) }), groups_and_projects_organization_path: groups_and_projects_organization_path(organization), # TODO: Update counts to use real data # https://gitlab.com/gitlab-org/gitlab/-/issues/424531 @@ -25,7 +25,7 @@ module Organizations def organization_settings_general_app_data(organization) { - organization: organization.slice(:id, :name, :path), + organization: organization.slice(:id, :name, :path).merge({ avatar: organization.avatar_url(size: 192) }), organizations_path: organizations_path, root_url: root_url }.to_json diff --git a/app/models/container_registry/protection/rule.rb b/app/models/container_registry/protection/rule.rb index a7324b3b3b8..34d00bdef2f 100644 --- a/app/models/container_registry/protection/rule.rb +++ b/app/models/container_registry/protection/rule.rb @@ -19,6 +19,23 @@ module ContainerRegistry validates :repository_path_pattern, presence: true, uniqueness: { scope: :project_id }, length: { maximum: 255 } validates :delete_protected_up_to_access_level, presence: true validates :push_protected_up_to_access_level, presence: true + + scope :for_repository_path, ->(repository_path) do + return none if repository_path.blank? + + where( + ":repository_path ILIKE #{::Gitlab::SQL::Glob.to_like('repository_path_pattern')}", + repository_path: repository_path + ) + end + + def self.for_push_exists?(access_level:, repository_path:) + return false if access_level.blank? || repository_path.blank? + + where(push_protected_up_to_access_level: access_level..) + .for_repository_path(repository_path) + .exists? + end end end end diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb index 764378a5d19..dad57dca9e6 100644 --- a/app/models/organizations/organization.rb +++ b/app/models/organizations/organization.rb @@ -28,7 +28,7 @@ module Organizations 'organizations/path': true, length: { minimum: 2, maximum: 255 } - delegate :description, :avatar, :avatar_url, to: :organization_detail + delegate :description, :avatar, :avatar_url, :remove_avatar!, to: :organization_detail accepts_nested_attributes_for :organization_detail diff --git a/app/services/organizations/update_service.rb b/app/services/organizations/update_service.rb index bc3a2d29abf..6e3a2cddddb 100644 --- a/app/services/organizations/update_service.rb +++ b/app/services/organizations/update_service.rb @@ -17,6 +17,10 @@ module Organizations def execute return error_no_permissions unless allowed? + if params[:organization_detail_attributes].key?(:avatar) && params[:organization_detail_attributes][:avatar].nil? + organization.remove_avatar! + end + if organization.update(params) ServiceResponse.success(payload: { organization: organization }) else diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 4f3ca9fd71b..1b0bd10db77 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -104,7 +104,10 @@ - if todos_filter_empty? %p - = (s_("Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item.") % { strongStart: '<strong>', strongEnd: '</strong>', openIssuesLinkStart: "<a href=\"#{issues_dashboard_path}\">", openIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path}\">", mergeRequestLinkEnd: '</a>' }).html_safe + = (s_("Todos|Not sure where to go next? Take a look at your %{strongStart}%{assignedIssuesLinkStart}assigned issues%{assignedIssuesLinkEnd}%{strongEnd} or %{strongStart}%{mergeRequestLinkStart}merge requests%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}.") % { strongStart: '<strong>', strongEnd: '</strong>', assignedIssuesLinkStart: "<a href=\"#{issues_dashboard_path(assignee_username: current_user.username)}\">", assignedIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path(assignee_username: current_user.username)}\">", mergeRequestLinkEnd: '</a>' }).html_safe + %p + = link_to s_("Todos| What actions create to-do items?"), help_page_path('user/todos', anchor: 'actions-that-create-to-do-items'), target: '_blank', rel: 'noopener noreferrer' + - elsif todos_has_filtered_results? %p = link_to s_("Todos|Do you want to remove the filters?"), todos_filter_path(without: [:project_id, :author_id, :type, :action_id]) |