Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-19 15:10:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-19 15:10:37 +0300
commita4db97517ad095914c0652a07486ac607d99dab4 (patch)
tree58f57b42c52b1b4231cab44ef3934cbe55991d25 /app
parent17295c75a1a28df78f719e0098dd31fe45ce0446 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue1
-rw-r--r--app/assets/javascripts/organizations/new/components/app.vue5
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/organization_settings.vue27
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue21
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js1
-rw-r--r--app/assets/javascripts/organizations/show/components/organization_avatar.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue112
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/breadcrumbs.scss21
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss22
-rw-r--r--app/assets/stylesheets/framework/header.scss124
-rw-r--r--app/assets/stylesheets/framework/top_bar.scss18
-rw-r--r--app/controllers/uploads_controller.rb5
-rw-r--r--app/helpers/organizations/organization_helper.rb4
-rw-r--r--app/models/container_registry/protection/rule.rb17
-rw-r--r--app/models/organizations/organization.rb2
-rw-r--r--app/services/organizations/update_service.rb4
-rw-r--r--app/views/dashboard/todos/index.html.haml5
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])