diff --git a/README.md b/README.md index 9fd168f208e..e9e8445d07d 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL/OpenSUSE -- Ruby (MRI) 3.0.5 +- Ruby (MRI) 3.1.4 - Git 2.33+ - Redis 6.0+ - PostgreSQL 12+ diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js index 93f7f800fd0..e48281a7453 100644 --- a/app/assets/javascripts/analytics/shared/constants.js +++ b/app/assets/javascripts/analytics/shared/constants.js @@ -6,6 +6,7 @@ import { getDateInPast, getCurrentUtcDate, nWeeksBefore, + nYearsBefore, } from '~/lib/utils/datetime_utility'; import { s__, __, sprintf, n__ } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; @@ -251,3 +252,43 @@ export const METRICS_POPOVER_CONTENT = { ), }, }; + +export const USAGE_OVERVIEW_NO_DATA_ERROR = s__( + 'ValueStreamAnalytics|Failed to load usage overview data', +); + +export const USAGE_OVERVIEW_DEFAULT_DATE_RANGE = { + endDate: TODAY, + startDate: nYearsBefore(TODAY, 1), +}; + +export const USAGE_OVERVIEW_IDENTIFIER_GROUPS = 'groups'; +export const USAGE_OVERVIEW_IDENTIFIER_PROJECTS = 'projects'; +export const USAGE_OVERVIEW_IDENTIFIER_ISSUES = 'issues'; +export const USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS = 'merge_requests'; +export const USAGE_OVERVIEW_IDENTIFIER_PIPELINES = 'pipelines'; + +// Defines the constants used for querying the API as well as the order they appear +export const USAGE_OVERVIEW_METADATA = { + [USAGE_OVERVIEW_IDENTIFIER_GROUPS]: { options: { title: __('Groups'), titleIcon: 'group' } }, + [USAGE_OVERVIEW_IDENTIFIER_PROJECTS]: { + options: { title: __('Projects'), titleIcon: 'project' }, + }, + [USAGE_OVERVIEW_IDENTIFIER_ISSUES]: { + options: { title: __('Issues'), titleIcon: 'issues' }, + }, + [USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS]: { + options: { title: __('Merge requests'), titleIcon: 'merge-request' }, + }, + [USAGE_OVERVIEW_IDENTIFIER_PIPELINES]: { + options: { title: __('Pipelines'), titleIcon: 'pipeline' }, + }, +}; + +export const USAGE_OVERVIEW_QUERY_INCLUDE_KEYS = { + [USAGE_OVERVIEW_IDENTIFIER_GROUPS]: 'includeGroups', + [USAGE_OVERVIEW_IDENTIFIER_PROJECTS]: 'includeProjects', + [USAGE_OVERVIEW_IDENTIFIER_ISSUES]: 'includeIssues', + [USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS]: 'includeMergeRequests', + [USAGE_OVERVIEW_IDENTIFIER_PIPELINES]: 'includePipelines', +}; diff --git a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js index 4e0d19f2c2a..6484fcff769 100644 --- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js @@ -412,6 +412,15 @@ export const nYearsAfter = (date, numberOfYears) => { return clone; }; +/** + * Returns the date `n` years before the date provided. + * + * @param {Date} date the initial date + * @param {Number} numberOfYears number of years before + * @return {Date} A `Date` object `n` years before the provided `Date` + */ +export const nYearsBefore = (date, numberOfYears) => nYearsAfter(date, -numberOfYears); + /** * Returns the date after the date provided * diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue index cb9a560f9e1..445938ad56a 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue @@ -33,6 +33,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: ['isGroup'], props: { fullPath: { type: String, @@ -152,6 +153,7 @@ export default { note: this.note, name, fullPath: this.fullPath, + isGroup: this.isGroup, workItemIid: this.workItemIid, }), }); diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue index 75a8a7b29c0..c3b3c0e6db7 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue @@ -8,6 +8,7 @@ export default { components: { AwardsList, }, + inject: ['isGroup'], props: { fullPath: { type: String, @@ -73,6 +74,7 @@ export default { note: this.note, name, fullPath: this.fullPath, + isGroup: this.isGroup, workItemIid: this.workItemIid, }), }); diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue index 6756acd4495..83958ee4ef3 100644 --- a/app/assets/javascripts/work_items/components/work_item_notes.vue +++ b/app/assets/javascripts/work_items/components/work_item_notes.vue @@ -28,6 +28,7 @@ import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_ite import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql'; import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql'; import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation.graphql'; +import groupWorkItemNotesByIidQuery from '../graphql/notes/group_work_item_notes_by_iid.query.graphql'; import workItemNotesByIidQuery from '../graphql/notes/work_item_notes_by_iid.query.graphql'; import WorkItemAddNote from './notes/work_item_add_note.vue'; @@ -46,6 +47,7 @@ export default { WorkItemNotesActivityHeader, WorkItemHistoryOnlyFilterNote, }, + inject: ['isGroup'], props: { fullPath: { type: String, @@ -169,7 +171,9 @@ export default { }, apollo: { workItemNotes: { - query: workItemNotesByIidQuery, + query() { + return this.isGroup ? groupWorkItemNotesByIidQuery : workItemNotesByIidQuery; + }, variables() { return { fullPath: this.fullPath, diff --git a/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql new file mode 100644 index 00000000000..f86176b2836 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql @@ -0,0 +1,32 @@ +#import "~/graphql_shared/fragments/page_info.fragment.graphql" +#import "ee_else_ce/work_items/graphql/notes/work_item_note.fragment.graphql" + +query groupWorkItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) { + workspace: group(fullPath: $fullPath) { + id + workItems(iid: $iid) { + nodes { + id + iid + widgets { + ... on WorkItemWidgetNotes { + type + discussions(first: $pageSize, after: $after, filter: ALL_NOTES) { + pageInfo { + ...PageInfo + } + nodes { + id + notes { + nodes { + ...WorkItemNote + } + } + } + } + } + } + } + } + } +} diff --git a/app/assets/javascripts/work_items/notes/award_utils.js b/app/assets/javascripts/work_items/notes/award_utils.js index 5351a22d593..4f35b06a685 100644 --- a/app/assets/javascripts/work_items/notes/award_utils.js +++ b/app/assets/javascripts/work_items/notes/award_utils.js @@ -5,6 +5,7 @@ import { updateCacheAfterAddingAwardEmojiToNote, updateCacheAfterRemovingAwardEmojiFromNote, } from '~/work_items/graphql/cache_utils'; +import groupWorkItemNotesByIidQuery from '../graphql/notes/group_work_item_notes_by_iid.query.graphql'; import workItemNotesByIidQuery from '../graphql/notes/work_item_notes_by_iid.query.graphql'; import addAwardEmojiMutation from '../graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; import removeAwardEmojiMutation from '../graphql/notes/work_item_note_remove_award_emoji.mutation.graphql'; @@ -32,7 +33,7 @@ export function getMutation({ note, name }) { }; } -export function optimisticAwardUpdate({ note, name, fullPath, workItemIid }) { +export function optimisticAwardUpdate({ note, name, fullPath, isGroup, workItemIid }) { const { mutation } = getMutation({ note, name }); const currentUserId = window.gon.current_user_id; @@ -40,7 +41,7 @@ export function optimisticAwardUpdate({ note, name, fullPath, workItemIid }) { return (store) => { store.updateQuery( { - query: workItemNotesByIidQuery, + query: isGroup ? groupWorkItemNotesByIidQuery : workItemNotesByIidQuery, variables: { fullPath, iid: workItemIid }, }, (sourceData) => { diff --git a/db/post_migrate/20231120070345_cleanup_ci_stages_pipeline_id_bigint.rb b/db/post_migrate/20231120070345_cleanup_ci_stages_pipeline_id_bigint.rb new file mode 100644 index 00000000000..c9238eb7272 --- /dev/null +++ b/db/post_migrate/20231120070345_cleanup_ci_stages_pipeline_id_bigint.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class CleanupCiStagesPipelineIdBigint < Gitlab::Database::Migration[2.2] + include Gitlab::Database::MigrationHelpers::WraparoundAutovacuum + + disable_ddl_transaction! + milestone "16.7" + + TABLE = :ci_stages + REFERENCING_TABLE = :ci_pipelines + COLUMN = :pipeline_id + OLD_COLUMN = :pipeline_id_convert_to_bigint + INDEXES = { + 'index_ci_stages_on_pipeline_id_convert_to_bigint_and_name' => [ + [:pipeline_id_convert_to_bigint, :name], { unique: true } + ], + 'index_ci_stages_on_pipeline_id_convert_to_bigint' => [ + [:pipeline_id_convert_to_bigint], {} + ], + 'index_ci_stages_on_pipeline_id_convert_to_bigint_and_id' => [ + [:pipeline_id_convert_to_bigint, :id], { where: 'status = ANY (ARRAY[0, 1, 2, 8, 9, 10])' } + ], + 'index_ci_stages_on_pipeline_id_convert_to_bigint_and_position' => [ + [:pipeline_id_convert_to_bigint, :position], {} + ] + } + OLD_FK_NAME = :fk_c5ddde695f + + def up + return unless can_execute_on?(:ci_pipelines, :ci_stages) + + with_lock_retries(raise_on_exhaustion: true) do + lock_tables(REFERENCING_TABLE, TABLE) + cleanup_conversion_of_integer_to_bigint(TABLE, [COLUMN]) + end + end + + def down + return unless can_execute_on?(:ci_pipelines, :ci_stages) + + restore_conversion_of_integer_to_bigint(TABLE, [COLUMN]) + + INDEXES.each do |index_name, (columns, options)| + add_concurrent_index(TABLE, columns, name: index_name, **options) + end + + add_concurrent_foreign_key( + TABLE, REFERENCING_TABLE, + column: OLD_COLUMN, name: OLD_FK_NAME, + on_delete: :cascade, validate: true, reverse_lock_order: true + ) + end +end diff --git a/db/schema_migrations/20231120070345 b/db/schema_migrations/20231120070345 new file mode 100644 index 00000000000..70ba566885a --- /dev/null +++ b/db/schema_migrations/20231120070345 @@ -0,0 +1 @@ +7f3abae7002d20e30f9e4a30d580e49c5d72a7728d13ee45a5392fb4396da13b \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index bf669db0172..82ebc8d0582 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -531,15 +531,6 @@ RETURN NULL; END $$; -CREATE FUNCTION trigger_07bc3c48f407() RETURNS trigger - LANGUAGE plpgsql - AS $$ -BEGIN - NEW."pipeline_id_convert_to_bigint" := NEW."pipeline_id"; - RETURN NEW; -END; -$$; - CREATE FUNCTION trigger_10ee1357e825() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -14721,7 +14712,6 @@ ALTER SEQUENCE ci_sources_projects_id_seq OWNED BY ci_sources_projects.id; CREATE TABLE ci_stages ( project_id integer, - pipeline_id_convert_to_bigint integer, created_at timestamp without time zone, updated_at timestamp without time zone, name character varying, @@ -32344,14 +32334,6 @@ CREATE UNIQUE INDEX index_ci_stages_on_pipeline_id_and_name ON ci_stages USING b CREATE INDEX index_ci_stages_on_pipeline_id_and_position ON ci_stages USING btree (pipeline_id, "position"); -CREATE INDEX index_ci_stages_on_pipeline_id_convert_to_bigint ON ci_stages USING btree (pipeline_id_convert_to_bigint); - -CREATE INDEX index_ci_stages_on_pipeline_id_convert_to_bigint_and_id ON ci_stages USING btree (pipeline_id_convert_to_bigint, id) WHERE (status = ANY (ARRAY[0, 1, 2, 8, 9, 10])); - -CREATE UNIQUE INDEX index_ci_stages_on_pipeline_id_convert_to_bigint_and_name ON ci_stages USING btree (pipeline_id_convert_to_bigint, name); - -CREATE INDEX index_ci_stages_on_pipeline_id_convert_to_bigint_and_position ON ci_stages USING btree (pipeline_id_convert_to_bigint, "position"); - CREATE INDEX index_ci_stages_on_project_id ON ci_stages USING btree (project_id); CREATE INDEX index_ci_subscriptions_projects_author_id ON ci_subscriptions_projects USING btree (author_id); @@ -37036,8 +37018,6 @@ CREATE TRIGGER push_rules_loose_fk_trigger AFTER DELETE ON push_rules REFERENCIN CREATE TRIGGER tags_loose_fk_trigger AFTER DELETE ON tags REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); -CREATE TRIGGER trigger_07bc3c48f407 BEFORE INSERT OR UPDATE ON ci_stages FOR EACH ROW EXECUTE FUNCTION trigger_07bc3c48f407(); - CREATE TRIGGER trigger_10ee1357e825 BEFORE INSERT OR UPDATE ON p_ci_builds FOR EACH ROW EXECUTE FUNCTION trigger_10ee1357e825(); CREATE TRIGGER trigger_b2d852e1e2cb BEFORE INSERT OR UPDATE ON ci_pipelines FOR EACH ROW EXECUTE FUNCTION trigger_b2d852e1e2cb(); @@ -37953,9 +37933,6 @@ ALTER TABLE ONLY timelogs ALTER TABLE ONLY geo_event_log ADD CONSTRAINT fk_c4b1c1f66e FOREIGN KEY (repository_deleted_event_id) REFERENCES geo_repository_deleted_events(id) ON DELETE CASCADE; -ALTER TABLE ONLY ci_stages - ADD CONSTRAINT fk_c5ddde695f FOREIGN KEY (pipeline_id_convert_to_bigint) REFERENCES ci_pipelines(id) ON DELETE CASCADE; - ALTER TABLE ONLY issues ADD CONSTRAINT fk_c63cbf6c25 FOREIGN KEY (closed_by_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index c4b0816492a..3a1a0a970e5 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -37,7 +37,7 @@ Audit events can be viewed at the group, project, instance, and sign-in level. E To view a group's audit events: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Audit events**. +1. Select **Secure > Audit events**. 1. Filter the audit events by the member of the project (user) who performed the action and date range. Group audit events can also be accessed using the [Group Audit Events API](../api/audit_events.md#group-audit-events). Group audit event queries are limited to a maximum of 30 days. @@ -45,7 +45,7 @@ Group audit events can also be accessed using the [Group Audit Events API](../ap ### Project audit events 1. On the left sidebar, select **Search or go to** and find your project. -1. On the left sidebar, select **Secure > Audit events**. +1. Select **Secure > Audit events**. 1. Filter the audit events by the member of the project (user) who performed the action and date range. Project audit events can also be accessed using the [Project Audit Events API](../api/audit_events.md#project-audit-events). Project audit event queries are limited to a maximum of 30 days. @@ -56,7 +56,7 @@ You can view audit events from user actions across an entire GitLab instance. To view instance audit events: 1. On the left sidebar, at the bottom, select **Admin Area**. -1. On the left sidebar, select **Monitoring > Audit Events**. +1. Select **Monitoring > Audit Events**. 1. Filter by the following: - Member of the project (user) who performed the action - Group @@ -82,7 +82,7 @@ You can export the current view (including filters) of your instance audit event CSV(comma-separated values) file. To export the instance audit events to CSV: 1. On the left sidebar, at the bottom, select **Admin Area**. -1. On the left sidebar, select **Monitoring > Audit Events**. +1. Select **Monitoring > Audit Events**. 1. Select the available search filters. 1. Select **Export as CSV**. diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 33e10e2144c..4ca2a72fddb 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1010,8 +1010,11 @@ To open the Admin Area: ```markdown 1. On the left sidebar, at the bottom, select **Admin Area**. +1. Select **Settings > CI/CD**. ``` +You do not need to repeat `On the left sidebar` in your second step. + To open the **Your work** menu item: ```markdown diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index 556fba5c69a..610328806bc 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -380,6 +380,18 @@ Use **compute minutes** instead of these (or similar) terms: For more information, see [epic 2150](https://gitlab.com/groups/gitlab-com/-/epics/2150). +## configuration + +When you update a collection of settings, call it a **configuration**. + +## configure + +Use **configure** after a feature or product has been [set up](#setup-set-up). +For example: + +1. Set up your installation. +1. Configure your installation. + ## confirmation dialog Use **confirmation dialog** to describe the dialog that asks you to confirm an action. For example: @@ -1335,6 +1347,10 @@ see the [Microsoft Style Guide](https://learn.microsoft.com/en-us/style-guide/a- Use **Premium**, in uppercase, for the subscription tier. When you refer to **Premium** in the context of other subscription tiers, follow [the subscription tier](#subscription-tier) guidance. +## preferences + +Use **preferences** to describe user-specific, system-level settings like theme and layout. + ## prerequisites Use **prerequisites** when documenting the tasks that must be completed or the conditions that must be met before a user can complete a task. Do not use **requirements**. @@ -1556,6 +1572,17 @@ Use **setup** as a noun, and **set up** as a verb. For example: - Your remote office setup is amazing. - To set up your remote office correctly, consider the ergonomics of your work area. +Do not confuse **set up** with [**configure**](#configure). +**Set up** implies that it's the first time you've done something. For example: + +1. Set up your installation. +1. Configure your installation. + +## settings + +A **setting** changes the default behavior of the product. A **setting** consists of a key/value pair, +typically represented by a label with one or more options. + ## sign in, sign-in To describe the action of signing in, use: diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md index 2319b7a5185..6ff1f5e6990 100644 --- a/doc/user/product_analytics/index.md +++ b/doc/user/product_analytics/index.md @@ -24,7 +24,6 @@ To leave feedback about Product Analytics bugs or functionality: - Comment on [issue 391970](https://gitlab.com/gitlab-org/gitlab/-/issues/391970). - Create an issue with the `group::product analytics` label. -- [Schedule a call](https://calendly.com/jheimbuck/30-minute-call) with the team. ## How product analytics works diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b6cafab7c4d..66468232633 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -53029,6 +53029,9 @@ msgstr "" msgid "ValueStreamAnalytics|Edit Value Stream: %{name}" msgstr "" +msgid "ValueStreamAnalytics|Failed to load usage overview data" +msgstr "" + msgid "ValueStreamAnalytics|Go to docs" msgstr "" diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index 330bfca7029..73a4af2c85d 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -800,6 +800,21 @@ describe('date addition/subtraction methods', () => { ); }); + describe('nYearsBefore', () => { + it.each` + date | numberOfYears | expected + ${'2020-07-06'} | ${4} | ${'2016-07-06'} + ${'2020-07-06'} | ${1} | ${'2019-07-06'} + `( + 'returns $expected for "$numberOfYears year(s) before $date"', + ({ date, numberOfYears, expected }) => { + expect(datetimeUtility.nYearsBefore(new Date(date), numberOfYears)).toEqual( + new Date(expected), + ); + }, + ); + }); + describe('nMonthsBefore', () => { // The previous month (February) has 28 days const march2019 = '2019-03-15T00:00:00.000Z'; diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js index 596283a9590..c820c60fe13 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js @@ -64,6 +64,7 @@ describe('Work Item Note Actions', () => { projectName, }, provide: { + isGroup: false, glFeatures: { workItemsMvc2: true, }, diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js index ce915635946..939dd3e870b 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js @@ -9,6 +9,7 @@ import AwardsList from '~/vue_shared/components/awards_list.vue'; import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue'; import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql'; +import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; import { mockWorkItemNotesResponseWithComments, @@ -45,6 +46,7 @@ describe('Work Item Note Awards List', () => { const findAwardsList = () => wrapper.findComponent(AwardsList); const createComponent = ({ + isGroup = false, note = firstNote, addAwardEmojiMutationHandler = addAwardEmojiMutationSuccessHandler, removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler, @@ -55,12 +57,15 @@ describe('Work Item Note Awards List', () => { ]); apolloProvider.clients.defaultClient.writeQuery({ - query: workItemNotesByIidQuery, + query: isGroup ? groupWorkItemNotesByIidQuery : workItemNotesByIidQuery, variables: { fullPath, iid: workItemIid }, ...mockWorkItemNotesResponseWithComments, }); wrapper = shallowMount(WorkItemNoteAwardsList, { + provide: { + isGroup, + }, propsData: { fullPath, workItemIid, @@ -89,17 +94,20 @@ describe('Work Item Note Awards List', () => { expect(findAwardsList().props('canAwardEmoji')).toBe(hasAwardEmojiPermission); }); - it('adds award if not already awarded', async () => { - createComponent(); - await waitForPromises(); + it.each([true, false])( + 'adds award if not already awarded in both group and project contexts', + async (isGroup) => { + createComponent({ isGroup }); + await waitForPromises(); - findAwardsList().vm.$emit('award', EMOJI_THUMBSUP); + findAwardsList().vm.$emit('award', EMOJI_THUMBSUP); - expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({ - awardableId: firstNote.id, - name: EMOJI_THUMBSUP, - }); - }); + expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({ + awardableId: firstNote.id, + name: EMOJI_THUMBSUP, + }); + }, + ); it('emits error if awarding emoji fails', async () => { createComponent({ @@ -114,20 +122,23 @@ describe('Work Item Note Awards List', () => { expect(wrapper.emitted('error')).toEqual([[__('Failed to add emoji. Please try again')]]); }); - it('removes award if already awarded', async () => { - const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler; + it.each([true, false])( + 'removes award if already awarded in both group and project contexts', + async (isGroup) => { + const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler; - createComponent({ removeAwardEmojiMutationHandler }); + createComponent({ isGroup, removeAwardEmojiMutationHandler }); - findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN); + findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN); - await waitForPromises(); + await waitForPromises(); - expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({ - awardableId: firstNote.id, - name: EMOJI_THUMBSDOWN, - }); - }); + expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({ + awardableId: firstNote.id, + name: EMOJI_THUMBSDOWN, + }); + }, + ); it('restores award if remove fails', async () => { createComponent({ diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index 9e02e0708d4..2620242000e 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -10,6 +10,7 @@ import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue'; import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; +import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql'; import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql'; @@ -63,6 +64,9 @@ describe('WorkItemNotes component', () => { const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index); const findDeleteNoteModal = () => wrapper.findComponent(GlModal); + const groupWorkItemNotesQueryHandler = jest + .fn() + .mockResolvedValue(mockWorkItemNotesByIidResponse); const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesByIidResponse); const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse); const workItemNotesWithCommentsQueryHandler = jest @@ -87,17 +91,22 @@ describe('WorkItemNotes component', () => { workItemIid = mockWorkItemIid, defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler, deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler, + isGroup = false, isModal = false, isWorkItemConfidential = false, } = {}) => { wrapper = shallowMount(WorkItemNotes, { apolloProvider: createMockApollo([ [workItemNotesByIidQuery, defaultWorkItemNotesQueryHandler], + [groupWorkItemNotesByIidQuery, groupWorkItemNotesQueryHandler], [deleteWorkItemNoteMutation, deleteWINoteMutationHandler], [workItemNoteCreatedSubscription, notesCreateSubscriptionHandler], [workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler], [workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler], ]), + provide: { + isGroup, + }, propsData: { fullPath: 'test-path', workItemId, @@ -354,4 +363,22 @@ describe('WorkItemNotes component', () => { expect(findWorkItemCommentNoteAtIndex(0).props('isWorkItemConfidential')).toBe(true); }); + + describe('when project context', () => { + it('calls the project work item query', async () => { + createComponent(); + await waitForPromises(); + + expect(workItemNotesQueryHandler).toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('calls the group work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(groupWorkItemNotesQueryHandler).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js index 8ae32ce5f40..43eceb13b67 100644 --- a/spec/frontend/work_items/notes/award_utils_spec.js +++ b/spec/frontend/work_items/notes/award_utils_spec.js @@ -2,6 +2,7 @@ import { getMutation, optimisticAwardUpdate } from '~/work_items/notes/award_uti import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import mockApollo from 'helpers/mock_apollo_helper'; import { __ } from '~/locale'; +import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql'; import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql'; import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql'; import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql'; @@ -105,5 +106,22 @@ describe('Work item note award utils', () => { expect(updatedNote.awardEmoji.nodes).toEqual([]); }); + + it.each` + description | isGroup | query + ${'calls project query when in project context'} | ${false} | ${workItemNotesByIidQuery} + ${'calls group query when in group context'} | ${true} | ${groupWorkItemNotesByIidQuery} + `('$description', ({ isGroup, query }) => { + const note = firstNote; + const { name } = mockAwardEmojiThumbsUp; + const cacheSpy = { updateQuery: jest.fn() }; + + optimisticAwardUpdate({ note, name, fullPath, isGroup, workItemIid })(cacheSpy); + + expect(cacheSpy.updateQuery).toHaveBeenCalledWith( + { query, variables: { fullPath, iid: workItemIid } }, + expect.any(Function), + ); + }); }); }); diff --git a/spec/support/helpers/database/duplicate_indexes.yml b/spec/support/helpers/database/duplicate_indexes.yml index 37aa4374608..5ae529ea8ef 100644 --- a/spec/support/helpers/database/duplicate_indexes.yml +++ b/spec/support/helpers/database/duplicate_indexes.yml @@ -42,10 +42,6 @@ ci_stages: - index_ci_stages_on_pipeline_id index_ci_stages_on_pipeline_id_and_position: - index_ci_stages_on_pipeline_id - index_ci_stages_on_pipeline_id_convert_to_bigint_and_name: - - index_ci_stages_on_pipeline_id_convert_to_bigint - index_ci_stages_on_pipeline_id_convert_to_bigint_and_position: - - index_ci_stages_on_pipeline_id_convert_to_bigint dast_site_tokens: index_dast_site_token_on_project_id_and_url: - index_dast_site_tokens_on_project_id