From a2f61bec6e65f9dfc84ffc6c76a879dcfce13df1 Mon Sep 17 00:00:00 2001
From: Jan Provaznik <jprovaznik@gitlab.com>
Date: Thu, 16 Feb 2023 10:10:17 +0000
Subject: [PATCH] Add widget definitions table

Moves definitions of mapping widgets for each work item type into DB.

Changelog: added
---
 app/finders/work_items/work_items_finder.rb   |   2 +-
 .../mutations/work_items/widgetable.rb        |   2 +-
 app/graphql/resolvers/work_items_resolver.rb  |   2 +-
 .../types/work_items/widget_type_enum.rb      |   4 +-
 app/models/work_items/type.rb                 |  61 +-------
 app/models/work_items/widget_definition.rb    |  54 +++++++
 db/docs/work_item_types.yml                   |   4 +-
 db/docs/work_item_widget_definitions.yml      |  12 ++
 .../20230129094140_add_widget_definitions.rb  |  24 +++
 ...30129154126_add_widget_def_namespace_fk.rb |  16 ++
 ...154202_add_widget_def_work_item_type_fk.rb |  15 ++
 ...9154819_add_widgets_for_work_item_types.rb | 143 ++++++++++++++++++
 db/schema_migrations/20230129094140           |   1 +
 db/schema_migrations/20230129154126           |   1 +
 db/schema_migrations/20230129154202           |   1 +
 db/schema_migrations/20230129154819           |   1 +
 db/structure.sql                              |  36 +++++
 doc/development/work_items_widgets.md         |  25 +++
 ee/app/models/ee/work_items/type.rb           |  40 -----
 ...type_spec.rb => widget_definition_spec.rb} |   2 +-
 .../mutations/work_items/update_spec.rb       |   8 +-
 .../work_items/update_service_spec.rb         |   2 +-
 .../work_items/base_type_importer.rb          | 102 +++++++++++++
 .../create_base_work_item_types_spec.rb       |   2 +-
 .../create_base_work_item_types_spec.rb       |   2 +-
 .../work_items/widget_definitions.rb          |  11 ++
 .../users/participants_resolver_spec.rb       |   4 +-
 .../resolvers/work_items_resolver_spec.rb     |   4 +-
 .../work_items/base_type_importer_spec.rb     |   2 +-
 spec/models/work_items/type_spec.rb           |  51 +++----
 .../work_items/widget_definition_spec.rb      |  92 +++++++++++
 spec/policies/issue_policy_spec.rb            |  16 +-
 spec/policies/note_policy_spec.rb             |   4 +-
 spec/requests/api/discussions_spec.rb         |   3 +-
 .../mutations/notes/create/note_spec.rb       |   4 +-
 .../graphql/mutations/notes/destroy_spec.rb   |   3 +-
 .../mutations/notes/update/note_spec.rb       |   3 +-
 .../mutations/work_items/update_spec.rb       |   9 +-
 spec/requests/api/graphql/notes/note_spec.rb  |   3 +-
 .../notes/synthetic_note_resolver_spec.rb     |   3 +-
 spec/requests/api/notes_spec.rb               |   3 +-
 .../issuable/discussions_list_service_spec.rb |   3 +-
 .../work_items/create_service_spec.rb         |   2 +-
 spec/support/db_cleaner.rb                    |   2 +-
 .../work_item_base_types_importer.rb          |  60 +++++++-
 45 files changed, 668 insertions(+), 176 deletions(-)
 create mode 100644 app/models/work_items/widget_definition.rb
 create mode 100644 db/docs/work_item_widget_definitions.yml
 create mode 100644 db/migrate/20230129094140_add_widget_definitions.rb
 create mode 100644 db/migrate/20230129154126_add_widget_def_namespace_fk.rb
 create mode 100644 db/migrate/20230129154202_add_widget_def_work_item_type_fk.rb
 create mode 100644 db/migrate/20230129154819_add_widgets_for_work_item_types.rb
 create mode 100644 db/schema_migrations/20230129094140
 create mode 100644 db/schema_migrations/20230129154126
 create mode 100644 db/schema_migrations/20230129154202
 create mode 100644 db/schema_migrations/20230129154819
 delete mode 100644 ee/app/models/ee/work_items/type.rb
 rename ee/spec/models/ee/work_items/{type_spec.rb => widget_definition_spec.rb} (91%)
 create mode 100644 spec/factories/work_items/widget_definitions.rb
 create mode 100644 spec/models/work_items/widget_definition_spec.rb

diff --git a/app/finders/work_items/work_items_finder.rb b/app/finders/work_items/work_items_finder.rb
index 62cca06bf5e79..07010adcf0d92 100644
--- a/app/finders/work_items/work_items_finder.rb
+++ b/app/finders/work_items/work_items_finder.rb
@@ -19,7 +19,7 @@ def filter_items(items)
     end
 
     def by_widgets(items)
-      WorkItems::Type.available_widgets.each do |widget_class|
+      WorkItems::WidgetDefinition.available_widgets.each do |widget_class|
         widget_filter = widget_filter_for(widget_class)
 
         next unless widget_filter
diff --git a/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb b/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb
index 508e1627032fd..3f32cd51ae76b 100644
--- a/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb
+++ b/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb
@@ -7,7 +7,7 @@ module Widgetable
 
       def extract_widget_params!(work_item_type, attributes)
         # Get the list of widgets for the work item's type to extract only the supported attributes
-        widget_keys = ::WorkItems::Type.available_widgets.map(&:api_symbol)
+        widget_keys = ::WorkItems::WidgetDefinition.available_widgets.map(&:api_symbol)
         widget_params = attributes.extract!(*widget_keys)
 
         not_supported_keys = widget_params.keys - work_item_type.widgets.map(&:api_symbol)
diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
index 42ec41d9d8d8e..0c9aac802749a 100644
--- a/app/graphql/resolvers/work_items_resolver.rb
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -43,7 +43,7 @@ def preloads
       {
         work_item_type: :work_item_type,
         web_url: { project: { namespace: :route } },
-        widgets: :work_item_type
+        widgets: { work_item_type: :enabled_widget_definitions }
       }
     end
 
diff --git a/app/graphql/types/work_items/widget_type_enum.rb b/app/graphql/types/work_items/widget_type_enum.rb
index 4e5933bff869b..2ad951d421b46 100644
--- a/app/graphql/types/work_items/widget_type_enum.rb
+++ b/app/graphql/types/work_items/widget_type_enum.rb
@@ -6,8 +6,8 @@ class WidgetTypeEnum < BaseEnum
       graphql_name 'WorkItemWidgetType'
       description 'Type of a work item widget'
 
-      ::WorkItems::Type.available_widgets.each do |widget|
-        value widget.type.to_s.upcase, value: widget.type, description: "#{widget.type.to_s.titleize} widget."
+      ::WorkItems::WidgetDefinition.widget_classes.each do |cls|
+        value cls.type.to_s.upcase, value: cls.type, description: "#{cls.type.to_s.titleize} widget."
       end
     end
   end
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index 258a86d73164b..6a619dbab21b0 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -35,56 +35,6 @@ class Type < ApplicationRecord
       key_result: { name: TYPE_NAMES[:key_result], icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only
     }.freeze
 
-    WIDGETS_FOR_TYPE = {
-      issue: [
-        Widgets::Assignees,
-        Widgets::Labels,
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::StartAndDueDate,
-        Widgets::Milestone,
-        Widgets::Notes
-      ],
-      incident: [
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::Notes
-      ],
-      test_case: [
-        Widgets::Description,
-        Widgets::Notes
-      ],
-      requirement: [
-        Widgets::Description,
-        Widgets::Notes
-      ],
-      task: [
-        Widgets::Assignees,
-        Widgets::Labels,
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::StartAndDueDate,
-        Widgets::Milestone,
-        Widgets::Notes
-      ],
-      objective: [
-        Widgets::Assignees,
-        Widgets::Labels,
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::Milestone,
-        Widgets::Notes
-      ],
-      key_result: [
-        Widgets::Assignees,
-        Widgets::Labels,
-        Widgets::Description,
-        Widgets::Hierarchy,
-        Widgets::StartAndDueDate,
-        Widgets::Notes
-      ]
-    }.freeze
-
     # A list of types user can change between - both original and new
     # type must be included in this list. This is needed for legacy issues
     # where it's possible to switch between issue and incident.
@@ -98,6 +48,9 @@ class Type < ApplicationRecord
 
     belongs_to :namespace, optional: true
     has_many :work_items, class_name: 'Issue', foreign_key: :work_item_type_id, inverse_of: :work_item_type
+    has_many :widget_definitions, foreign_key: :work_item_type_id, inverse_of: :work_item_type
+    has_many :enabled_widget_definitions, -> { where(disabled: false) }, foreign_key: :work_item_type_id,
+      inverse_of: :work_item_type, class_name: 'WorkItems::WidgetDefinition'
 
     before_validation :strip_whitespace
 
@@ -112,10 +65,6 @@ class Type < ApplicationRecord
     scope :order_by_name_asc, -> { order(arel_table[:name].lower.asc) }
     scope :by_type, ->(base_type) { where(base_type: base_type) }
 
-    def self.available_widgets
-      WIDGETS_FOR_TYPE.values.flatten.uniq
-    end
-
     def self.default_by_type(type)
       found_type = find_by(namespace_id: nil, base_type: type)
       return found_type if found_type
@@ -138,7 +87,7 @@ def default?
     end
 
     def widgets
-      WIDGETS_FOR_TYPE[base_type.to_sym]
+      enabled_widget_definitions.filter_map(&:widget_class)
     end
 
     def supports_assignee?
@@ -156,5 +105,3 @@ def strip_whitespace
     end
   end
 end
-
-WorkItems::Type.prepend_mod
diff --git a/app/models/work_items/widget_definition.rb b/app/models/work_items/widget_definition.rb
new file mode 100644
index 0000000000000..5d4414e95d8a6
--- /dev/null
+++ b/app/models/work_items/widget_definition.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module WorkItems
+  class WidgetDefinition < ApplicationRecord
+    self.table_name = 'work_item_widget_definitions'
+
+    belongs_to :namespace, optional: true
+    belongs_to :work_item_type, class_name: 'WorkItems::Type', inverse_of: :widget_definitions
+
+    validates :name, presence: true
+    validates :name, uniqueness: { case_sensitive: false, scope: [:namespace_id, :work_item_type_id] }
+    validates :name, length: { maximum: 255 }
+
+    scope :enabled, -> { where(disabled: false) }
+    scope :global, -> { where(namespace: nil) }
+
+    enum widget_type: {
+      assignees: 0,
+      description: 1,
+      hierarchy: 2,
+      labels: 3,
+      milestone: 4,
+      notes: 5,
+      start_and_due_date: 6,
+      health_status: 7, # EE-only
+      weight: 8, # EE-only
+      iteration: 9, # EE-only
+      progress: 10, # EE-only
+      status: 11, # EE-only
+      requirement_legacy: 12, # EE-only
+      test_reports: 13 # EE-only
+    }
+
+    def self.available_widgets
+      global.enabled.filter_map(&:widget_class).uniq
+    end
+
+    def self.widget_classes
+      WorkItems::WidgetDefinition.widget_types.keys.filter_map do |type|
+        WorkItems::Widgets.const_get(type.camelize, false)
+      rescue NameError
+        nil
+      end
+    end
+
+    def widget_class
+      return unless widget_type
+
+      WorkItems::Widgets.const_get(widget_type.camelize, false)
+    rescue NameError
+      nil
+    end
+  end
+end
diff --git a/db/docs/work_item_types.yml b/db/docs/work_item_types.yml
index 21ec69da15256..37d2c47de252c 100644
--- a/db/docs/work_item_types.yml
+++ b/db/docs/work_item_types.yml
@@ -1,10 +1,12 @@
 ---
 table_name: work_item_types
 classes:
+- AddWidgetsForWorkItemTypes::WorkItemType
 - WorkItems::Type
 feature_categories:
 - team_planning
-description: The work item type related to an issue. Currently one of a predefined set but in future will support custom types.
+description: The work item type related to an issue. Currently one of a predefined
+  set but in future will support custom types.
 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55705
 milestone: '14.2'
 gitlab_schema: gitlab_main
diff --git a/db/docs/work_item_widget_definitions.yml b/db/docs/work_item_widget_definitions.yml
new file mode 100644
index 0000000000000..59cbca1490895
--- /dev/null
+++ b/db/docs/work_item_widget_definitions.yml
@@ -0,0 +1,12 @@
+---
+table_name: work_item_widget_definitions
+classes:
+- AddWidgetsForWorkItemTypes::WidgetDefinition
+- WorkItems::WidgetDefinition
+feature_categories:
+- team_planning
+description: Mapping of widgets for each work item type. Currently one of a predefined
+  set but in future will support custom types.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107582
+milestone: '15.9'
+gitlab_schema: gitlab_main
diff --git a/db/migrate/20230129094140_add_widget_definitions.rb b/db/migrate/20230129094140_add_widget_definitions.rb
new file mode 100644
index 0000000000000..09816f7386d26
--- /dev/null
+++ b/db/migrate/20230129094140_add_widget_definitions.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddWidgetDefinitions < Gitlab::Database::Migration[2.1]
+  UNIQUE_INDEX_NAME = 'index_work_item_widget_definitions_on_namespace_type_and_name'
+  UNIQUE_DEFAULT_NAMESPACE_INDEX_NAME = 'index_work_item_widget_definitions_on_default_witype_and_name'
+
+  def up
+    create_table :work_item_widget_definitions do |t|
+      t.references :namespace, index: false
+      t.references :work_item_type, index: true, null: false
+      t.integer :widget_type, null: false, limit: 2
+      t.boolean :disabled, default: false
+      t.text :name, limit: 255
+
+      t.index [:namespace_id, :work_item_type_id, :name], unique: true, name: UNIQUE_INDEX_NAME
+      t.index [:work_item_type_id, :name], where: "namespace_id is NULL",
+        unique: true, name: UNIQUE_DEFAULT_NAMESPACE_INDEX_NAME
+    end
+  end
+
+  def down
+    drop_table :work_item_widget_definitions
+  end
+end
diff --git a/db/migrate/20230129154126_add_widget_def_namespace_fk.rb b/db/migrate/20230129154126_add_widget_def_namespace_fk.rb
new file mode 100644
index 0000000000000..cf3f83fdbfef5
--- /dev/null
+++ b/db/migrate/20230129154126_add_widget_def_namespace_fk.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddWidgetDefNamespaceFk < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :work_item_widget_definitions, :work_item_types,
+      column: :work_item_type_id, on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :work_item_widget_definitions, column: :work_item_type_id
+    end
+  end
+end
diff --git a/db/migrate/20230129154202_add_widget_def_work_item_type_fk.rb b/db/migrate/20230129154202_add_widget_def_work_item_type_fk.rb
new file mode 100644
index 0000000000000..530f2c78198ca
--- /dev/null
+++ b/db/migrate/20230129154202_add_widget_def_work_item_type_fk.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddWidgetDefWorkItemTypeFk < Gitlab::Database::Migration[2.1]
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :work_item_widget_definitions, :namespaces, column: :namespace_id, on_delete: :cascade
+  end
+
+  def down
+    with_lock_retries do
+      remove_foreign_key :work_item_widget_definitions, column: :namespace_id
+    end
+  end
+end
diff --git a/db/migrate/20230129154819_add_widgets_for_work_item_types.rb b/db/migrate/20230129154819_add_widgets_for_work_item_types.rb
new file mode 100644
index 0000000000000..b936ea2e409bd
--- /dev/null
+++ b/db/migrate/20230129154819_add_widgets_for_work_item_types.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+class AddWidgetsForWorkItemTypes < Gitlab::Database::Migration[2.1]
+  class WorkItemType < MigrationRecord
+    self.table_name = 'work_item_types'
+  end
+
+  class WidgetDefinition < MigrationRecord
+    self.table_name = 'work_item_widget_definitions'
+  end
+
+  restrict_gitlab_migration gitlab_schema: :gitlab_main
+  disable_ddl_transaction!
+
+  def up
+    widget_names = {
+      assignees: 'Assignees',
+      labels: 'Labels',
+      description: 'Description',
+      hierarchy: 'Hierarchy',
+      start_and_due_date: 'Start and due date',
+      milestone: 'Milestone',
+      notes: 'Notes',
+      iteration: 'Iteration',
+      weight: 'Weight',
+      health_status: 'Health status',
+      progress: 'Progress',
+      status: 'Status',
+      requirement_legacy: 'Requirement legacy',
+      test_reports: 'Test reports'
+    }
+
+    widgets_for_type = {
+      'Issue' => [
+        :assignees,
+        :labels,
+        :description,
+        :hierarchy,
+        :start_and_due_date,
+        :milestone,
+        :notes,
+        # EE widgets
+        :iteration,
+        :weight,
+        :health_status
+      ],
+      'Incident' => [
+        :description,
+        :hierarchy,
+        :notes
+      ],
+      'Test Case' => [
+        :description,
+        :notes
+      ],
+      'Requirement' => [
+        :description,
+        :notes,
+        :status,
+        :requirement_legacy,
+        :test_reports
+      ],
+      'Task' => [
+        :assignees,
+        :labels,
+        :description,
+        :hierarchy,
+        :start_and_due_date,
+        :milestone,
+        :notes,
+        :iteration,
+        :weight
+      ],
+      'Objective' => [
+        :assignees,
+        :labels,
+        :description,
+        :hierarchy,
+        :milestone,
+        :notes,
+        :health_status,
+        :progress
+      ],
+      'Key Result' => [
+        :assignees,
+        :labels,
+        :description,
+        :hierarchy,
+        :start_and_due_date,
+        :notes,
+        :health_status,
+        :progress
+      ]
+    }
+
+    widgets_enum = {
+      assignees: 0,
+      description: 1,
+      hierarchy: 2,
+      labels: 3,
+      milestone: 4,
+      notes: 5,
+      start_and_due_date: 6,
+      health_status: 7, # EE-only
+      weight: 8, # EE-only
+      iteration: 9, # EE-only
+      progress: 10, # EE-only
+      status: 11, # EE-only
+      requirement_legacy: 12, # EE-only
+      test_reports: 13
+    }
+
+    widgets = []
+    widgets_for_type.each do |type_name, widget_syms|
+      type = WorkItemType.find_by_name_and_namespace_id(type_name, nil)
+
+      unless type
+        Gitlab::AppLogger.warn("type #{type_name} is missing, not adding widgets")
+
+        next
+      end
+
+      widgets += widget_syms.map do |widget_sym|
+        {
+          work_item_type_id: type.id,
+          name: widget_names[widget_sym],
+          widget_type: widgets_enum[widget_sym]
+        }
+      end
+    end
+
+    return if widgets.empty?
+
+    WidgetDefinition.upsert_all(
+      widgets,
+      unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
+    )
+  end
+
+  def down
+    WidgetDefinition.delete_all
+  end
+end
diff --git a/db/schema_migrations/20230129094140 b/db/schema_migrations/20230129094140
new file mode 100644
index 0000000000000..1e8543ebfd632
--- /dev/null
+++ b/db/schema_migrations/20230129094140
@@ -0,0 +1 @@
+e14187450e98a7ea699beecbc41733f6a524a1612cc0acbea3aa5b75a4f7be49
\ No newline at end of file
diff --git a/db/schema_migrations/20230129154126 b/db/schema_migrations/20230129154126
new file mode 100644
index 0000000000000..f7cecf227d98f
--- /dev/null
+++ b/db/schema_migrations/20230129154126
@@ -0,0 +1 @@
+685bc851446d875d72f5712533b13baea90f6f3bc82d383f1fff10859c341e49
\ No newline at end of file
diff --git a/db/schema_migrations/20230129154202 b/db/schema_migrations/20230129154202
new file mode 100644
index 0000000000000..6af55471b918f
--- /dev/null
+++ b/db/schema_migrations/20230129154202
@@ -0,0 +1 @@
+a6146c49a1930b1cad7f56e6c3a8dbd433bd605d965a083f2dea3ab25261b94d
\ No newline at end of file
diff --git a/db/schema_migrations/20230129154819 b/db/schema_migrations/20230129154819
new file mode 100644
index 0000000000000..42bae9a9e2020
--- /dev/null
+++ b/db/schema_migrations/20230129154819
@@ -0,0 +1 @@
+c8d2063f94253e79ff3c707e2af75a963863bcf601993c81e3043bbc5c2ae21b
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e9bdeb7201fac..2faa6fb4d5752 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -23987,6 +23987,25 @@ CREATE SEQUENCE work_item_types_id_seq
 
 ALTER SEQUENCE work_item_types_id_seq OWNED BY work_item_types.id;
 
+CREATE TABLE work_item_widget_definitions (
+    id bigint NOT NULL,
+    namespace_id bigint,
+    work_item_type_id bigint NOT NULL,
+    widget_type smallint NOT NULL,
+    disabled boolean DEFAULT false,
+    name text,
+    CONSTRAINT check_050f2e2328 CHECK ((char_length(name) <= 255))
+);
+
+CREATE SEQUENCE work_item_widget_definitions_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE work_item_widget_definitions_id_seq OWNED BY work_item_widget_definitions.id;
+
 CREATE TABLE x509_certificates (
     id bigint NOT NULL,
     created_at timestamp with time zone NOT NULL,
@@ -25194,6 +25213,8 @@ ALTER TABLE ONLY work_item_parent_links ALTER COLUMN id SET DEFAULT nextval('wor
 
 ALTER TABLE ONLY work_item_types ALTER COLUMN id SET DEFAULT nextval('work_item_types_id_seq'::regclass);
 
+ALTER TABLE ONLY work_item_widget_definitions ALTER COLUMN id SET DEFAULT nextval('work_item_widget_definitions_id_seq'::regclass);
+
 ALTER TABLE ONLY x509_certificates ALTER COLUMN id SET DEFAULT nextval('x509_certificates_id_seq'::regclass);
 
 ALTER TABLE ONLY x509_commit_signatures ALTER COLUMN id SET DEFAULT nextval('x509_commit_signatures_id_seq'::regclass);
@@ -27674,6 +27695,9 @@ ALTER TABLE ONLY work_item_progresses
 ALTER TABLE ONLY work_item_types
     ADD CONSTRAINT work_item_types_pkey PRIMARY KEY (id);
 
+ALTER TABLE ONLY work_item_widget_definitions
+    ADD CONSTRAINT work_item_widget_definitions_pkey PRIMARY KEY (id);
+
 ALTER TABLE ONLY x509_certificates
     ADD CONSTRAINT x509_certificates_pkey PRIMARY KEY (id);
 
@@ -32149,6 +32173,12 @@ CREATE UNIQUE INDEX index_work_item_parent_links_on_work_item_id ON work_item_pa
 
 CREATE INDEX index_work_item_parent_links_on_work_item_parent_id ON work_item_parent_links USING btree (work_item_parent_id);
 
+CREATE UNIQUE INDEX index_work_item_widget_definitions_on_default_witype_and_name ON work_item_widget_definitions USING btree (work_item_type_id, name) WHERE (namespace_id IS NULL);
+
+CREATE UNIQUE INDEX index_work_item_widget_definitions_on_namespace_type_and_name ON work_item_widget_definitions USING btree (namespace_id, work_item_type_id, name);
+
+CREATE INDEX index_work_item_widget_definitions_on_work_item_type_id ON work_item_widget_definitions USING btree (work_item_type_id);
+
 CREATE INDEX index_x509_certificates_on_subject_key_identifier ON x509_certificates USING btree (subject_key_identifier);
 
 CREATE INDEX index_x509_certificates_on_x509_issuer_id ON x509_certificates USING btree (x509_issuer_id);
@@ -34048,6 +34078,9 @@ ALTER TABLE ONLY user_achievements
 ALTER TABLE ONLY merge_requests
     ADD CONSTRAINT fk_6149611a04 FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL;
 
+ALTER TABLE ONLY work_item_widget_definitions
+    ADD CONSTRAINT fk_61bfa96db5 FOREIGN KEY (work_item_type_id) REFERENCES work_item_types(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY deployment_approvals
     ADD CONSTRAINT fk_61cdbdc5b9 FOREIGN KEY (approval_rule_id) REFERENCES protected_environment_approval_rules(id) ON DELETE SET NULL;
 
@@ -34591,6 +34624,9 @@ ALTER TABLE ONLY pages_domains
 ALTER TABLE ONLY merge_requests_compliance_violations
     ADD CONSTRAINT fk_ec881c1c6f FOREIGN KEY (violating_user_id) REFERENCES users(id) ON DELETE CASCADE;
 
+ALTER TABLE ONLY work_item_widget_definitions
+    ADD CONSTRAINT fk_ecf57512f7 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
 ALTER TABLE ONLY events
     ADD CONSTRAINT fk_edfd187b6f FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE;
 
diff --git a/doc/development/work_items_widgets.md b/doc/development/work_items_widgets.md
index ba15a3f0163d1..5b9602595bb0a 100644
--- a/doc/development/work_items_widgets.md
+++ b/doc/development/work_items_widgets.md
@@ -112,3 +112,28 @@ for work items widgets development:
 
 - `ee/app/assets/javascripts/boards/components/assignee_select.vue`
 - `ee/app/assets/javascripts/boards/components/milestone_select.vue`
+
+## Mapping widgets to work item types
+
+All Work Item types share the same pool of predefined widgets and are customized by which widgets are active on a specific type. Because we plan to allow users to create new Work Item types and define a set of widgets for them, mapping of widgets for each Work Item type is stored in database. Mapping of widgets is stored in widget_definitions table and it can be used for defining widgets both for default Work Item types and also in future for custom types. More details about expected database table structure can be found in [this issue description](https://gitlab.com/gitlab-org/gitlab/-/issues/374092).
+
+### Adding new widget to a work item type
+
+Because information about what widgets are assigned to each work item type is stored in database, adding new widget to a work item type needs to be done through a database migration. Also widgets importer (`lib/gitlab/database_importers/work_items/widgets_importer.rb`) should be updated.
+
+### Structure of widget definitions table
+
+Each record in the table defines mapping of a widget to a work item type. Currently only "global" definitions (definitions with NULL namespace_id) are used. In next iterations we plan to allow customization of these mappings. For example table below defines that:
+
+- Weight widget is enabled for work item types 0 and 1
+- in namespace 1 Weight widget is renamed to MyWeight. When user renames widget's name, it makes sense to rename all widget mappings in the namespace - because `name` attribute is denormalized, we have to create namespaced mappings for all work item types for this widget type.
+- Weight widget can be disabled for specific work item types (in namespace 3 it's disabled for work item type 0, while still left enabled for work item type 1)
+
+| ID | Namespace_id | Work_item_type_id | Widget_type_enum | Position | Name      | Disabled |
+|----| ------------ | ----------------- |----------------- |--------- |---------- |-------|
+| 1  |              | 0                 | 1                | 1        | Weight    | false |
+| 2  |              | 1                 | 1                | 1        | Weight    | false |
+| 3  | 1            | 0                 | 1                | 0        | MyWeight  | false |
+| 4  | 1            | 1                 | 1                | 0        | MyWeight  | false |
+| 5  | 2            | 0                 | 1                | 1        | Other Weight | false |
+| 6  | 3            | 0                 | 1                | 1        | Weight | true |
diff --git a/ee/app/models/ee/work_items/type.rb b/ee/app/models/ee/work_items/type.rb
deleted file mode 100644
index 913e4ce4cc201..0000000000000
--- a/ee/app/models/ee/work_items/type.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module EE
-  module WorkItems
-    module Type
-      extend ActiveSupport::Concern
-      extend ::Gitlab::Utils::Override
-
-      EE_WIDGETS_FOR_TYPE = {
-        issue: [
-          ::WorkItems::Widgets::Iteration,
-          ::WorkItems::Widgets::Weight,
-          ::WorkItems::Widgets::HealthStatus
-        ],
-        requirement: [
-          ::WorkItems::Widgets::Status,
-          ::WorkItems::Widgets::RequirementLegacy,
-          ::WorkItems::Widgets::TestReports
-        ],
-        task: [::WorkItems::Widgets::Iteration, ::WorkItems::Widgets::Weight],
-        objective: [::WorkItems::Widgets::HealthStatus, ::WorkItems::Widgets::Progress],
-        key_result: [::WorkItems::Widgets::HealthStatus, ::WorkItems::Widgets::Progress]
-      }.freeze
-
-      class_methods do
-        extend ::Gitlab::Utils::Override
-
-        override :available_widgets
-        def available_widgets
-          [*EE_WIDGETS_FOR_TYPE.values.flatten.uniq, *super]
-        end
-      end
-
-      override :widgets
-      def widgets
-        [*EE_WIDGETS_FOR_TYPE[base_type.to_sym], *super]
-      end
-    end
-  end
-end
diff --git a/ee/spec/models/ee/work_items/type_spec.rb b/ee/spec/models/ee/work_items/widget_definition_spec.rb
similarity index 91%
rename from ee/spec/models/ee/work_items/type_spec.rb
rename to ee/spec/models/ee/work_items/widget_definition_spec.rb
index c0ef26aba6f8e..ab6a1c3d612b5 100644
--- a/ee/spec/models/ee/work_items/type_spec.rb
+++ b/ee/spec/models/ee/work_items/widget_definition_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe WorkItems::Type do
+RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
   describe '.available_widgets' do
     subject { described_class.available_widgets }
 
diff --git a/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 550d88ef1e15f..37d6d4ffa6cca 100644
--- a/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -237,7 +237,8 @@
             end
 
             before do
-              stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+              WorkItems::Type.default_by_type(:task).widget_definitions
+                .find_by_widget_type(:weight).update!(disabled: true)
             end
 
             it_behaves_like 'work item is not updated'
@@ -505,14 +506,15 @@ def work_item_status
           end
 
           context 'when the work item type does not support the health status widget' do
-            let_it_be(:work_item) { create(:work_item, :task, project: project) }
+            let_it_be(:work_item) { create(:work_item, :issue, project: project) }
 
             let(:input) do
               { 'descriptionWidget' => { 'description' => "Updating health status.\n/health_status on_track" } }
             end
 
             before do
-              stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+              WorkItems::Type.default_by_type(:issue).widget_definitions
+                .find_by_widget_type(:health_status).update!(disabled: true)
             end
 
             it_behaves_like 'work item is not updated'
diff --git a/ee/spec/services/work_items/update_service_spec.rb b/ee/spec/services/work_items/update_service_spec.rb
index 34db1b447ce3c..87a2d4b109ac2 100644
--- a/ee/spec/services/work_items/update_service_spec.rb
+++ b/ee/spec/services/work_items/update_service_spec.rb
@@ -6,7 +6,7 @@
   let_it_be(:developer) { create(:user) }
   let_it_be(:group) { create(:group) }
   let_it_be(:project) { create(:project, group: group).tap { |proj| proj.add_developer(developer) } }
-  let_it_be(:work_item) { create(:work_item, project: project) }
+  let_it_be_with_refind(:work_item) { create(:work_item, project: project) }
 
   let(:spam_params) { double }
   let(:current_user) { developer }
diff --git a/lib/gitlab/database_importers/work_items/base_type_importer.rb b/lib/gitlab/database_importers/work_items/base_type_importer.rb
index 1e29ae7761bd1..9796a5905e35c 100644
--- a/lib/gitlab/database_importers/work_items/base_type_importer.rb
+++ b/lib/gitlab/database_importers/work_items/base_type_importer.rb
@@ -4,6 +4,85 @@ module Gitlab
   module DatabaseImporters
     module WorkItems
       module BaseTypeImporter
+        WIDGET_NAMES = {
+          assignees: 'Assignees',
+          labels: 'Labels',
+          description: 'Description',
+          hierarchy: 'Hierarchy',
+          start_and_due_date: 'Start and due date',
+          milestone: 'Milestone',
+          notes: 'Notes',
+          iteration: 'Iteration',
+          weight: 'Weight',
+          health_status: 'Health status',
+          progress: 'Progress',
+          status: 'Status',
+          requirement_legacy: 'Requirement legacy',
+          test_reports: 'Test reports'
+        }.freeze
+
+        WIDGETS_FOR_TYPE = {
+          issue: [
+            :assignees,
+            :labels,
+            :description,
+            :hierarchy,
+            :start_and_due_date,
+            :milestone,
+            :notes,
+            :iteration,
+            :weight,
+            :health_status
+          ],
+          incident: [
+            :description,
+            :hierarchy,
+            :notes
+          ],
+          test_case: [
+            :description,
+            :notes
+          ],
+          requirement: [
+            :description,
+            :notes,
+            :status,
+            :requirement_legacy,
+            :test_reports
+          ],
+          task: [
+            :assignees,
+            :labels,
+            :description,
+            :hierarchy,
+            :start_and_due_date,
+            :milestone,
+            :notes,
+            :iteration,
+            :weight
+          ],
+          objective: [
+            :assignees,
+            :labels,
+            :description,
+            :hierarchy,
+            :milestone,
+            :notes,
+            :health_status,
+            :progress
+          ],
+          key_result: [
+            :assignees,
+            :labels,
+            :description,
+            :hierarchy,
+            :start_and_due_date,
+            :notes,
+            :health_status,
+            :progress
+          ]
+        }.freeze
+
         def self.upsert_types
           current_time = Time.current
 
@@ -16,6 +95,29 @@ def self.upsert_types
             base_types,
             unique_by: :idx_work_item_types_on_namespace_id_and_name_null_namespace
           )
+
+          upsert_widgets
+        end
+
+        def self.upsert_widgets
+          type_ids_by_name = ::WorkItems::Type.default.pluck(:name, :id).to_h # rubocop: disable CodeReuse/ActiveRecord
+
+          widgets = WIDGETS_FOR_TYPE.flat_map do |type_sym, widget_syms|
+            type_name = ::WorkItems::Type::TYPE_NAMES[type_sym]
+
+            widget_syms.map do |widget_sym|
+              {
+                work_item_type_id: type_ids_by_name[type_name],
+                name: WIDGET_NAMES[widget_sym],
+                widget_type: ::WorkItems::WidgetDefinition.widget_types[widget_sym]
+              }
+            end
+          end
+
+          ::WorkItems::WidgetDefinition.upsert_all(
+            widgets,
+            unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
+          )
         end
       end
     end
diff --git a/spec/db/development/create_base_work_item_types_spec.rb b/spec/db/development/create_base_work_item_types_spec.rb
index 914b84d866888..7652ccdc487da 100644
--- a/spec/db/development/create_base_work_item_types_spec.rb
+++ b/spec/db/development/create_base_work_item_types_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe 'Create base work item types in development' do
+RSpec.describe 'Create base work item types in development', feature_category: :team_planning do
   subject { load Rails.root.join('db', 'fixtures', 'development', '001_create_base_work_item_types.rb') }
 
   it_behaves_like 'work item base types importer'
diff --git a/spec/db/production/create_base_work_item_types_spec.rb b/spec/db/production/create_base_work_item_types_spec.rb
index 81d80104bb4cc..f6c3b0f6395ef 100644
--- a/spec/db/production/create_base_work_item_types_spec.rb
+++ b/spec/db/production/create_base_work_item_types_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe 'Create base work item types in production' do
+RSpec.describe 'Create base work item types in production', feature_category: :team_planning do
   subject { load Rails.root.join('db', 'fixtures', 'production', '003_create_base_work_item_types.rb') }
 
   it_behaves_like 'work item base types importer'
diff --git a/spec/factories/work_items/widget_definitions.rb b/spec/factories/work_items/widget_definitions.rb
new file mode 100644
index 0000000000000..bbd7c1e743265
--- /dev/null
+++ b/spec/factories/work_items/widget_definitions.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :widget_definition, class: 'WorkItems::WidgetDefinition' do
+    work_item_type
+    namespace
+
+    name { 'Description' }
+    widget_type { 'description' }
+  end
+end
diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb
index 27c3b9643ce8d..224213d1521a1 100644
--- a/spec/graphql/resolvers/users/participants_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb
@@ -115,8 +115,8 @@
           create(:award_emoji, name: 'thumbsup', awardable: public_note)
 
           # 1 extra query per source (3 emojis + 2 notes) to fetch participables collection
-          # 1 extra query to load work item widgets collection
-          expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(6)
+          # 2 extra queries to load work item widgets collection
+          expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(7)
         end
 
         it 'does not execute N+1 for system note metadata relation' do
diff --git a/spec/graphql/resolvers/work_items_resolver_spec.rb b/spec/graphql/resolvers/work_items_resolver_spec.rb
index d89ccc7f8067e..6da62e3adb7f3 100644
--- a/spec/graphql/resolvers/work_items_resolver_spec.rb
+++ b/spec/graphql/resolvers/work_items_resolver_spec.rb
@@ -101,7 +101,7 @@
       end
 
       it 'batches queries that only include IIDs', :request_store do
-        result = batch_sync(max_queries: 7) do
+        result = batch_sync(max_queries: 8) do
           [item1, item2]
             .map { |item| resolve_items(iid: item.iid.to_s) }
             .flat_map(&:to_a)
@@ -111,7 +111,7 @@
       end
 
       it 'finds a specific item with iids', :request_store do
-        result = batch_sync(max_queries: 7) do
+        result = batch_sync(max_queries: 8) do
           resolve_items(iids: [item1.iid]).to_a
         end
 
diff --git a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
index d044170dc75da..3b6d10f4a7e46 100644
--- a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
+++ b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter do
+RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter, feature_category: :team_planning do
   subject { described_class.upsert_types }
 
   it_behaves_like 'work item base types importer'
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index 65c6b22f5c243..e5c88634b2636 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -10,6 +10,20 @@
   describe 'associations' do
     it { is_expected.to have_many(:work_items).with_foreign_key('work_item_type_id') }
     it { is_expected.to belong_to(:namespace) }
+
+    it 'has many `widget_definitions`' do
+      is_expected.to have_many(:widget_definitions)
+        .class_name('::WorkItems::WidgetDefinition')
+        .with_foreign_key('work_item_type_id')
+    end
+
+    it 'has many `enabled_widget_definitions`' do
+      type = create(:work_item_type)
+      widget1 = create(:widget_definition, work_item_type: type)
+      create(:widget_definition, work_item_type: type, disabled: true)
+
+      expect(type.enabled_widget_definitions).to match_array([widget1])
+    end
   end
 
   describe 'scopes' do
@@ -60,29 +74,14 @@
     it { is_expected.not_to allow_value('s' * 256).for(:icon_name) }
   end
 
-  describe '.available_widgets' do
-    subject { described_class.available_widgets }
-
-    it 'returns list of all possible widgets' do
-      is_expected.to include(
-        ::WorkItems::Widgets::Description,
-        ::WorkItems::Widgets::Hierarchy,
-        ::WorkItems::Widgets::Labels,
-        ::WorkItems::Widgets::Assignees,
-        ::WorkItems::Widgets::StartAndDueDate,
-        ::WorkItems::Widgets::Milestone,
-        ::WorkItems::Widgets::Notes
-      )
-    end
-  end
-
   describe '.default_by_type' do
     let(:default_issue_type) { described_class.find_by(namespace_id: nil, base_type: :issue) }
 
     subject { described_class.default_by_type(:issue) }
 
     it 'returns default work item type by base type without calling importer' do
-      expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_types)
+      expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_types).and_call_original
+      expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_widgets)
       expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).not_to receive(:upsert_restrictions)
 
       expect(subject).to eq(default_issue_type)
@@ -94,7 +93,8 @@
       end
 
       it 'creates types and restrictions and returns default work item type by base type' do
-        expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_types)
+        expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_types).and_call_original
+        expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_widgets)
         expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).to receive(:upsert_restrictions)
 
         expect(subject).to eq(default_issue_type)
@@ -128,19 +128,18 @@
   end
 
   describe '#supports_assignee?' do
-    subject(:supports_assignee) { build(:work_item_type, :task).supports_assignee? }
+    let_it_be_with_reload(:work_item_type) { create(:work_item_type) }
+    let_it_be_with_reload(:widget_definition) do
+      create(:widget_definition, work_item_type: work_item_type, widget_type: :assignees)
+    end
 
-    context 'when the assignees widget is supported' do
-      before do
-        stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Assignees] })
-      end
+    subject(:supports_assignee) { work_item_type.supports_assignee? }
 
-      it { is_expected.to be_truthy }
-    end
+    it { is_expected.to be_truthy }
 
     context 'when the assignees widget is not supported' do
       before do
-        stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [] })
+        widget_definition.update!(disabled: true)
       end
 
       it { is_expected.to be_falsey }
diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb
new file mode 100644
index 0000000000000..08f8f4d9663bd
--- /dev/null
+++ b/spec/models/work_items/widget_definition_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
+  let(:all_widget_classes) do
+    list = [
+      ::WorkItems::Widgets::Description,
+      ::WorkItems::Widgets::Hierarchy,
+      ::WorkItems::Widgets::Labels,
+      ::WorkItems::Widgets::Assignees,
+      ::WorkItems::Widgets::StartAndDueDate,
+      ::WorkItems::Widgets::Milestone,
+      ::WorkItems::Widgets::Notes
+    ]
+
+    if Gitlab.ee?
+      list += [
+        ::WorkItems::Widgets::Iteration,
+        ::WorkItems::Widgets::Weight,
+        ::WorkItems::Widgets::Status,
+        ::WorkItems::Widgets::HealthStatus,
+        ::WorkItems::Widgets::Progress,
+        ::WorkItems::Widgets::RequirementLegacy,
+        ::WorkItems::Widgets::TestReports
+      ]
+    end
+
+    list
+  end
+
+  describe 'associations' do
+    it { is_expected.to belong_to(:namespace) }
+    it { is_expected.to belong_to(:work_item_type) }
+  end
+
+  describe 'validations' do
+    it { is_expected.to validate_presence_of(:name) }
+    it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to([:namespace_id, :work_item_type_id]) }
+    it { is_expected.to validate_length_of(:name).is_at_most(255) }
+  end
+
+  context 'with some widgets disabled' do
+    before do
+      described_class.global.where(widget_type: :notes).update_all(disabled: true)
+    end
+
+    describe '.available_widgets' do
+      subject { described_class.available_widgets }
+
+      it 'returns all global widgets excluding the disabled ones' do
+        # WorkItems::Widgets::Notes is excluded from widget class because:
+        # * although widget_definition below is enabled and uses notes widget, it's namespaced (has namespace != nil)
+        # * available_widgets takes into account only global definitions (which have namespace=nil)
+        namespace = create(:namespace)
+        create(:widget_definition, namespace: namespace, widget_type: :notes)
+
+        is_expected.to match_array(all_widget_classes - [::WorkItems::Widgets::Notes])
+      end
+
+      it 'returns all global widgets if there is at least one global widget definition which is enabled' do
+        create(:widget_definition, namespace: nil, widget_type: :notes)
+
+        is_expected.to match_array(all_widget_classes)
+      end
+    end
+
+    describe '.widget_classes' do
+      subject { described_class.widget_classes }
+
+      it 'returns all widget classes no matter if disabled or not' do
+        is_expected.to match_array(all_widget_classes)
+      end
+    end
+  end
+
+  describe '#widget_class' do
+    it 'returns widget class based on widget_type' do
+      expect(build(:widget_definition, widget_type: :description).widget_class).to eq(::WorkItems::Widgets::Description)
+    end
+
+    it 'returns nil if there is no class for the widget_type' do
+      described_class.first.update_column(:widget_type, -1)
+
+      expect(described_class.first.widget_class).to be_nil
+    end
+
+    it 'returns nil if there is no class for the widget_type' do
+      expect(build(:widget_definition, widget_type: nil).widget_class).to be_nil
+    end
+  end
+end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 0040d9dff7e50..1755878796607 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -425,19 +425,15 @@ def permissions(user, issue)
     context 'when accounting for notes widget' do
       let(:policy) { described_class.new(reporter, note) }
 
-      before do
-        widgets_per_type = WorkItems::Type::WIDGETS_FOR_TYPE.dup
-        widgets_per_type[:task] = [::WorkItems::Widgets::Description]
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', widgets_per_type)
-      end
-
-      context 'and notes widget is disabled for task' do
-        let(:task) { create(:work_item, :task, project: project) }
+      context 'and notes widget is disabled for issue' do
+        before do
+          WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
+        end
 
         it 'does not allow accessing notes' do
           # if notes widget is disabled not even maintainer can access notes
-          expect(permissions(maintainer, task)).to be_disallowed(:create_note, :read_note, :mark_note_as_internal, :read_internal_note)
-          expect(permissions(admin, task)).to be_disallowed(:create_note, :read_note, :read_internal_note, :mark_note_as_internal, :set_note_created_at)
+          expect(permissions(maintainer, issue)).to be_disallowed(:create_note, :read_note, :mark_note_as_internal, :read_internal_note)
+          expect(permissions(admin, issue)).to be_disallowed(:create_note, :read_note, :read_internal_note, :mark_note_as_internal, :set_note_created_at)
         end
       end
 
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index f4abe3a223c1c..b2191e6925df2 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -260,9 +260,7 @@
             let(:policy) { described_class.new(developer, note) }
 
             before do
-              widgets_per_type = WorkItems::Type::WIDGETS_FOR_TYPE.dup
-              widgets_per_type[:task] = [::WorkItems::Widgets::Description]
-              stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', widgets_per_type)
+              WorkItems::Type.default_by_type(:task).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
             end
 
             context 'when noteable is task' do
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 38016375b8f7c..c5126dbd1c206 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -42,8 +42,7 @@
 
     context 'with work item without notes widget' do
       before do
-        stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
       end
 
       context 'when fetching discussions' do
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index 00e259097465a..a6253ba424b16 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -122,8 +122,8 @@ def mutation_response
           let(:variables_extra) { {} }
 
           before do
-            stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-            stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+            WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes)
+              .update!(disabled: true)
           end
 
           it_behaves_like 'a Note mutation that does not create a Note'
diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
index eb45e2aa033b4..f40518a574b5b 100644
--- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
@@ -57,8 +57,7 @@ def mutation_response
 
     context 'without notes widget' do
       before do
-        stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
       end
 
       it 'does not update the Note' do
diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
index dff8a87314b84..7918bc860fe64 100644
--- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
@@ -50,8 +50,7 @@ def mutation_response
 
       context 'without notes widget' do
         before do
-          stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-          stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+          WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
         end
 
         it 'does not update the Note' do
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 271c2b917ad8d..ddd294e8f829d 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -252,7 +252,8 @@
         let(:input) { { 'descriptionWidget' => { 'description' => "Updating labels.\n/labels ~\"#{label1.name}\"" } } }
 
         before do
-          stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+          WorkItems::Type.default_by_type(:task).widget_definitions
+            .find_by_widget_type(:labels).update!(disabled: true)
         end
 
         it 'ignores the quick action' do
@@ -370,7 +371,8 @@
           let(:input) { { 'descriptionWidget' => { 'description' => "Updating due date.\n/due today" } } }
 
           before do
-            stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+            WorkItems::Type.default_by_type(:task).widget_definitions
+              .find_by_widget_type(:start_and_due_date).update!(disabled: true)
           end
 
           it 'ignores the quick action' do
@@ -736,7 +738,8 @@
         end
 
         before do
-          stub_const('::WorkItems::Type::WIDGETS_FOR_TYPE', { task: [::WorkItems::Widgets::Description] })
+          WorkItems::Type.default_by_type(:task).widget_definitions
+            .find_by_widget_type(:assignees).update!(disabled: true)
         end
 
         it 'ignores the quick action' do
diff --git a/spec/requests/api/graphql/notes/note_spec.rb b/spec/requests/api/graphql/notes/note_spec.rb
index 180e54290f861..daceaec0b94cd 100644
--- a/spec/requests/api/graphql/notes/note_spec.rb
+++ b/spec/requests/api/graphql/notes/note_spec.rb
@@ -66,7 +66,8 @@
 
     context 'and notes widget is not available' do
       before do
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions
+          .find_by_widget_type(:notes).update!(disabled: true)
       end
 
       it 'returns nil' do
diff --git a/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb b/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb
index 9b11406ae00ea..1199aeb4c39b2 100644
--- a/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb
+++ b/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb
@@ -44,7 +44,8 @@
 
     context 'and notes widget is not available' do
       before do
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions
+          .find_by_widget_type(:notes).update!(disabled: true)
       end
 
       it 'returns nil' do
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index c2d9db1e6fbba..c0276e02eb787 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -210,8 +210,7 @@
         let(:request_path) { "/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes" }
 
         before do
-          stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-          stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+          WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
         end
 
         it 'does not fetch notes' do
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index ecdd8d031c925..2252bb15d51df 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -22,8 +22,7 @@
       let_it_be(:issuable) { create(:work_item, :issue, project: project) }
 
       before do
-        stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
-        stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+        WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
       end
 
       it "returns no notes" do
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 049c90f20b071..7c8b600032d4f 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -188,7 +188,7 @@
           {
             title: 'Awesome work_item',
             description: 'please fix',
-            work_item_type: create(:work_item_type, :task)
+            work_item_type: WorkItems::Type.default_by_type(:task)
           }
         end
 
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 588fe466a42ae..b9a99eff41359 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -12,7 +12,7 @@ def delete_from_all_tables!(except: [])
   end
 
   def deletion_except_tables
-    %w[work_item_types work_item_hierarchy_restrictions]
+    %w[work_item_types work_item_hierarchy_restrictions work_item_widget_definitions]
   end
 
   def setup_database_cleaner
diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb
index b101103758495..1703d400aeaae 100644
--- a/spec/support/shared_examples/work_item_base_types_importer.rb
+++ b/spec/support/shared_examples/work_item_base_types_importer.rb
@@ -15,6 +15,22 @@
     expect(WorkItems::Type.all).to all(be_valid)
   end
 
+  it 'creates all default widget definitions' do
+    WorkItems::WidgetDefinition.delete_all
+    widget_mapping = ::Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter::WIDGETS_FOR_TYPE
+
+    expect { subject }.to change { WorkItems::WidgetDefinition.count }.from(0).to(widget_mapping.values.flatten.count)
+
+    created_widgets = WorkItems::WidgetDefinition.global.map do |widget|
+      { name: widget.work_item_type.name, type: widget.widget_type }
+    end
+    expected_widgets = widget_mapping.flat_map do |type_sym, widget_types|
+      widget_types.map { |type| { name: ::WorkItems::Type::TYPE_NAMES[type_sym], type: type.to_s } }
+    end
+
+    expect(created_widgets).to match_array(expected_widgets)
+  end
+
   it 'upserts base work item types if they already exist' do
     first_type = WorkItems::Type.first
     original_name = first_type.name
@@ -29,8 +45,34 @@
     )
   end
 
-  it 'executes a single INSERT query' do
-    expect { subject }.to make_queries_matching(/INSERT/, 1)
+  it 'upserts default widget definitions if they already exist and type changes' do
+    widget = WorkItems::WidgetDefinition.global.find_by_widget_type(:labels)
+
+    widget.update!(widget_type: :weight)
+
+    expect do
+      subject
+      widget.reload
+    end.to not_change(WorkItems::WidgetDefinition, :count).and(
+      change { widget.widget_type }.from('weight').to('labels')
+    )
+  end
+
+  it 'does not change default widget definitions if they already exist with changed disabled status' do
+    widget = WorkItems::WidgetDefinition.global.find_by_widget_type(:labels)
+
+    widget.update!(disabled: true)
+
+    expect do
+      subject
+      widget.reload
+    end.to not_change(WorkItems::WidgetDefinition, :count).and(
+      not_change { widget.disabled }
+    )
+  end
+
+  it 'executes single INSERT query per types and widget definitions' do
+    expect { subject }.to make_queries_matching(/INSERT/, 2)
   end
 
   context 'when some base types exist' do
@@ -39,10 +81,22 @@
     end
 
     it 'inserts all types and does nothing if some already existed' do
-      expect { subject }.to make_queries_matching(/INSERT/, 1).and(
+      expect { subject }.to make_queries_matching(/INSERT/, 2).and(
         change { WorkItems::Type.count }.by(1)
       )
       expect(WorkItems::Type.count).to eq(WorkItems::Type::BASE_TYPES.count)
     end
   end
+
+  context 'when some widget definitions exist' do
+    before do
+      WorkItems::WidgetDefinition.limit(1).delete_all
+    end
+
+    it 'inserts all widget definitions and does nothing if some already existed' do
+      expect { subject }.to make_queries_matching(/INSERT/, 2).and(
+        change { WorkItems::WidgetDefinition.count }.by(1)
+      )
+    end
+  end
 end
-- 
GitLab