Merge "Allow admin to create repository submit requirements"
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index 1e5736b..17b9e73 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -459,6 +459,7 @@
       <div class="main table breadcrumbs">
         <gr-repo-submit-requirements
           .repo=${this.repoViewState.repo}
+          .params=${this.repoViewState}
         ></gr-repo-submit-requirements>
       </div>
     `;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts
index 6de8705..ff65e26 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements.ts
@@ -3,98 +3,218 @@
  * Copyright 2025 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {RepoName, SubmitRequirementInfo} from '../../../types/common';
+import {
+  RepoName,
+  SubmitRequirementInfo,
+  SubmitRequirementInput,
+} from '../../../types/common';
 import {firePageError} from '../../../utils/event-util';
 import {getAppContext} from '../../../services/app-context';
 import {ErrorCallback} from '../../../api/rest';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
-import {LitElement, css, html, PropertyValues} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import {LitElement, css, html, PropertyValues, nothing} from 'lit';
+import {customElement, property, state, query} from 'lit/decorators.js';
 import {when} from 'lit/directives/when.js';
+import {grFormStyles} from '../../../styles/gr-form-styles';
+import {assertIsDefined} from '../../../utils/common-util';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
+import {subscribe} from '../../lit/subscription-controller';
+import '../../shared/gr-list-view/gr-list-view';
+import {
+  createRepoUrl,
+  RepoDetailView,
+  RepoViewState,
+} from '../../../models/views/repo';
+import '@polymer/iron-input/iron-input';
 
 @customElement('gr-repo-submit-requirements')
 export class GrRepoSubmitRequirements extends LitElement {
   @property({type: String})
   repo?: RepoName;
 
+  @property({type: Object})
+  params?: RepoViewState;
+
+  @query('#createDialog')
+  private readonly createDialog?: HTMLDialogElement;
+
   @state()
   loading = true;
 
   @state()
   submitRequirements?: SubmitRequirementInfo[];
 
+  @state()
+  showCreateDialog = false;
+
+  @state() isAdmin = false;
+
+  @state()
+  newRequirement: SubmitRequirementInput = this.getEmptyRequirement();
+
+  @state() offset = 0;
+
+  @state() filter = '';
+
+  @state() itemsPerPage = 25;
+
   private readonly restApiService = getAppContext().restApiService;
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
   static override get styles() {
     return [
       sharedStyles,
       tableStyles,
+      grFormStyles,
+      modalStyles,
       css`
         :host {
           display: block;
           margin-bottom: var(--spacing-xxl);
         }
+        .actions {
+          display: flex;
+          justify-content: flex-end;
+          margin-bottom: var(--spacing-m);
+          padding: var(--spacing-l);
+        }
+        .createButton {
+          margin-left: var(--spacing-m);
+        }
+        div.title-flex,
+        div.value-flex {
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+        }
+        input {
+          width: 20em;
+          box-sizing: border-box;
+        }
+        div.gr-form-styles section {
+          margin: var(--spacing-m) 0;
+        }
+        div.gr-form-styles span.title {
+          width: 13em;
+        }
+        section .title gr-icon {
+          vertical-align: top;
+        }
+        textarea {
+          width: 20em;
+          min-height: 100px;
+          resize: vertical;
+          box-sizing: border-box;
+        }
+        gr-dialog {
+          width: 36em;
+        }
       `,
     ];
   }
 
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getUserModel().isAdmin$,
+      x => (this.isAdmin = x)
+    );
+  }
+
   override render() {
-    return html` <table id="list" class="genericList">
-      <tbody>
-        <tr class="headerRow">
-          <th class="topHeader">Name</th>
-          <th class="topHeader">Description</th>
-          <th class="topHeader">Applicability Expression</th>
-          <th class="topHeader">Submittability Expression</th>
-          <th class="topHeader">Override Expression</th>
-          <th
-            class="topHeader"
-            title="Whether override is allowed in child projects"
-          >
-            Allow Override
-          </th>
-        </tr>
-      </tbody>
-      <tbody id="submit-requirements">
-        ${when(
-          this.loading,
-          () => html`<tr id="loadingContainer">
-            <td>Loading...</td>
-          </tr>`,
-          () =>
-            html` ${(this.submitRequirements ?? []).map(
-              item => html`
-                <tr class="table">
-                  <td class="name">${item.name}</td>
-                  <td class="desc">${item.description}</td>
-                  <td class="applicability">
-                    ${item.applicability_expression}
-                  </td>
-                  <td class="submittability">
-                    ${item.submittability_expression}
-                  </td>
-                  <td class="override">${item.override_expression}</td>
-                  <td class="allowOverride">
-                    ${this.renderCheckmark(
-                      item.allow_override_in_child_projects
-                    )}
-                  </td>
-                </tr>
-              `
-            )}`
-        )}
-      </tbody>
-    </table>`;
+    return html`
+      <gr-list-view
+        .createNew=${this.isAdmin}
+        .filter=${this.filter}
+        .itemsPerPage=${this.itemsPerPage}
+        .items=${this.submitRequirements}
+        .loading=${this.loading}
+        .offset=${this.offset}
+        .path=${createRepoUrl({
+          repo: this.repo,
+          detail: RepoDetailView.SUBMIT_REQUIREMENTS,
+        })}
+        @create-clicked=${() => this.handleCreateClick()}
+      >
+        <table id="list" class="genericList">
+          <tbody>
+            <tr class="headerRow">
+              <th class="topHeader">Name</th>
+              <th class="topHeader">Description</th>
+              <th class="topHeader">Applicability Expression</th>
+              <th class="topHeader">Submittability Expression</th>
+              <th class="topHeader">Override Expression</th>
+              <th
+                class="topHeader"
+                title="Whether override is allowed in child projects"
+              >
+                Allow Override
+              </th>
+            </tr>
+          </tbody>
+          <tbody id="submit-requirements">
+            ${when(
+              this.loading,
+              () => html`<tr id="loadingContainer">
+                <td>Loading...</td>
+              </tr>`,
+              () =>
+                html` ${(this.submitRequirements ?? []).map(
+                  item => html`
+                    <tr class="table">
+                      <td class="name">${item.name}</td>
+                      <td class="desc">${item.description}</td>
+                      <td class="applicability">
+                        ${item.applicability_expression}
+                      </td>
+                      <td class="submittability">
+                        ${item.submittability_expression}
+                      </td>
+                      <td class="override">${item.override_expression}</td>
+                      <td class="allowOverride">
+                        ${this.renderCheckmark(
+                          item.allow_override_in_child_projects
+                        )}
+                      </td>
+                    </tr>
+                  `
+                )}`
+            )}
+          </tbody>
+        </table>
+      </gr-list-view>
+
+      ${this.renderCreateDialog()}
+    `;
   }
 
   override updated(changedProperties: PropertyValues) {
     if (changedProperties.has('repo')) {
-      this.repoChanged();
+      this.getSubmitRequirements();
     }
   }
 
-  private repoChanged() {
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('params')) {
+      this._paramsChanged();
+    }
+  }
+
+  async _paramsChanged() {
+    const params = this.params;
+    this.loading = true;
+    this.filter = params?.filter ?? '';
+    this.offset = Number(params?.offset ?? 0);
+
+    await this.getSubmitRequirements(this.filter, this.offset);
+  }
+
+  private getSubmitRequirements(filter?: string, offset?: number) {
     const repo = this.repo;
     this.loading = true;
     if (!repo) {
@@ -112,7 +232,13 @@
           return;
         }
 
-        this.submitRequirements = res;
+        this.submitRequirements = res
+          .filter(item =>
+            filter === undefined
+              ? true
+              : item.name.toLowerCase().includes(filter.toLowerCase())
+          )
+          .slice(offset ?? 0, (offset ?? 0) + this.itemsPerPage);
         this.loading = false;
       });
   }
@@ -120,6 +246,208 @@
   private renderCheckmark(check?: boolean) {
     return check ? '✓' : '';
   }
+
+  private handleCreateClick() {
+    assertIsDefined(this.createDialog, 'createDialog');
+    this.createDialog.showModal();
+  }
+
+  private handleCreateCancel() {
+    assertIsDefined(this.createDialog, 'createDialog');
+    this.createDialog.close();
+    this.newRequirement = this.getEmptyRequirement();
+  }
+
+  private handleCreateConfirm() {
+    if (!this.repo) return;
+    if (
+      !this.newRequirement.name ||
+      !this.newRequirement.submittability_expression
+    ) {
+      return;
+    }
+
+    const errFn: ErrorCallback = response => {
+      firePageError(response);
+    };
+
+    this.restApiService
+      .createSubmitRequirement(this.repo, this.newRequirement, errFn)
+      .then(() => {
+        this.createDialog?.close();
+        this.newRequirement = this.getEmptyRequirement();
+        this.getSubmitRequirements(this.filter, this.offset);
+      });
+  }
+
+  private getEmptyRequirement(): SubmitRequirementInput {
+    return {
+      name: '',
+      description: '',
+      applicability_expression: '',
+      submittability_expression: '',
+      override_expression: '',
+      allow_override_in_child_projects: false,
+    };
+  }
+
+  private renderCreateDialog() {
+    if (!this.isAdmin) return nothing;
+
+    return html`
+      <dialog id="createDialog" tabindex="-1">
+        <gr-dialog
+          confirm-label="Create"
+          cancel-label="Cancel"
+          ?disabled=${!this.newRequirement.name ||
+          !this.newRequirement.submittability_expression}
+          @confirm=${this.handleCreateConfirm}
+          @cancel=${this.handleCreateCancel}
+        >
+          <div class="header" slot="header">Create Submit Requirement</div>
+          <div class="main" slot="main">
+            <div class="gr-form-styles">
+              <div id="form">
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Name</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <iron-input
+                        .bindValue=${this.newRequirement.name}
+                        @bind-value-changed=${(e: Event) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            name: (e as CustomEvent).detail.value,
+                          };
+                        }}
+                      >
+                        <input id="name" type="text" required />
+                      </iron-input>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Description</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <textarea
+                        id="description"
+                        .value=${this.newRequirement.description}
+                        placeholder="Optional"
+                      ></textarea>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Applicability Expression</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <iron-input
+                        .bindValue=${this.newRequirement
+                          .applicability_expression}
+                        @bind-value-changed=${(e: Event) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            applicability_expression: (e as CustomEvent).detail
+                              .value,
+                          };
+                        }}
+                      >
+                        <input
+                          id="applicability"
+                          type="text"
+                          placeholder="Optional"
+                        />
+                      </iron-input>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Submittability Expression</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <iron-input
+                        .bindValue=${this.newRequirement
+                          .submittability_expression}
+                        @bind-value-changed=${(e: Event) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            submittability_expression: (e as CustomEvent).detail
+                              .value,
+                          };
+                        }}
+                      >
+                        <input id="submittability" type="text" required />
+                      </iron-input>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Override Expression</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <iron-input
+                        .bindValue=${this.newRequirement.override_expression}
+                        @bind-value-changed=${(e: Event) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            override_expression: (e as CustomEvent).detail
+                              .value,
+                          };
+                        }}
+                      >
+                        <input
+                          id="override"
+                          type="text"
+                          placeholder="Optional"
+                        />
+                      </iron-input>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">Allow Override in Child Projects</span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <gr-select
+                        id="allowOverride"
+                        .bindValue=${this.newRequirement
+                          .allow_override_in_child_projects}
+                        @bind-value-changed=${(e: Event) => {
+                          this.newRequirement = {
+                            ...this.newRequirement,
+                            allow_override_in_child_projects:
+                              (e as CustomEvent).detail.value === 'true',
+                          };
+                        }}
+                      >
+                        <select>
+                          <option value="true">True</option>
+                          <option value="false">False</option>
+                        </select>
+                      </gr-select>
+                    </span>
+                  </div>
+                </section>
+              </div>
+            </div>
+          </div>
+        </gr-dialog>
+      </dialog>
+    `;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts
index f438a68..dd0df37 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-submit-requirements/gr-repo-submit-requirements_test.ts
@@ -38,36 +38,7 @@
       element.repo = 'test2' as RepoName;
       assert.shadowDom.equal(
         element,
-        /* HTML */ `<table class="genericList" id="list">
-          <tbody>
-            <tr class="headerRow">
-              <th class="topHeader">Name</th>
-              <th class="topHeader">Description</th>
-              <th class="topHeader">Applicability Expression</th>
-              <th class="topHeader">Submittability Expression</th>
-              <th class="topHeader">Override Expression</th>
-              <th
-                class="topHeader"
-                title="Whether override is allowed in child projects"
-              >
-                Allow Override
-              </th>
-            </tr>
-          </tbody>
-          <tbody id="submit-requirements">
-            <tr id="loadingContainer">
-              <td>Loading...</td>
-            </tr>
-          </tbody>
-        </table>`
-      );
-    });
-
-    test('render', async () => {
-      await waitEventLoop();
-      assert.shadowDom.equal(
-        element,
-        /* HTML */ `
+        /* HTML */ `<gr-list-view>
           <table class="genericList" id="list">
             <tbody>
               <tr class="headerRow">
@@ -85,20 +56,215 @@
               </tr>
             </tbody>
             <tbody id="submit-requirements">
-              <tr class="table">
-                <td class="name">Verified</td>
-                <td class="desc">
-                  CI result status for build and tests is passing
-                </td>
-                <td class="applicability"></td>
-                <td class="submittability">
-                  label:Verified=MAX AND -label:Verified=MIN
-                </td>
-                <td class="override"></td>
-                <td class="allowOverride"></td>
+              <tr id="loadingContainer">
+                <td>Loading...</td>
               </tr>
             </tbody>
           </table>
+        </gr-list-view>`
+      );
+    });
+
+    test('render', async () => {
+      await waitEventLoop();
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <gr-list-view>
+            <table class="genericList" id="list">
+              <tbody>
+                <tr class="headerRow">
+                  <th class="topHeader">Name</th>
+                  <th class="topHeader">Description</th>
+                  <th class="topHeader">Applicability Expression</th>
+                  <th class="topHeader">Submittability Expression</th>
+                  <th class="topHeader">Override Expression</th>
+                  <th
+                    class="topHeader"
+                    title="Whether override is allowed in child projects"
+                  >
+                    Allow Override
+                  </th>
+                </tr>
+              </tbody>
+              <tbody id="submit-requirements">
+                <tr class="table">
+                  <td class="name">Verified</td>
+                  <td class="desc">
+                    CI result status for build and tests is passing
+                  </td>
+                  <td class="applicability"></td>
+                  <td class="submittability">
+                    label:Verified=MAX AND -label:Verified=MIN
+                  </td>
+                  <td class="override"></td>
+                  <td class="allowOverride"></td>
+                </tr>
+              </tbody>
+            </table>
+          </gr-list-view>
+        `
+      );
+    });
+
+    test('render as admin', async () => {
+      await waitEventLoop();
+      element.isAdmin = true;
+      await element.updateComplete;
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <gr-list-view>
+            <table class="genericList" id="list">
+              <tbody>
+                <tr class="headerRow">
+                  <th class="topHeader">Name</th>
+                  <th class="topHeader">Description</th>
+                  <th class="topHeader">Applicability Expression</th>
+                  <th class="topHeader">Submittability Expression</th>
+                  <th class="topHeader">Override Expression</th>
+                  <th
+                    class="topHeader"
+                    title="Whether override is allowed in child projects"
+                  >
+                    Allow Override
+                  </th>
+                </tr>
+              </tbody>
+              <tbody id="submit-requirements">
+                <tr class="table">
+                  <td class="name">Verified</td>
+                  <td class="desc">
+                    CI result status for build and tests is passing
+                  </td>
+                  <td class="applicability"></td>
+                  <td class="submittability">
+                    label:Verified=MAX AND -label:Verified=MIN
+                  </td>
+                  <td class="override"></td>
+                  <td class="allowOverride"></td>
+                </tr>
+              </tbody>
+            </table>
+          </gr-list-view>
+          <dialog id="createDialog" tabindex="-1">
+            <gr-dialog cancel-label="Cancel" confirm-label="Create" disabled="">
+              <div class="header" slot="header">Create Submit Requirement</div>
+              <div class="main" slot="main">
+                <div class="gr-form-styles">
+              <div id="form">
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Name
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <iron-input>
+                        <input
+                          id="name"
+                          required=""
+                          type="text"
+                        >
+                      </iron-input>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Description
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <textarea
+                        id="description"
+                        placeholder="Optional"
+                      >
+                      </textarea>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Applicability Expression
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <iron-input>
+                      <input
+                        id="applicability"
+                        placeholder="Optional"
+                        type="text"
+                      >
+                      </iron-input>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Submittability Expression
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <iron-input>
+                      <input
+                        id="submittability"
+                        required=""
+                        type="text"
+                        >
+                      </iron-input>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Override Expression
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <iron-input>
+                        <input
+                          id="override"
+                          placeholder="Optional"
+                          type="text"
+                        >
+                      </iron-input>
+                    </span>
+                  </div>
+                </section>
+                <section>
+                  <div class="title-flex">
+                    <span class="title">
+                      Allow Override in Child Projects
+                    </span>
+                  </div>
+                  <div class="value-flex">
+                    <span class="value">
+                      <gr-select id="allowOverride">
+                        <select>
+                          <option value="true">
+                            True
+                          </option>
+                          <option value="false">
+                            False
+                          </option>
+                        </select>
+                      </gr-select>
+                    </span>
+                  </div>
+                </section>
+              </div>
+            </gr-dialog>
+          </dialog>
         `
       );
     });
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 9f87d99..87a3e6a 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -167,7 +167,8 @@
   // Matches /admin/repos/<repos>,access.
   REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
 
-  REPO_SUBMIT_REQUIREMENTS: /^\/admin\/repos\/(.+),submit-requirements$/,
+  REPO_SUBMIT_REQUIREMENTS:
+    /^\/admin\/repos\/(.+),submit-requirements\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
 
   // Matches /admin/plugins with optional filter and offset.
   PLUGIN_LIST: /^\/admin\/plugins\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
@@ -1229,6 +1230,8 @@
       view: GerritView.REPO,
       detail: RepoDetailView.SUBMIT_REQUIREMENTS,
       repo,
+      filter: ctx.params[1] ?? null,
+      offset: ctx.params[2] ?? '0',
     };
     // Note that router model view must be updated before view models.
     this.setState(state);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index d33fa1d..ec2e48d 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -734,6 +734,8 @@
           ...createRepoViewState(),
           detail: RepoDetailView.SUBMIT_REQUIREMENTS,
           repo: '4321' as RepoName,
+          filter: '',
+          offset: '',
         });
       });
 
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 4601bd2..bc4bec8 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -107,6 +107,7 @@
   ListChangesOption,
   ReviewResult,
   SubmitRequirementInfo,
+  SubmitRequirementInput,
 } from '../../types/common';
 import {
   DiffInfo,
@@ -1785,6 +1786,24 @@
     }) as Promise<SubmitRequirementInfo[] | undefined>;
   }
 
+  createSubmitRequirement(
+    repoName: RepoName,
+    input: SubmitRequirementInput,
+    errFn?: ErrorCallback
+  ): Promise<SubmitRequirementInfo | undefined> {
+    return this._restApiHelper.fetchJSON({
+      url: `/projects/${encodeURIComponent(
+        repoName
+      )}/submit_requirements/${encodeURIComponent(input.name)}`,
+      fetchOptions: getFetchOptions({
+        method: HttpMethod.PUT,
+        body: input,
+      }),
+      errFn,
+      anonymizedUrl: '/projects/*/submit_requirements/*',
+    }) as Promise<SubmitRequirementInfo | undefined>;
+  }
+
   getRepoAccessRights(
     repoName: RepoName,
     errFn?: ErrorCallback
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index ea6451a..11f8b8c 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -92,6 +92,7 @@
   DraftInfo,
   ReviewResult,
   SubmitRequirementInfo,
+  SubmitRequirementInput,
 } from '../../types/common';
 import {
   DiffInfo,
@@ -291,6 +292,12 @@
     errFn?: ErrorCallback
   ): Promise<SubmitRequirementInfo[] | undefined>;
 
+  createSubmitRequirement(
+    repoName: RepoName,
+    input: SubmitRequirementInput,
+    errFn?: ErrorCallback
+  ): Promise<SubmitRequirementInfo | undefined>;
+
   getRepoAccessRights(
     repoName: RepoName,
     errFn?: ErrorCallback
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 95f160d..e4abf5e 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -397,6 +397,9 @@
   getRepoSubmitRequirements(): Promise<SubmitRequirementInfo[] | undefined> {
     return Promise.resolve([]);
   },
+  createSubmitRequirement(): Promise<SubmitRequirementInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
   getRepoAccessRights(): Promise<ProjectAccessInfo | undefined> {
     return Promise.resolve(undefined);
   },
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 47669f4..02a8cc6 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -740,6 +740,19 @@
 }
 
 /**
+ * The SubmitRequirementInput entity describes a submit requirement input.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#submit-requirement-input
+ */
+export interface SubmitRequirementInput {
+  name: string;
+  description?: string;
+  applicability_expression?: string;
+  submittability_expression: string;
+  override_expression?: string;
+  allow_override_in_child_projects?: boolean;
+}
+
+/**
  * The ProjectAccessInfo entity contains information about the access rights for
  * a project.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-access.html#project-access-info