diff --git a/app/assets/javascripts/work_items/components/work_item_notifications_widget.vue b/app/assets/javascripts/work_items/components/work_item_notifications_widget.vue index a445fec7932..92b9c48b5aa 100644 --- a/app/assets/javascripts/work_items/components/work_item_notifications_widget.vue +++ b/app/assets/javascripts/work_items/components/work_item_notifications_widget.vue @@ -89,6 +89,7 @@ export default { v-gl-tooltip.hover category="secondary" data-testid="subscribe-button" + :data-subscribed="subscribedToNotifications ? 'true' : 'false'" :title="notificationTooltip" class="btn-icon" @click="toggleNotifications(!subscribedToNotifications)" diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue index 7ecc6a69e95..3aae442f207 100644 --- a/app/assets/javascripts/work_items/pages/create_work_item.vue +++ b/app/assets/javascripts/work_items/pages/create_work_item.vue @@ -10,6 +10,7 @@ import { BASE_ALLOWED_CREATE_TYPES, WORK_ITEM_TYPE_NAME_ISSUE, WORK_ITEM_TYPE_NAME_INCIDENT, + WORK_ITEM_TYPE_NAME_TASK, } from '../constants'; import workItemRelatedItemQuery from '../graphql/work_item_related_item.query.graphql'; import { convertTypeEnumToName } from '../utils'; @@ -69,14 +70,17 @@ export default { }, }, computed: { - isIssue() { - return this.workItemType === WORK_ITEM_TYPE_NAME_ISSUE; - }, isIncident() { return this.workItemType === WORK_ITEM_TYPE_NAME_INCIDENT; }, allowedWorkItemTypes() { - if (this.isIssue || this.isIncident) { + if ( + [ + WORK_ITEM_TYPE_NAME_ISSUE, + WORK_ITEM_TYPE_NAME_INCIDENT, + WORK_ITEM_TYPE_NAME_TASK, + ].includes(this.workItemType) + ) { return BASE_ALLOWED_CREATE_TYPES; } @@ -150,7 +154,7 @@ export default { :is-group="isGroup" :related-item="relatedItem" :should-discard-draft="shouldDiscardDraft" - :always-show-work-item-type-select="isIncident || isIssue" + :always-show-work-item-type-select="!isGroup" :allowed-work-item-types="allowedWorkItemTypes" @updateType="updateWorkItemType($event)" @confirmCancel="handleConfirmCancellation" diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index b42a36e4097..24dc340db08 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -9,6 +9,10 @@ class Groups::BoardsController < Groups::ApplicationController push_frontend_feature_flag(:board_multi_select, group) push_frontend_feature_flag(:issues_list_drawer, group) push_force_frontend_feature_flag(:work_items_beta, !!group&.work_items_beta_feature_flag_enabled?) + push_frontend_feature_flag(:notifications_todos_buttons) + push_force_frontend_feature_flag(:glql_integration, !!group&.glql_integration_feature_flag_enabled?) + push_force_frontend_feature_flag(:continue_indented_text, !!group&.continue_indented_text_feature_flag_enabled?) + push_frontend_feature_flag(:work_item_status_feature_flag, group&.root_ancestor) end feature_category :team_planning diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 9a8e98c429a..499dd6571ea 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -9,6 +9,10 @@ class Projects::BoardsController < Projects::ApplicationController push_frontend_feature_flag(:board_multi_select, project) push_frontend_feature_flag(:issues_list_drawer, project) push_force_frontend_feature_flag(:work_items_beta, !!project&.work_items_beta_feature_flag_enabled?) + push_frontend_feature_flag(:notifications_todos_buttons) + push_force_frontend_feature_flag(:glql_integration, !!project&.glql_integration_feature_flag_enabled?) + push_force_frontend_feature_flag(:continue_indented_text, !!project&.continue_indented_text_feature_flag_enabled?) + push_frontend_feature_flag(:work_item_status_feature_flag, project&.root_ancestor) end feature_category :team_planning diff --git a/doc/development/application_settings.md b/doc/development/application_settings.md index 67f98b626e0..1b697c9578e 100644 --- a/doc/development/application_settings.md +++ b/doc/development/application_settings.md @@ -74,18 +74,32 @@ validates :new_setting, ## Migrate a database column to a JSONB column To migrate a column to JSONB, add the new setting under the JSONB accessor. -Follow the [process to add a new application setting](#add-a-new-application-setting). -You can use the same name as the existing column to maintain consistency. During the -transition period, Rails writes the same information to both the existing database +### Adding the JSONB setting + +- Follow the [process to add a new application setting](#add-a-new-application-setting). +- Use the same name as the existing column to maintain consistency. +- During transition, Rails writes the same information to both the existing database column and the field under the new JSONB column. This ensures data consistency and prevents downtime. -You must follow the [process for dropping columns](database/avoiding_downtime_in_migrations.md#dropping-columns) to remove the original column. -This a required multi-milestone process that involves: +### Required cleanup steps + +You must follow the [process for dropping columns](database/avoiding_downtime_in_migrations.md#dropping-columns) +to remove the original column. This a required multi-milestone process that involves: 1. Ignoring the column. 1. Dropping the column. 1. Removing the ignore rule. +{{< alert type="warning" >}} + Dropping the original column before ignoring it in the model can cause problems with zero-downtime migrations. + +{{< /alert >}} + +### Default values + +When migrating settings to JSONB columns with `jsonb_accessor` defaults, +remove them from `ApplicationSettingImplementation.defaults` because +JSONB accessors take precedence over the `defaults` method. diff --git a/doc/development/cells/_index.md b/doc/development/cells/_index.md index 63d939148f7..3659ba97917 100644 --- a/doc/development/cells/_index.md +++ b/doc/development/cells/_index.md @@ -259,7 +259,6 @@ to the table's database dictionary file. This can be used for: - JiHu specific tables, since they do not have any data on the .com database. [!145905](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145905) - tables that are marked to be dropped soon, like `operations_feature_flag_scopes`. [!147541](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147541) -- tables that mandatorily need to be present per cell to support a cell's operations, have unique data per cell, but cannot have a sharding key defined. For example, `zoekt_nodes`. When tables are exempted from sharding key requirements, they also do not show up in our [progress dashboard](https://cells-progress-tracker-gitlab-org-tenant-scale-g-f4ad96bf01d25f.gitlab.io/sharding_keys). diff --git a/doc/development/sec/analyzer_development_guide.md b/doc/development/sec/analyzer_development_guide.md index 06fdd805a77..c5c75c9b261 100644 --- a/doc/development/sec/analyzer_development_guide.md +++ b/doc/development/sec/analyzer_development_guide.md @@ -322,6 +322,10 @@ The `GITLAB_TOKEN` for the [@gl-service-dev-secure-analyzers-automation](https:/ The `ci-templates` project requires the `GITLAB_TOKEN` to allow certain scripts to execute API calls. This step can be removed after [allow JOB-TOKEN access to CI/lint endpoint](https://gitlab.com/gitlab-org/gitlab/-/issues/438781) has been completed. + 1. `GITLAB_TOKEN` CI/CD variable for the [`gitlab-org/secure/tools/security-triage-automation`](https://gitlab.com/gitlab-org/secure/tools/security-triage-automation) project. + + This must be explicitly configured because the `security-triage-automation` project is not nested under the `gitlab-org/security-products/analyzers` namespace, and therefore _does not inherit_ the `GITLAB_TOKEN` value. + 1. `SEC_REGISTRY_PASSWORD` CI/CD variable for [`gitlab-advanced-sast`](https://gitlab.com/gitlab-org/security-products/analyzers/gitlab-advanced-sast/-/settings/ci_cd#js-cicd-variables-settings). This allows our [tagging script](https://gitlab.com/gitlab-org/security-products/ci-templates/blob/cfe285a/scripts/tag_image.sh) to pull from the private container registry in the development project `registry.gitlab.com/gitlab-org/security-products/analyzers//tmp`, and push to the publicly accessible container registry `registry.gitlab.com/security-products/`. diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 765a52cbc0e..269891573ac 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -21,6 +21,7 @@ RSpec.describe 'Project issue boards sidebar', :js, feature_category: :portfolio context 'when issues drawer is disabled' do before do stub_feature_flags(issues_list_drawer: false) + stub_feature_flags(notifications_todos_buttons: false) sign_in(user) visit project_board_path(project, board) diff --git a/spec/frontend/work_items/components/work_item_notifications_widget_spec.js b/spec/frontend/work_items/components/work_item_notifications_widget_spec.js index 420f83b979a..cfbba11189b 100644 --- a/spec/frontend/work_items/components/work_item_notifications_widget_spec.js +++ b/spec/frontend/work_items/components/work_item_notifications_widget_spec.js @@ -131,6 +131,18 @@ describe('WorkItemActions component', () => { expect(findNotificationsButton().findComponent(GlIcon).props('name')).toBe(icon); }); + it.each` + scenario | subscribedToNotifications | dataSubscribed + ${'notifications are off'} | ${false} | ${'false'} + ${'notifications are on'} | ${true} | ${'true'} + `( + 'has the correct data-subscribed attribute when $scenario', + ({ subscribedToNotifications, dataSubscribed }) => { + createComponent({ subscribedToNotifications }); + expect(findNotificationsButton().attributes('data-subscribed')).toBe(dataSubscribed); + }, + ); + it('emits error when the update notification mutation fails', async () => { createComponent({ notificationsMutationHandler: toggleNotificationsFailureHandler, diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js index 7b802e5239d..33d62a18e6c 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -81,6 +81,16 @@ describe('Create work item page component', () => { }); }); + it('passes alwaysShowWorkItemTypeSelect prop as `true` to the CreateWorkItem component when isGroup is false', () => { + const pushMock = jest.fn(); + createComponent({ push: pushMock }, false); + + expect(findCreateWorkItem().props()).toMatchObject({ + alwaysShowWorkItemTypeSelect: true, + allowedWorkItemTypes: ['Incident', 'Issue', 'Task'], + }); + }); + it('visits work item detail page after create if router is not present', () => { createComponent(); diff --git a/spec/support/matchers/invoke_rop_steps.rb b/spec/support/matchers/invoke_rop_steps.rb index d8b9830dd47..1270abe59f1 100644 --- a/spec/support/matchers/invoke_rop_steps.rb +++ b/spec/support/matchers/invoke_rop_steps.rb @@ -111,6 +111,7 @@ module InvokeRopSteps context_passed_along_steps: ) expected_rop_steps = [] + skip_unless_inspect = false rop_steps.each do |rop_step| step_class = rop_step[0] @@ -121,13 +122,11 @@ module InvokeRopSteps step_action: step_action } + next if skip_unless_inspect && [:inspect_ok, :inspect_err].freeze.exclude?(step_action) + if err_results_for_steps.key?(step_class) expected_rop_step[:returned_object] = err_results_for_steps[step_class] - - # Currently, only a single error step is supported, so we assign expected_rop_step as the last entry - # in expected_rop_steps, break out of the loop early, and do not add any more steps - expected_rop_steps << expected_rop_step - break + skip_unless_inspect = true elsif ok_results_for_steps.key?(step_class) expected_rop_step[:returned_object] = ok_results_for_steps[step_class] elsif step_action == :and_then @@ -169,7 +168,9 @@ module InvokeRopSteps context_passed_along_steps:, returned_object: ) - expect(step_class).to receive(step_class_method).with(context_passed_along_steps).ordered do + expectation = expect(step_class).to receive(step_class_method) + expectation = expectation.with(context_passed_along_steps) if step_class_method != :observe + expectation.ordered do returned_object end end diff --git a/spec/support/shared_examples/features/work_items/work_item_drawer_shared_examples.rb b/spec/support/shared_examples/features/work_items/work_item_drawer_shared_examples.rb index 8f0937c86ad..d65fe510255 100644 --- a/spec/support/shared_examples/features/work_items/work_item_drawer_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items/work_item_drawer_shared_examples.rb @@ -64,38 +64,21 @@ RSpec.shared_examples 'work item drawer' do end context 'when in notifications subscription' do - before do - within_testid('work-item-drawer') do - find_by_testid('work-item-actions-dropdown').click - end - end - - it 'displays notifications toggle', :aggregate_failures do - within_testid('work-item-drawer') do - expect(page).to have_selector('[data-testid="notifications-toggle-form"]') - expect(page).to have_content('Notifications') - expect(page).not_to have_content('Disabled by project owner') - end - end - it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do - within_testid('notifications-toggle-form') do - subscription_button = find('[data-testid="notifications-toggle"] button') + subscribe_button = find_by_testid('subscribe-button') + expect(page).to have_selector("button[data-testid='subscribe-button'][data-subscribed='false']") - expect(page).not_to have_css("button.is-checked") + subscribe_button.click + wait_for_requests - subscription_button.click + expect(page).to have_content("Notifications turned on.") + expect(page).to have_selector("button[data-testid='subscribe-button'][data-subscribed='true']") - wait_for_requests + subscribe_button.click + wait_for_requests - expect(page).to have_css("button.is-checked") - - subscription_button.click - - wait_for_requests - - expect(page).not_to have_css("button.is-checked") - end + expect(page).to have_content("Notifications turned off.") + expect(page).to have_selector("button[data-testid='subscribe-button'][data-subscribed='false']") end end