diff options
19 files changed, 310 insertions, 184 deletions
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index 69abf886ad7..bcf5b12b209 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -79,7 +79,7 @@ export default { 'is-collapsed': list.collapsed, 'board-type-assignee': list.listType === 'assignee', }" - :data-id="list.id" + :data-list-id="list.id" class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable" data-qa-selector="board_list" > diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index 53b071aaed1..cbebb9de070 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -76,19 +76,6 @@ export default { const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list; el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' }); }, - handleDragOnEnd(params) { - const { item, newIndex, oldIndex, to } = params; - - const listId = item.dataset.id; - const replacedListId = to.children[newIndex].dataset.id; - - this.moveList({ - listId, - replacedListId, - newIndex, - adjustmentValue: newIndex < oldIndex ? 1 : -1, - }); - }, }, }; </script> @@ -104,7 +91,7 @@ export default { ref="list" v-bind="draggableOptions" class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap" - @end="handleDragOnEnd" + @end="moveList" > <component :is="boardColumnComponent" diff --git a/app/assets/javascripts/boards/graphql/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql index eb922f162f8..734867c77e9 100644 --- a/app/assets/javascripts/boards/graphql/board_lists.query.graphql +++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql @@ -9,6 +9,7 @@ query ListIssues( ) { group(fullPath: $fullPath) @include(if: $isGroup) { board(id: $boardId) { + hideBacklogList lists(issueFilters: $filters) { nodes { ...BoardListFragment @@ -18,6 +19,7 @@ query ListIssues( } project(fullPath: $fullPath) @include(if: $isProject) { board(id: $boardId) { + hideBacklogList lists(issueFilters: $filters) { nodes { ...BoardListFragment diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 0f1b72146c9..0d6a62e53a9 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/browser'; +import { sortBy } from 'lodash'; import { BoardType, ListType, @@ -216,33 +217,48 @@ export default { }, moveList: ( - { state, commit, dispatch }, - { listId, replacedListId, newIndex, adjustmentValue }, + { state: { boardLists }, commit, dispatch }, + { + item: { + dataset: { listId: movedListId }, + }, + newIndex, + to: { children }, + }, ) => { - if (listId === replacedListId) { + const displacedListId = children[newIndex].dataset.listId; + if (movedListId === displacedListId) { return; } - const { boardLists } = state; - const backupList = { ...boardLists }; - const movedList = boardLists[listId]; - - const newPosition = newIndex - 1; - const listAtNewIndex = boardLists[replacedListId]; + const listIds = sortBy( + Object.keys(boardLists).filter( + (listId) => + listId !== movedListId && + boardLists[listId].listType !== ListType.backlog && + boardLists[listId].listType !== ListType.closed, + ), + (i) => boardLists[i].position, + ); - movedList.position = newPosition; - listAtNewIndex.position += adjustmentValue; - commit(types.MOVE_LIST, { - movedList, - listAtNewIndex, - }); + const targetPosition = boardLists[displacedListId].position; + // When the dragged list moves left, displaced list should shift right. + const shiftOffset = Number(boardLists[movedListId].position < targetPosition); + const displacedListIndex = listIds.findIndex((listId) => listId === displacedListId); - dispatch('updateList', { listId, position: newPosition, backupList }); + commit( + types.MOVE_LISTS, + listIds + .slice(0, displacedListIndex + shiftOffset) + .concat([movedListId], listIds.slice(displacedListIndex + shiftOffset)) + .map((listId, index) => ({ listId, position: index })), + ); + dispatch('updateList', { listId: movedListId, position: targetPosition }); }, updateList: ( - { commit, state: { issuableType, boardItemsByListId = {} }, dispatch }, - { listId, position, collapsed, backupList }, + { state: { issuableType, boardItemsByListId = {} }, dispatch }, + { listId, position, collapsed }, ) => { gqlClient .mutate({ @@ -255,8 +271,7 @@ export default { }) .then(({ data }) => { if (data?.updateBoardList?.errors.length) { - commit(types.UPDATE_LIST_FAILURE, backupList); - return; + throw new Error(); } // Only fetch when board items havent been fetched on a collapsed list @@ -265,10 +280,19 @@ export default { } }) .catch(() => { - commit(types.UPDATE_LIST_FAILURE, backupList); + dispatch('handleUpdateListFailure'); }); }, + handleUpdateListFailure: ({ dispatch, commit }) => { + dispatch('fetchLists'); + + commit( + types.SET_ERROR, + s__('Boards|An error occurred while updating the board list. Please try again.'), + ); + }, + toggleListCollapsed: ({ commit }, { listId, collapsed }) => { commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed }); }, diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 38c54bc8c5d..0a6ee59955c 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -10,8 +10,7 @@ export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE'; export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST'; export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; -export const MOVE_LIST = 'MOVE_LIST'; -export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE'; +export const MOVE_LISTS = 'MOVE_LISTS'; export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED'; export const REMOVE_LIST = 'REMOVE_LIST'; export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index a32a100fa11..ca795dfb10c 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -1,4 +1,4 @@ -import { pull, union } from 'lodash'; +import { cloneDeep, pull, union } from 'lodash'; import Vue from 'vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { s__ } from '~/locale'; @@ -103,15 +103,12 @@ export default { Vue.set(state.boardLists, list.id, list); }, - [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => { - const { boardLists } = state; - Vue.set(boardLists, movedList.id, movedList); - Vue.set(boardLists, listAtNewIndex.id, listAtNewIndex); - }, - - [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => { - state.error = s__('Boards|An error occurred while updating the list. Please try again.'); - Vue.set(state, 'boardLists', backupList); + [mutationTypes.MOVE_LISTS]: (state, movedLists) => { + const updatedBoardList = movedLists.reduce((acc, { listId, position }) => { + acc[listId].position = position; + return acc; + }, cloneDeep(state.boardLists)); + Vue.set(state, 'boardLists', updatedBoardList); }, [mutationTypes.TOGGLE_LIST_COLLAPSED]: (state, { listId, collapsed }) => { diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss index 7af97505749..87593f53136 100644 --- a/app/assets/stylesheets/components/whats_new.scss +++ b/app/assets/stylesheets/components/whats_new.scss @@ -39,6 +39,14 @@ margin-top: calc(#{$performance-bar-height} + #{$header-height}); } +.with-system-header .whats-new-drawer { + margin-top: $system-header-height + $header-height; +} + +.with-performance-bar.with-system-header .whats-new-drawer { + margin-top: $performance-bar-height + $system-header-height + $header-height; +} + .gl-badge.whats-new-item-badge { background-color: $purple-light; color: $purple; diff --git a/db/post_migrate/20210709024048_finalize_push_event_payloads_bigint_conversion_2.rb b/db/post_migrate/20210709024048_finalize_push_event_payloads_bigint_conversion_2.rb new file mode 100644 index 00000000000..717bdb2a887 --- /dev/null +++ b/db/post_migrate/20210709024048_finalize_push_event_payloads_bigint_conversion_2.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class FinalizePushEventPayloadsBigintConversion2 < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + TABLE_NAME = 'push_event_payloads' + INDEX_NAME = 'index_push_event_payloads_on_event_id_convert_to_bigint' + + def up + ensure_batched_background_migration_is_finished( + job_class_name: 'CopyColumnUsingBackgroundMigrationJob', + table_name: TABLE_NAME, + column_name: 'event_id', + job_arguments: [["event_id"], ["event_id_convert_to_bigint"]] + ) + + swap_columns + end + + def down + swap_columns + end + + private + + def swap_columns + add_concurrent_index TABLE_NAME, :event_id_convert_to_bigint, unique: true, name: INDEX_NAME + + # Add a foreign key on `event_id_convert_to_bigint` before we swap the columns and drop the old FK (fk_36c74129da) + add_concurrent_foreign_key TABLE_NAME, :events, column: :event_id_convert_to_bigint, on_delete: :cascade + + with_lock_retries(raise_on_exhaustion: true) do + # We'll need ACCESS EXCLUSIVE lock on the related tables, + # lets make sure it can be acquired from the start + execute "LOCK TABLE #{TABLE_NAME}, events IN ACCESS EXCLUSIVE MODE" + + # Swap column names + temp_name = 'event_id_tmp' + execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:event_id)} TO #{quote_column_name(temp_name)}" + execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:event_id_convert_to_bigint)} TO #{quote_column_name(:event_id)}" + execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(temp_name)} TO #{quote_column_name(:event_id_convert_to_bigint)}" + + # We need to update the trigger function in order to make PostgreSQL to + # regenerate the execution plan for it. This is to avoid type mismatch errors like + # "type of parameter 15 (bigint) does not match that when preparing the plan (integer)" + function_name = Gitlab::Database::UnidirectionalCopyTrigger.on_table(TABLE_NAME).name(:event_id, :event_id_convert_to_bigint) + execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL" + + # Swap defaults + change_column_default TABLE_NAME, :event_id, nil + change_column_default TABLE_NAME, :event_id_convert_to_bigint, 0 + + # Swap PK constraint + execute "ALTER TABLE #{TABLE_NAME} DROP CONSTRAINT push_event_payloads_pkey" + rename_index TABLE_NAME, INDEX_NAME, 'push_event_payloads_pkey' + execute "ALTER TABLE #{TABLE_NAME} ADD CONSTRAINT push_event_payloads_pkey PRIMARY KEY USING INDEX push_event_payloads_pkey" + + # Drop original FK on the old int4 `event_id` (fk_36c74129da) + remove_foreign_key TABLE_NAME, name: concurrent_foreign_key_name(TABLE_NAME, :event_id) + # We swapped the columns but the FK for event_id is still using the old name for the event_id_convert_to_bigint column + # So we have to also swap the FK name now that we dropped the other one with the same + rename_constraint( + TABLE_NAME, + concurrent_foreign_key_name(TABLE_NAME, :event_id_convert_to_bigint), + concurrent_foreign_key_name(TABLE_NAME, :event_id) + ) + end + end +end diff --git a/db/schema_migrations/20210709024048 b/db/schema_migrations/20210709024048 new file mode 100644 index 00000000000..52e089cd5a7 --- /dev/null +++ b/db/schema_migrations/20210709024048 @@ -0,0 +1 @@ +d35079b6d6ed38ce8f212a09e684988f7499d456d28f70b6178914b1b17eee5b
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a9b56afdfd0..dc7ab5fc539 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17503,7 +17503,7 @@ ALTER SEQUENCE protected_tags_id_seq OWNED BY protected_tags.id; CREATE TABLE push_event_payloads ( commit_count bigint NOT NULL, - event_id integer NOT NULL, + event_id_convert_to_bigint integer DEFAULT 0 NOT NULL, action smallint NOT NULL, ref_type smallint NOT NULL, commit_from bytea, @@ -17511,7 +17511,7 @@ CREATE TABLE push_event_payloads ( ref text, commit_title character varying(70), ref_count integer, - event_id_convert_to_bigint bigint DEFAULT 0 NOT NULL + event_id bigint NOT NULL ); CREATE TABLE push_rules ( diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 1b06e554e5e..f2efa8fb4c0 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -83,7 +83,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD which use the characters `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, and `~`. - The `CODE_CHALLENGE` is an URL-safe base64-encoded string of the SHA256 hash of the `CODE_VERIFIER` - - In Ruby, you can set that up with `Base64.urlsafe_encode64(Digest::SHA256.digest(CODE_VERIFIER))`. + - In Ruby, you can set that up with `Base64.urlsafe_encode64(Digest::SHA256.digest(CODE_VERIFIER), padding: false)`. 1. Request authorization code. To do that, you should redirect the user to the `/oauth/authorize` page with the following query parameters: diff --git a/doc/integration/jira/issues.md b/doc/integration/jira/issues.md index f2402b1cd14..06b0afb55bb 100644 --- a/doc/integration/jira/issues.md +++ b/doc/integration/jira/issues.md @@ -45,7 +45,7 @@ ENTITY_TITLE You can [disable comments](#disable-comments-on-jira-issues) on issues. -### Require associated Jira issue for merge requests to be merged +### Require associated Jira issue for merge requests to be merged **(ULTIMATE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280766) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.12 behind a feature flag, disabled by default. > - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default. diff --git a/lib/gitlab/kubernetes/default_namespace.rb b/lib/gitlab/kubernetes/default_namespace.rb index c95362b024b..c22c2fe394d 100644 --- a/lib/gitlab/kubernetes/default_namespace.rb +++ b/lib/gitlab/kubernetes/default_namespace.rb @@ -36,14 +36,17 @@ module Gitlab end end - def default_project_namespace(slug) - namespace_slug = "#{project.path}-#{project.id}".downcase - - if cluster.namespace_per_environment? - namespace_slug += "-#{slug}" - end + def default_project_namespace(environment_slug) + maybe_environment_suffix = cluster.namespace_per_environment? ? "-#{environment_slug}" : '' + suffix = "-#{project.id}#{maybe_environment_suffix}" + namespace = project_path_slug(63 - suffix.length) + suffix + Gitlab::NamespaceSanitizer.sanitize(namespace) + end - Gitlab::NamespaceSanitizer.sanitize(namespace_slug) + def project_path_slug(max_length) + Gitlab::NamespaceSanitizer + .sanitize(project.path.downcase) + .first(max_length) end ## diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c22d4b60109..0ae5acccea0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5371,7 +5371,7 @@ msgstr "" msgid "Boards|An error occurred while removing the list. Please try again." msgstr "" -msgid "Boards|An error occurred while updating the list. Please try again." +msgid "Boards|An error occurred while updating the board list. Please try again." msgstr "" msgid "Boards|Blocked by %{blockedByCount} %{issuableType}" diff --git a/qa/qa/page/project/registry/show.rb b/qa/qa/page/project/registry/show.rb index dffdb9eebba..03c547fc8b5 100644 --- a/qa/qa/page/project/registry/show.rb +++ b/qa/qa/page/project/registry/show.rb @@ -31,7 +31,7 @@ module QA def click_delete click_element(:tag_delete_button) - find_button('Confirm').click + find_button('Delete').click end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 4b52bb953ed..84d25fe7dbd 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -70,13 +70,6 @@ RSpec.describe 'Project issue boards', :js do stub_feature_flags(board_new_list: false) visit_project_board_path_without_query_limit(project, board) - - wait_for_requests - - expect(page).to have_selector('.board', count: 4) - expect(find('.board:nth-child(2)')).to have_selector('.board-card') - expect(find('.board:nth-child(3)')).to have_selector('.board-card') - expect(find('.board:nth-child(4)')).to have_selector('.board-card') end it 'shows description tooltip on list title', :quarantine do @@ -221,18 +214,35 @@ RSpec.describe 'Project issue boards', :js do it 'changes position of list' do drag(list_from_index: 2, list_to_index: 1, selector: '.board-header') - wait_for_board_cards(2, 2) - wait_for_board_cards(3, 8) - wait_for_board_cards(4, 1) - - expect(find('.board:nth-child(2)')).to have_content(development.title) - expect(find('.board:nth-child(3)')).to have_content(planning.title) + expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title) + expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) # Make sure list positions are preserved after a reload visit_project_board_path_without_query_limit(project, board) - expect(find('.board:nth-child(2)')).to have_content(development.title) - expect(find('.board:nth-child(3)')).to have_content(planning.title) + expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title) + expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) + end + + context 'without backlog and closed lists' do + let_it_be(:board) { create(:board, project: project, hide_backlog_list: true, hide_closed_list: true) } + let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } + let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } + + it 'changes position of list' do + visit_project_board_path_without_query_limit(project, board) + + drag(list_from_index: 0, list_to_index: 1, selector: '.board-header') + + expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) + expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) + + # Make sure list positions are preserved after a reload + visit_project_board_path_without_query_limit(project, board) + + expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) + expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) + end end it 'dragging does not duplicate list' do @@ -682,6 +692,8 @@ RSpec.describe 'Project issue boards', :js do def visit_project_board_path_without_query_limit(project, board) inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do visit project_board_path(project, board) + + wait_for_requests end end end diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 5e16e389ddc..37817eecebc 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/browser'; +import { cloneDeep } from 'lodash'; import { inactiveId, ISSUABLE, @@ -419,75 +420,94 @@ describe('fetchLabels', () => { }); describe('moveList', () => { - it('should commit MOVE_LIST mutation and dispatch updateList action', (done) => { - const initialBoardListsState = { - 'gid://gitlab/List/1': mockLists[0], - 'gid://gitlab/List/2': mockLists[1], - }; + const backlogListId = 'gid://1'; + const closedListId = 'gid://5'; - const state = { - fullPath: 'gitlab-org', - fullBoardId: 'gid://gitlab/Board/1', - boardType: 'group', - disabled: false, - boardLists: initialBoardListsState, - }; + const boardLists1 = { + 'gid://3': { listType: '', position: 0 }, + 'gid://4': { listType: '', position: 1 }, + 'gid://5': { listType: '', position: 2 }, + }; - testAction( - actions.moveList, - { - listId: 'gid://gitlab/List/1', - replacedListId: 'gid://gitlab/List/2', - newIndex: 1, - adjustmentValue: 1, - }, - state, - [ - { - type: types.MOVE_LIST, - payload: { movedList: mockLists[0], listAtNewIndex: mockLists[1] }, - }, - ], - [ - { - type: 'updateList', - payload: { - listId: 'gid://gitlab/List/1', - position: 0, - backupList: initialBoardListsState, + const boardLists2 = { + [backlogListId]: { listType: ListType.backlog, position: -Infinity }, + [closedListId]: { listType: ListType.closed, position: Infinity }, + ...cloneDeep(boardLists1), + }; + + const movableListsOrder = ['gid://3', 'gid://4', 'gid://5']; + const allListsOrder = [backlogListId, ...movableListsOrder, closedListId]; + + describe.each` + draggableFrom | draggableTo | boardLists | boardListsOrder | expectedMovableListsOrder + ${0} | ${2} | ${boardLists1} | ${movableListsOrder} | ${['gid://4', 'gid://5', 'gid://3']} + ${2} | ${0} | ${boardLists1} | ${movableListsOrder} | ${['gid://5', 'gid://3', 'gid://4']} + ${0} | ${1} | ${boardLists1} | ${movableListsOrder} | ${['gid://4', 'gid://3', 'gid://5']} + ${1} | ${2} | ${boardLists1} | ${movableListsOrder} | ${['gid://3', 'gid://5', 'gid://4']} + ${2} | ${1} | ${boardLists1} | ${movableListsOrder} | ${['gid://3', 'gid://5', 'gid://4']} + ${1} | ${3} | ${boardLists2} | ${allListsOrder} | ${['gid://4', 'gid://5', 'gid://3']} + ${3} | ${1} | ${boardLists2} | ${allListsOrder} | ${['gid://5', 'gid://3', 'gid://4']} + ${1} | ${2} | ${boardLists2} | ${allListsOrder} | ${['gid://4', 'gid://3', 'gid://5']} + ${2} | ${3} | ${boardLists2} | ${allListsOrder} | ${['gid://3', 'gid://5', 'gid://4']} + ${3} | ${2} | ${boardLists2} | ${allListsOrder} | ${['gid://3', 'gid://5', 'gid://4']} + `( + 'when moving a list from position $draggableFrom to $draggableTo with lists $boardListsOrder', + ({ draggableFrom, draggableTo, boardLists, boardListsOrder, expectedMovableListsOrder }) => { + const movedListId = boardListsOrder[draggableFrom]; + const displacedListId = boardListsOrder[draggableTo]; + const buildDraggablePayload = () => { + return { + item: { dataset: { listId: boardListsOrder[draggableFrom] } }, + newIndex: draggableTo, + to: { + children: boardListsOrder.map((listId) => ({ dataset: { listId } })), }, - }, - ], - done, - ); - }); + }; + }; - it('should not commit MOVE_LIST or dispatch updateList if listId and replacedListId are the same', () => { - const initialBoardListsState = { - 'gid://gitlab/List/1': mockLists[0], - 'gid://gitlab/List/2': mockLists[1], - }; + it('should commit MOVE_LIST mutations and dispatch updateList action with correct payloads', () => { + return testAction({ + action: actions.moveList, + payload: buildDraggablePayload(), + state: { boardLists }, + expectedMutations: [ + { + type: types.MOVE_LISTS, + payload: expectedMovableListsOrder.map((listId, i) => ({ listId, position: i })), + }, + ], + expectedActions: [ + { + type: 'updateList', + payload: { + listId: movedListId, + position: movableListsOrder.findIndex((i) => i === displacedListId), + }, + }, + ], + }); + }); + }, + ); - const state = { - fullPath: 'gitlab-org', - fullBoardId: 'gid://gitlab/Board/1', - boardType: 'group', - disabled: false, - boardLists: initialBoardListsState, - }; + describe('when moving from and to the same position', () => { + it('should not commit MOVE_LIST and should not dispatch updateList', () => { + const listId = 'gid://1000'; - testAction( - actions.moveList, - { - listId: 'gid://gitlab/List/1', - replacedListId: 'gid://gitlab/List/1', - newIndex: 1, - adjustmentValue: 1, - }, - state, - [], - [], - ); + return testAction({ + action: actions.moveList, + payload: { + item: { dataset: { listId } }, + newIndex: 0, + to: { + children: [{ dataset: { listId } }], + }, + }, + state: { boardLists: { [listId]: { position: 0 } } }, + expectedMutations: [], + expectedActions: [], + }); + }); }); }); @@ -549,7 +569,7 @@ describe('updateList', () => { }); }); - it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', (done) => { + it('should dispatch handleUpdateListFailure when API returns an error', () => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ data: { updateBoardList: { @@ -559,17 +579,31 @@ describe('updateList', () => { }, }); - testAction( + return testAction( actions.updateList, { listId: 'gid://gitlab/List/1', position: 1 }, createState(), - [{ type: types.UPDATE_LIST_FAILURE }], [], - done, + [{ type: 'handleUpdateListFailure' }], ); }); }); +describe('handleUpdateListFailure', () => { + it('should dispatch fetchLists action and commit SET_ERROR mutation', async () => { + await testAction({ + action: actions.handleUpdateListFailure, + expectedMutations: [ + { + type: types.SET_ERROR, + payload: 'An error occurred while updating the board list. Please try again.', + }, + ], + expectedActions: [{ type: 'fetchLists' }], + }); + }); +}); + describe('toggleListCollapsed', () => { it('should commit TOGGLE_LIST_COLLAPSED mutation', async () => { const payload = { listId: 'gid://gitlab/List/1', collapsed: true }; diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 37f0969a39a..a2ba1e9eb5e 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -165,40 +165,26 @@ describe('Board Store Mutations', () => { }); }); - describe('MOVE_LIST', () => { - it('updates boardLists state with reordered lists', () => { + describe('MOVE_LISTS', () => { + it('updates the positions of board lists', () => { state = { ...state, boardLists: initialBoardListsState, }; - mutations.MOVE_LIST(state, { - movedList: mockLists[0], - listAtNewIndex: mockLists[1], - }); - - expect(state.boardLists).toEqual({ - 'gid://gitlab/List/2': mockLists[1], - 'gid://gitlab/List/1': mockLists[0], - }); - }); - }); - - describe('UPDATE_LIST_FAILURE', () => { - it('updates boardLists state with previous order and sets error message', () => { - state = { - ...state, - boardLists: { - 'gid://gitlab/List/2': mockLists[1], - 'gid://gitlab/List/1': mockLists[0], + mutations.MOVE_LISTS(state, [ + { + listId: mockLists[0].id, + position: 1, }, - error: undefined, - }; - - mutations.UPDATE_LIST_FAILURE(state, initialBoardListsState); + { + listId: mockLists[1].id, + position: 0, + }, + ]); - expect(state.boardLists).toEqual(initialBoardListsState); - expect(state.error).toEqual('An error occurred while updating the list. Please try again.'); + expect(state.boardLists[mockLists[0].id].position).toBe(1); + expect(state.boardLists[mockLists[1].id].position).toBe(0); }); }); diff --git a/spec/lib/gitlab/kubernetes/default_namespace_spec.rb b/spec/lib/gitlab/kubernetes/default_namespace_spec.rb index 976fe4a0a87..b6816a18baa 100644 --- a/spec/lib/gitlab/kubernetes/default_namespace_spec.rb +++ b/spec/lib/gitlab/kubernetes/default_namespace_spec.rb @@ -32,6 +32,14 @@ RSpec.describe Gitlab::Kubernetes::DefaultNamespace do subject { generator.from_environment_slug(environment.slug) } + shared_examples_for 'handles very long project paths' do + before do + allow(project).to receive(:path).and_return 'x' * 100 + end + + it { is_expected.to satisfy { |s| s.length <= 63 } } + end + context 'namespace per environment is enabled' do context 'platform namespace is specified' do let(:platform_namespace) { 'platform-namespace' } @@ -47,15 +55,12 @@ RSpec.describe Gitlab::Kubernetes::DefaultNamespace do context 'platform namespace is blank' do let(:platform_namespace) { nil } - let(:mock_namespace) { 'mock-namespace' } - it 'constructs a namespace from the project and environment' do - expect(Gitlab::NamespaceSanitizer).to receive(:sanitize) - .with("#{project.path}-#{project.id}-#{environment.slug}".downcase) - .and_return(mock_namespace) - - expect(subject).to eq mock_namespace + it 'constructs a namespace from the project and environment slug' do + expect(subject).to eq "path-with-capitals-#{project.id}-#{environment.slug}" end + + it_behaves_like 'handles very long project paths' end end @@ -70,15 +75,12 @@ RSpec.describe Gitlab::Kubernetes::DefaultNamespace do context 'platform namespace is blank' do let(:platform_namespace) { nil } - let(:mock_namespace) { 'mock-namespace' } - it 'constructs a namespace from the project and environment' do - expect(Gitlab::NamespaceSanitizer).to receive(:sanitize) - .with("#{project.path}-#{project.id}".downcase) - .and_return(mock_namespace) - - expect(subject).to eq mock_namespace + it 'constructs a namespace from just the project' do + expect(subject).to eq "path-with-capitals-#{project.id}" end + + it_behaves_like 'handles very long project paths' end end end |