Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b40ff326b9
commit
c7c4e203a0
3
Gemfile
3
Gemfile
|
|
@ -260,7 +260,7 @@ gem 'rouge', '~> 4.4.0', feature_category: :shared
|
|||
gem 'truncato', '~> 0.7.12', feature_category: :team_planning
|
||||
gem 'nokogiri', '~> 1.16', feature_category: :shared
|
||||
gem 'gitlab-glfm-markdown', '~> 0.0.21', feature_category: :markdown
|
||||
gem 'tanuki_emoji', '~> 0.9', feature_category: :markdown
|
||||
gem 'tanuki_emoji', '~> 0.13', feature_category: :markdown
|
||||
gem 'unicode-emoji', '~> 3.6', feature_category: :markdown
|
||||
|
||||
# Calendar rendering
|
||||
|
|
@ -380,7 +380,6 @@ gem 'addressable', '~> 2.8' # rubocop:todo Gemfile/MissingFeatureCategory
|
|||
gem 'gon', '~> 6.4.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'request_store', '~> 1.5.1' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'base32', '~> 0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
gem 'gitlab-license', '~> 2.5', feature_category: :shared
|
||||
|
||||
# Protect against bruteforcing
|
||||
|
|
|
|||
|
|
@ -704,7 +704,7 @@
|
|||
{"name":"sys-filesystem","version":"1.4.3","platform":"ruby","checksum":"390919de89822ad6d3ba3daf694d720be9d83ed95cdf7adf54d4573c98b17421"},
|
||||
{"name":"sysexits","version":"1.2.0","platform":"ruby","checksum":"598241c4ae57baa403c125182dfdcc0d1ac4c0fb606dd47fbed57e4aaf795662"},
|
||||
{"name":"table_print","version":"1.5.7","platform":"ruby","checksum":"436664281f93387b882335795e16cfeeb839ad0c785ff7f9110fc0f17c68b5cb"},
|
||||
{"name":"tanuki_emoji","version":"0.9.0","platform":"ruby","checksum":"009f0b283f61b7aed5f57d7d1f050225f2a5df8eec121550a67bdd7b95c74056"},
|
||||
{"name":"tanuki_emoji","version":"0.13.0","platform":"ruby","checksum":"dee2182a5cad6f88ed91cd4e39088bd2a8f313e24f83ff5d4b5b0fecf29f6d93"},
|
||||
{"name":"telesign","version":"2.2.4","platform":"ruby","checksum":"dcc6e96ea7bcb4da1e2ae786bfe7a4d670a4b5f94ae95dfcdde77d547c544c42"},
|
||||
{"name":"telesignenterprise","version":"2.2.2","platform":"ruby","checksum":"f147a03263a8c2fe0a0db1a7a9454a6ee37d9e8abd58eaca305bdd8081f9f1b3"},
|
||||
{"name":"temple","version":"0.8.2","platform":"ruby","checksum":"c12071214346c606dbd219b4117276d04a9f2c20d65e66a66b2c4ec18efc1f18"},
|
||||
|
|
|
|||
|
|
@ -1801,7 +1801,8 @@ GEM
|
|||
ffi (~> 1.1)
|
||||
sysexits (1.2.0)
|
||||
table_print (1.5.7)
|
||||
tanuki_emoji (0.9.0)
|
||||
tanuki_emoji (0.13.0)
|
||||
i18n (~> 1.14)
|
||||
telesign (2.2.4)
|
||||
net-http-persistent (>= 3.0.0, < 5.0)
|
||||
telesignenterprise (2.2.2)
|
||||
|
|
@ -2296,7 +2297,7 @@ DEPENDENCIES
|
|||
stackprof (~> 0.2.26)
|
||||
state_machines-activerecord (~> 0.8.0)
|
||||
sys-filesystem (~> 1.4.3)
|
||||
tanuki_emoji (~> 0.9)
|
||||
tanuki_emoji (~> 0.13)
|
||||
telesignenterprise (~> 2.2)
|
||||
terser (= 1.0.2)
|
||||
test-prof (~> 1.4.0)
|
||||
|
|
|
|||
|
|
@ -719,7 +719,7 @@
|
|||
{"name":"sys-filesystem","version":"1.4.3","platform":"ruby","checksum":"390919de89822ad6d3ba3daf694d720be9d83ed95cdf7adf54d4573c98b17421"},
|
||||
{"name":"sysexits","version":"1.2.0","platform":"ruby","checksum":"598241c4ae57baa403c125182dfdcc0d1ac4c0fb606dd47fbed57e4aaf795662"},
|
||||
{"name":"table_print","version":"1.5.7","platform":"ruby","checksum":"436664281f93387b882335795e16cfeeb839ad0c785ff7f9110fc0f17c68b5cb"},
|
||||
{"name":"tanuki_emoji","version":"0.9.0","platform":"ruby","checksum":"009f0b283f61b7aed5f57d7d1f050225f2a5df8eec121550a67bdd7b95c74056"},
|
||||
{"name":"tanuki_emoji","version":"0.13.0","platform":"ruby","checksum":"dee2182a5cad6f88ed91cd4e39088bd2a8f313e24f83ff5d4b5b0fecf29f6d93"},
|
||||
{"name":"telesign","version":"2.2.4","platform":"ruby","checksum":"dcc6e96ea7bcb4da1e2ae786bfe7a4d670a4b5f94ae95dfcdde77d547c544c42"},
|
||||
{"name":"telesignenterprise","version":"2.2.2","platform":"ruby","checksum":"f147a03263a8c2fe0a0db1a7a9454a6ee37d9e8abd58eaca305bdd8081f9f1b3"},
|
||||
{"name":"temple","version":"0.8.2","platform":"ruby","checksum":"c12071214346c606dbd219b4117276d04a9f2c20d65e66a66b2c4ec18efc1f18"},
|
||||
|
|
|
|||
|
|
@ -1828,7 +1828,8 @@ GEM
|
|||
ffi (~> 1.1)
|
||||
sysexits (1.2.0)
|
||||
table_print (1.5.7)
|
||||
tanuki_emoji (0.9.0)
|
||||
tanuki_emoji (0.13.0)
|
||||
i18n (~> 1.14)
|
||||
telesign (2.2.4)
|
||||
net-http-persistent (>= 3.0.0, < 5.0)
|
||||
telesignenterprise (2.2.2)
|
||||
|
|
@ -2323,7 +2324,7 @@ DEPENDENCIES
|
|||
stackprof (~> 0.2.26)
|
||||
state_machines-activerecord (~> 0.8.0)
|
||||
sys-filesystem (~> 1.4.3)
|
||||
tanuki_emoji (~> 0.9)
|
||||
tanuki_emoji (~> 0.13)
|
||||
telesignenterprise (~> 2.2)
|
||||
terser (= 1.0.2)
|
||||
test-prof (~> 1.4.0)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
---
|
||||
migration_job_name: BackfillDesignManagementVersionsNamespaceId
|
||||
description: Backfills sharding key `design_management_versions.namespace_id` from `issues`.
|
||||
description: Backfills sharding key `design_management_versions.namespace_id` from
|
||||
`issues`.
|
||||
feature_category: design_management
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154281
|
||||
milestone: '17.1'
|
||||
queued_migration_version: 20240530121656
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: '20241105232559'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
table_name: subscription_user_add_on_assignment_versions
|
||||
description: Stores user add-on assignment versioning data
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169180
|
||||
milestone: '17.6'
|
||||
feature_categories:
|
||||
- subscription_management
|
||||
classes:
|
||||
- GitlabSubscriptions::UserAddOnAssignmentVersion
|
||||
gitlab_schema: gitlab_main_cell
|
||||
sharding_key:
|
||||
organization_id: organizations
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateUserAddOnAssignmentVersions < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.6'
|
||||
|
||||
def change
|
||||
create_table :subscription_user_add_on_assignment_versions do |t| # rubocop:disable Migration/EnsureFactoryForTable -- No factory needed
|
||||
t.references :organization,
|
||||
foreign_key: true,
|
||||
null: false,
|
||||
index: { name: 'idx_user_add_on_assignment_versions_on_organization_id' }
|
||||
|
||||
t.bigint :item_id
|
||||
t.bigint :purchase_id
|
||||
t.bigint :user_id
|
||||
t.datetime_with_timezone :created_at
|
||||
t.text :item_type, null: false, limit: 255
|
||||
t.text :event, null: false, limit: 255
|
||||
t.text :namespace_path, limit: 255
|
||||
t.text :add_on_name, limit: 255
|
||||
t.text :whodunnit, limit: 255
|
||||
t.jsonb :object
|
||||
|
||||
t.index :item_id, name: 'idx_user_add_on_assignment_versions_on_item_id'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeBackfillDesignManagementVersionsNamespaceId < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.6'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'BackfillDesignManagementVersionsNamespaceId',
|
||||
table_name: :design_management_versions,
|
||||
column_name: :id,
|
||||
job_arguments: [:namespace_id, :issues, :namespace_id, :issue_id],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
b798beb93f31b748dfb0bcc50528e45054288339ab713f2b8cebb9845b3eafcf
|
||||
|
|
@ -0,0 +1 @@
|
|||
2de004a9d362510d745076d491485c5304683f09bb5e74075093a24bdeb36b71
|
||||
|
|
@ -19798,6 +19798,35 @@ CREATE SEQUENCE subscription_seat_assignments_id_seq
|
|||
|
||||
ALTER SEQUENCE subscription_seat_assignments_id_seq OWNED BY subscription_seat_assignments.id;
|
||||
|
||||
CREATE TABLE subscription_user_add_on_assignment_versions (
|
||||
id bigint NOT NULL,
|
||||
organization_id bigint NOT NULL,
|
||||
item_id bigint,
|
||||
purchase_id bigint,
|
||||
user_id bigint,
|
||||
created_at timestamp with time zone,
|
||||
item_type text NOT NULL,
|
||||
event text NOT NULL,
|
||||
namespace_path text,
|
||||
add_on_name text,
|
||||
whodunnit text,
|
||||
object jsonb,
|
||||
CONSTRAINT check_211bad6d65 CHECK ((char_length(item_type) <= 255)),
|
||||
CONSTRAINT check_34ca72be24 CHECK ((char_length(event) <= 255)),
|
||||
CONSTRAINT check_839913a25d CHECK ((char_length(namespace_path) <= 255)),
|
||||
CONSTRAINT check_9ceaa5668c CHECK ((char_length(add_on_name) <= 255)),
|
||||
CONSTRAINT check_e185bf0c82 CHECK ((char_length(whodunnit) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE subscription_user_add_on_assignment_versions_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE subscription_user_add_on_assignment_versions_id_seq OWNED BY subscription_user_add_on_assignment_versions.id;
|
||||
|
||||
CREATE TABLE subscription_user_add_on_assignments (
|
||||
id bigint NOT NULL,
|
||||
add_on_purchase_id bigint NOT NULL,
|
||||
|
|
@ -23606,6 +23635,8 @@ ALTER TABLE ONLY subscription_add_ons ALTER COLUMN id SET DEFAULT nextval('subsc
|
|||
|
||||
ALTER TABLE ONLY subscription_seat_assignments ALTER COLUMN id SET DEFAULT nextval('subscription_seat_assignments_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY subscription_user_add_on_assignment_versions ALTER COLUMN id SET DEFAULT nextval('subscription_user_add_on_assignment_versions_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY subscription_user_add_on_assignments ALTER COLUMN id SET DEFAULT nextval('subscription_user_add_on_assignments_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY subscriptions ALTER COLUMN id SET DEFAULT nextval('subscriptions_id_seq'::regclass);
|
||||
|
|
@ -26348,6 +26379,9 @@ ALTER TABLE ONLY subscription_add_ons
|
|||
ALTER TABLE ONLY subscription_seat_assignments
|
||||
ADD CONSTRAINT subscription_seat_assignments_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY subscription_user_add_on_assignment_versions
|
||||
ADD CONSTRAINT subscription_user_add_on_assignment_versions_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY subscription_user_add_on_assignments
|
||||
ADD CONSTRAINT subscription_user_add_on_assignments_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
@ -28309,6 +28343,10 @@ CREATE UNIQUE INDEX idx_uniq_analytics_dashboards_pointers_on_project_id ON anal
|
|||
|
||||
CREATE UNIQUE INDEX idx_usages_on_cmpt_used_by_project_cmpt_and_last_used_date ON catalog_resource_component_last_usages USING btree (component_id, used_by_project_id, last_used_date);
|
||||
|
||||
CREATE INDEX idx_user_add_on_assignment_versions_on_item_id ON subscription_user_add_on_assignment_versions USING btree (item_id);
|
||||
|
||||
CREATE INDEX idx_user_add_on_assignment_versions_on_organization_id ON subscription_user_add_on_assignment_versions USING btree (organization_id);
|
||||
|
||||
CREATE INDEX idx_user_add_on_assignments_on_add_on_purchase_id_and_id ON subscription_user_add_on_assignments USING btree (add_on_purchase_id, id);
|
||||
|
||||
CREATE INDEX idx_user_audit_events_on_author_id_created_at_id ON ONLY user_audit_events USING btree (author_id, created_at, id);
|
||||
|
|
@ -36686,6 +36724,9 @@ ALTER TABLE ONLY issue_assignment_events
|
|||
ALTER TABLE ONLY security_policies
|
||||
ADD CONSTRAINT fk_rails_08722e8ac7 FOREIGN KEY (security_policy_management_project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY subscription_user_add_on_assignment_versions
|
||||
ADD CONSTRAINT fk_rails_091e013a61 FOREIGN KEY (organization_id) REFERENCES organizations(id);
|
||||
|
||||
ALTER TABLE ONLY trending_projects
|
||||
ADD CONSTRAINT fk_rails_09feecd872 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -218,9 +218,10 @@ Same as when column is dropped, after the rename is completed, we need to [remov
|
|||
## Changing column constraints
|
||||
|
||||
Adding or removing a `NOT NULL` clause (or another constraint) can typically be
|
||||
done without requiring downtime. However, this does require that any application
|
||||
changes are deployed _first_. Thus, changing the constraints of a column should
|
||||
happen in a post-deployment migration.
|
||||
done without requiring downtime. Adding a `NOT NULL` contraint requires that any application
|
||||
changes are deployed _first_, so it should happen in a post-deployment migration.
|
||||
In contrary removing a `NOT NULL` contraint should be done in a regular migration.
|
||||
This way any code which insers `NULL` values can safely run for the column.
|
||||
|
||||
Avoid using `change_column` as it produces an inefficient query because it re-defines
|
||||
the whole column type.
|
||||
|
|
|
|||
|
|
@ -179,7 +179,17 @@ Executor.
|
|||
If you would like to start Duo Workflow with the VS Code extension instead,
|
||||
follow [these steps](../../user/duo_workflow/index.md#prerequisites).
|
||||
|
||||
If you are debugging or making changes to the VSCode extension and need to run the extension in development mode, you can do that following [these instructions](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/main/CONTRIBUTING.md#configuring-development-environment).
|
||||
If you would like to start Duo Workflow with a locally running VS Code extension and GitLab Language Server (for debugging or making changes to the extension)
|
||||
|
||||
1. Clone [language server](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp).
|
||||
1. Clone [VSCode extension](https://gitlab.com/gitlab-org/gitlab-vscode-extension).
|
||||
1. Change directory (`cd`) into language server.
|
||||
1. Run `npm install`.
|
||||
1. Run `npm run watch -- --editor=vscode --packages webview-duo-workflow workflow-api --vscode-path path-to-vscode-extension-from-step-2`.
|
||||
1. Open VSCode extension project in VSCode.
|
||||
1. Click **Run and Debug**, choose **Run Extension** in the dropdown and select **Play**.
|
||||
1. If prompted with **All installed extensions are temporarily disabled**, do not click **Reload and Enable extensions** because that will use native extensions.
|
||||
1. In the command palette, run `GitLab: Show Duo Workflow`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -14,17 +14,22 @@ NOTE:
|
|||
## How to update Emojis
|
||||
|
||||
1. Update the [`tanuki_emoji`](https://gitlab.com/gitlab-org/ruby/gems/tanuki_emoji) gem.
|
||||
1. Update `fixtures/emojis/index.json` from [Gemojione](https://github.com/bonusly/gemojione/blob/master/config/index.json).
|
||||
In the future, we could grab the file directly from the gem.
|
||||
We should probably make a PR on the Gemojione project to get access to
|
||||
all emojis after being parsed or just a raw path to the `json` file itself.
|
||||
1. Ensure [`emoji-unicode-version`](https://www.npmjs.com/package/emoji-unicode-version)
|
||||
is up to date with the latest version.
|
||||
1. Use the [`tanuki_emoji`](https://gitlab.com/gitlab-org/ruby/gems/tanuki_emoji) gem's [Rake tasks](../rake_tasks.md) to update aliases, digests, and sprites:
|
||||
1. Run `bundle exec rake tanuki_emoji:aliases`
|
||||
1. Run `bundle exec rake tanuki_emoji:digests`
|
||||
1. Run `bundle exec rake tanuki_emoji:sprite`
|
||||
1. Ensure new sprite sheets generated for 1x and 2x
|
||||
1. Update `EMOJI_VERSION` in `lib/gitlab/emoji.rb`
|
||||
1. Update `EMOJI_VERSION` in `app/assets/javascripts/emoji/index.js`
|
||||
1. Use the [`tanuki_emoji`](https://gitlab.com/gitlab-org/ruby/gems/tanuki_emoji) gem's [Rake tasks](../rake_tasks.md) to update aliases, fallback images, digests, and sprites. Run in the following order:
|
||||
1. `bundle exec rake tanuki_emoji:aliases` - updates `fixtures/emojis/aliases.json`
|
||||
1. `bundle exec rake tanuki_emoji:import` - imports all the images into `public/-/emojis` directory
|
||||
1. `bundle exec rake tanuki_emoji:digests` - updates `public/-/emojis/VERSION/emojis.json` and `fixtures/emojis/digests.json`
|
||||
1. `bundle exec rake tanuki_emoji:sprite` - creates new sprite sheets
|
||||
|
||||
If new emoji are added, the sprite sheet may change size. To compensate for
|
||||
such changes, first generate the `app/assets/images/emoji.png` sprite sheet with the above Rake
|
||||
task, then check the dimensions of the new sprite sheet and update the
|
||||
`SPRITESHEET_WIDTH` and `SPRITESHEET_HEIGHT` constants in `lib/tasks/tanuki_emoji.rake` accordingly.
|
||||
Then re-run the task.
|
||||
|
||||
- Use [ImageOptim](https://imageoptim.com) or similar program to optimize the images for size
|
||||
1. Ensure new sprite sheets were generated for 1x and 2x
|
||||
- `app/assets/images/emoji.png`
|
||||
- `app/assets/images/emoji@2x.png`
|
||||
1. Update `fixtures/emojis/intents.json` with any new emoji that we would like to highlight as having positive or negative intent.
|
||||
|
|
@ -38,3 +43,10 @@ NOTE:
|
|||
that do not support a certain emoji and we need to fallback to an image.
|
||||
See `app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js`
|
||||
and `app/assets/javascripts/emoji/support/unicode_support_map.js`
|
||||
- if a new version of Unicode emojis is being added, update the list in `app/assets/javascripts/emoji/support/unicode_support_map.js`
|
||||
1. Ensure you use the version of [emoji-regex](https://github.com/mathiasbynens/emoji-regex) that corresponds
|
||||
to the version of Unicode that is being supported. This should be updated in `package.json`. Used for
|
||||
filtering emojis in `app/assets/javascripts/emoji/index.js`.
|
||||
1. Have there been any changes to the category names? If so then `app/assets/javascripts/emoji/constants.js`
|
||||
will need to be updated
|
||||
1. See an [example MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166790)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ module Gitlab
|
|||
module Emoji
|
||||
def emoji_unicode_version(name)
|
||||
@emoji_unicode_versions_by_name ||=
|
||||
JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
|
||||
JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'digests.json')))
|
||||
@emoji_unicode_versions_by_name[name]
|
||||
end
|
||||
end
|
||||
|
|
@ -113,7 +113,7 @@ module Gitlab
|
|||
|
||||
def emoji_unicode_versions_by_name
|
||||
@emoji_unicode_versions_by_name ||=
|
||||
JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
|
||||
JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'digests.json')))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -378,26 +378,26 @@ following:
|
|||
bundle exec rake tanuki_emoji:aliases
|
||||
```
|
||||
|
||||
To update the Emoji digests file (used for Emoji autocomplete), run the
|
||||
following:
|
||||
To import the fallback Emoji images, run the following:
|
||||
|
||||
```shell
|
||||
bundle exec rake tanuki_emoji:import
|
||||
```
|
||||
|
||||
To update the Emoji digests file (used for Emoji autocomplete) based on the currently
|
||||
available Emoji, run the following:
|
||||
|
||||
```shell
|
||||
bundle exec rake tanuki_emoji:digests
|
||||
```
|
||||
|
||||
This updates the file `fixtures/emojis/digests.json` based on the currently
|
||||
available Emoji.
|
||||
|
||||
To generate a sprite file containing all the Emoji, run:
|
||||
|
||||
```shell
|
||||
bundle exec rake tanuki_emoji:sprite
|
||||
```
|
||||
|
||||
If new emoji are added, the sprite sheet may change size. To compensate for
|
||||
such changes, first generate the `emoji.png` sprite sheet with the above Rake
|
||||
task, then check the dimensions of the new sprite sheet and update the
|
||||
`SPRITESHEET_WIDTH` and `SPRITESHEET_HEIGHT` constants accordingly.
|
||||
See [How to update Emojis](fe_guide/emojis.md) for detailed instructions.
|
||||
|
||||
## Update project templates
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ module Banzai
|
|||
prepend Concerns::PipelineTimingCheck
|
||||
|
||||
IGNORED_ANCESTOR_TAGS = %w[pre code tt].to_set
|
||||
IGNORE_UNICODE_EMOJIS = %w[™ © ®].freeze
|
||||
|
||||
def call
|
||||
@emoji_count = 0
|
||||
|
|
@ -42,12 +43,7 @@ module Banzai
|
|||
.gsub_with_limit(text, emoji_pattern, limit: Banzai::Filter::FILTER_ITEM_LIMIT) do |match_data|
|
||||
emoji = TanukiEmoji.find_by_alpha_code(match_data[0])
|
||||
|
||||
if emoji
|
||||
@emoji_count += 1
|
||||
Gitlab::Emoji.gl_emoji_tag(emoji)
|
||||
else
|
||||
match_data[0]
|
||||
end
|
||||
process_emoji_tag(emoji, match_data[0])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -59,17 +55,27 @@ module Banzai
|
|||
def emoji_unicode_element_unicode_filter(text)
|
||||
Gitlab::Utils::Gsub
|
||||
.gsub_with_limit(text, emoji_unicode_pattern, limit: Banzai::Filter::FILTER_ITEM_LIMIT) do |match_data|
|
||||
emoji = TanukiEmoji.find_by_codepoints(match_data[0])
|
||||
|
||||
if emoji
|
||||
@emoji_count += 1
|
||||
Gitlab::Emoji.gl_emoji_tag(emoji)
|
||||
else
|
||||
if ignore_emoji?(match_data[0])
|
||||
match_data[0]
|
||||
else
|
||||
emoji = TanukiEmoji.find_by_codepoints(match_data[0])
|
||||
|
||||
process_emoji_tag(emoji, match_data[0])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_emoji_tag(emoji, fallback)
|
||||
return fallback unless emoji
|
||||
|
||||
@emoji_count += 1
|
||||
Gitlab::Emoji.gl_emoji_tag(emoji)
|
||||
end
|
||||
|
||||
def ignore_emoji?(text)
|
||||
IGNORE_UNICODE_EMOJIS.include?(text)
|
||||
end
|
||||
|
||||
# Build a regexp that matches all valid :emoji: names.
|
||||
def self.emoji_pattern
|
||||
@emoji_pattern ||= TanukiEmoji.index.alpha_code_pattern
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module Gitlab
|
|||
|
||||
# When updating emoji assets increase the version below
|
||||
# and update the version number in `app/assets/javascripts/emoji/index.js`
|
||||
EMOJI_VERSION = 3
|
||||
EMOJI_VERSION = 4
|
||||
|
||||
# Return a Pathname to emoji's current versioned folder
|
||||
#
|
||||
|
|
|
|||
|
|
@ -117,10 +117,10 @@ namespace :tanuki_emoji do
|
|||
SIZE = 20
|
||||
RETINA = SIZE * 2
|
||||
|
||||
# Update these values to the width and height of the spritesheet when
|
||||
# Update these values to the width and height of the sprite sheet when
|
||||
# new emoji are added.
|
||||
SPRITESHEET_WIDTH = 860
|
||||
SPRITESHEET_HEIGHT = 840
|
||||
SPRITESHEET_WIDTH = 1240
|
||||
SPRITESHEET_HEIGHT = 1220
|
||||
|
||||
emoji_dir = Gitlab::Emoji.emoji_public_absolute_path
|
||||
|
||||
|
|
@ -235,7 +235,7 @@ namespace :tanuki_emoji do
|
|||
To enable this task, *temporarily* add the following lines to Gemfile and
|
||||
re-bundle:
|
||||
|
||||
gem 'rmagick', '~> 3.2'
|
||||
gem 'rmagick', '~> 6.0'
|
||||
|
||||
It depends on ImageMagick 6, which can be installed via HomeBrew with:
|
||||
|
||||
|
|
|
|||
|
|
@ -243,7 +243,8 @@ RSpec.describe 'Database schema',
|
|||
namespace_settings: %w[early_access_program_joined_by_id], # isn't used inside product itself. Only through Snowflake
|
||||
workspaces_agent_config_versions: %w[item_id], # polymorphic associations
|
||||
work_item_types: %w[correct_id], # temporary column that is not a foreign key
|
||||
instance_integrations: %w[project_id group_id inherit_from_id] # these columns are not used in instance integrations
|
||||
instance_integrations: %w[project_id group_id inherit_from_id], # these columns are not used in instance integrations
|
||||
subscription_user_add_on_assignment_versions: %w[item_id user_id purchase_id] # Managed by paper_trail gem, no need for FK on the historical data
|
||||
}.with_indifferent_access.freeze
|
||||
end
|
||||
|
||||
|
|
@ -418,7 +419,8 @@ RSpec.describe 'Database schema',
|
|||
"Releases::Evidence" => %w[summary],
|
||||
"Vulnerabilities::Finding::Evidence" => %w[data], # Validation work in progress
|
||||
"Ai::DuoWorkflows::Checkpoint" => %w[checkpoint metadata], # https://gitlab.com/gitlab-org/gitlab/-/issues/468632
|
||||
"RemoteDevelopment::WorkspacesAgentConfigVersion" => %w[object object_changes] # Managed by paper_trail gem
|
||||
"RemoteDevelopment::WorkspacesAgentConfigVersion" => %w[object object_changes], # Managed by paper_trail gem
|
||||
"GitlabSubscriptions::UserAddOnAssignmentVersion" => %w[object] # Managed by paper_trail gem
|
||||
}.freeze
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ RSpec.describe 'Dropdown emoji', :js, feature_category: :team_planning do
|
|||
it 'loads all the emojis when opened' do
|
||||
select_tokens 'My-Reaction', '='
|
||||
|
||||
# Expect None, Any, star, thumbs_up, thumbs_down
|
||||
# Expect None, Any, star, thumbsup, thumbsdown
|
||||
expect_suggestion_count 5
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
|
|||
visit(project_issue_path(project, issue))
|
||||
end
|
||||
|
||||
it 'toggles the thumbs_up award emoji', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27959' do
|
||||
it 'toggles the thumbsup award emoji', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27959' do
|
||||
page.within('.awards') do
|
||||
thumbsup = page.first('.award-control')
|
||||
thumbsup.click
|
||||
|
|
@ -281,14 +281,14 @@ RSpec.describe 'User interacts with awards', feature_category: :team_planning do
|
|||
wait_for_requests
|
||||
end
|
||||
|
||||
context 'click the thumbs_down emoji' do
|
||||
it 'increments the thumbs_down emoji', :js do
|
||||
context 'click the thumbsdown emoji' do
|
||||
it 'increments the thumbsdown emoji', :js do
|
||||
find(%([data-name="#{AwardEmoji::THUMBS_DOWN}"])).click
|
||||
wait_for_requests
|
||||
expect(thumbsdown_emoji).to have_text("1")
|
||||
end
|
||||
|
||||
it 'decrements the thumbs_up emoji', :js do
|
||||
it 'decrements the thumbsup emoji', :js do
|
||||
expect(thumbsup_emoji).to have_text("0")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ RSpec.describe 'Merge request > User creates custom emoji', :js, feature_categor
|
|||
|
||||
wait_for_requests
|
||||
|
||||
find_by_testid("custom-emoji-name-input").set 'parrot'
|
||||
find_by_testid("custom-emoji-name-input").set 'flying_parrot'
|
||||
find_by_testid("custom-emoji-url-input").set 'https://example.com'
|
||||
|
||||
click_button 'Save'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(':parrot:')
|
||||
expect(page).to have_content(':flying_parrot:')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ exports[`releases/util.js convertAllReleasesGraphQLResponse matches snapshot 1`]
|
|||
<gl-emoji
|
||||
data-name="shrug"
|
||||
data-unicode-version="9.0"
|
||||
title="shrug"
|
||||
title="person shrugging"
|
||||
>
|
||||
🤷
|
||||
</gl-emoji>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
|
|||
end
|
||||
|
||||
describe 'awards_sort' do
|
||||
it 'sorts a hash so thumbs_up and thumbs_down are always on top' do
|
||||
it 'sorts a hash so thumbsup and thumbsdown are always on top' do
|
||||
data = { AwardEmoji::THUMBS_DOWN => 'some value', 'lifter' => 'some value', AwardEmoji::THUMBS_UP => 'some value' }
|
||||
expect(awards_sort(data).keys).to eq(%W[#{AwardEmoji::THUMBS_UP} #{AwardEmoji::THUMBS_DOWN} lifter])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ RSpec.describe Banzai::Filter::EmojiFilter, feature_category: :markdown do
|
|||
|
||||
it 'replaces supported name emoji' do
|
||||
doc = filter('<p>:heart:</p>')
|
||||
expect(doc.css('gl-emoji').first.text).to eq '❤'
|
||||
expect(doc.css('gl-emoji').first.text).to eq '❤️'
|
||||
end
|
||||
|
||||
it 'replaces supported unicode emoji' do
|
||||
doc = filter('<p>❤️</p>')
|
||||
expect(doc.css('gl-emoji').first.text).to eq '❤'
|
||||
expect(doc.css('gl-emoji').first.text).to eq '❤️'
|
||||
end
|
||||
|
||||
it 'ignores unicode versions of trademark, copyright, and registered trademark' do
|
||||
|
|
@ -159,7 +159,7 @@ RSpec.describe Banzai::Filter::EmojiFilter, feature_category: :markdown do
|
|||
context 'when using TanukiEmoji' do
|
||||
# the regex doesn't find emoji components, and they are not really meant to be used
|
||||
# by themselves, so ignore them.
|
||||
let(:exclude_components) { "🏻🏼🏽🏾🏿" }
|
||||
let(:exclude_components) { "🏻🏼🏽🏾🏿🦰🦱🦳🦲" }
|
||||
|
||||
it 'finds all unicode emoji codepoints with regex' do
|
||||
TanukiEmoji.index.all.each do |emoji| # rubocop:disable Rails/FindEach -- not a Rails model
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ RSpec.describe Banzai::Pipeline::IncidentManagement::TimelineEventPipeline do
|
|||
it 'renders emojis wrapped in <gl-emoji> tag' do
|
||||
# rubocop:disable Layout/LineLength
|
||||
is_expected.to eq(
|
||||
%(<p><gl-emoji title="thumbs up sign" data-name="#{AwardEmoji::THUMBS_UP}" data-unicode-version="6.0">👍</gl-emoji><gl-emoji title="thumbs up sign" data-name="#{AwardEmoji::THUMBS_UP}" data-unicode-version="6.0">👍</gl-emoji></p>)
|
||||
%(<p><gl-emoji title="thumbs up" data-name="#{AwardEmoji::THUMBS_UP}" data-unicode-version="6.0">👍</gl-emoji><gl-emoji title="thumbs up" data-name="#{AwardEmoji::THUMBS_UP}" data-unicode-version="6.0">👍</gl-emoji></p>)
|
||||
)
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ RSpec.describe Gitlab::Emoji do
|
|||
emoji = TanukiEmoji.find_by_alpha_code('small_airplane')
|
||||
gl_tag = described_class.gl_emoji_tag(emoji)
|
||||
|
||||
expect(gl_tag).to eq('<gl-emoji title="small airplane" data-name="airplane_small" data-unicode-version="7.0">🛩</gl-emoji>')
|
||||
expect(gl_tag).to eq('<gl-emoji title="small airplane" data-name="airplane_small" data-unicode-version="7.0">🛩️</gl-emoji>')
|
||||
end
|
||||
|
||||
it 'returns nil if emoji is not found' do
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ RSpec.describe CustomEmoji do
|
|||
end
|
||||
|
||||
describe 'exclusion of duplicated emoji' do
|
||||
let(:emoji_name) { TanukiEmoji.index.all.sample.name }
|
||||
let(:group) { create(:group, :private) }
|
||||
|
||||
it 'disallows emoji names of built-in emoji' do
|
||||
emoji_name = TanukiEmoji.index.all.sample.name until emoji_name && emoji_name.size < 36
|
||||
new_emoji = build(:custom_emoji, name: emoji_name, group: group)
|
||||
|
||||
expect(new_emoji).not_to be_valid
|
||||
|
|
@ -70,13 +70,13 @@ RSpec.describe CustomEmoji do
|
|||
|
||||
describe '#for_namespaces' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:custom_emoji) { create(:custom_emoji, namespace: group, name: 'parrot') }
|
||||
let_it_be(:custom_emoji) { create(:custom_emoji, namespace: group, name: 'flying_parrot') }
|
||||
|
||||
it { expect(described_class.for_namespaces([group.id])).to eq([custom_emoji]) }
|
||||
|
||||
context 'with subgroup' do
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:subgroup_emoji) { create(:custom_emoji, namespace: subgroup, name: 'parrot') }
|
||||
let_it_be(:subgroup_emoji) { create(:custom_emoji, namespace: subgroup, name: 'flying_parrot') }
|
||||
|
||||
it { expect(described_class.for_namespaces([subgroup.id, group.id])).to eq([subgroup_emoji]) }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ RSpec.describe IncidentManagement::TimelineEvent do
|
|||
|
||||
# rubocop:disable Layout/LineLength
|
||||
let(:expected_emoji_html) do
|
||||
%(<gl-emoji title="thumbs up sign" data-name="#{AwardEmoji::THUMBS_UP}" data-unicode-version="6.0">👍</gl-emoji><gl-emoji title="thumbs up sign" data-name="#{AwardEmoji::THUMBS_UP}" data-unicode-version="6.0">👍</gl-emoji>)
|
||||
%(<gl-emoji title="thumbs up" data-name="#{AwardEmoji::THUMBS_UP}" data-unicode-version="6.0">👍</gl-emoji><gl-emoji title="thumbs up" data-name="#{AwardEmoji::THUMBS_UP}" data-unicode-version="6.0">👍</gl-emoji>)
|
||||
end
|
||||
|
||||
let(:expected_note_html) do
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ RSpec.describe API::AwardEmoji, feature_category: :shared do
|
|||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it "normalizes +1 as thumbs_up award" do
|
||||
it "normalizes +1 as thumbsup award" do
|
||||
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), params: { name: '+1' }
|
||||
|
||||
expect(issue.award_emoji.last.name).to eq(AwardEmoji::THUMBS_UP)
|
||||
|
|
@ -306,7 +306,7 @@ RSpec.describe API::AwardEmoji, feature_category: :shared do
|
|||
expect(todo.reload).to be_done
|
||||
end
|
||||
|
||||
it "normalizes +1 as thumbs_up award" do
|
||||
it "normalizes +1 as thumbsup award" do
|
||||
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: '+1' }
|
||||
|
||||
expect(note.award_emoji.last.name).to eq(AwardEmoji::THUMBS_UP)
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ RSpec.describe GroupChildEntity do
|
|||
let(:description) { ':smile:' }
|
||||
|
||||
it 'has the correct markdown_description' do
|
||||
expect(json[:markdown_description]).to eq('<p dir="auto"><gl-emoji title="smiling face with open mouth and smiling eyes" data-name="smile" data-unicode-version="6.0">😄</gl-emoji></p>')
|
||||
expect(json[:markdown_description]).to eq('<p dir="auto"><gl-emoji title="grinning face with smiling eyes" data-name="smile" data-unicode-version="6.0">😄</gl-emoji></p>')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ RSpec.shared_examples 'work items award emoji' do
|
|||
within(award_section_selector) do
|
||||
expect(page).to have_selector(selected_award_button_selector)
|
||||
|
||||
# As the user2 has already awarded the `:thumbs_up:` emoji, the emoji count will be 2
|
||||
# As the user2 has already awarded the `:thumbsup:` emoji, the emoji count will be 2
|
||||
expect(first(award_button_selector)).to have_content '2'
|
||||
end
|
||||
expect(page.find(tooltip_selector)).to have_content("John and you reacted with :#{AwardEmoji::THUMBS_UP}:")
|
||||
|
|
@ -491,7 +491,7 @@ RSpec.shared_examples 'work items award emoji' do
|
|||
select_emoji
|
||||
|
||||
page.within(award_section_selector) do
|
||||
# As the user2 has already awarded the `:thumbs_up:` emoji, the emoji count will be 2
|
||||
# As the user2 has already awarded the `:thumbsup:` emoji, the emoji count will be 2
|
||||
expect(first(award_button_selector)).to have_content '2'
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -744,51 +744,51 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
|
|||
end
|
||||
end
|
||||
|
||||
context 'user searches by "thumbs_up" reaction' do
|
||||
context 'user searches by "thumbsup" reaction' do
|
||||
let(:params) { { my_reaction_emoji: AwardEmoji::THUMBS_UP } }
|
||||
|
||||
it 'returns items that the user thumbs_up to' do
|
||||
it 'returns items that the user thumbsup to' do
|
||||
expect(items).to contain_exactly(item1)
|
||||
end
|
||||
|
||||
context 'using NOT' do
|
||||
let(:params) { { not: { my_reaction_emoji: AwardEmoji::THUMBS_UP } } }
|
||||
|
||||
it 'returns items that the user did not thumbs_up to' do
|
||||
it 'returns items that the user did not thumbsup to' do
|
||||
expect(items).to contain_exactly(item2, item3, item4, item5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'user2 searches by "thumbs_up" reaction' do
|
||||
context 'user2 searches by "thumbsup" reaction' do
|
||||
let(:search_user) { user2 }
|
||||
|
||||
let(:params) { { my_reaction_emoji: AwardEmoji::THUMBS_UP } }
|
||||
|
||||
it 'returns items that the user2 thumbs_up to' do
|
||||
it 'returns items that the user2 thumbsup to' do
|
||||
expect(items).to contain_exactly(item2)
|
||||
end
|
||||
|
||||
context 'using NOT' do
|
||||
let(:params) { { not: { my_reaction_emoji: AwardEmoji::THUMBS_UP } } }
|
||||
|
||||
it 'returns items that the user2 thumbs_up to' do
|
||||
it 'returns items that the user2 thumbsup to' do
|
||||
expect(items).to contain_exactly(item3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'user searches by "thumbs_down" reaction' do
|
||||
context 'user searches by "thumbsdown" reaction' do
|
||||
let(:params) { { my_reaction_emoji: AwardEmoji::THUMBS_DOWN } }
|
||||
|
||||
it 'returns items that the user thumbs_down to' do
|
||||
it 'returns items that the user thumbsdown to' do
|
||||
expect(items).to contain_exactly(item3)
|
||||
end
|
||||
|
||||
context 'using NOT' do
|
||||
let(:params) { { not: { my_reaction_emoji: AwardEmoji::THUMBS_DOWN } } }
|
||||
|
||||
it 'returns items that the user thumbs_down to' do
|
||||
it 'returns items that the user thumbsdown to' do
|
||||
expect(items).to contain_exactly(item1, item2, item4, item5)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'a model with paper trail configured' do
|
||||
describe 'paper_trail' do
|
||||
subject(:object) { create(factory) } # rubocop:disable Rails/SaveBang -- False positive, this is a factory bot method.
|
||||
|
||||
# making duplication of object, and it does not reload when object updated
|
||||
let(:new_object_before_change) { object }
|
||||
|
||||
shared_examples 'saving additional properties' do
|
||||
it 'saves additional properties' do
|
||||
version = object.versions.last
|
||||
|
||||
additional_properties.each do |attr, value|
|
||||
expect(version[attr]).to eq(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'on creation' do
|
||||
it 'contains version with 1' do
|
||||
expect(object.versions.length).to be 1
|
||||
end
|
||||
|
||||
it 'create version has nil object' do
|
||||
expect(object.versions[0].reify).to be_nil
|
||||
end
|
||||
|
||||
it_behaves_like 'saving additional properties'
|
||||
end
|
||||
|
||||
context 'on update' do
|
||||
before do
|
||||
object.update!(attributes_to_update)
|
||||
end
|
||||
|
||||
it 'contains version with 2' do
|
||||
expect(object.versions.length).to be 2
|
||||
end
|
||||
|
||||
it 'contains version before update' do
|
||||
reified_object = object.versions.last.reify
|
||||
|
||||
expect(reified_object).to eql(object)
|
||||
end
|
||||
|
||||
it_behaves_like 'saving additional properties'
|
||||
end
|
||||
|
||||
context 'on destroy' do
|
||||
before do
|
||||
object.destroy!
|
||||
end
|
||||
|
||||
it 'contains version with 2' do
|
||||
expect(object.versions.length).to be 2
|
||||
end
|
||||
|
||||
it 'contains version before destroy' do
|
||||
reified_object = object.versions.last.reify
|
||||
|
||||
expect(reified_object).to eql(object)
|
||||
end
|
||||
|
||||
it_behaves_like 'saving additional properties'
|
||||
end
|
||||
|
||||
context 'on delete' do
|
||||
before do
|
||||
object.delete
|
||||
end
|
||||
|
||||
it 'contains version with 1' do
|
||||
expect(object.versions.length).to be 1
|
||||
end
|
||||
|
||||
it 'does not contain version before delete' do
|
||||
reified_object = object.versions.last.reify
|
||||
|
||||
expect(reified_object).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'on touch' do
|
||||
before do
|
||||
object.touch
|
||||
end
|
||||
|
||||
it 'contains version with 2' do
|
||||
expect(object.versions.length).to be 2
|
||||
end
|
||||
|
||||
it 'contains version before touch' do
|
||||
reified_object = object.versions.last.reify
|
||||
|
||||
expect(reified_object).to eql(new_object_before_change)
|
||||
end
|
||||
|
||||
it_behaves_like 'saving additional properties'
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue