Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2023-07-29 00:10:22 +00:00
parent 0e2a219d6a
commit aa02d34e84
15 changed files with 675 additions and 65 deletions

View File

@ -232,10 +232,10 @@ export const getFilterTokens = (locationSearch) =>
}; };
}); });
const isNotEmptySearchToken = (token) => export const isNotEmptySearchToken = (token) =>
!(token.type === FILTERED_SEARCH_TERM && !token.value.data); !(token.type === FILTERED_SEARCH_TERM && !token.value.data);
const isSpecialFilter = (type, data) => { export const isSpecialFilter = (type, data) => {
const isAssigneeIdParam = const isAssigneeIdParam =
type === TOKEN_TYPE_ASSIGNEE && type === TOKEN_TYPE_ASSIGNEE &&
isPositiveInteger(data) && isPositiveInteger(data) &&

View File

@ -2,15 +2,38 @@
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isEmpty } from 'lodash';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { getParameterByName } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { issuableListTabs } from '~/vue_shared/issuable/list/constants'; import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
import {
convertToSearchQuery,
convertToApiParams,
getInitialPageParams,
getFilterTokens,
isSortKey,
} from '~/issues/list/utils';
import { import {
OPERATORS_IS_NOT, OPERATORS_IS_NOT,
OPERATORS_IS_NOT_OR, OPERATORS_IS_NOT_OR,
} from '~/vue_shared/components/filtered_search_bar/constants'; } from '~/vue_shared/components/filtered_search_bar/constants';
import {
MAX_LIST_SIZE,
ISSUE_REFERENCE,
PARAM_STATE,
PARAM_FIRST_PAGE_SIZE,
PARAM_LAST_PAGE_SIZE,
PARAM_PAGE_AFTER,
PARAM_PAGE_BEFORE,
PARAM_SORT,
CREATED_DESC,
UPDATED_DESC,
urlSortParams,
} from '~/issues/list/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants'; import { TYPENAME_USER } from '~/graphql_shared/constants';
import searchUsersQuery from '~/issues/list/queries/search_users.query.graphql'; import searchUsersQuery from '~/issues/list/queries/search_users.query.graphql';
@ -24,12 +47,12 @@ import {
noSearchNoFilterTitle, noSearchNoFilterTitle,
searchPlaceholder, searchPlaceholder,
SERVICE_DESK_BOT_USERNAME, SERVICE_DESK_BOT_USERNAME,
MAX_LIST_SIZE,
STATUS_OPEN, STATUS_OPEN,
STATUS_CLOSED, STATUS_CLOSED,
STATUS_ALL, STATUS_ALL,
WORKSPACE_PROJECT, WORKSPACE_PROJECT,
} from '../constants'; } from '../constants';
import { convertToUrlParams } from '../utils';
import { import {
searchWithinTokenBase, searchWithinTokenBase,
assigneeTokenBase, assigneeTokenBase,
@ -68,6 +91,7 @@ export default {
'fullPath', 'fullPath',
'isServiceDeskSupported', 'isServiceDeskSupported',
'hasAnyIssues', 'hasAnyIssues',
'initialSort',
], ],
props: { props: {
eeSearchTokens: { eeSearchTokens: {
@ -81,7 +105,12 @@ export default {
serviceDeskIssues: [], serviceDeskIssues: [],
serviceDeskIssuesCounts: {}, serviceDeskIssuesCounts: {},
sortOptions: [], sortOptions: [],
filterTokens: [],
pageInfo: {},
pageParams: {},
sortKey: CREATED_DESC,
state: STATUS_OPEN, state: STATUS_OPEN,
pageSize: DEFAULT_PAGE_SIZE,
issuesError: null, issuesError: null,
}; };
}, },
@ -109,7 +138,7 @@ export default {
Sentry.captureException(error); Sentry.captureException(error);
}, },
skip() { skip() {
return !this.hasAnyIssues; return this.shouldSkipQuery;
}, },
}, },
serviceDeskIssuesCounts: { serviceDeskIssuesCounts: {
@ -124,6 +153,9 @@ export default {
this.issuesError = this.$options.i18n.errorFetchingCounts; this.issuesError = this.$options.i18n.errorFetchingCounts;
Sentry.captureException(error); Sentry.captureException(error);
}, },
skip() {
return this.shouldSkipQuery;
},
context: { context: {
isSingleRequest: true, isSingleRequest: true,
}, },
@ -131,14 +163,23 @@ export default {
}, },
computed: { computed: {
queryVariables() { queryVariables() {
const isIidSearch = ISSUE_REFERENCE.test(this.searchQuery);
return { return {
fullPath: this.fullPath, fullPath: this.fullPath,
iid: isIidSearch ? this.searchQuery.slice(1) : undefined,
isProject: this.isProject, isProject: this.isProject,
isSignedIn: this.isSignedIn, isSignedIn: this.isSignedIn,
authorUsername: SERVICE_DESK_BOT_USERNAME, authorUsername: SERVICE_DESK_BOT_USERNAME,
sort: this.sortKey,
state: this.state, state: this.state,
...this.pageParams,
...this.apiFilterParams,
search: isIidSearch ? undefined : this.searchQuery,
}; };
}, },
shouldSkipQuery() {
return !this.hasAnyIssues || isEmpty(this.pageParams);
},
tabCounts() { tabCounts() {
const { openedIssues, closedIssues, allIssues } = this.serviceDeskIssuesCounts; const { openedIssues, closedIssues, allIssues } = this.serviceDeskIssuesCounts;
return { return {
@ -147,12 +188,40 @@ export default {
[STATUS_ALL]: allIssues?.count, [STATUS_ALL]: allIssues?.count,
}; };
}, },
urlParams() {
return {
sort: urlSortParams[this.sortKey],
state: this.state,
...this.urlFilterParams,
first_page_size: this.pageParams.firstPageSize,
last_page_size: this.pageParams.lastPageSize,
page_after: this.pageParams.afterCursor ?? undefined,
page_before: this.pageParams.beforeCursor ?? undefined,
};
},
isInfoBannerVisible() { isInfoBannerVisible() {
return this.isServiceDeskSupported && this.hasAnyIssues; return this.isServiceDeskSupported && this.hasAnyIssues;
}, },
hasOrFeature() { hasOrFeature() {
return this.glFeatures.orIssuableQueries; return this.glFeatures.orIssuableQueries;
}, },
hasSearch() {
return Boolean(
this.searchQuery ||
Object.keys(this.urlFilterParams).length ||
this.pageParams.afterCursor ||
this.pageParams.beforeCursor,
);
},
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
urlFilterParams() {
return convertToUrlParams(this.filterTokens);
},
searchQuery() {
return convertToSearchQuery(this.filterTokens);
},
searchTokens() { searchTokens() {
const preloadedUsers = []; const preloadedUsers = [];
@ -219,7 +288,15 @@ export default {
return tokens; return tokens;
}, },
}, },
watch: {
$route(newValue, oldValue) {
if (newValue.fullPath !== oldValue.fullPath) {
this.updateData(getParameterByName(PARAM_SORT));
}
},
},
created() { created() {
this.updateData(this.initialSort);
this.cache = {}; this.cache = {};
}, },
methods: { methods: {
@ -287,6 +364,37 @@ export default {
return; return;
} }
this.state = state; this.state = state;
this.pageParams = getInitialPageParams(this.pageSize);
this.$router.push({ query: this.urlParams });
},
handleFilter(tokens) {
this.filterTokens = tokens;
this.pageParams = getInitialPageParams(this.pageSize);
this.$router.push({ query: this.urlParams });
},
updateData(sortValue) {
const firstPageSize = getParameterByName(PARAM_FIRST_PAGE_SIZE);
const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
const state = getParameterByName(PARAM_STATE);
const defaultSortKey = state === STATUS_CLOSED ? UPDATED_DESC : CREATED_DESC;
const graphQLSortKey = isSortKey(sortValue?.toUpperCase()) && sortValue.toUpperCase();
const sortKey = graphQLSortKey || defaultSortKey;
this.filterTokens = getFilterTokens(window.location.search);
this.pageParams = getInitialPageParams(
this.pageSize,
isPositiveInteger(firstPageSize) ? parseInt(firstPageSize, 10) : undefined,
isPositiveInteger(lastPageSize) ? parseInt(lastPageSize, 10) : undefined,
getParameterByName(PARAM_PAGE_AFTER),
getParameterByName(PARAM_PAGE_BEFORE),
);
this.sortKey = sortKey;
this.state = state || STATUS_OPEN;
}, },
}, },
}; };
@ -297,16 +405,22 @@ export default {
<info-banner v-if="isInfoBannerVisible" /> <info-banner v-if="isInfoBannerVisible" />
<issuable-list <issuable-list
namespace="service-desk" namespace="service-desk"
recent-searches-storage-key="issues" recent-searches-storage-key="service-desk-issues"
:error="issuesError" :error="issuesError"
:search-input-placeholder="$options.i18n.searchPlaceholder" :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens" :search-tokens="searchTokens"
:initial-filter-value="filterTokens"
:show-filtered-search-friendly-text="hasOrFeature"
:sort-options="sortOptions" :sort-options="sortOptions"
:initial-sort-by="sortKey"
:issuables="serviceDeskIssues" :issuables="serviceDeskIssues"
:tabs="$options.issuableListTabs" :tabs="$options.issuableListTabs"
:tab-counts="tabCounts" :tab-counts="tabCounts"
:current-tab="state" :current-tab="state"
:default-page-size="pageSize"
sync-filter-and-sort
@click-tab="handleClickTab" @click-tab="handleClickTab"
@filter="handleFilter"
> >
<template #empty-state> <template #empty-state>
<gl-empty-state <gl-empty-state

View File

@ -1,12 +1,231 @@
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS,
OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_EPIC,
TOKEN_TYPE_HEALTH,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
TOKEN_TYPE_SEARCH_WITHIN,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
ALTERNATIVE_FILTER,
API_PARAM,
NORMAL_FILTER,
SPECIAL_FILTER,
URL_PARAM,
} from '~/issues/list/constants';
export const SERVICE_DESK_BOT_USERNAME = 'support-bot'; export const SERVICE_DESK_BOT_USERNAME = 'support-bot';
export const MAX_LIST_SIZE = 10; export const ISSUE_REFERENCE = /^#\d+$/;
export const STATUS_ALL = 'all'; export const STATUS_ALL = 'all';
export const STATUS_CLOSED = 'closed'; export const STATUS_CLOSED = 'closed';
export const STATUS_OPEN = 'opened'; export const STATUS_OPEN = 'opened';
export const WORKSPACE_PROJECT = 'project'; export const WORKSPACE_PROJECT = 'project';
export const filtersMap = {
[FILTERED_SEARCH_TERM]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'search',
},
[URL_PARAM]: {
[undefined]: {
[NORMAL_FILTER]: 'search',
},
},
},
[TOKEN_TYPE_SEARCH_WITHIN]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'in',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'in',
},
},
},
[TOKEN_TYPE_ASSIGNEE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'assigneeUsernames',
[SPECIAL_FILTER]: 'assigneeId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'assignee_username[]',
[SPECIAL_FILTER]: 'assignee_id',
[ALTERNATIVE_FILTER]: 'assignee_username',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username][]',
},
[OPERATOR_OR]: {
[NORMAL_FILTER]: 'or[assignee_username][]',
},
},
},
[TOKEN_TYPE_MILESTONE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'milestoneTitle',
[SPECIAL_FILTER]: 'milestoneWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'milestone_title',
[SPECIAL_FILTER]: 'milestone_title',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[milestone_title]',
[SPECIAL_FILTER]: 'not[milestone_title]',
},
},
},
[TOKEN_TYPE_LABEL]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'labelName',
[SPECIAL_FILTER]: 'labelName',
[ALTERNATIVE_FILTER]: 'labelNames',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]',
[SPECIAL_FILTER]: 'label_name[]',
[ALTERNATIVE_FILTER]: 'label_name',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
},
[OPERATOR_OR]: {
[ALTERNATIVE_FILTER]: 'or[label_name][]',
},
},
},
[TOKEN_TYPE_TYPE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'types',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'type[]',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[type][]',
},
},
},
[TOKEN_TYPE_RELEASE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'releaseTag',
[SPECIAL_FILTER]: 'releaseTagWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'release_tag',
[SPECIAL_FILTER]: 'release_tag',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[release_tag]',
},
},
},
[TOKEN_TYPE_MY_REACTION]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'myReactionEmoji',
[SPECIAL_FILTER]: 'myReactionEmoji',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[my_reaction_emoji]',
},
},
},
[TOKEN_TYPE_CONFIDENTIAL]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'confidential',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential',
},
},
},
[TOKEN_TYPE_ITERATION]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'iterationId',
[SPECIAL_FILTER]: 'iterationWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_id',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[iteration_id]',
[SPECIAL_FILTER]: 'not[iteration_id]',
},
},
},
[TOKEN_TYPE_EPIC]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'epicId',
[SPECIAL_FILTER]: 'epicId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[epic_id]',
},
},
},
[TOKEN_TYPE_WEIGHT]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
},
},
[TOKEN_TYPE_HEALTH]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'healthStatusFilter',
[SPECIAL_FILTER]: 'healthStatusFilter',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'health_status',
[SPECIAL_FILTER]: 'health_status',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[health_status]',
},
},
},
};
export const errorFetchingCounts = __('An error occurred while getting issue counts'); export const errorFetchingCounts = __('An error occurred while getting issue counts');
export const errorFetchingIssues = __('An error occurred while loading issues'); export const errorFetchingIssues = __('An error occurred while loading issues');
export const noSearchNoFilterTitle = __('Please select at least one filter to see results'); export const noSearchNoFilterTitle = __('Please select at least one filter to see results');

View File

@ -1,5 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import ServiceDeskListApp from 'ee_else_ce/service_desk/components/service_desk_list_app.vue'; import ServiceDeskListApp from 'ee_else_ce/service_desk/components/service_desk_list_app.vue';
import { gqlClient } from './graphql'; import { gqlClient } from './graphql';
@ -23,6 +24,7 @@ export async function mountServiceDeskListApp() {
projectDataIsProject, projectDataIsProject,
projectDataIsSignedIn, projectDataIsSignedIn,
projectDataHasAnyIssues, projectDataHasAnyIssues,
projectDataInitialSort,
serviceDeskEmailAddress, serviceDeskEmailAddress,
canAdminIssues, canAdminIssues,
canEditProjectSettings, canEditProjectSettings,
@ -34,6 +36,7 @@ export async function mountServiceDeskListApp() {
} = el.dataset; } = el.dataset;
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(VueRouter);
return new Vue({ return new Vue({
el, el,
@ -41,6 +44,11 @@ export async function mountServiceDeskListApp() {
apolloProvider: new VueApollo({ apolloProvider: new VueApollo({
defaultClient: await gqlClient(), defaultClient: await gqlClient(),
}), }),
router: new VueRouter({
base: window.location.pathname,
mode: 'history',
routes: [{ path: '/' }],
}),
provide: { provide: {
releasesPath: projectDataReleasesPath, releasesPath: projectDataReleasesPath,
autocompleteAwardEmojisPath: projectDataAutocompleteAwardEmojisPath, autocompleteAwardEmojisPath: projectDataAutocompleteAwardEmojisPath,
@ -61,6 +69,7 @@ export async function mountServiceDeskListApp() {
isServiceDeskSupported: parseBoolean(isServiceDeskSupported), isServiceDeskSupported: parseBoolean(isServiceDeskSupported),
isServiceDeskEnabled: parseBoolean(isServiceDeskEnabled), isServiceDeskEnabled: parseBoolean(isServiceDeskEnabled),
hasAnyIssues: parseBoolean(projectDataHasAnyIssues), hasAnyIssues: parseBoolean(projectDataHasAnyIssues),
initialSort: projectDataInitialSort,
}, },
render: (createComponent) => createComponent(ServiceDeskListApp), render: (createComponent) => createComponent(ServiceDeskListApp),
}); });

View File

@ -0,0 +1,37 @@
import {
OPERATOR_OR,
TOKEN_TYPE_LABEL,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { isSpecialFilter, isNotEmptySearchToken } from '~/issues/list/utils';
import {
ALTERNATIVE_FILTER,
NORMAL_FILTER,
SPECIAL_FILTER,
URL_PARAM,
} from '~/issues/list/constants';
import { filtersMap } from './constants';
const getFilterType = ({ type, value: { data, operator } }) => {
const isUnionedLabel = type === TOKEN_TYPE_LABEL && operator === OPERATOR_OR;
if (isUnionedLabel) {
return ALTERNATIVE_FILTER;
}
if (isSpecialFilter(type, data)) {
return SPECIAL_FILTER;
}
return NORMAL_FILTER;
};
export const convertToUrlParams = (filterTokens) => {
const urlParamsMap = filterTokens.filter(isNotEmptySearchToken).reduce((acc, token) => {
const filterType = getFilterType(token);
const urlParam = filtersMap[token.type][URL_PARAM][token.value.operator]?.[filterType];
return acc.set(
urlParam,
acc.has(urlParam) ? [acc.get(urlParam), token.value.data].flat() : token.value.data,
);
}, new Map());
return Object.fromEntries(urlParamsMap);
};

View File

@ -41,7 +41,7 @@ To enable the agent server on a single node:
1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). 1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation).
For additional configuration options, see the **Enable GitLab KAS** section of the For additional configuration options, see the **Enable GitLab KAS** section of the
[`gitlab.rb.template`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/master/files/gitlab-config-template/gitlab.rb.template). [`gitlab.rb.template`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/be52c36c243a3422ec38b7d45d459682a07e195f/files/gitlab-config-template/gitlab.rb.template#L1951).
##### Configure KAS to listen on a UNIX socket ##### Configure KAS to listen on a UNIX socket

View File

@ -236,37 +236,61 @@ The project for a new Gem should always be created in [`gitlab-org/ruby/gems` na
1. Determine a suitable name for the gem. If it's a GitLab-owned gem, prefix 1. Determine a suitable name for the gem. If it's a GitLab-owned gem, prefix
the gem name with `gitlab-`. For example, `gitlab-sidekiq-fetcher`. the gem name with `gitlab-`. For example, `gitlab-sidekiq-fetcher`.
1. Create the gem or fork as necessary. 1. Locally create the gem or fork as necessary.
1. Ensure the `gitlab_rubygems` group is an owner of the new gem by running: 1. [Publish an empty `0.0.1` version of the gem to rubygems.org](https://guides.rubygems.org/publishing/#publishing-to-rubygemsorg) to ensure the gem name is reserved.
1. Add the [`gitlab_rubygems`](https://rubygems.org/profiles/gitlab_rubygems) and [`gitlab-qa`](https://rubygems.org/profiles/gitlab-qa) users as owners of the new gem by running:
```shell ```shell
gem owner <gem-name> --add gitlab_rubygems gem owner <gem-name> --add gitlab_rubygems
gem owner <gem-name> --add gitlab-qa
``` ```
1. [Publish the gem to rubygems.org](https://guides.rubygems.org/publishing/#publishing-to-rubygemsorg) 1. Optional. Add some or all of the following users as co-owners:
1. Visit `https://rubygems.org/gems/<gem-name>` and verify that the gem published - [Marin Jankovski](https://rubygems.org/profiles/marinjankovski)
successfully and `gitlab_rubygems` is also an owner. - [Rémy Coutable](https://rubygems.org/profiles/rymai)
1. Create a project in [`gitlab-org/ruby/gems` namespace](https://gitlab.com/gitlab-org/ruby/gems/). - [Stan Hu](https://rubygems.org/profiles/stanhu)
1. Optional. Add any other relevant developers as co-owners.
1. Visit `https://rubygems.org/gems/<gem-name>` and verify that the gem was published
successfully and `gitlab_rubygems` & `gitlab-qa` are also owners.
1. Create a project in the [`gitlab-org/ruby/gems` group](https://gitlab.com/gitlab-org/ruby/gems/). To create this project:
1. Follow the [instructions for new projects](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#creating-a-new-project).
1. Follow the instructions for setting up a [CI/CD configuration](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#cicd-configuration).
1. Use the [shared CI/CD config](https://gitlab.com/gitlab-org/quality/pipeline-common/-/blob/master/ci/gem-release.yml)
to release and publish new gem versions by adding the following to their `.gitlab-ci.yml`:
- To create this project: ```yaml
1. Follow the [instructions for new projects](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#creating-a-new-project). include:
1. Follow the instructions for setting up a [CI/CD configuration](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#cicd-configuration). - project: 'gitlab-org/quality/pipeline-common'
1. Follow the instructions for [publishing a project](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#publishing-a-project). file: '/ci/gem-release.yml'
- See [issue #325463](https://gitlab.com/gitlab-org/gitlab/-/issues/325463) ```
for an example.
- In some cases we may want to move a gem to its own namespace. Some
examples might be that it will naturally have more than one project
(say, something that has plugins as separate libraries), or that we
expect users outside GitLab to be maintainers on this project as
well as GitLab team members.
The latter situation (maintainers from outside GitLab) could also This job will handle building and publishing the gem (it uses a `gilab-qa` Rubygems.org
apply if someone who currently works at GitLab wants to maintain API token inherited from the `gitlab-org/ruby/gems` group, in order to publish the gem
the gem beyond their time working at GitLab. package), as well as creating the tag, release and populating its release notes by
using the
[Generate changelog data](../api/repositories.md#generate-changelog-data)
API endpoint.
When publishing a gem to RubyGems.org, also note the section on For instructions for when and how to generate a changelog entry file, see the
[gem owners](https://about.gitlab.com/handbook/developer-onboarding/#ruby-gems) dedicated [Changelog entries](changelog.md)
in the handbook. page.
[To be consistent with the GitLab project](changelog.md),
Gem projects could also define a changelog YAML configuration file at
`.gitlab/changelog_config.yml` with the same content
as [in the `gitlab-styles` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/blob/master/.gitlab/changelog_config.yml).
1. To ease the release process, you could also create a `.gitlab/merge_request_templates/Release.md` MR template with the same content
as [in the `gitlab-styles` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/raw/master/.gitlab/merge_request_templates/Release.md)
(make sure to replace `gitlab-styles` with the actual gem name).
1. Follow the instructions for [publishing a project](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#publishing-a-project).
Notes: In some cases we may want to move a gem to its own namespace. Some
examples might be that it will naturally have more than one project
(say, something that has plugins as separate libraries), or that we
expect users outside GitLab to be maintainers on this project as
well as GitLab team members.
The latter situation (maintainers from outside GitLab) could also
apply if someone who currently works at GitLab wants to maintain
the gem beyond their time working at GitLab.
## The `vendor/gems/` ## The `vendor/gems/`

View File

@ -1,14 +1,16 @@
--- ---
stage: Deploy stage: Deploy
group: Environments group: Environments
info: A tutorial for structuring a repository for GitOps deployments info: A tutorial for deploying a GitLab repository using Flux
--- ---
# Tutorial: Structure your repository for GitOps deployments **(FREE)** # Tutorial: Deploy a Git repository using Flux **(FREE)**
In this tutorial, you'll create a GitLab project that builds and deploys an application In this tutorial, you'll create a GitLab project that builds and deploys an application
to a Kubernetes cluster using Flux. You'll set up a sample manifest project, configure it to to a Kubernetes cluster using Flux. You'll set up a sample manifest project, configure it to
push manifests to a deployment branch, and configure Flux to sync the deployment branch. push manifests to a deployment branch, and configure Flux to sync the deployment branch. With this
setup, you can run additional steps in GitLab pipelines before Flux picks up the changes
from the repository.
This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md). This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md).

View File

@ -8,7 +8,9 @@ info: A tutorial for deploying an OCI artifact using Flux
This tutorial teaches you how to package your Kubernetes manifests into an [OCI](https://opencontainers.org/) This tutorial teaches you how to package your Kubernetes manifests into an [OCI](https://opencontainers.org/)
artifact and deploy them to your cluster using Flux. You'll set up a sample manifest project, configure it to artifact and deploy them to your cluster using Flux. You'll set up a sample manifest project, configure it to
store manifests as an artifact in the project's Container Registry, and configure Flux to sync the artifact. store manifests as an artifact in the project's Container Registry, and configure Flux to sync the artifact. With this
setup, you can run additional steps in GitLab pipelines before Flux picks up the changes
from the OCI image.
This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md). This tutorial deploys an application from a public project. If you want to add a non-public project, you should create a [project deploy token](../../../project/deploy_tokens/index.md).

View File

@ -124,7 +124,7 @@
"clipboard": "^2.0.8", "clipboard": "^2.0.8",
"compression-webpack-plugin": "^5.0.2", "compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"core-js": "^3.31.1", "core-js": "^3.32.0",
"cron-validator": "^1.1.1", "cron-validator": "^1.1.1",
"cronstrue": "^1.122.0", "cronstrue": "^1.122.0",
"cropper": "^2.3.0", "cropper": "^2.3.0",
@ -202,8 +202,8 @@
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"traverse": "^0.6.7", "traverse": "^0.6.7",
"unified": "^10.1.2", "unified": "^10.1.2",
"unist-builder": "^3.0.1", "unist-builder": "^4.0.0",
"unist-util-visit-parents": "^5.1.3", "unist-util-visit-parents": "5.1.3",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"uuid": "8.1.0", "uuid": "8.1.0",
"visibilityjs": "^1.2.4", "visibilityjs": "^1.2.4",

View File

@ -2,14 +2,17 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import VueRouter from 'vue-router';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { issuableListTabs } from '~/vue_shared/issuable/list/constants'; import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
import { TYPENAME_USER } from '~/graphql_shared/constants'; import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils'; import { convertToGraphQLId } from '~/graphql_shared/utils';
import { STATUS_CLOSED, STATUS_OPEN } from '~/service_desk/constants'; import { STATUS_CLOSED, STATUS_OPEN, STATUS_ALL } from '~/service_desk/constants';
import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues.query.graphql'; import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues.query.graphql';
import getServiceDeskIssuesCountsQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql'; import getServiceDeskIssuesCountsQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql';
import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue'; import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue';
@ -27,14 +30,19 @@ import {
import { import {
getServiceDeskIssuesQueryResponse, getServiceDeskIssuesQueryResponse,
getServiceDeskIssuesCountsQueryResponse, getServiceDeskIssuesCountsQueryResponse,
filteredTokens,
urlParams,
locationSearch,
} from '../mock_data'; } from '../mock_data';
jest.mock('@sentry/browser'); jest.mock('@sentry/browser');
describe('ServiceDeskListApp', () => { describe('CE ServiceDeskListApp', () => {
let wrapper; let wrapper;
let router;
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(VueRouter);
const defaultProvide = { const defaultProvide = {
releasesPath: 'releases/path', releasesPath: 'releases/path',
@ -49,6 +57,7 @@ describe('ServiceDeskListApp', () => {
fullPath: 'path/to/project', fullPath: 'path/to/project',
isServiceDeskSupported: true, isServiceDeskSupported: true,
hasAnyIssues: true, hasAnyIssues: true,
initialSort: '',
}; };
let defaultQueryResponse = getServiceDeskIssuesQueryResponse; let defaultQueryResponse = getServiceDeskIssuesQueryResponse;
@ -82,6 +91,8 @@ describe('ServiceDeskListApp', () => {
[getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponseHandler], [getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponseHandler],
]; ];
router = new VueRouter({ mode: 'history' });
return shallowMount(ServiceDeskListApp, { return shallowMount(ServiceDeskListApp, {
apolloProvider: createMockApollo( apolloProvider: createMockApollo(
requestHandlers, requestHandlers,
@ -98,6 +109,7 @@ describe('ServiceDeskListApp', () => {
}, },
}, },
), ),
router,
provide: { provide: {
...defaultProvide, ...defaultProvide,
...provide, ...provide,
@ -106,6 +118,7 @@ describe('ServiceDeskListApp', () => {
}; };
beforeEach(() => { beforeEach(() => {
setWindowLocation(TEST_HOST);
wrapper = createComponent(); wrapper = createComponent();
return waitForPromises(); return waitForPromises();
}); });
@ -113,7 +126,7 @@ describe('ServiceDeskListApp', () => {
it('fetches service desk issues and renders them in the issuable list', () => { it('fetches service desk issues and renders them in the issuable list', () => {
expect(findIssuableList().props()).toMatchObject({ expect(findIssuableList().props()).toMatchObject({
namespace: 'service-desk', namespace: 'service-desk',
recentSearchesStorageKey: 'issues', recentSearchesStorageKey: 'service-desk-issues',
issuables: defaultQueryResponse.data.project.issues.nodes, issuables: defaultQueryResponse.data.project.issues.nodes,
tabs: issuableListTabs, tabs: issuableListTabs,
currentTab: STATUS_OPEN, currentTab: STATUS_OPEN,
@ -145,6 +158,36 @@ describe('ServiceDeskListApp', () => {
}); });
}); });
describe('Initial url params', () => {
describe('search', () => {
it('is set from the url params', () => {
setWindowLocation(locationSearch);
wrapper = createComponent();
expect(router.history.current.query).toMatchObject({ search: 'find issues' });
});
});
describe('state', () => {
it('is set from the url params', () => {
const initialState = STATUS_ALL;
setWindowLocation(`?state=${initialState}`);
wrapper = createComponent();
expect(findIssuableList().props('currentTab')).toBe(initialState);
});
});
describe('filter tokens', () => {
it('are set from the url params', () => {
setWindowLocation(locationSearch);
wrapper = createComponent();
expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
});
});
});
describe('Tokens', () => { describe('Tokens', () => {
const mockCurrentUser = { const mockCurrentUser = {
id: 1, id: 1,
@ -200,14 +243,36 @@ describe('ServiceDeskListApp', () => {
describe('Events', () => { describe('Events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => { describe('when "click-tab" event is emitted by IssuableList', () => {
it('updates ui to the new tab', async () => { beforeEach(() => {
createComponent(); wrapper = createComponent();
router.push = jest.fn();
findIssuableList().vm.$emit('click-tab', STATUS_CLOSED); findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
});
await nextTick(); it('updates ui to the new tab', () => {
expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED); expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
}); });
it('updates url to the new tab', () => {
expect(router.push).toHaveBeenCalledWith({
query: expect.objectContaining({ state: STATUS_CLOSED }),
});
});
});
describe('when "filter" event is emitted by IssuableList', () => {
it('updates IssuableList with url params', async () => {
wrapper = createComponent();
router.push = jest.fn();
findIssuableList().vm.$emit('filter', filteredTokens);
await nextTick();
expect(router.push).toHaveBeenCalledWith({
query: expect.objectContaining(urlParams),
});
});
}); });
}); });

View File

@ -1,3 +1,20 @@
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS,
OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_EPIC,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_WEIGHT,
TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getServiceDeskIssuesQueryResponse = { export const getServiceDeskIssuesQueryResponse = {
data: { data: {
project: { project: {
@ -116,3 +133,104 @@ export const getServiceDeskIssuesCountsQueryResponse = {
}, },
}, },
}; };
export const filteredTokens = [
{ type: FILTERED_SEARCH_TERM, value: { data: 'find issues', operator: 'undefined' } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lisa', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: '5', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'carl', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lenny', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'comedy', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'sitcom', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_EPIC, value: { data: '12', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: '1', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: OPERATOR_NOT } },
];
export const urlParams = {
search: 'find issues',
'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
'or[assignee_username][]': ['carl', 'lenny'],
milestone_title: ['season 3', 'season 4'],
'not[milestone_title]': ['season 20', 'season 30'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
'or[label_name][]': ['comedy', 'sitcom'],
release_tag: ['v3', 'v4'],
'not[release_tag]': ['v20', 'v30'],
my_reaction_emoji: 'thumbsup',
'not[my_reaction_emoji]': 'thumbsdown',
confidential: 'yes',
iteration_id: ['4', '12'],
'not[iteration_id]': ['20', '42'],
epic_id: '12',
'not[epic_id]': '34',
weight: '1',
'not[weight]': '3',
health_status: 'atRisk',
'not[health_status]': 'onTrack',
};
export const locationSearch = [
'?search=find+issues',
'assignee_username[]=bart',
'assignee_username[]=lisa',
'assignee_username[]=5',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
'or[assignee_username][]=carl',
'or[assignee_username][]=lenny',
'milestone_title=season+3',
'milestone_title=season+4',
'not[milestone_title]=season+20',
'not[milestone_title]=season+30',
'label_name[]=cartoon',
'label_name[]=tv',
'not[label_name][]=live action',
'not[label_name][]=drama',
'or[label_name][]=comedy',
'or[label_name][]=sitcom',
'release_tag=v3',
'release_tag=v4',
'not[release_tag]=v20',
'not[release_tag]=v30',
'my_reaction_emoji=thumbsup',
'not[my_reaction_emoji]=thumbsdown',
'confidential=yes',
'iteration_id=4',
'iteration_id=12',
'not[iteration_id]=20',
'not[iteration_id]=42',
'epic_id=12',
'not[epic_id]=34',
'weight=1',
'not[weight]=3',
'health_status=atRisk',
'not[health_status]=onTrack',
].join('&');

View File

@ -23,10 +23,12 @@ RSpec.describe Tooling::Danger::ModelValidations, feature_category: :tooling do
describe '#add_comment_for_added_validations' do describe '#add_comment_for_added_validations' do
let(:file_lines) { file_diff.map { |line| line.delete_prefix('+').delete_prefix('-') } } let(:file_lines) { file_diff.map { |line| line.delete_prefix('+').delete_prefix('-') } }
let(:filename) { 'app/models/user.rb' } let(:filename) { 'app/models/user.rb' }
let(:added_filename) { 'app/models/user.rb' }
before do before do
allow(model_validations.project_helper).to receive(:file_lines).and_return(file_lines) allow(model_validations.project_helper).to receive(:file_lines).and_return(file_lines)
allow(model_validations.helper).to receive(:all_changed_files).and_return([filename]) allow(model_validations.helper).to receive(:added_files).and_return([added_filename])
allow(model_validations.helper).to receive(:modified_files).and_return([filename])
allow(model_validations.helper).to receive(:changed_lines).with(filename).and_return(file_diff) allow(model_validations.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
end end
@ -83,11 +85,13 @@ RSpec.describe Tooling::Danger::ModelValidations, feature_category: :tooling do
app/models/users/user_follow_user.rb app/models/users/user_follow_user.rb
ee/app/models/ee/user.rb ee/app/models/ee/user.rb
ee/app/models/sca/license_policy.rb ee/app/models/sca/license_policy.rb
app/models/concerns/presentable.rb
] ]
end end
before do before do
all_new_files = %w[ added_files = %w[app/models/user_preferences.rb app/models/concerns/presentable.rb]
modified_files = %w[
app/models/user.rb app/models/user.rb
app/models/users/user_follow_user.rb app/models/users/user_follow_user.rb
ee/app/models/ee/user.rb ee/app/models/ee/user.rb
@ -96,7 +100,8 @@ RSpec.describe Tooling::Danger::ModelValidations, feature_category: :tooling do
app/assets/index.js app/assets/index.js
] ]
allow(model_validations.helper).to receive(:all_changed_files).and_return(all_new_files) allow(model_validations.helper).to receive(:added_files).and_return(added_files)
allow(model_validations.helper).to receive(:modified_files).and_return(modified_files)
end end
it 'returns added and modified files' do it 'returns added and modified files' do

View File

@ -7,8 +7,11 @@ module Tooling
module ModelValidations module ModelValidations
include ::Tooling::Danger::Suggestor include ::Tooling::Danger::Suggestor
MODEL_FILES_REGEX = 'app/models' MODEL_FILES_PATH = 'app/models'
MODEL_CONCERN_FILES_PATH = 'app/models/concerns'
EE_PREFIX = 'ee/' EE_PREFIX = 'ee/'
MODEL_FILES_REGEX = %r{\A(#{EE_PREFIX})?#{MODEL_FILES_PATH}}
MODEL_CONCERN_FILES_REGEX = %r{\A(#{EE_PREFIX})?#{MODEL_CONCERN_FILES_PATH}}
VALIDATION_METHODS = %w[validates validate validates_each validates_with validates_associated].freeze VALIDATION_METHODS = %w[validates validate validates_each validates_with validates_associated].freeze
VALIDATIONS_REGEX = /^\+\s*(.*\.)?(#{VALIDATION_METHODS.join('|')})[( ]/ VALIDATIONS_REGEX = /^\+\s*(.*\.)?(#{VALIDATION_METHODS.join('|')})[( ]/
@ -32,10 +35,10 @@ module Tooling
end end
def changed_model_files def changed_model_files
changed_files = helper.all_changed_files added_files = helper.added_files
ee_folder_prefix = "(#{EE_PREFIX})?" modified_files = helper.modified_files
changed_files.grep(%r{\A#{ee_folder_prefix}#{MODEL_FILES_REGEX}}) modified_files.grep(MODEL_FILES_REGEX) + added_files.grep(MODEL_CONCERN_FILES_REGEX)
end end
end end
end end

View File

@ -2478,7 +2478,12 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/unist@*", "@types/unist@^2.0.0": "@types/unist@*", "@types/unist@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.0.tgz#988ae8af1e5239e89f9fbb1ade4c935f4eeedf9a"
integrity sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==
"@types/unist@^2.0.0":
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
@ -4267,10 +4272,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
core-js@^3.29.1, core-js@^3.31.1, core-js@^3.6.5: core-js@^3.29.1, core-js@^3.32.0, core-js@^3.6.5:
version "3.31.1" version "3.32.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.31.1.tgz#f2b0eea9be9da0def2c5fece71064a7e5d687653" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.0.tgz#7643d353d899747ab1f8b03d2803b0312a0fb3b6"
integrity sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ== integrity sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==
core-util-is@~1.0.0: core-util-is@~1.0.0:
version "1.0.3" version "1.0.3"
@ -12554,13 +12559,20 @@ unique-slug@^2.0.0:
dependencies: dependencies:
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
unist-builder@^3.0.0, unist-builder@^3.0.1: unist-builder@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.1.tgz#258b89dcadd3c973656b2327b347863556907f58" resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.1.tgz#258b89dcadd3c973656b2327b347863556907f58"
integrity sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ== integrity sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ==
dependencies: dependencies:
"@types/unist" "^2.0.0" "@types/unist" "^2.0.0"
unist-builder@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-4.0.0.tgz#817b326c015a6f9f5e92bb55b8e8bc5e578fe243"
integrity sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==
dependencies:
"@types/unist" "^3.0.0"
unist-util-generated@^2.0.0: unist-util-generated@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113" resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113"
@ -12585,18 +12597,18 @@ unist-util-stringify-position@^3.0.0:
dependencies: dependencies:
"@types/unist" "^2.0.0" "@types/unist" "^2.0.0"
unist-util-visit-parents@^4.0.0: unist-util-visit-parents@5.1.3, unist-util-visit-parents@^5.0.0:
version "4.1.1" version "5.1.3"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb"
integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw== integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==
dependencies: dependencies:
"@types/unist" "^2.0.0" "@types/unist" "^2.0.0"
unist-util-is "^5.0.0" unist-util-is "^5.0.0"
unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.3: unist-util-visit-parents@^4.0.0:
version "5.1.3" version "4.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2"
integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== integrity sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==
dependencies: dependencies:
"@types/unist" "^2.0.0" "@types/unist" "^2.0.0"
unist-util-is "^5.0.0" unist-util-is "^5.0.0"