aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-01-10 09:58:08 +0100
committerChocobozzz <me@florianbigard.com>2019-01-10 11:32:38 +0100
commit3866f1a02f73665541468fbadcc3cd2cc459aef2 (patch)
treefc653d6a43fad579b4de3f0628b07a5cdf80aa76 /client
parenta4101923e699e49ceb9ff36e971c75417fafc9f0 (diff)
downloadPeerTube-3866f1a02f73665541468fbadcc3cd2cc459aef2.tar.gz
PeerTube-3866f1a02f73665541468fbadcc3cd2cc459aef2.tar.zst
PeerTube-3866f1a02f73665541468fbadcc3cd2cc459aef2.zip
Add contact form checkbox in admin form
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html495
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts203
-rw-r--r--client/src/app/core/server/server.service.ts3
-rw-r--r--client/src/app/shared/forms/form-reactive.ts46
-rw-r--r--client/src/app/shared/forms/form-validators/form-validator.service.ts33
-rw-r--r--client/src/app/shared/misc/help.component.scss2
-rw-r--r--client/src/app/shared/user-subscription/remote-subscribe.component.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts2
8 files changed, 387 insertions, 399 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 6ece7e8bc..52eb00d93 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -7,169 +7,169 @@
7 7
8 <div i18n class="inner-form-title">Instance</div> 8 <div i18n class="inner-form-title">Instance</div>
9 9
10 <div class="form-group"> 10 <ng-container formGroupName="instance">
11 <label i18n for="instanceName">Name</label> 11 <div class="form-group">
12 <input 12 <label i18n for="instanceName">Name</label>
13 type="text" id="instanceName" 13 <input
14 formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }" 14 type="text" id="instanceName"
15 > 15 formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
16 <div *ngIf="formErrors.instanceName" class="form-error"> 16 >
17 {{ formErrors.instanceName }} 17 <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
18 </div> 18 </div>
19 </div>
20 19
21 <div class="form-group"> 20 <div class="form-group">
22 <label i18n for="instanceShortDescription">Short description</label> 21 <label i18n for="instanceShortDescription">Short description</label>
23 <textarea 22 <textarea
24 id="instanceShortDescription" formControlName="instanceShortDescription" 23 id="instanceShortDescription" formControlName="shortDescription"
25 [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }" 24 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
26 ></textarea> 25 ></textarea>
27 <div *ngIf="formErrors.instanceShortDescription" class="form-error"> 26 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
28 {{ formErrors.instanceShortDescription }}
29 </div> 27 </div>
30 </div>
31 28
32 <div class="form-group"> 29 <div class="form-group">
33 <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help> 30 <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
34 <my-markdown-textarea 31 <my-markdown-textarea
35 id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true" 32 id="instanceDescription" formControlName="description" textareaWidth="500px" [previewColumn]="true"
36 [classes]="{ 'input-error': formErrors['instanceDescription'] }" 33 [classes]="{ 'input-error': formErrors['instance.description'] }"
37 ></my-markdown-textarea> 34 ></my-markdown-textarea>
38 <div *ngIf="formErrors.instanceDescription" class="form-error"> 35 <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
39 {{ formErrors.instanceDescription }}
40 </div> 36 </div>
41 </div>
42 37
43 <div class="form-group"> 38 <div class="form-group">
44 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> 39 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
45 <my-markdown-textarea 40 <my-markdown-textarea
46 id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true" 41 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
47 [ngClass]="{ 'input-error': formErrors['instanceTerms'] }" 42 [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
48 ></my-markdown-textarea> 43 ></my-markdown-textarea>
49 <div *ngIf="formErrors.instanceTerms" class="form-error"> 44 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
50 {{ formErrors.instanceTerms }}
51 </div> 45 </div>
52 </div>
53 46
54 <div class="form-group"> 47 <div class="form-group">
55 <label i18n for="instanceDefaultClientRoute">Default client route</label> 48 <label i18n for="instanceDefaultClientRoute">Default client route</label>
56 <div class="peertube-select-container"> 49 <div class="peertube-select-container">
57 <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute"> 50 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
58 <option i18n value="/videos/overview">Videos Overview</option> 51 <option i18n value="/videos/overview">Videos Overview</option>
59 <option i18n value="/videos/trending">Videos Trending</option> 52 <option i18n value="/videos/trending">Videos Trending</option>
60 <option i18n value="/videos/recently-added">Videos Recently Added</option> 53 <option i18n value="/videos/recently-added">Videos Recently Added</option>
61 <option i18n value="/videos/local">Local videos</option> 54 <option i18n value="/videos/local">Local videos</option>
62 </select> 55 </select>
56 </div>
57 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
63 </div> 58 </div>
64 <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error"> 59
65 {{ formErrors.instanceDefaultClientRoute }} 60 <div class="form-group">
61 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
62 <my-help
63 helpType="custom" i18n-customHtml
64 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
65 ></my-help>
66
67 <div class="peertube-select-container">
68 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
69 <option i18n value="do_not_list">Do not list</option>
70 <option i18n value="blur">Blur thumbnails</option>
71 <option i18n value="display">Display</option>
72 </select>
73 </div>
74 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
66 </div> 75 </div>
67 </div> 76 </ng-container>
68 77
69 <div class="form-group"> 78 <div i18n class="inner-form-title">Signup</div>
70 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
71 <my-help
72 helpType="custom" i18n-customHtml
73 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
74 ></my-help>
75 79
76 <div class="peertube-select-container"> 80 <ng-container formGroupName="signup">
77 <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy"> 81 <div class="form-group">
78 <option i18n value="do_not_list">Do not list</option> 82 <my-peertube-checkbox
79 <option i18n value="blur">Blur thumbnails</option> 83 inputName="signupEnabled" formControlName="enabled"
80 <option i18n value="display">Display</option> 84 i18n-labelText labelText="Signup enabled"
81 </select> 85 ></my-peertube-checkbox>
82 </div> 86 </div>
83 <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error"> 87
84 {{ formErrors.instanceDefaultNSFWPolicy }} 88 <div class="form-group">
89 <my-peertube-checkbox *ngIf="isSignupEnabled()"
90 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
91 i18n-labelText labelText="Signup requires email verification"
92 ></my-peertube-checkbox>
85 </div> 93 </div>
86 </div>
87 94
88 <div i18n class="inner-form-title">Signup</div> 95 <div *ngIf="isSignupEnabled()" class="form-group">
96 <label i18n for="signupLimit">Signup limit</label>
97 <input
98 type="text" id="signupLimit"
99 formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
100 >
101 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
102 </div>
103 </ng-container>
89 104
90 <div class="form-group"> 105 <div i18n class="inner-form-title">Users</div>
91 <my-peertube-checkbox
92 inputName="signupEnabled" formControlName="signupEnabled"
93 i18n-labelText labelText="Signup enabled"
94 ></my-peertube-checkbox>
95 </div>
96 106
97 <div class="form-group"> 107 <ng-container formGroupName="user">
98 <my-peertube-checkbox *ngIf="isSignupEnabled()" 108 <div class="form-group">
99 inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification" 109 <label i18n for="userVideoQuota">User default video quota</label>
100 i18n-labelText labelText="Signup requires email verification" 110 <div class="peertube-select-container">
101 ></my-peertube-checkbox> 111 <select id="userVideoQuota" formControlName="videoQuota">
102 </div> 112 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
113 {{ videoQuotaOption.label }}
114 </option>
115 </select>
116 </div>
117 <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
118 </div>
103 119
104 <div *ngIf="isSignupEnabled()" class="form-group"> 120 <div class="form-group">
105 <label i18n for="signupLimit">Signup limit</label> 121 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
106 <input 122 <div class="peertube-select-container">
107 type="text" id="signupLimit" 123 <select id="userVideoQuotaDaily" formControlName="videoQuotaDaily">
108 formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }" 124 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
109 > 125 {{ videoQuotaDailyOption.label }}
110 <div *ngIf="formErrors.signupLimit" class="form-error"> 126 </option>
111 {{ formErrors.signupLimit }} 127 </select>
128 </div>
129 <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
112 </div> 130 </div>
113 </div> 131 </ng-container>
114 132
115 <div i18n class="inner-form-title">Import</div> 133 <div i18n class="inner-form-title">Import</div>
116 134
117 <div class="form-group"> 135 <ng-container formGroupName="import">
118 <my-peertube-checkbox 136 <ng-container formGroupName="videos">
119 inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled"
120 i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
121 ></my-peertube-checkbox>
122 </div>
123 137
124 <div class="form-group"> 138 <div class="form-group" formGroupName="http">
125 <my-peertube-checkbox 139 <my-peertube-checkbox
126 inputName="importVideosTorrentEnabled" formControlName="importVideosTorrentEnabled" 140 inputName="importVideosHttpEnabled" formControlName="enabled"
127 i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled" 141 i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
128 ></my-peertube-checkbox> 142 ></my-peertube-checkbox>
129 </div> 143 </div>
144
145 <div class="form-group" formGroupName="torrent">
146 <my-peertube-checkbox
147 inputName="importVideosTorrentEnabled" formControlName="enabled"
148 i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
149 ></my-peertube-checkbox>
150 </div>
151
152 </ng-container>
153 </ng-container>
130 154
131 <div i18n class="inner-form-title">Administrator</div> 155 <div i18n class="inner-form-title">Administrator</div>
132 156
133 <div class="form-group"> 157 <div class="form-group" formGroupName="admin">
134 <label i18n for="adminEmail">Admin email</label> 158 <label i18n for="adminEmail">Admin email</label>
135 <input 159 <input
136 type="text" id="adminEmail" 160 type="text" id="adminEmail"
137 formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }" 161 formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
138 > 162 >
139 <div *ngIf="formErrors.adminEmail" class="form-error"> 163 <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
140 {{ formErrors.adminEmail }}
141 </div>
142 </div> 164 </div>
143 165
144 <div i18n class="inner-form-title">Users</div> 166 <div class="form-group" formGroupName="contactForm">
145 167 <my-peertube-checkbox
146 <div class="form-group"> 168 inputName="enableContactForm" formControlName="enabled"
147 <label i18n for="userVideoQuota">User default video quota</label> 169 i18n-labelText labelText="Enable contact form"
148 <div class="peertube-select-container"> 170 ></my-peertube-checkbox>
149 <select id="userVideoQuota" formControlName="userVideoQuota">
150 <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
151 {{ videoQuotaOption.label }}
152 </option>
153 </select>
154 </div>
155 <div *ngIf="formErrors.userVideoQuota" class="form-error">
156 {{ formErrors.userVideoQuota }}
157 </div>
158 </div> 171 </div>
159 172
160 <div class="form-group">
161 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
162 <div class="peertube-select-container">
163 <select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily">
164 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
165 {{ videoQuotaDailyOption.label }}
166 </option>
167 </select>
168 </div>
169 <div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
170 {{ formErrors.userVideoQuotaDaily }}
171 </div>
172 </div>
173 </ng-template> 173 </ng-template>
174 </ngb-tab> 174 </ngb-tab>
175 175
@@ -177,30 +177,35 @@
177 <ng-template ngbTabContent> 177 <ng-template ngbTabContent>
178 <div i18n class="inner-form-title">Twitter</div> 178 <div i18n class="inner-form-title">Twitter</div>
179 179
180 <div class="form-group"> 180 <ng-container formGroupName="services">
181 <label i18n for="signupLimit">Your Twitter username</label> 181 <ng-container formGroupName="twitter">
182 <my-help 182
183 helpType="custom" i18n-customHtml 183 <div class="form-group">
184 customHtml="Indicates the Twitter account for the website or platform on which the content was published." 184 <label i18n for="signupLimit">Your Twitter username</label>
185 ></my-help> 185 <my-help
186 <input 186 helpType="custom" i18n-customHtml
187 type="text" id="servicesTwitterUsername" 187 customHtml="Indicates the Twitter account for the website or platform on which the content was published."
188 formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }" 188 ></my-help>
189 > 189 <input
190 <div *ngIf="formErrors.servicesTwitterUsername" class="form-error"> 190 type="text" id="servicesTwitterUsername"
191 {{ formErrors.servicesTwitterUsername }} 191 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
192 </div> 192 >
193 </div> 193 <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
194 </div>
195
196 <div class="form-group">
197 <my-peertube-checkbox
198 inputName="servicesTwitterWhitelisted" formControlName="whitelisted"
199 i18n-labelText labelText="Instance whitelisted by Twitter"
200 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 />
201 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
202 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."
203 ></my-peertube-checkbox>
204 </div>
205
206 </ng-container>
207 </ng-container>
194 208
195 <div class="form-group">
196 <my-peertube-checkbox
197 inputName="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted"
198 i18n-labelText labelText="Instance whitelisted by Twitter"
199 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 />
200 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
201 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."
202 ></my-peertube-checkbox>
203 </div>
204 </ng-template> 209 </ng-template>
205 </ngb-tab> 210 </ngb-tab>
206 211
@@ -209,45 +214,48 @@
209 214
210 <div i18n class="inner-form-title">Transcoding</div> 215 <div i18n class="inner-form-title">Transcoding</div>
211 216
212 <div class="form-group"> 217 <ng-container formGroupName="transcoding">
213 <my-peertube-checkbox
214 inputName="transcodingEnabled" formControlName="transcodingEnabled"
215 i18n-labelText labelText="Transcoding enabled"
216 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
217 ></my-peertube-checkbox>
218 </div>
219
220 <ng-template [ngIf]="isTranscodingEnabled()">
221
222 <div class="form-group"> 218 <div class="form-group">
223 <my-peertube-checkbox 219 <my-peertube-checkbox
224 inputName="transcodingAllowAdditionalExtensions" formControlName="transcodingAllowAdditionalExtensions" 220 inputName="transcodingEnabled" formControlName="enabled"
225 i18n-labelText labelText="Allow additional extensions" 221 i18n-labelText labelText="Transcoding enabled"
226 i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos" 222 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
227 ></my-peertube-checkbox> 223 ></my-peertube-checkbox>
228 </div> 224 </div>
229 225
230 <div class="form-group"> 226 <ng-container *ngIf="isTranscodingEnabled()">
231 <label i18n for="transcodingThreads">Transcoding threads</label> 227
232 <div class="peertube-select-container"> 228 <div class="form-group">
233 <select id="transcodingThreads" formControlName="transcodingThreads"> 229 <my-peertube-checkbox
234 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value"> 230 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
235 {{ transcodingThreadOption.label }} 231 i18n-labelText labelText="Allow additional extensions"
236 </option> 232 i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
237 </select> 233 ></my-peertube-checkbox>
238 </div> 234 </div>
239 <div *ngIf="formErrors.transcodingThreads" class="form-error"> 235
240 {{ formErrors.transcodingThreads }} 236 <div class="form-group">
237 <label i18n for="transcodingThreads">Transcoding threads</label>
238 <div class="peertube-select-container">
239 <select id="transcodingThreads" formControlName="threads">
240 <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
241 {{ transcodingThreadOption.label }}
242 </option>
243 </select>
244 </div>
245 <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
241 </div> 246 </div>
242 </div>
243 247
244 <div class="form-group" *ngFor="let resolution of resolutions"> 248 <ng-container formGroupName="resolutions">
245 <my-peertube-checkbox 249 <div class="form-group" *ngFor="let resolution of resolutions">
246 [inputName]="getResolutionKey(resolution)" [formControlName]="getResolutionKey(resolution)" 250 <my-peertube-checkbox
247 i18n-labelText labelText="Resolution {{resolution}} enabled" 251 [inputName]="getResolutionKey(resolution)" [formControlName]="resolution"
248 ></my-peertube-checkbox> 252 i18n-labelText labelText="Resolution {{resolution}} enabled"
249 </div> 253 ></my-peertube-checkbox>
250 </ng-template> 254 </div>
255 </ng-container>
256
257 </ng-container>
258 </ng-container>
251 259
252 <div i18n class="inner-form-title"> 260 <div i18n class="inner-form-title">
253 Cache 261 Cache
@@ -258,74 +266,73 @@
258 ></my-help> 266 ></my-help>
259 </div> 267 </div>
260 268
261 <div class="form-group"> 269 <ng-container formGroupName="cache">
262 <label i18n for="cachePreviewsSize">Previews cache size</label> 270 <div class="form-group" formGroupName="previews">
263 <input 271 <label i18n for="cachePreviewsSize">Previews cache size</label>
264 type="text" id="cachePreviewsSize" 272 <input
265 formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }" 273 type="text" id="cachePreviewsSize"
266 > 274 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
267 <div *ngIf="formErrors.cachePreviewsSize" class="form-error"> 275 >
268 {{ formErrors.cachePreviewsSize }} 276 <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
269 </div> 277 </div>
270 </div>
271 278
272 <div class="form-group"> 279 <div class="form-group" formGroupName="captions">
273 <label i18n for="cachePreviewsSize">Video captions cache size</label> 280 <label i18n for="cacheCaptionsSize">Video captions cache size</label>
274 <input 281 <input
275 type="text" id="cacheCaptionsSize" 282 type="text" id="cacheCaptionsSize"
276 formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }" 283 formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
277 > 284 >
278 <div *ngIf="formErrors.cacheCaptionsSize" class="form-error"> 285 <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
279 {{ formErrors.cacheCaptionsSize }}
280 </div> 286 </div>
281 </div> 287 </ng-container>
282 288
283 <div i18n class="inner-form-title">Customizations</div> 289 <div i18n class="inner-form-title">Customizations</div>
284 290
285 <div class="form-group"> 291 <ng-container formGroupName="instance">
286 <label i18n for="customizationJavascript">JavaScript</label> 292 <ng-container formGroupName="customizations">
287 <my-help 293 <div class="form-group">
288 helpType="custom" i18n-customHtml 294 <label i18n for="customizationJavascript">JavaScript</label>
289 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>" 295 <my-help
290 ></my-help> 296 helpType="custom" i18n-customHtml
291 <textarea 297 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
292 id="customizationJavascript" formControlName="customizationJavascript" 298 ></my-help>
293 [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }" 299 <textarea
294 ></textarea> 300 id="customizationJavascript" formControlName="javascript"
295 <div *ngIf="formErrors.customizationJavascript" class="form-error"> 301 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
296 {{ formErrors.customizationJavascript }} 302 ></textarea>
297 </div> 303 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
298 </div> 304 </div>
305
306 <div class="form-group">
307 <label for="customizationCSS">CSS</label>
308 <my-help
309 helpType="custom"
310 i18n-customHtml
311 customHtml="
312 Write directly CSS code. Example:<br />
313 <pre>
314 body {{ '{' }}
315 background-color: red;
316 {{ '}' }}
317 </pre>
318
319 Prepend with <em>#custom-css</em> to override styles. Example:
320 <pre>
321 #custom-css .logged-in-email {{ '{' }}
322 color: red;
323 {{ '}' }}
324 </pre>
325 "
326 ></my-help>
327 <textarea
328 id="customizationCSS" formControlName="css"
329 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
330 ></textarea>
331 <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
332 </div>
333 </ng-container>
334 </ng-container>
299 335
300 <div class="form-group">
301 <label for="customizationCSS">CSS</label>
302 <my-help
303 helpType="custom"
304 i18n-customHtml
305 customHtml="
306 Write directly CSS code. Example:<br />
307 <pre>
308 body {{ '{' }}
309 background-color: red;
310 {{ '}' }}
311 </pre>
312
313 Prepend with <em>#custom-css</em> to override styles. Example:
314 <pre>
315 #custom-css .logged-in-email {{ '{' }}
316 color: red;
317 {{ '}' }}
318 </pre>
319 "
320 ></my-help>
321 <textarea
322 id="customizationCSS" formControlName="customizationCSS"
323 [ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
324 ></textarea>
325 <div *ngIf="formErrors.customizationCSS" class="form-error">
326 {{ formErrors.customizationCSS }}
327 </div>
328 </div>
329 </ng-template> 336 </ng-template>
330 </ngb-tab> 337 </ngb-tab>
331 </ngb-tabset> 338 </ngb-tabset>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index ee877ee31..654a076b0 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -18,9 +18,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
18 resolutions: string[] = [] 18 resolutions: string[] = []
19 transcodingThreadOptions: { label: string, value: number }[] = [] 19 transcodingThreadOptions: { label: string, value: number }[] = []
20 20
21 private oldCustomJavascript: string
22 private oldCustomCSS: string
23
24 constructor ( 21 constructor (
25 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
26 private customConfigValidatorsService: CustomConfigValidatorsService, 23 private customConfigValidatorsService: CustomConfigValidatorsService,
@@ -58,41 +55,78 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
58 } 55 }
59 56
60 getResolutionKey (resolution: string) { 57 getResolutionKey (resolution: string) {
61 return 'transcodingResolution' + resolution 58 return 'transcoding.resolutions.' + resolution
62 } 59 }
63 60
64 ngOnInit () { 61 ngOnInit () {
65 const formGroupData: { [key: string]: any } = { 62 const formGroupData: { [key in keyof CustomConfig ]: any } = {
66 instanceName: this.customConfigValidatorsService.INSTANCE_NAME, 63 instance: {
67 instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, 64 name: this.customConfigValidatorsService.INSTANCE_NAME,
68 instanceDescription: null, 65 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
69 instanceTerms: null, 66 description: null,
70 instanceDefaultClientRoute: null, 67 terms: null,
71 instanceDefaultNSFWPolicy: null, 68 defaultClientRoute: null,
72 servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, 69 defaultNSFWPolicy: null,
73 servicesTwitterWhitelisted: null, 70 customizations: {
74 cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE, 71 javascript: null,
75 cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, 72 css: null
76 signupEnabled: null, 73 }
77 signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, 74 },
78 signupRequiresEmailVerification: null, 75 services: {
79 importVideosHttpEnabled: null, 76 twitter: {
80 importVideosTorrentEnabled: null, 77 username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
81 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, 78 whitelisted: null
82 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 79 }
83 userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, 80 },
84 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, 81 cache: {
85 transcodingAllowAdditionalExtensions: null, 82 previews: {
86 transcodingEnabled: null, 83 size: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE
87 customizationJavascript: null, 84 },
88 customizationCSS: null 85 captions: {
86 size: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE
87 }
88 },
89 signup: {
90 enabled: null,
91 limit: this.customConfigValidatorsService.SIGNUP_LIMIT,
92 requiresEmailVerification: null
93 },
94 import: {
95 videos: {
96 http: {
97 enabled: null
98 },
99 torrent: {
100 enabled: null
101 }
102 }
103 },
104 admin: {
105 email: this.customConfigValidatorsService.ADMIN_EMAIL
106 },
107 contactForm: {
108 enabled: null
109 },
110 user: {
111 videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
112 videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY
113 },
114 transcoding: {
115 enabled: null,
116 threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
117 allowAdditionalExtensions: null,
118 resolutions: {}
119 }
89 } 120 }
90 121
91 const defaultValues: BuildFormDefaultValues = {} 122 const defaultValues = {
123 transcoding: {
124 resolutions: {}
125 }
126 }
92 for (const resolution of this.resolutions) { 127 for (const resolution of this.resolutions) {
93 const key = this.getResolutionKey(resolution) 128 defaultValues.transcoding.resolutions[resolution] = 'false'
94 defaultValues[key] = 'false' 129 formGroupData.transcoding.resolutions[resolution] = null
95 formGroupData[key] = null
96 } 130 }
97 131
98 this.buildForm(formGroupData) 132 this.buildForm(formGroupData)
@@ -102,9 +136,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
102 res => { 136 res => {
103 this.customConfig = res 137 this.customConfig = res
104 138
105 this.oldCustomCSS = this.customConfig.instance.customizations.css
106 this.oldCustomJavascript = this.customConfig.instance.customizations.javascript
107
108 this.updateForm() 139 this.updateForm()
109 // Force form validation 140 // Force form validation
110 this.forceCheck() 141 this.forceCheck()
@@ -115,78 +146,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
115 } 146 }
116 147
117 isTranscodingEnabled () { 148 isTranscodingEnabled () {
118 return this.form.value['transcodingEnabled'] === true 149 return this.form.value['transcoding']['enabled'] === true
119 } 150 }
120 151
121 isSignupEnabled () { 152 isSignupEnabled () {
122 return this.form.value['signupEnabled'] === true 153 return this.form.value['signup']['enabled'] === true
123 } 154 }
124 155
125 async formValidated () { 156 async formValidated () {
126 const data: CustomConfig = { 157 this.configService.updateCustomConfig(this.form.value)
127 instance: {
128 name: this.form.value['instanceName'],
129 shortDescription: this.form.value['instanceShortDescription'],
130 description: this.form.value['instanceDescription'],
131 terms: this.form.value['instanceTerms'],
132 defaultClientRoute: this.form.value['instanceDefaultClientRoute'],
133 defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'],
134 customizations: {
135 javascript: this.form.value['customizationJavascript'],
136 css: this.form.value['customizationCSS']
137 }
138 },
139 services: {
140 twitter: {
141 username: this.form.value['servicesTwitterUsername'],
142 whitelisted: this.form.value['servicesTwitterWhitelisted']
143 }
144 },
145 cache: {
146 previews: {
147 size: this.form.value['cachePreviewsSize']
148 },
149 captions: {
150 size: this.form.value['cacheCaptionsSize']
151 }
152 },
153 signup: {
154 enabled: this.form.value['signupEnabled'],
155 limit: this.form.value['signupLimit'],
156 requiresEmailVerification: this.form.value['signupRequiresEmailVerification']
157 },
158 admin: {
159 email: this.form.value['adminEmail']
160 },
161 user: {
162 videoQuota: this.form.value['userVideoQuota'],
163 videoQuotaDaily: this.form.value['userVideoQuotaDaily']
164 },
165 transcoding: {
166 enabled: this.form.value['transcodingEnabled'],
167 allowAdditionalExtensions: this.form.value['transcodingAllowAdditionalExtensions'],
168 threads: this.form.value['transcodingThreads'],
169 resolutions: {
170 '240p': this.form.value[this.getResolutionKey('240p')],
171 '360p': this.form.value[this.getResolutionKey('360p')],
172 '480p': this.form.value[this.getResolutionKey('480p')],
173 '720p': this.form.value[this.getResolutionKey('720p')],
174 '1080p': this.form.value[this.getResolutionKey('1080p')]
175 }
176 },
177 import: {
178 videos: {
179 http: {
180 enabled: this.form.value['importVideosHttpEnabled']
181 },
182 torrent: {
183 enabled: this.form.value['importVideosTorrentEnabled']
184 }
185 }
186 }
187 }
188
189 this.configService.updateCustomConfig(data)
190 .subscribe( 158 .subscribe(
191 res => { 159 res => {
192 this.customConfig = res 160 this.customConfig = res
@@ -204,38 +172,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
204 } 172 }
205 173
206 private updateForm () { 174 private updateForm () {
207 const data: { [key: string]: any } = { 175 this.form.patchValue(this.customConfig)
208 instanceName: this.customConfig.instance.name,
209 instanceShortDescription: this.customConfig.instance.shortDescription,
210 instanceDescription: this.customConfig.instance.description,
211 instanceTerms: this.customConfig.instance.terms,
212 instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
213 instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
214 servicesTwitterUsername: this.customConfig.services.twitter.username,
215 servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
216 cachePreviewsSize: this.customConfig.cache.previews.size,
217 cacheCaptionsSize: this.customConfig.cache.captions.size,
218 signupEnabled: this.customConfig.signup.enabled,
219 signupLimit: this.customConfig.signup.limit,
220 signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
221 adminEmail: this.customConfig.admin.email,
222 userVideoQuota: this.customConfig.user.videoQuota,
223 userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
224 transcodingThreads: this.customConfig.transcoding.threads,
225 transcodingEnabled: this.customConfig.transcoding.enabled,
226 transcodingAllowAdditionalExtensions: this.customConfig.transcoding.allowAdditionalExtensions,
227 customizationJavascript: this.customConfig.instance.customizations.javascript,
228 customizationCSS: this.customConfig.instance.customizations.css,
229 importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
230 importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled
231 }
232
233 for (const resolution of this.resolutions) {
234 const key = this.getResolutionKey(resolution)
235 data[key] = this.customConfig.transcoding.resolutions[resolution]
236 }
237
238 this.form.patchValue(data)
239 } 176 }
240 177
241} 178}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 6eccb8336..5351f18d5 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -40,6 +40,9 @@ export class ServerService {
40 email: { 40 email: {
41 enabled: false 41 enabled: false
42 }, 42 },
43 contactForm: {
44 enabled: false
45 },
43 serverVersion: 'Unknown', 46 serverVersion: 'Unknown',
44 signup: { 47 signup: {
45 allowed: false, 48 allowed: false,
diff --git a/client/src/app/shared/forms/form-reactive.ts b/client/src/app/shared/forms/form-reactive.ts
index 0bb7d25e6..2d0e8359f 100644
--- a/client/src/app/shared/forms/form-reactive.ts
+++ b/client/src/app/shared/forms/form-reactive.ts
@@ -1,11 +1,9 @@
1import { FormGroup } from '@angular/forms' 1import { FormGroup } from '@angular/forms'
2import { BuildFormArgument, BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 2import { BuildFormArgument, BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
3 3
4export type FormReactiveErrors = { [ id: string ]: string } 4export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
5export type FormReactiveValidationMessages = { 5export type FormReactiveValidationMessages = {
6 [ id: string ]: { 6 [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
7 [ name: string ]: string
8 }
9} 7}
10 8
11export abstract class FormReactive { 9export abstract class FormReactive {
@@ -23,29 +21,49 @@ export abstract class FormReactive {
23 this.formErrors = formErrors 21 this.formErrors = formErrors
24 this.validationMessages = validationMessages 22 this.validationMessages = validationMessages
25 23
26 this.form.valueChanges.subscribe(() => this.onValueChanged(false)) 24 this.form.valueChanges.subscribe(() => this.onValueChanged(this.form, this.formErrors, this.validationMessages, false))
25 }
26
27 protected forceCheck () {
28 return this.onValueChanged(this.form, this.formErrors, this.validationMessages, true)
29 }
30
31 protected check () {
32 return this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)
27 } 33 }
28 34
29 protected onValueChanged (forceCheck = false) { 35 private onValueChanged (
30 for (const field in this.formErrors) { 36 form: FormGroup,
37 formErrors: FormReactiveErrors,
38 validationMessages: FormReactiveValidationMessages,
39 forceCheck = false
40 ) {
41 for (const field of Object.keys(formErrors)) {
42 if (formErrors[field] && typeof formErrors[field] === 'object') {
43 this.onValueChanged(
44 form.controls[field] as FormGroup,
45 formErrors[field] as FormReactiveErrors,
46 validationMessages[field] as FormReactiveValidationMessages,
47 forceCheck
48 )
49 continue
50 }
51
31 // clear previous error message (if any) 52 // clear previous error message (if any)
32 this.formErrors[ field ] = '' 53 formErrors[ field ] = ''
33 const control = this.form.get(field) 54 const control = form.get(field)
34 55
35 if (control.dirty) this.formChanged = true 56 if (control.dirty) this.formChanged = true
36 57
37 // Don't care if dirty on force check 58 // Don't care if dirty on force check
38 const isDirty = control.dirty || forceCheck === true 59 const isDirty = control.dirty || forceCheck === true
39 if (control && isDirty && !control.valid) { 60 if (control && isDirty && !control.valid) {
40 const messages = this.validationMessages[ field ] 61 const messages = validationMessages[ field ]
41 for (const key in control.errors) { 62 for (const key in control.errors) {
42 this.formErrors[ field ] += messages[ key ] + ' ' 63 formErrors[ field ] += messages[ key ] + ' '
43 } 64 }
44 } 65 }
45 } 66 }
46 } 67 }
47 68
48 protected forceCheck () {
49 return this.onValueChanged(true)
50 }
51} 69}
diff --git a/client/src/app/shared/forms/form-validators/form-validator.service.ts b/client/src/app/shared/forms/form-validators/form-validator.service.ts
index 19a8bef25..249fdf119 100644
--- a/client/src/app/shared/forms/form-validators/form-validator.service.ts
+++ b/client/src/app/shared/forms/form-validators/form-validator.service.ts
@@ -7,10 +7,10 @@ export type BuildFormValidator = {
7 MESSAGES: { [ name: string ]: string } 7 MESSAGES: { [ name: string ]: string }
8} 8}
9export type BuildFormArgument = { 9export type BuildFormArgument = {
10 [ id: string ]: BuildFormValidator 10 [ id: string ]: BuildFormValidator | BuildFormArgument
11} 11}
12export type BuildFormDefaultValues = { 12export type BuildFormDefaultValues = {
13 [ name: string ]: string | string[] 13 [ name: string ]: string | string[] | BuildFormDefaultValues
14} 14}
15 15
16@Injectable() 16@Injectable()
@@ -29,7 +29,16 @@ export class FormValidatorService {
29 formErrors[name] = '' 29 formErrors[name] = ''
30 30
31 const field = obj[name] 31 const field = obj[name]
32 if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES 32 if (this.isRecursiveField(field)) {
33 const result = this.buildForm(field as BuildFormArgument, defaultValues[name] as BuildFormDefaultValues)
34 group[name] = result.form
35 formErrors[name] = result.formErrors
36 validationMessages[name] = result.validationMessages
37
38 continue
39 }
40
41 if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string }
33 42
34 const defaultValue = defaultValues[name] || '' 43 const defaultValue = defaultValues[name] || ''
35 44
@@ -52,13 +61,27 @@ export class FormValidatorService {
52 formErrors[name] = '' 61 formErrors[name] = ''
53 62
54 const field = obj[name] 63 const field = obj[name]
55 if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES 64 if (this.isRecursiveField(field)) {
65 this.updateForm(
66 form[name],
67 formErrors[name] as FormReactiveErrors,
68 validationMessages[name] as FormReactiveValidationMessages,
69 obj[name] as BuildFormArgument,
70 defaultValues[name] as BuildFormDefaultValues
71 )
72 continue
73 }
74
75 if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string }
56 76
57 const defaultValue = defaultValues[name] || '' 77 const defaultValue = defaultValues[name] || ''
58 78
59 if (field && field.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS)) 79 if (field && field.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS as ValidatorFn[]))
60 else form.addControl(name, new FormControl(defaultValue)) 80 else form.addControl(name, new FormControl(defaultValue))
61 } 81 }
62 } 82 }
63 83
84 private isRecursiveField (field: any) {
85 return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS
86 }
64} 87}
diff --git a/client/src/app/shared/misc/help.component.scss b/client/src/app/shared/misc/help.component.scss
index 6a5c3b1fa..047e53fab 100644
--- a/client/src/app/shared/misc/help.component.scss
+++ b/client/src/app/shared/misc/help.component.scss
@@ -12,7 +12,7 @@
12} 12}
13 13
14/deep/ { 14/deep/ {
15 .popover-help.popover { 15 .help-popover {
16 max-width: 300px; 16 max-width: 300px;
17 17
18 .popover-body { 18 .popover-body {
diff --git a/client/src/app/shared/user-subscription/remote-subscribe.component.ts b/client/src/app/shared/user-subscription/remote-subscribe.component.ts
index 49722ce40..ba2a45df1 100644
--- a/client/src/app/shared/user-subscription/remote-subscribe.component.ts
+++ b/client/src/app/shared/user-subscription/remote-subscribe.component.ts
@@ -29,7 +29,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
29 } 29 }
30 30
31 onValidKey () { 31 onValidKey () {
32 this.onValueChanged() 32 this.check()
33 if (!this.form.valid) return 33 if (!this.form.valid) return
34 34
35 this.formValidated() 35 this.formValidated()
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index 6b7e62042..fd85c28f2 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -70,7 +70,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
70 } 70 }
71 71
72 onValidKey () { 72 onValidKey () {
73 this.onValueChanged() 73 this.check()
74 if (!this.form.valid) return 74 if (!this.form.valid) return
75 75
76 this.formValidated() 76 this.formValidated()