Add contact form checkbox in admin form
authorChocobozzz <me@florianbigard.com>
Thu, 10 Jan 2019 08:58:08 +0000 (09:58 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 10 Jan 2019 10:32:38 +0000 (11:32 +0100)
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/core/server/server.service.ts
client/src/app/shared/forms/form-reactive.ts
client/src/app/shared/forms/form-validators/form-validator.service.ts
client/src/app/shared/misc/help.component.scss
client/src/app/shared/user-subscription/remote-subscribe.component.ts
client/src/app/videos/+video-watch/comment/video-comment-add.component.ts

index 6ece7e8bcba26d93733d40998fcb3fe72d1d7eb5..52eb00d93f4accf0039190e53324ebb9e71d7abc 100644 (file)
 
         <div i18n class="inner-form-title">Instance</div>
 
-        <div class="form-group">
-          <label i18n for="instanceName">Name</label>
-          <input
-            type="text" id="instanceName"
-            formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
-          >
-          <div *ngIf="formErrors.instanceName" class="form-error">
-            {{ formErrors.instanceName }}
+        <ng-container formGroupName="instance">
+          <div class="form-group">
+            <label i18n for="instanceName">Name</label>
+            <input
+              type="text" id="instanceName"
+              formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
+            >
+            <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label i18n for="instanceShortDescription">Short description</label>
-          <textarea
-            id="instanceShortDescription" formControlName="instanceShortDescription"
-            [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }"
-          ></textarea>
-          <div *ngIf="formErrors.instanceShortDescription" class="form-error">
-            {{ formErrors.instanceShortDescription }}
+          <div class="form-group">
+            <label i18n for="instanceShortDescription">Short description</label>
+            <textarea
+              id="instanceShortDescription" formControlName="shortDescription"
+              [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
+            ></textarea>
+            <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
-          <my-markdown-textarea
-            id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true"
-            [classes]="{ 'input-error': formErrors['instanceDescription'] }"
-          ></my-markdown-textarea>
-          <div *ngIf="formErrors.instanceDescription" class="form-error">
-            {{ formErrors.instanceDescription }}
+          <div class="form-group">
+            <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
+            <my-markdown-textarea
+              id="instanceDescription" formControlName="description" textareaWidth="500px" [previewColumn]="true"
+              [classes]="{ 'input-error': formErrors['instance.description'] }"
+            ></my-markdown-textarea>
+            <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
-          <my-markdown-textarea
-            id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true"
-            [ngClass]="{ 'input-error': formErrors['instanceTerms'] }"
-          ></my-markdown-textarea>
-          <div *ngIf="formErrors.instanceTerms" class="form-error">
-            {{ formErrors.instanceTerms }}
+          <div class="form-group">
+            <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
+            <my-markdown-textarea
+              id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
+              [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
+            ></my-markdown-textarea>
+            <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label i18n for="instanceDefaultClientRoute">Default client route</label>
-          <div class="peertube-select-container">
-            <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute">
-              <option i18n value="/videos/overview">Videos Overview</option>
-              <option i18n value="/videos/trending">Videos Trending</option>
-              <option i18n value="/videos/recently-added">Videos Recently Added</option>
-              <option i18n value="/videos/local">Local videos</option>
-            </select>
+          <div class="form-group">
+            <label i18n for="instanceDefaultClientRoute">Default client route</label>
+            <div class="peertube-select-container">
+              <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
+                <option i18n value="/videos/overview">Videos Overview</option>
+                <option i18n value="/videos/trending">Videos Trending</option>
+                <option i18n value="/videos/recently-added">Videos Recently Added</option>
+                <option i18n value="/videos/local">Local videos</option>
+              </select>
+            </div>
+            <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
           </div>
-          <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error">
-            {{ formErrors.instanceDefaultClientRoute }}
+
+          <div class="form-group">
+            <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
+            <my-help
+              helpType="custom" i18n-customHtml
+              customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
+            ></my-help>
+
+            <div class="peertube-select-container">
+              <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
+                <option i18n value="do_not_list">Do not list</option>
+                <option i18n value="blur">Blur thumbnails</option>
+                <option i18n value="display">Display</option>
+              </select>
+            </div>
+            <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
           </div>
-        </div>
+        </ng-container>
 
-        <div class="form-group">
-          <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
-          <my-help
-            helpType="custom" i18n-customHtml
-            customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
-          ></my-help>
+        <div i18n class="inner-form-title">Signup</div>
 
-          <div class="peertube-select-container">
-            <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy">
-              <option i18n value="do_not_list">Do not list</option>
-              <option i18n value="blur">Blur thumbnails</option>
-              <option i18n value="display">Display</option>
-            </select>
+        <ng-container formGroupName="signup">
+          <div class="form-group">
+            <my-peertube-checkbox
+              inputName="signupEnabled" formControlName="enabled"
+              i18n-labelText labelText="Signup enabled"
+            ></my-peertube-checkbox>
           </div>
-          <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error">
-            {{ formErrors.instanceDefaultNSFWPolicy }}
+
+          <div class="form-group">
+            <my-peertube-checkbox *ngIf="isSignupEnabled()"
+              inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
+              i18n-labelText labelText="Signup requires email verification"
+            ></my-peertube-checkbox>
           </div>
-        </div>
 
-        <div i18n class="inner-form-title">Signup</div>
+          <div *ngIf="isSignupEnabled()" class="form-group">
+            <label i18n for="signupLimit">Signup limit</label>
+            <input
+              type="text" id="signupLimit"
+              formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
+            >
+            <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
+          </div>
+        </ng-container>
 
-        <div class="form-group">
-          <my-peertube-checkbox
-            inputName="signupEnabled" formControlName="signupEnabled"
-            i18n-labelText labelText="Signup enabled"
-          ></my-peertube-checkbox>
-        </div>
+        <div i18n class="inner-form-title">Users</div>
 
-        <div class="form-group">
-          <my-peertube-checkbox *ngIf="isSignupEnabled()"
-            inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification"
-            i18n-labelText labelText="Signup requires email verification"
-          ></my-peertube-checkbox>
-        </div>
+        <ng-container formGroupName="user">
+          <div class="form-group">
+            <label i18n for="userVideoQuota">User default video quota</label>
+            <div class="peertube-select-container">
+              <select id="userVideoQuota" formControlName="videoQuota">
+                <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
+                  {{ videoQuotaOption.label }}
+                </option>
+              </select>
+            </div>
+            <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
+          </div>
 
-        <div *ngIf="isSignupEnabled()" class="form-group">
-          <label i18n for="signupLimit">Signup limit</label>
-          <input
-            type="text" id="signupLimit"
-            formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }"
-          >
-          <div *ngIf="formErrors.signupLimit" class="form-error">
-            {{ formErrors.signupLimit }}
+          <div class="form-group">
+            <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
+            <div class="peertube-select-container">
+              <select id="userVideoQuotaDaily" formControlName="videoQuotaDaily">
+                <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
+                  {{ videoQuotaDailyOption.label }}
+                </option>
+              </select>
+            </div>
+            <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
           </div>
-        </div>
+        </ng-container>
 
         <div i18n class="inner-form-title">Import</div>
 
-        <div class="form-group">
-          <my-peertube-checkbox
-            inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled"
-            i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
-          ></my-peertube-checkbox>
-        </div>
+        <ng-container formGroupName="import">
+          <ng-container formGroupName="videos">
 
-        <div class="form-group">
-          <my-peertube-checkbox
-            inputName="importVideosTorrentEnabled" formControlName="importVideosTorrentEnabled"
-            i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
-          ></my-peertube-checkbox>
-        </div>
+            <div class="form-group" formGroupName="http">
+              <my-peertube-checkbox
+                inputName="importVideosHttpEnabled" formControlName="enabled"
+                i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
+              ></my-peertube-checkbox>
+            </div>
+
+            <div class="form-group" formGroupName="torrent">
+              <my-peertube-checkbox
+                inputName="importVideosTorrentEnabled" formControlName="enabled"
+                i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
+              ></my-peertube-checkbox>
+            </div>
+
+          </ng-container>
+        </ng-container>
 
         <div i18n class="inner-form-title">Administrator</div>
 
-        <div class="form-group">
+        <div class="form-group" formGroupName="admin">
           <label i18n for="adminEmail">Admin email</label>
           <input
             type="text" id="adminEmail"
-            formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }"
+            formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
           >
-          <div *ngIf="formErrors.adminEmail" class="form-error">
-            {{ formErrors.adminEmail }}
-          </div>
+          <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
         </div>
 
-        <div i18n class="inner-form-title">Users</div>
-
-        <div class="form-group">
-          <label i18n for="userVideoQuota">User default video quota</label>
-          <div class="peertube-select-container">
-            <select id="userVideoQuota" formControlName="userVideoQuota">
-              <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
-                {{ videoQuotaOption.label }}
-              </option>
-            </select>
-          </div>
-          <div *ngIf="formErrors.userVideoQuota" class="form-error">
-            {{ formErrors.userVideoQuota }}
-          </div>
+        <div class="form-group" formGroupName="contactForm">
+          <my-peertube-checkbox
+            inputName="enableContactForm" formControlName="enabled"
+            i18n-labelText labelText="Enable contact form"
+          ></my-peertube-checkbox>
         </div>
 
-        <div class="form-group">
-          <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
-          <div class="peertube-select-container">
-            <select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily">
-              <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
-                {{ videoQuotaDailyOption.label }}
-              </option>
-            </select>
-          </div>
-          <div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
-            {{ formErrors.userVideoQuotaDaily }}
-          </div>
-        </div>
       </ng-template>
     </ngb-tab>
 
       <ng-template ngbTabContent>
         <div i18n class="inner-form-title">Twitter</div>
 
-        <div class="form-group">
-          <label i18n for="signupLimit">Your Twitter username</label>
-          <my-help
-            helpType="custom" i18n-customHtml
-            customHtml="Indicates the Twitter account for the website or platform on which the content was published."
-          ></my-help>
-          <input
-            type="text" id="servicesTwitterUsername"
-            formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }"
-          >
-          <div *ngIf="formErrors.servicesTwitterUsername" class="form-error">
-            {{ formErrors.servicesTwitterUsername }}
-          </div>
-        </div>
+        <ng-container formGroupName="services">
+          <ng-container formGroupName="twitter">
+
+            <div class="form-group">
+              <label i18n for="signupLimit">Your Twitter username</label>
+              <my-help
+                helpType="custom" i18n-customHtml
+                customHtml="Indicates the Twitter account for the website or platform on which the content was published."
+              ></my-help>
+              <input
+                type="text" id="servicesTwitterUsername"
+                formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
+              >
+              <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
+            </div>
+
+            <div class="form-group">
+              <my-peertube-checkbox
+                inputName="servicesTwitterWhitelisted" formControlName="whitelisted"
+                i18n-labelText labelText="Instance whitelisted by Twitter"
+                i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
+        If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
+        Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
+              ></my-peertube-checkbox>
+            </div>
+
+          </ng-container>
+        </ng-container>
 
-        <div class="form-group">
-          <my-peertube-checkbox
-            inputName="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted"
-            i18n-labelText labelText="Instance whitelisted by Twitter"
-            i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
-    If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
-    Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
-          ></my-peertube-checkbox>
-        </div>
     </ng-template>
     </ngb-tab>
 
 
         <div i18n class="inner-form-title">Transcoding</div>
 
-        <div class="form-group">
-          <my-peertube-checkbox
-            inputName="transcodingEnabled" formControlName="transcodingEnabled"
-            i18n-labelText labelText="Transcoding enabled"
-            i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
-          ></my-peertube-checkbox>
-        </div>
-
-        <ng-template [ngIf]="isTranscodingEnabled()">
-
+        <ng-container formGroupName="transcoding">
           <div class="form-group">
             <my-peertube-checkbox
-              inputName="transcodingAllowAdditionalExtensions" formControlName="transcodingAllowAdditionalExtensions"
-              i18n-labelText labelText="Allow additional extensions"
-              i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
+              inputName="transcodingEnabled" formControlName="enabled"
+              i18n-labelText labelText="Transcoding enabled"
+              i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
             ></my-peertube-checkbox>
           </div>
 
-          <div class="form-group">
-            <label i18n for="transcodingThreads">Transcoding threads</label>
-            <div class="peertube-select-container">
-              <select id="transcodingThreads" formControlName="transcodingThreads">
-                <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
-                  {{ transcodingThreadOption.label }}
-                </option>
-              </select>
+          <ng-container *ngIf="isTranscodingEnabled()">
+
+            <div class="form-group">
+              <my-peertube-checkbox
+                inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
+                i18n-labelText labelText="Allow additional extensions"
+                i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
+              ></my-peertube-checkbox>
             </div>
-            <div *ngIf="formErrors.transcodingThreads" class="form-error">
-              {{ formErrors.transcodingThreads }}
+
+            <div class="form-group">
+              <label i18n for="transcodingThreads">Transcoding threads</label>
+              <div class="peertube-select-container">
+                <select id="transcodingThreads" formControlName="threads">
+                  <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
+                    {{ transcodingThreadOption.label }}
+                  </option>
+                </select>
+              </div>
+              <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
             </div>
-          </div>
 
-          <div class="form-group" *ngFor="let resolution of resolutions">
-            <my-peertube-checkbox
-              [inputName]="getResolutionKey(resolution)" [formControlName]="getResolutionKey(resolution)"
-              i18n-labelText labelText="Resolution {{resolution}} enabled"
-            ></my-peertube-checkbox>
-          </div>
-        </ng-template>
+            <ng-container formGroupName="resolutions">
+              <div class="form-group" *ngFor="let resolution of resolutions">
+                <my-peertube-checkbox
+                  [inputName]="getResolutionKey(resolution)" [formControlName]="resolution"
+                  i18n-labelText labelText="Resolution {{resolution}} enabled"
+                ></my-peertube-checkbox>
+              </div>
+            </ng-container>
+
+          </ng-container>
+        </ng-container>
 
         <div i18n class="inner-form-title">
           Cache
           ></my-help>
         </div>
 
-        <div class="form-group">
-          <label i18n for="cachePreviewsSize">Previews cache size</label>
-          <input
-            type="text" id="cachePreviewsSize"
-            formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
-          >
-          <div *ngIf="formErrors.cachePreviewsSize" class="form-error">
-            {{ formErrors.cachePreviewsSize }}
+        <ng-container formGroupName="cache">
+          <div class="form-group" formGroupName="previews">
+            <label i18n for="cachePreviewsSize">Previews cache size</label>
+            <input
+              type="text" id="cachePreviewsSize"
+              formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
+            >
+            <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
           </div>
-        </div>
 
-        <div class="form-group">
-          <label i18n for="cachePreviewsSize">Video captions cache size</label>
-          <input
-            type="text" id="cacheCaptionsSize"
-            formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }"
-          >
-          <div *ngIf="formErrors.cacheCaptionsSize" class="form-error">
-            {{ formErrors.cacheCaptionsSize }}
+          <div class="form-group" formGroupName="captions">
+            <label i18n for="cacheCaptionsSize">Video captions cache size</label>
+            <input
+              type="text" id="cacheCaptionsSize"
+              formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
+            >
+            <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
           </div>
-        </div>
+        </ng-container>
 
         <div i18n class="inner-form-title">Customizations</div>
 
-        <div class="form-group">
-          <label i18n for="customizationJavascript">JavaScript</label>
-          <my-help
-            helpType="custom" i18n-customHtml
-            customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
-          ></my-help>
-          <textarea
-            id="customizationJavascript" formControlName="customizationJavascript"
-            [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }"
-          ></textarea>
-          <div *ngIf="formErrors.customizationJavascript" class="form-error">
-            {{ formErrors.customizationJavascript }}
-          </div>
-        </div>
+        <ng-container formGroupName="instance">
+          <ng-container formGroupName="customizations">
+            <div class="form-group">
+              <label i18n for="customizationJavascript">JavaScript</label>
+              <my-help
+                helpType="custom" i18n-customHtml
+                customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
+              ></my-help>
+              <textarea
+                id="customizationJavascript" formControlName="javascript"
+                [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
+              ></textarea>
+              <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
+            </div>
+
+            <div class="form-group">
+              <label for="customizationCSS">CSS</label>
+              <my-help
+                  helpType="custom"
+                  i18n-customHtml
+                  customHtml="
+                    Write directly CSS code. Example:<br />
+                    <pre>
+          body {{ '{' }}
+            background-color: red;
+          {{ '}' }}
+                    </pre>
+
+                    Prepend with <em>#custom-css</em> to override styles. Example:
+                    <pre>
+          #custom-css .logged-in-email {{ '{' }}
+            color: red;
+          {{ '}' }}
+                    </pre>
+                  "
+              ></my-help>
+              <textarea
+                id="customizationCSS" formControlName="css"
+                [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
+              ></textarea>
+              <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
+            </div>
+          </ng-container>
+        </ng-container>
 
-        <div class="form-group">
-          <label for="customizationCSS">CSS</label>
-          <my-help
-              helpType="custom"
-              i18n-customHtml
-              customHtml="
-                Write directly CSS code. Example:<br />
-                <pre>
-      body {{ '{' }}
-        background-color: red;
-      {{ '}' }}
-                </pre>
-
-                Prepend with <em>#custom-css</em> to override styles. Example:
-                <pre>
-      #custom-css .logged-in-email {{ '{' }}
-        color: red;
-      {{ '}' }}
-                </pre>
-              "
-          ></my-help>
-          <textarea
-            id="customizationCSS" formControlName="customizationCSS"
-            [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
-          ></textarea>
-          <div *ngIf="formErrors.customizationCSS" class="form-error">
-            {{ formErrors.customizationCSS }}
-          </div>
-        </div>
       </ng-template>
     </ngb-tab>
   </ngb-tabset>
index ee877ee31e328ced59439cd3c48e2f4a7b2437e1..654a076b0d242088c3bc2567ac4eb7ae3990a90b 100644 (file)
@@ -18,9 +18,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
   resolutions: string[] = []
   transcodingThreadOptions: { label: string, value: number }[] = []
 
-  private oldCustomJavascript: string
-  private oldCustomCSS: string
-
   constructor (
     protected formValidatorService: FormValidatorService,
     private customConfigValidatorsService: CustomConfigValidatorsService,
@@ -58,41 +55,78 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
   }
 
   getResolutionKey (resolution: string) {
-    return 'transcodingResolution' + resolution
+    return 'transcoding.resolutions.' + resolution
   }
 
   ngOnInit () {
-    const formGroupData: { [key: string]: any } = {
-      instanceName: this.customConfigValidatorsService.INSTANCE_NAME,
-      instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
-      instanceDescription: null,
-      instanceTerms: null,
-      instanceDefaultClientRoute: null,
-      instanceDefaultNSFWPolicy: null,
-      servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
-      servicesTwitterWhitelisted: null,
-      cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE,
-      cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE,
-      signupEnabled: null,
-      signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT,
-      signupRequiresEmailVerification: null,
-      importVideosHttpEnabled: null,
-      importVideosTorrentEnabled: null,
-      adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
-      userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
-      userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY,
-      transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS,
-      transcodingAllowAdditionalExtensions: null,
-      transcodingEnabled: null,
-      customizationJavascript: null,
-      customizationCSS: null
+    const formGroupData: { [key in keyof CustomConfig ]: any } = {
+      instance: {
+        name: this.customConfigValidatorsService.INSTANCE_NAME,
+        shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
+        description: null,
+        terms: null,
+        defaultClientRoute: null,
+        defaultNSFWPolicy: null,
+        customizations: {
+          javascript: null,
+          css: null
+        }
+      },
+      services: {
+        twitter: {
+          username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
+          whitelisted: null
+        }
+      },
+      cache: {
+        previews: {
+          size: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE
+        },
+        captions: {
+          size: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE
+        }
+      },
+      signup: {
+        enabled: null,
+        limit: this.customConfigValidatorsService.SIGNUP_LIMIT,
+        requiresEmailVerification: null
+      },
+      import: {
+        videos: {
+          http: {
+            enabled: null
+          },
+          torrent: {
+            enabled: null
+          }
+        }
+      },
+      admin: {
+        email: this.customConfigValidatorsService.ADMIN_EMAIL
+      },
+      contactForm: {
+        enabled: null
+      },
+      user: {
+        videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
+        videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY
+      },
+      transcoding: {
+        enabled: null,
+        threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
+        allowAdditionalExtensions: null,
+        resolutions: {}
+      }
     }
 
-    const defaultValues: BuildFormDefaultValues = {}
+    const defaultValues = {
+      transcoding: {
+        resolutions: {}
+      }
+    }
     for (const resolution of this.resolutions) {
-      const key = this.getResolutionKey(resolution)
-      defaultValues[key] = 'false'
-      formGroupData[key] = null
+      defaultValues.transcoding.resolutions[resolution] = 'false'
+      formGroupData.transcoding.resolutions[resolution] = null
     }
 
     this.buildForm(formGroupData)
@@ -102,9 +136,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
         res => {
           this.customConfig = res
 
-          this.oldCustomCSS = this.customConfig.instance.customizations.css
-          this.oldCustomJavascript = this.customConfig.instance.customizations.javascript
-
           this.updateForm()
           // Force form validation
           this.forceCheck()
@@ -115,78 +146,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
   }
 
   isTranscodingEnabled () {
-    return this.form.value['transcodingEnabled'] === true
+    return this.form.value['transcoding']['enabled'] === true
   }
 
   isSignupEnabled () {
-    return this.form.value['signupEnabled'] === true
+    return this.form.value['signup']['enabled'] === true
   }
 
   async formValidated () {
-    const data: CustomConfig = {
-      instance: {
-        name: this.form.value['instanceName'],
-        shortDescription: this.form.value['instanceShortDescription'],
-        description: this.form.value['instanceDescription'],
-        terms: this.form.value['instanceTerms'],
-        defaultClientRoute: this.form.value['instanceDefaultClientRoute'],
-        defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'],
-        customizations: {
-          javascript: this.form.value['customizationJavascript'],
-          css: this.form.value['customizationCSS']
-        }
-      },
-      services: {
-        twitter: {
-          username: this.form.value['servicesTwitterUsername'],
-          whitelisted: this.form.value['servicesTwitterWhitelisted']
-        }
-      },
-      cache: {
-        previews: {
-          size: this.form.value['cachePreviewsSize']
-        },
-        captions: {
-          size: this.form.value['cacheCaptionsSize']
-        }
-      },
-      signup: {
-        enabled: this.form.value['signupEnabled'],
-        limit: this.form.value['signupLimit'],
-        requiresEmailVerification: this.form.value['signupRequiresEmailVerification']
-      },
-      admin: {
-        email: this.form.value['adminEmail']
-      },
-      user: {
-        videoQuota: this.form.value['userVideoQuota'],
-        videoQuotaDaily: this.form.value['userVideoQuotaDaily']
-      },
-      transcoding: {
-        enabled: this.form.value['transcodingEnabled'],
-        allowAdditionalExtensions: this.form.value['transcodingAllowAdditionalExtensions'],
-        threads: this.form.value['transcodingThreads'],
-        resolutions: {
-          '240p': this.form.value[this.getResolutionKey('240p')],
-          '360p': this.form.value[this.getResolutionKey('360p')],
-          '480p': this.form.value[this.getResolutionKey('480p')],
-          '720p': this.form.value[this.getResolutionKey('720p')],
-          '1080p': this.form.value[this.getResolutionKey('1080p')]
-        }
-      },
-      import: {
-        videos: {
-          http: {
-            enabled: this.form.value['importVideosHttpEnabled']
-          },
-          torrent: {
-            enabled: this.form.value['importVideosTorrentEnabled']
-          }
-        }
-      }
-    }
-
-    this.configService.updateCustomConfig(data)
+    this.configService.updateCustomConfig(this.form.value)
       .subscribe(
         res => {
           this.customConfig = res
@@ -204,38 +172,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
   }
 
   private updateForm () {
-    const data: { [key: string]: any } = {
-      instanceName: this.customConfig.instance.name,
-      instanceShortDescription: this.customConfig.instance.shortDescription,
-      instanceDescription: this.customConfig.instance.description,
-      instanceTerms: this.customConfig.instance.terms,
-      instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
-      instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
-      servicesTwitterUsername: this.customConfig.services.twitter.username,
-      servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
-      cachePreviewsSize: this.customConfig.cache.previews.size,
-      cacheCaptionsSize: this.customConfig.cache.captions.size,
-      signupEnabled: this.customConfig.signup.enabled,
-      signupLimit: this.customConfig.signup.limit,
-      signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
-      adminEmail: this.customConfig.admin.email,
-      userVideoQuota: this.customConfig.user.videoQuota,
-      userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
-      transcodingThreads: this.customConfig.transcoding.threads,
-      transcodingEnabled: this.customConfig.transcoding.enabled,
-      transcodingAllowAdditionalExtensions: this.customConfig.transcoding.allowAdditionalExtensions,
-      customizationJavascript: this.customConfig.instance.customizations.javascript,
-      customizationCSS: this.customConfig.instance.customizations.css,
-      importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
-      importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled
-    }
-
-    for (const resolution of this.resolutions) {
-      const key = this.getResolutionKey(resolution)
-      data[key] = this.customConfig.transcoding.resolutions[resolution]
-    }
-
-    this.form.patchValue(data)
+    this.form.patchValue(this.customConfig)
   }
 
 }
index 6eccb833698812c9f77b2b32714cdbb45dc21bc0..5351f18d5fd74742fec663f725b12b64b00d4152 100644 (file)
@@ -40,6 +40,9 @@ export class ServerService {
     email: {
       enabled: false
     },
+    contactForm: {
+      enabled: false
+    },
     serverVersion: 'Unknown',
     signup: {
       allowed: false,
index 0bb7d25e6f5c606da996ae66195930b9262f14bc..2d0e8359f890cff19dbf1a1683af80cf7af57f89 100644 (file)
@@ -1,11 +1,9 @@
 import { FormGroup } from '@angular/forms'
 import { BuildFormArgument, BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
 
-export type FormReactiveErrors = { [ id: string ]: string }
+export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
 export type FormReactiveValidationMessages = {
-  [ id: string ]: {
-    [ name: string ]: string
-  }
+  [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
 }
 
 export abstract class FormReactive {
@@ -23,29 +21,49 @@ export abstract class FormReactive {
     this.formErrors = formErrors
     this.validationMessages = validationMessages
 
-    this.form.valueChanges.subscribe(() => this.onValueChanged(false))
+    this.form.valueChanges.subscribe(() => this.onValueChanged(this.form, this.formErrors, this.validationMessages, false))
+  }
+
+  protected forceCheck () {
+    return this.onValueChanged(this.form, this.formErrors, this.validationMessages, true)
+  }
+
+  protected check () {
+    return this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)
   }
 
-  protected onValueChanged (forceCheck = false) {
-    for (const field in this.formErrors) {
+  private onValueChanged (
+    form: FormGroup,
+    formErrors: FormReactiveErrors,
+    validationMessages: FormReactiveValidationMessages,
+    forceCheck = false
+  ) {
+    for (const field of Object.keys(formErrors)) {
+      if (formErrors[field] && typeof formErrors[field] === 'object') {
+        this.onValueChanged(
+          form.controls[field] as FormGroup,
+          formErrors[field] as FormReactiveErrors,
+          validationMessages[field] as FormReactiveValidationMessages,
+          forceCheck
+        )
+        continue
+      }
+
       // clear previous error message (if any)
-      this.formErrors[ field ] = ''
-      const control = this.form.get(field)
+      formErrors[ field ] = ''
+      const control = form.get(field)
 
       if (control.dirty) this.formChanged = true
 
       // Don't care if dirty on force check
       const isDirty = control.dirty || forceCheck === true
       if (control && isDirty && !control.valid) {
-        const messages = this.validationMessages[ field ]
+        const messages = validationMessages[ field ]
         for (const key in control.errors) {
-          this.formErrors[ field ] += messages[ key ] + ' '
+          formErrors[ field ] += messages[ key ] + ' '
         }
       }
     }
   }
 
-  protected forceCheck () {
-    return this.onValueChanged(true)
-  }
 }
index 19a8bef25c6f008f4c030e90144ebdab44adc8be..249fdf1198bb418e6de8464ec4a9dfa856b6c111 100644 (file)
@@ -7,10 +7,10 @@ export type BuildFormValidator = {
   MESSAGES: { [ name: string ]: string }
 }
 export type BuildFormArgument = {
-  [ id: string ]: BuildFormValidator
+  [ id: string ]: BuildFormValidator | BuildFormArgument
 }
 export type BuildFormDefaultValues = {
-  [ name: string ]: string | string[]
+  [ name: string ]: string | string[] | BuildFormDefaultValues
 }
 
 @Injectable()
@@ -29,7 +29,16 @@ export class FormValidatorService {
       formErrors[name] = ''
 
       const field = obj[name]
-      if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES
+      if (this.isRecursiveField(field)) {
+        const result = this.buildForm(field as BuildFormArgument, defaultValues[name] as BuildFormDefaultValues)
+        group[name] = result.form
+        formErrors[name] = result.formErrors
+        validationMessages[name] = result.validationMessages
+
+        continue
+      }
+
+      if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string }
 
       const defaultValue = defaultValues[name] || ''
 
@@ -52,13 +61,27 @@ export class FormValidatorService {
       formErrors[name] = ''
 
       const field = obj[name]
-      if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES
+      if (this.isRecursiveField(field)) {
+        this.updateForm(
+          form[name],
+          formErrors[name] as FormReactiveErrors,
+          validationMessages[name] as FormReactiveValidationMessages,
+          obj[name] as BuildFormArgument,
+          defaultValues[name] as BuildFormDefaultValues
+        )
+        continue
+      }
+
+      if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string }
 
       const defaultValue = defaultValues[name] || ''
 
-      if (field && field.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS))
+      if (field && field.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS as ValidatorFn[]))
       else form.addControl(name, new FormControl(defaultValue))
     }
   }
 
+  private isRecursiveField (field: any) {
+    return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS
+  }
 }
index 6a5c3b1fa5f5bc0fb1e39ca6e281cd2a2e16cb8f..047e53fabd44b282fbc8f1364ae9aaedb7a753c3 100644 (file)
@@ -12,7 +12,7 @@
 }
 
 /deep/ {
-  .popover-help.popover {
+  .help-popover {
     max-width: 300px;
 
     .popover-body {
index 49722ce401dd3bc1ae580455c247565d3f8d4027..ba2a45df136897c9dbe95e9dad32be4437cdd643 100644 (file)
@@ -29,7 +29,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
   }
 
   onValidKey () {
-    this.onValueChanged()
+    this.check()
     if (!this.form.valid) return
 
     this.formValidated()
index 6b7e62042b4608e6e37dfbe91aff1605f6ef819d..fd85c28f2cb77c1af01ed456fa3ac8149b3e7eee 100644 (file)
@@ -70,7 +70,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
   }
 
   onValidKey () {
-    this.onValueChanged()
+    this.check()
     if (!this.form.valid) return
 
     this.formValidated()