diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 15:10:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 15:10:24 +0300 |
commit | e6d048d769240760008f0dbb6b811e1ebc675292 (patch) | |
tree | e005f05e66518044c7e8bc33e263f12f2561708d /app | |
parent | 611b009e924c25fcc8706b52af90542c4b78f0c0 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/diffs/components/app.vue | 28 | ||||
-rw-r--r-- | app/assets/javascripts/diffs/components/diff_file.vue | 6 | ||||
-rw-r--r-- | app/assets/javascripts/diffs/components/tree_list.vue | 4 | ||||
-rw-r--r-- | app/assets/javascripts/diffs/store/actions.js | 11 | ||||
-rw-r--r-- | app/assets/javascripts/issues/constants.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/milestones/index.js | 26 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue | 67 | ||||
-rw-r--r-- | app/controllers/groups/milestones_controller.rb | 14 | ||||
-rw-r--r-- | app/controllers/projects/merge_requests_controller.rb | 1 | ||||
-rw-r--r-- | app/controllers/projects/milestones_controller.rb | 11 | ||||
-rw-r--r-- | app/finders/crm/organizations_finder.rb | 38 | ||||
-rw-r--r-- | app/helpers/safe_format_helper.rb | 13 | ||||
-rw-r--r-- | app/mailers/emails/service_desk.rb | 5 | ||||
-rw-r--r-- | app/models/group.rb | 2 | ||||
-rw-r--r-- | app/services/groups/transfer_service.rb | 2 | ||||
-rw-r--r-- | app/views/projects/merge_requests/_page.html.haml | 2 | ||||
-rw-r--r-- | app/views/shared/milestones/_description.html.haml | 4 |
17 files changed, 124 insertions, 111 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 02307150e2f..f644c69f0a0 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -325,7 +325,7 @@ export default { this.adjustView(); }, viewDiffsFileByFile(newViewFileByFile) { - if (!newViewFileByFile && this.diffsIncomplete && this.glFeatures.singleFileFileByFile) { + if (!newViewFileByFile && this.diffsIncomplete) { this.refetchDiffData({ refetchMeta: false }); } }, @@ -467,26 +467,19 @@ export default { subscribeToEvents() { notesEventHub.$once('fetchDiffData', this.fetchData); notesEventHub.$on('refetchDiffData', this.refetchDiffData); - if (this.glFeatures.singleFileFileByFile) { - diffsEventHub.$on('diffFilesModified', this.setDiscussions); - notesEventHub.$on('fetchedNotesData', this.rereadNoteHash); - } + notesEventHub.$on('fetchedNotesData', this.rereadNoteHash); + diffsEventHub.$on('diffFilesModified', this.setDiscussions); diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData); }, unsubscribeFromEvents() { diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData); - if (this.glFeatures.singleFileFileByFile) { - notesEventHub.$off('fetchedNotesData', this.rereadNoteHash); - diffsEventHub.$off('diffFilesModified', this.setDiscussions); - } + diffsEventHub.$off('diffFilesModified', this.setDiscussions); + notesEventHub.$off('fetchedNotesData', this.rereadNoteHash); notesEventHub.$off('refetchDiffData', this.refetchDiffData); notesEventHub.$off('fetchDiffData', this.fetchData); }, navigateToDiffFileNumber(number) { - this.navigateToDiffFileIndex({ - index: number - 1, - singleFile: this.glFeatures.singleFileFileByFile, - }); + this.navigateToDiffFileIndex(number - 1); }, refetchDiffData({ refetchMeta = true } = {}) { this.fetchData({ toggleTree: false, fetchMeta: refetchMeta }); @@ -506,7 +499,7 @@ export default { if (data) { realSize = data.real_size; - if (this.viewDiffsFileByFile && this.glFeatures.singleFileFileByFile) { + if (this.viewDiffsFileByFile) { this.fetchFileByFile(); } } @@ -527,7 +520,7 @@ export default { }); } - if (!this.viewDiffsFileByFile || !this.glFeatures.singleFileFileByFile) { + if (!this.viewDiffsFileByFile) { this.fetchDiffFilesBatch() .then(() => { if (toggleTree) this.setTreeDisplay(); @@ -618,10 +611,7 @@ export default { jumpToFile(step) { const targetIndex = this.currentDiffIndex + step; if (targetIndex >= 0 && targetIndex < this.flatBlobsList.length) { - this.goToFile({ - path: this.flatBlobsList[targetIndex].path, - singleFile: this.glFeatures.singleFileFileByFile, - }); + this.goToFile({ path: this.flatBlobsList[targetIndex].path }); } }, setTreeDisplay() { diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 4c2cb83ffb3..64a7c047cb4 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -209,12 +209,6 @@ export default { if (this.hasDiff) { this.postRender(); - } else if ( - this.viewDiffsFileByFile && - !this.isCollapsed && - !this.glFeatures.singleFileFileByFile - ) { - this.requestDiff(); } this.manageViewedEffects(); diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 4f1875e9175..544bbbfe9d8 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -5,7 +5,6 @@ import micromatch from 'micromatch'; import { debounce } from 'lodash'; import { getModifierKey } from '~/constants'; import { s__, sprintf } from '~/locale'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { RecycleScroller } from 'vendor/vue-virtual-scroller'; import DiffFileRow from './diff_file_row.vue'; @@ -20,7 +19,6 @@ export default { DiffFileRow, RecycleScroller, }, - mixins: [glFeatureFlagsMixin()], props: { hideFileStats: { type: Boolean, @@ -177,7 +175,7 @@ export default { :class="{ 'tree-list-parent': item.level > 0 }" class="gl-relative" @toggleTreeOpen="toggleTreeOpen" - @clickFile="(path) => goToFile({ singleFile: glFeatures.singleFileFileByFile, path })" + @clickFile="(path) => goToFile({ path })" /> </template> <template #after> diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 3be6562f586..30c34e9a5be 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -608,8 +608,8 @@ export const setCurrentFileHash = ({ commit }, hash) => { commit(types.SET_CURRENT_DIFF_FILE, hash); }; -export const goToFile = ({ state, commit, dispatch, getters }, { path, singleFile }) => { - if (!state.viewDiffsFileByFile || !singleFile) { +export const goToFile = ({ state, commit, dispatch, getters }, { path }) => { + if (!state.viewDiffsFileByFile) { dispatch('scrollToFile', { path }); } else { if (!state.treeEntries[path]) return; @@ -943,16 +943,13 @@ export const setCurrentDiffFileIdFromNote = ({ commit, getters, rootGetters }, n } }; -export const navigateToDiffFileIndex = ( - { state, getters, commit, dispatch }, - { index, singleFile }, -) => { +export const navigateToDiffFileIndex = ({ state, getters, commit, dispatch }, index) => { const { fileHash } = getters.flatBlobsList[index]; document.location.hash = fileHash; commit(types.SET_CURRENT_DIFF_FILE, fileHash); - if (state.viewDiffsFileByFile && singleFile) { + if (state.viewDiffsFileByFile) { dispatch('fetchFileByFile'); } }; diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js index c79612ad5d0..444ee704521 100644 --- a/app/assets/javascripts/issues/constants.js +++ b/app/assets/javascripts/issues/constants.js @@ -14,6 +14,7 @@ export const TYPE_EPIC = 'epic'; export const TYPE_INCIDENT = 'incident'; export const TYPE_ISSUE = 'issue'; export const TYPE_MERGE_REQUEST = 'merge_request'; +export const TYPE_MILESTONE = 'milestone'; export const TYPE_TEST_CASE = 'test_case'; export const WORKSPACE_GROUP = 'group'; diff --git a/app/assets/javascripts/milestones/index.js b/app/assets/javascripts/milestones/index.js index 8780d931588..420f7cee4d2 100644 --- a/app/assets/javascripts/milestones/index.js +++ b/app/assets/javascripts/milestones/index.js @@ -9,12 +9,18 @@ import Sidebar from '~/right_sidebar'; import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar'; import Translate from '~/vue_shared/translate'; import ZenMode from '~/zen_mode'; +import TaskList from '~/task_list'; +import { TYPE_MILESTONE } from '~/issues/constants'; +import { createAlert } from '~/alert'; +import { __ } from '~/locale'; import DeleteMilestoneModal from './components/delete_milestone_modal.vue'; import PromoteMilestoneModal from './components/promote_milestone_modal.vue'; import eventHub from './event_hub'; // See app/views/shared/milestones/_description.html.haml export const MILESTONE_DESCRIPTION_ELEMENT = '.milestone-detail .description'; +export const MILESTONE_DESCRIPTION_TASK_LIST_CONTAINER_ELEMENT = `${MILESTONE_DESCRIPTION_ELEMENT}.js-task-list-container`; +export const MILESTONE_DETAIL_ELEMENT = '.milestone-detail'; export function initForm(initGFM = true) { new ZenMode(); // eslint-disable-line no-new @@ -40,6 +46,26 @@ export function initShow() { new MountMilestoneSidebar(); // eslint-disable-line no-new renderGFM(document.querySelector(MILESTONE_DESCRIPTION_ELEMENT)); + + const el = document.querySelector(MILESTONE_DESCRIPTION_TASK_LIST_CONTAINER_ELEMENT); + + if (!el) { + return null; + } + + return new TaskList({ + dataType: TYPE_MILESTONE, + fieldName: 'description', + selector: MILESTONE_DETAIL_ELEMENT, + lockVersion: el.dataset.lockVersion, + onError: () => { + createAlert({ + message: __( + 'Someone edited this milestone at the same time you did. Please refresh the page to see changes.', + ), + }); + }, + }); } export function initPromoteMilestoneModal() { diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue index 247f49c1345..04dc3b20dff 100644 --- a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue @@ -1,18 +1,12 @@ <script> -import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import { __ } from '~/locale'; -import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import { formatTimezone } from '~/lib/utils/datetime_utility'; export default { name: 'TimezoneDropdown', components: { - GlDropdown, - GlDropdownItem, - GlSearchBoxByType, - }, - directives: { - autofocusonshow, + GlCollapsibleListbox, }, props: { value: { @@ -52,11 +46,10 @@ export default { identifier: timezone.identifier, })); }, - filteredResults() { - const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.timezones.filter((timezone) => - timezone.formattedTimezone.toLowerCase().includes(lowerCasedSearchTerm), - ); + filteredListboxItems() { + return this.timezones + .filter((timezone) => timezone.formattedTimezone.toLowerCase().includes(this.searchTerm)) + .map(({ formattedTimezone }) => ({ value: formattedTimezone, text: formattedTimezone })); }, selectedTimezoneLabel() { return this.tzValue || __('Select timezone'); @@ -68,14 +61,14 @@ export default { }, }, methods: { - selectTimezone(selectedTimezone) { - this.tzValue = selectedTimezone.formattedTimezone; + selectTimezone(formattedTimezone) { + const selectedTimezone = this.timezones.find( + (timezone) => timezone.formattedTimezone === formattedTimezone, + ); + this.tzValue = formattedTimezone; this.$emit('input', selectedTimezone); this.searchTerm = ''; }, - isSelected(timezone) { - return this.tzValue === timezone.formattedTimezone; - }, initialTimezone(timezones, value) { if (!value) { return undefined; @@ -89,6 +82,9 @@ export default { return undefined; }, + setSearchTerm(value) { + this.searchTerm = value?.toLowerCase(); + }, }, }; </script> @@ -101,31 +97,16 @@ export default { :value="timezoneIdentifier || value" type="hidden" /> - <gl-dropdown - :text="selectedTimezoneLabel" - :class="additionalClass" + <gl-collapsible-listbox + :items="filteredListboxItems" + :toggle-text="selectedTimezoneLabel" + :toggle-class="additionalClass" + :no-results-text="$options.translations.noResultsText" + :selected="tzValue" block - lazy - menu-class="gl-w-full!" - v-bind="$attrs" - > - <gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus /> - <gl-dropdown-item - v-for="timezone in filteredResults" - :key="timezone.formattedTimezone" - :is-checked="isSelected(timezone)" - is-check-item - @click="selectTimezone(timezone)" - > - {{ timezone.formattedTimezone }} - </gl-dropdown-item> - <gl-dropdown-item - v-if="!filteredResults.length" - class="gl-pointer-events-none" - data-testid="noMatchingResults" - > - {{ $options.translations.noResultsText }} - </gl-dropdown-item> - </gl-dropdown> + searchable + @search="setSearchTerm" + @select="selectTimezone" + /> </div> </template> diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 903c8c214ae..5f6b55ea928 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -44,7 +44,19 @@ class Groups::MilestonesController < Groups::ApplicationController def update Milestones::UpdateService.new(@milestone.parent, current_user, milestone_params).execute(@milestone) - redirect_to milestone_path(@milestone) + respond_to do |format| + format.html do + redirect_to milestone_path(@milestone) + end + + format.json do + if @milestone.valid? + head :no_content + else + render json: { errors: @milestone.errors.full_messages }, status: :unprocessable_entity + end + end + end rescue ActiveRecord::StaleObjectError respond_to do |format| format.html do diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 91788444f1f..4423929e48b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -44,7 +44,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:refactor_security_extension, @project) push_frontend_feature_flag(:deprecate_vulnerabilities_feedback, @project) push_frontend_feature_flag(:moved_mr_sidebar, project) - push_frontend_feature_flag(:single_file_file_by_file, project) push_frontend_feature_flag(:mr_experience_survey, project) push_frontend_feature_flag(:realtime_mr_status_change, project) push_frontend_feature_flag(:realtime_approvals, project) diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 569a514b23b..35b65dbce7e 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -76,7 +76,6 @@ class Projects::MilestonesController < Projects::ApplicationController @milestone = Milestones::UpdateService.new(project, current_user, milestone_params).execute(milestone) respond_to do |format| - format.js format.html do if @milestone.valid? redirect_to project_milestone_path(@project, @milestone) @@ -84,6 +83,16 @@ class Projects::MilestonesController < Projects::ApplicationController render :edit end end + + format.js + + format.json do + if @milestone.valid? + head :no_content + else + render json: { errors: @milestone.errors.full_messages }, status: :unprocessable_entity + end + end end rescue ActiveRecord::StaleObjectError respond_to do |format| diff --git a/app/finders/crm/organizations_finder.rb b/app/finders/crm/organizations_finder.rb index 69f72235c71..8701d26dd6e 100644 --- a/app/finders/crm/organizations_finder.rb +++ b/app/finders/crm/organizations_finder.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Finder for retrieving organizations scoped to a group +# Finder for retrieving crm_organizations scoped to a group # # Arguments: # current_user - user performing the action. Must have the correct permission level for the group. @@ -29,22 +29,22 @@ module Crm def execute return CustomerRelations::Organization.none unless root_group - organizations = root_group.organizations - organizations = by_ids(organizations) - organizations = by_search(organizations) - organizations = by_state(organizations) - sort_organizations(organizations) + crm_organizations = root_group.crm_organizations + crm_organizations = by_ids(crm_organizations) + crm_organizations = by_search(crm_organizations) + crm_organizations = by_state(crm_organizations) + sort_crm_organizations(crm_organizations) end private - def sort_organizations(organizations) - return organizations.sort_by_name unless @params.key?(:sort) - return organizations if @params[:sort].nil? + def sort_crm_organizations(crm_organizations) + return crm_organizations.sort_by_name unless @params.key?(:sort) + return crm_organizations if @params[:sort].nil? field = @params[:sort][:field] direction = @params[:sort][:direction] - organizations.sort_by_field(field, direction) + crm_organizations.sort_by_field(field, direction) end def root_group @@ -57,22 +57,22 @@ module Crm end end - def by_search(organizations) - return organizations unless search? + def by_search(crm_organizations) + return crm_organizations unless search? - organizations.search(params[:search]) + crm_organizations.search(params[:search]) end - def by_state(organizations) - return organizations unless state? + def by_state(crm_organizations) + return crm_organizations unless state? - organizations.search_by_state(params[:state]) + crm_organizations.search_by_state(params[:state]) end - def by_ids(organizations) - return organizations unless ids? + def by_ids(crm_organizations) + return crm_organizations unless ids? - organizations.id_in(params[:ids]) + crm_organizations.id_in(params[:ids]) end def search? diff --git a/app/helpers/safe_format_helper.rb b/app/helpers/safe_format_helper.rb index f05cf5ab50f..96124e98d4e 100644 --- a/app/helpers/safe_format_helper.rb +++ b/app/helpers/safe_format_helper.rb @@ -1,23 +1,22 @@ # frozen_string_literal: true module SafeFormatHelper - # Returns a HTML-safe string where +format+ and +args+ are escaped via - # `html_escape` if they are not marked as HTML-safe. - # - # Argument +format+ must not be marked as HTML-safe via `.html_safe`. + # Returns a HTML-safe string where + # * +format+ is escaped via `html_escape_once` + # * +args+ are escaped via `html_escape` if they are not marked as HTML-safe # # Example: # safe_format('Some %{open}bold%{close} text.', open: '<strong>'.html_safe, close: '</strong>'.html_safe) # # => 'Some <strong>bold</strong>' # safe_format('See %{user_input}', user_input: '<b>bold</b>') # # => 'See <b>bold</b> + # safe_format('In < hour & more') + # # => 'In < hour & more' # def safe_format(format, **args) - raise ArgumentError, 'Argument `format` must not be marked as html_safe!' if format.html_safe? - # Use `Kernel.format` to avoid conflicts with ViewComponent's `format`. Kernel.format( - html_escape(format), + html_escape_once(format), args.transform_values { |value| html_escape(value) } ).html_safe end diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb index c627f4633e4..a51a2a68da0 100644 --- a/app/mailers/emails/service_desk.rb +++ b/app/mailers/emails/service_desk.rb @@ -176,6 +176,11 @@ module Emails .gsub(/%\{\s*SYSTEM_FOOTER\s*\}/, text_footer_message.to_s) .gsub(/%\{\s*UNSUBSCRIBE_URL\s*\}/, unsubscribe_sent_notification_url(@sent_notification)) .gsub(/%\{\s*ADDITIONAL_TEXT\s*\}/, service_desk_email_additional_text.to_s) + .gsub(/%\{\s*ISSUE_URL\s*\}/, full_issue_url) + end + + def full_issue_url + issue_url(@issue) end def issue_id diff --git a/app/models/group.rb b/app/models/group.rb index 844b7496913..9ef1d3f12f3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -92,7 +92,7 @@ class Group < Namespace has_many :badges, class_name: 'GroupBadge' # AR defaults to nullify when trying to delete via has_many associations unless we set dependent: :delete_all - has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :crm_organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :cluster_groups, class_name: 'Clusters::Group' diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index 1c8df157716..16454360ee2 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -69,7 +69,7 @@ module Groups return false if group.root_ancestor == @new_parent_group.root_ancestor return true if group.contacts.exists? && !current_user.can?(:admin_crm_contact, @new_parent_group.root_ancestor) - return true if group.organizations.exists? && !current_user.can?(:admin_crm_organization, @new_parent_group.root_ancestor) + return true if group.crm_organizations.exists? && !current_user.can?(:admin_crm_organization, @new_parent_group.root_ancestor) false end diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml index 3e56148f777..5ea67376a86 100644 --- a/app/views/projects/merge_requests/_page.html.haml +++ b/app/views/projects/merge_requests/_page.html.haml @@ -16,7 +16,7 @@ - add_page_specific_style 'page_bundles/ci_status' - add_page_startup_api_call @endpoint_metadata_url -- if mr_action == 'diffs' && (!@file_by_file_default || !single_file_file_by_file?) +- if mr_action == 'diffs' && !@file_by_file_default - add_page_startup_api_call @endpoint_diff_batch_url .merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } } diff --git a/app/views/shared/milestones/_description.html.haml b/app/views/shared/milestones/_description.html.haml index a63702661d0..3774fb0869f 100644 --- a/app/views/shared/milestones/_description.html.haml +++ b/app/views/shared/milestones/_description.html.haml @@ -9,5 +9,7 @@ - if milestone.try(:description).present? %div{ data: { qa_selector: "milestone_description_content" } } - .description.md.gl-px-0.gl-pt-4 + .description.md.gl-px-0.gl-pt-4{ class: ('js-task-list-container' if can?(current_user, :admin_milestone, milestone)), data: { lock_version: @milestone.lock_version } } = markdown_field(milestone, :description) + -# This textarea is necessary for `task_list.js` to work. + %textarea.hidden.js-task-list-field{ data: { value: milestone.description, update_url: milestone_path(milestone, format: :json)} } |