Feature: update user profile (#4678)

This commit is contained in:
shamoon
2023-12-02 08:26:42 -08:00
committed by GitHub
parent 7a98e3c1e7
commit 68e7a5266b
23 changed files with 1048 additions and 75 deletions

View File

@@ -403,11 +403,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">225</context>
<context context-type="linenumber">230</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">228</context>
<context context-type="linenumber">233</context>
</context-group>
</trans-unit>
<trans-unit id="8838884664569764142" datatype="html">
@@ -496,15 +496,15 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">45</context>
<context context-type="linenumber">50</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">203</context>
<context context-type="linenumber">208</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">206</context>
<context context-type="linenumber">211</context>
</context-group>
</trans-unit>
<trans-unit id="1685061484835793745" datatype="html">
@@ -973,7 +973,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">91</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="1595668988802980095" datatype="html">
@@ -1329,6 +1329,10 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">54</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">93</context>
@@ -1396,7 +1400,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
<context context-type="linenumber">119</context>
<context context-type="linenumber">116</context>
</context-group>
</trans-unit>
<trans-unit id="6839066544204061364" datatype="html">
@@ -1428,7 +1432,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">217</context>
<context context-type="linenumber">222</context>
</context-group>
</trans-unit>
<trans-unit id="103921551219467537" datatype="html">
@@ -1630,11 +1634,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">210</context>
<context context-type="linenumber">215</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">213</context>
<context context-type="linenumber">218</context>
</context-group>
</trans-unit>
<trans-unit id="4555457172864212828" datatype="html">
@@ -1780,6 +1784,10 @@
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">89</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">144</context>
</context-group>
</trans-unit>
<trans-unit id="2753185112875184719" datatype="html">
<source>Saved user &quot;<x id="PH" equiv-text="newUser.username"/>&quot;.</source>
@@ -1961,37 +1969,44 @@
<context context-type="linenumber">39</context>
</context-group>
</trans-unit>
<trans-unit id="2127032578120864096" datatype="html">
<source>My Profile</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">45</context>
</context-group>
</trans-unit>
<trans-unit id="3797778920049399855" datatype="html">
<source>Logout</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">50</context>
<context context-type="linenumber">55</context>
</context-group>
</trans-unit>
<trans-unit id="4895326106573044490" datatype="html">
<source>Documentation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">56</context>
<context context-type="linenumber">61</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">232</context>
<context context-type="linenumber">237</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">240</context>
</context-group>
</trans-unit>
<trans-unit id="6570363013146073520" datatype="html">
<source>Dashboard</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">75</context>
<context context-type="linenumber">80</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">78</context>
<context context-type="linenumber">83</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
@@ -2002,11 +2017,11 @@
<source>Documents</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">82</context>
<context context-type="linenumber">87</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">85</context>
<context context-type="linenumber">90</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
@@ -2033,36 +2048,36 @@
<source>Open documents</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">118</context>
<context context-type="linenumber">123</context>
</context-group>
</trans-unit>
<trans-unit id="5687256342387781369" datatype="html">
<source>Close all</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">139</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">137</context>
<context context-type="linenumber">142</context>
</context-group>
</trans-unit>
<trans-unit id="3897348120591552265" datatype="html">
<source>Manage</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">144</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit>
<trans-unit id="7437910965833684826" datatype="html">
<source>Correspondents</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">148</context>
<context context-type="linenumber">153</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">151</context>
<context context-type="linenumber">156</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
@@ -2073,11 +2088,11 @@
<source>Tags</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">155</context>
<context context-type="linenumber">160</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">158</context>
<context context-type="linenumber">163</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context>
@@ -2104,11 +2119,11 @@
<source>Document Types</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">162</context>
<context context-type="linenumber">167</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">165</context>
<context context-type="linenumber">170</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
@@ -2119,11 +2134,11 @@
<source>Storage Paths</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">169</context>
<context context-type="linenumber">174</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">172</context>
<context context-type="linenumber">177</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
@@ -2134,11 +2149,11 @@
<source>Custom Fields</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">176</context>
<context context-type="linenumber">181</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">179</context>
<context context-type="linenumber">184</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context>
@@ -2153,102 +2168,102 @@
<source>Consumption templates</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">188</context>
</context-group>
</trans-unit>
<trans-unit id="5433675495457939071" datatype="html">
<source>Templates</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">186</context>
<context context-type="linenumber">191</context>
</context-group>
</trans-unit>
<trans-unit id="1292737233370901804" datatype="html">
<source>Mail</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">190</context>
<context context-type="linenumber">195</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">193</context>
<context context-type="linenumber">198</context>
</context-group>
</trans-unit>
<trans-unit id="7844706011418789951" datatype="html">
<source>Administration</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">199</context>
<context context-type="linenumber">204</context>
</context-group>
</trans-unit>
<trans-unit id="5537285341303594392" datatype="html">
<source>File Tasks<x id="START_TAG_SPAN_1" ctype="x-span_1" equiv-text="&lt;span *ngIf=&quot;tasksService.failedFileTasks.length &gt; 0&quot;&gt;"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge bg-danger ms-2&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">221</context>
<context context-type="linenumber">226</context>
</context-group>
</trans-unit>
<trans-unit id="1534029177398918729" datatype="html">
<source>GitHub</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">241</context>
<context context-type="linenumber">246</context>
</context-group>
</trans-unit>
<trans-unit id="4112664765954374539" datatype="html">
<source>is available.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">247</context>
<context context-type="linenumber">252</context>
</context-group>
</trans-unit>
<trans-unit id="1175891574282637937" datatype="html">
<source>Click to view.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">247</context>
<context context-type="linenumber">252</context>
</context-group>
</trans-unit>
<trans-unit id="9811291095862612" datatype="html">
<source>Paperless-ngx can automatically check for updates</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">251</context>
<context context-type="linenumber">256</context>
</context-group>
</trans-unit>
<trans-unit id="894819944961861800" datatype="html">
<source> How does this work? </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">258,260</context>
<context context-type="linenumber">263,265</context>
</context-group>
</trans-unit>
<trans-unit id="509090351011426949" datatype="html">
<source>Update available</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">269</context>
<context context-type="linenumber">274</context>
</context-group>
</trans-unit>
<trans-unit id="1542489069631984294" datatype="html">
<source>Sidebar views updated</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
<context context-type="linenumber">247</context>
<context context-type="linenumber">252</context>
</context-group>
</trans-unit>
<trans-unit id="3547923076537026828" datatype="html">
<source>Error updating sidebar views</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
<context context-type="linenumber">250</context>
<context context-type="linenumber">255</context>
</context-group>
</trans-unit>
<trans-unit id="2526035785704676448" datatype="html">
<source>An error occurred while saving update checking settings.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
<context context-type="linenumber">271</context>
<context context-type="linenumber">276</context>
</context-group>
</trans-unit>
<trans-unit id="8700121026680200191" datatype="html">
@@ -2623,6 +2638,10 @@
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">53</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/select-dialog/select-dialog.component.html</context>
<context context-type="linenumber">12</context>
@@ -2843,6 +2862,10 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
<context context-type="linenumber">12</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="4249303448466017578" datatype="html">
<source>Password is token</source>
@@ -3307,6 +3330,10 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="5342432350421167093" datatype="html">
<source>First name</source>
@@ -3314,6 +3341,10 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">28</context>
</context-group>
</trans-unit>
<trans-unit id="3586674587150281199" datatype="html">
<source>Last name</source>
@@ -3321,6 +3352,10 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
<context context-type="linenumber">14</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="8204176479746810612" datatype="html">
<source>Active</source>
@@ -3483,6 +3518,13 @@
<context context-type="linenumber">155</context>
</context-group>
</trans-unit>
<trans-unit id="5554528553553249088" datatype="html">
<source>Show password</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/password/password.component.html</context>
<context context-type="linenumber">5</context>
</context-group>
</trans-unit>
<trans-unit id="594042705136125260" datatype="html">
<source>Edit Permissions</source>
<context-group purpose="location">
@@ -3637,6 +3679,109 @@
<context context-type="linenumber">61</context>
</context-group>
</trans-unit>
<trans-unit id="2984628903434675339" datatype="html">
<source>Edit Profile</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="8214169742072920158" datatype="html">
<source>Confirm Email</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">13</context>
</context-group>
</trans-unit>
<trans-unit id="3241357959735682038" datatype="html">
<source>Confirm Password</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="7554924397178347823" datatype="html">
<source>API Auth Token</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="4323470180912194028" datatype="html">
<source>Copy</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">39</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="595732867213154214" datatype="html">
<source>Regenerate auth token</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="5392341774767336507" datatype="html">
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="4369881772624105142" datatype="html">
<source>Warning: changing the token cannot be undone</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="6141884091799403188" datatype="html">
<source>Emails must match</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">94</context>
</context-group>
</trans-unit>
<trans-unit id="5281933990298241826" datatype="html">
<source>Passwords must match</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="4219429959475101385" datatype="html">
<source>Profile updated successfully</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">141</context>
</context-group>
</trans-unit>
<trans-unit id="3417726855410304962" datatype="html">
<source>Error saving profile</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">153</context>
</context-group>
</trans-unit>
<trans-unit id="154249228726292516" datatype="html">
<source>Error generating auth token</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">170</context>
</context-group>
</trans-unit>
<trans-unit id="3797570084942068182" datatype="html">
<source>Select</source>
<context-group purpose="location">
@@ -3677,13 +3822,6 @@
<context context-type="linenumber">10,12</context>
</context-group>
</trans-unit>
<trans-unit id="4323470180912194028" datatype="html">
<source>Copy</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="7419704019640008953" datatype="html">
<source>Share</source>
<context-group purpose="location">
@@ -3691,13 +3829,6 @@
<context context-type="linenumber">28</context>
</context-group>
</trans-unit>
<trans-unit id="5392341774767336507" datatype="html">
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="6811921365829755679" datatype="html">
<source>Share archive version</source>
<context-group purpose="location">
@@ -3727,7 +3858,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">90</context>
<context context-type="linenumber">93</context>
</context-group>
</trans-unit>
<trans-unit id="8542568275115626925" datatype="html">
@@ -3762,21 +3893,21 @@
<source><x id="PH" equiv-text="days"/> days</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">90</context>
<context context-type="linenumber">93</context>
</context-group>
</trans-unit>
<trans-unit id="2897042887615940599" datatype="html">
<source>Error deleting link</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">117</context>
<context context-type="linenumber">120</context>
</context-group>
</trans-unit>
<trans-unit id="8400747326190565173" datatype="html">
<source>Error creating link</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">145</context>
<context context-type="linenumber">148</context>
</context-group>
</trans-unit>
<trans-unit id="5611592591303869712" datatype="html">

View File

@@ -105,6 +105,7 @@ import { FileDropComponent } from './components/file-drop/file-drop.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
import localeAf from '@angular/common/locales/af'
import localeAr from '@angular/common/locales/ar'
@@ -256,6 +257,7 @@ function initializeApp(settings: SettingsService) {
CustomFieldsComponent,
CustomFieldEditDialogComponent,
CustomFieldsDropdownComponent,
ProfileEditDialogComponent,
],
imports: [
BrowserModule,

View File

@@ -89,7 +89,7 @@ export class UsersAndGroupsComponent
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/`
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
}, 2500)
} else {
this.toastService.showInfo(

View File

@@ -39,6 +39,11 @@
<p class="small mb-0 px-3 text-muted" i18n>Logged in as {{this.settingsService.displayName}}</p>
<div class="dropdown-divider"></div>
</div>
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person"/>
</svg><ng-container i18n>My Profile</ng-container>
</button>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>

View File

@@ -9,7 +9,7 @@ import {
fakeAsync,
tick,
} from '@angular/core/testing'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { BrowserModule } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing'
import { SettingsService } from 'src/app/services/settings.service'
@@ -32,6 +32,7 @@ import { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
const saved_views = [
{
@@ -86,6 +87,7 @@ describe('AppFrameComponent', () => {
let documentListViewService: DocumentListViewService
let router: Router
let savedViewSpy
let modalService: NgbModal
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -98,6 +100,7 @@ describe('AppFrameComponent', () => {
FormsModule,
ReactiveFormsModule,
DragDropModule,
NgbModalModule,
],
providers: [
SettingsService,
@@ -120,6 +123,7 @@ describe('AppFrameComponent', () => {
ToastService,
OpenDocumentsService,
SearchService,
NgbModal,
{
provide: ActivatedRoute,
useValue: {
@@ -148,6 +152,7 @@ describe('AppFrameComponent', () => {
openDocumentsService = TestBed.inject(OpenDocumentsService)
searchService = TestBed.inject(SearchService)
documentListViewService = TestBed.inject(DocumentListViewService)
modalService = TestBed.inject(NgbModal)
router = TestBed.inject(Router)
jest
@@ -363,4 +368,12 @@ describe('AppFrameComponent', () => {
>)
expect(toastSpy).toHaveBeenCalled()
})
it('should support edit profile', () => {
const modalSpy = jest.spyOn(modalService, 'open')
component.editProfile()
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
backdrop: 'static',
})
})
})

View File

@@ -39,6 +39,8 @@ import {
CdkDragDrop,
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
@Component({
selector: 'pngx-app-frame',
@@ -69,6 +71,7 @@ export class AppFrameComponent
public settingsService: SettingsService,
public tasksService: TasksService,
private readonly toastService: ToastService,
private modalService: NgbModal,
permissionsService: PermissionsService
) {
super()
@@ -121,6 +124,13 @@ export class AppFrameComponent
this.isMenuCollapsed = true
}
editProfile() {
this.modalService.open(ProfileEditDialogComponent, {
backdrop: 'static',
})
this.closeMenu()
}
get openDocuments(): PaperlessDocument[] {
return this.openDocumentsService.getOpenDocuments()
}

View File

@@ -1,8 +1,15 @@
<div class="mb-3">
<label class="form-label" [for]="inputId">{{title}}</label>
<input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<div class="input-group" [class.is-invalid]="error">
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<button *ngIf="showReveal" type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#eye" />
</svg>
</button>
</div>
<div class="invalid-feedback">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
</div>

View File

@@ -5,6 +5,7 @@ import {
NG_VALUE_ACCESSOR,
} from '@angular/forms'
import { PasswordComponent } from './password.component'
import { By } from '@angular/platform-browser'
describe('PasswordComponent', () => {
let component: PasswordComponent
@@ -33,4 +34,26 @@ describe('PasswordComponent', () => {
// fixture.detectChanges()
// expect(component.value).toEqual('foo')
})
it('should support toggling field visibility', () => {
expect(input.type).toEqual('password')
component.showReveal = true
fixture.detectChanges()
fixture.debugElement.query(By.css('button')).triggerEventHandler('click')
fixture.detectChanges()
expect(input.type).toEqual('text')
})
it('should empty field if password is obfuscated on focus', () => {
component.value = '*********'
component.onFocus()
expect(component.value).toEqual('')
component.onFocusOut()
expect(component.value).toEqual('**********')
})
it('should disable toggle button if no real password', () => {
component.value = '*********'
expect(component.disableRevealToggle).toBeTruthy()
})
})

View File

@@ -1,4 +1,4 @@
import { Component, forwardRef } from '@angular/core'
import { Component, Input, forwardRef } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@@ -15,7 +15,32 @@ import { AbstractInputComponent } from '../abstract-input'
styleUrls: ['./password.component.scss'],
})
export class PasswordComponent extends AbstractInputComponent<string> {
constructor() {
super()
@Input()
showReveal: boolean = false
@Input()
autocomplete: string
public textVisible: boolean = false
public toggleVisibility(): void {
this.textVisible = !this.textVisible
}
public onFocus() {
if (this.value?.replace(/\*/g, '').length === 0) {
this.writeValue('')
}
}
public onFocusOut() {
if (this.value?.length === 0) {
this.writeValue('**********')
this.onChange(this.value)
}
}
get disableRevealToggle(): boolean {
return this.value?.replace(/\*/g, '').length === 0
}
}

View File

@@ -9,7 +9,7 @@
</button>
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<div class="invalid-feedback position-absolute top-100">
{{error}}

View File

@@ -1,4 +1,4 @@
import { Component, forwardRef } from '@angular/core'
import { Component, Input, forwardRef } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@@ -15,6 +15,9 @@ import { AbstractInputComponent } from '../abstract-input'
styleUrls: ['./text.component.scss'],
})
export class TextComponent extends AbstractInputComponent<string> {
@Input()
autocomplete: string
constructor() {
super()
}

View File

@@ -0,0 +1,56 @@
<form [formGroup]="form" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>Edit Profile</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text>
<div ngbAccordion>
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent">
<div ngbAccordionCollapse>
<div ngbAccordionBody class="p-0 pb-3">
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text>
</div>
</div>
</div>
</div>
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password>
<div ngbAccordion>
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent">
<div ngbAccordionCollapse>
<div ngbAccordionBody class="p-0 pb-3">
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password>
</div>
</div>
</div>
</div>
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
<div class="mb-3">
<label class="form-label" i18n>API Auth Token</label>
<div class="position-relative">
<div class="input-group">
<input type="text" class="form-control" formControlName="auth_token" readonly>
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
<svg class="buttonicon-sm" fill="currentColor">
<use *ngIf="!copied" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
<use *ngIf="copied" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
</svg><span class="visually-hidden" i18n>Copy</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-repeat" />
</svg>
</button>
</div>
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
</div>
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || saveDisabled">Save</button>
</div>
</form>

View File

@@ -0,0 +1,9 @@
::ng-deep {
.accordion-body .mb-3 {
margin: 0 !important; // hack-ish, for animation
}
}
.copied-badge {
right: 8em;
}

View File

@@ -0,0 +1,222 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
import { ProfileService } from 'src/app/services/profile.service'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import {
NgbAccordionModule,
NgbActiveModal,
NgbModal,
NgbModalModule,
} from '@ng-bootstrap/ng-bootstrap'
import { HttpClientModule } from '@angular/common/http'
import { TextComponent } from '../input/text/text.component'
import { PasswordComponent } from '../input/password/password.component'
import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service'
import { Clipboard } from '@angular/cdk/clipboard'
const profile = {
email: 'foo@bar.com',
password: '*********',
first_name: 'foo',
last_name: 'bar',
auth_token: '123456789abcdef',
}
describe('ProfileEditDialogComponent', () => {
let component: ProfileEditDialogComponent
let fixture: ComponentFixture<ProfileEditDialogComponent>
let profileService: ProfileService
let toastService: ToastService
let clipboard: Clipboard
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
ProfileEditDialogComponent,
TextComponent,
PasswordComponent,
],
providers: [NgbActiveModal],
imports: [
HttpClientModule,
ReactiveFormsModule,
FormsModule,
NgbModalModule,
NgbAccordionModule,
],
})
profileService = TestBed.inject(ProfileService)
toastService = TestBed.inject(ToastService)
clipboard = TestBed.inject(Clipboard)
fixture = TestBed.createComponent(ProfileEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should get profile on init, display in form', () => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
expect(getSpy).toHaveBeenCalled()
fixture.detectChanges()
expect(component.form.get('email').value).toEqual(profile.email)
})
it('should update profile on save, display error if needed', () => {
const newProfile = {
email: 'foo@bar2.com',
password: profile.password,
first_name: 'foo2',
last_name: profile.last_name,
auth_token: profile.auth_token,
}
const updateSpy = jest.spyOn(profileService, 'update')
const errorSpy = jest.spyOn(toastService, 'showError')
updateSpy.mockReturnValueOnce(throwError(() => new Error('failed to save')))
component.save()
expect(errorSpy).toHaveBeenCalled()
updateSpy.mockClear()
const infoSpy = jest.spyOn(toastService, 'showInfo')
component.form.patchValue(newProfile)
updateSpy.mockReturnValueOnce(of(newProfile))
component.save()
expect(updateSpy).toHaveBeenCalledWith(newProfile)
expect(infoSpy).toHaveBeenCalled()
})
it('should close on cancel', () => {
const closeSpy = jest.spyOn(component.activeModal, 'close')
component.cancel()
expect(closeSpy).toHaveBeenCalled()
})
it('should show additional confirmation field when email changes, warn with error & disable save', () => {
expect(component.form.get('email_confirm').enabled).toBeFalsy()
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
component.form.get('email').patchValue('foo@bar2.com')
component.onEmailKeyUp({ target: { value: 'foo@bar2.com' } } as any)
fixture.detectChanges()
expect(component.form.get('email_confirm').enabled).toBeTruthy()
expect(fixture.debugElement.nativeElement.textContent).toContain(
'Emails must match'
)
expect(component.saveDisabled).toBeTruthy()
component.form.get('email_confirm').patchValue('foo@bar2.com')
component.onEmailConfirmKeyUp({ target: { value: 'foo@bar2.com' } } as any)
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Emails must match'
)
expect(component.saveDisabled).toBeFalsy()
component.form.get('email').patchValue(profile.email)
fixture.detectChanges()
expect(component.form.get('email_confirm').enabled).toBeFalsy()
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Emails must match'
)
expect(component.saveDisabled).toBeFalsy()
})
it('should show additional confirmation field when password changes, warn with error & disable save', () => {
expect(component.form.get('password_confirm').enabled).toBeFalsy()
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
component.form.get('password').patchValue('new*pass')
component.onPasswordKeyUp({
target: { value: 'new*pass', tagName: 'input' },
} as any)
component.onPasswordKeyUp({ target: { tagName: 'button' } } as any) // coverage
fixture.detectChanges()
expect(component.form.get('password_confirm').enabled).toBeTruthy()
expect(fixture.debugElement.nativeElement.textContent).toContain(
'Passwords must match'
)
expect(component.saveDisabled).toBeTruthy()
component.form.get('password_confirm').patchValue('new*pass')
component.onPasswordConfirmKeyUp({ target: { value: 'new*pass' } } as any)
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Passwords must match'
)
expect(component.saveDisabled).toBeFalsy()
component.form.get('password').patchValue(profile.password)
fixture.detectChanges()
expect(component.form.get('password_confirm').enabled).toBeFalsy()
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Passwords must match'
)
expect(component.saveDisabled).toBeFalsy()
})
it('should logout on save if password changed', fakeAsync(() => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
component['newPassword'] = 'new*pass'
component.form.get('password').patchValue('new*pass')
component.form.get('password_confirm').patchValue('new*pass')
const updateSpy = jest.spyOn(profileService, 'update')
updateSpy.mockReturnValue(of(null))
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/',
},
writable: true, // possibility to override
})
component.save()
expect(updateSpy).toHaveBeenCalled()
tick(2600)
expect(window.location.href).toContain('logout')
}))
it('should support auth token copy', fakeAsync(() => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
component.ngOnInit()
const copySpy = jest.spyOn(clipboard, 'copy')
component.copyAuthToken()
expect(copySpy).toHaveBeenCalledWith(profile.auth_token)
expect(component.copied).toBeTruthy()
tick(3000)
expect(component.copied).toBeFalsy()
}))
it('should support generate token, display error if needed', () => {
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
const generateSpy = jest.spyOn(profileService, 'generateAuthToken')
const errorSpy = jest.spyOn(toastService, 'showError')
generateSpy.mockReturnValueOnce(
throwError(() => new Error('failed to generate'))
)
component.generateAuthToken()
expect(errorSpy).toHaveBeenCalled()
generateSpy.mockClear()
const newToken = '789101112hijk'
generateSpy.mockReturnValueOnce(of(newToken))
component.generateAuthToken()
expect(generateSpy).toHaveBeenCalled()
expect(component.form.get('auth_token').value).not.toEqual(
profile.auth_token
)
expect(component.form.get('auth_token').value).toEqual(newToken)
})
})

View File

@@ -0,0 +1,184 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service'
import { Subject, takeUntil } from 'rxjs'
import { Clipboard } from '@angular/cdk/clipboard'
@Component({
selector: 'pngx-profile-edit-dialog',
templateUrl: './profile-edit-dialog.component.html',
styleUrls: ['./profile-edit-dialog.component.scss'],
})
export class ProfileEditDialogComponent implements OnInit, OnDestroy {
public networkActive: boolean = false
public error: any
private unsubscribeNotifier: Subject<any> = new Subject()
public form = new FormGroup({
email: new FormControl(''),
email_confirm: new FormControl({ value: null, disabled: true }),
password: new FormControl(null),
password_confirm: new FormControl({ value: null, disabled: true }),
first_name: new FormControl(''),
last_name: new FormControl(''),
auth_token: new FormControl(''),
})
private currentPassword: string
private newPassword: string
private passwordConfirm: string
public showPasswordConfirm: boolean = false
private currentEmail: string
private newEmail: string
private emailConfirm: string
public showEmailConfirm: boolean = false
public copied: boolean = false
constructor(
private profileService: ProfileService,
public activeModal: NgbActiveModal,
private toastService: ToastService,
private clipboard: Clipboard
) {}
ngOnInit(): void {
this.networkActive = true
this.profileService
.get()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((profile) => {
this.networkActive = false
this.form.patchValue(profile)
this.currentEmail = profile.email
this.form.get('email').valueChanges.subscribe((newEmail) => {
this.newEmail = newEmail
this.onEmailChange()
})
this.currentPassword = profile.password
this.form.get('password').valueChanges.subscribe((newPassword) => {
this.newPassword = newPassword
this.onPasswordChange()
})
})
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
}
get saveDisabled(): boolean {
return this.error?.password_confirm || this.error?.email_confirm
}
onEmailKeyUp(event: KeyboardEvent): void {
this.newEmail = (event.target as HTMLInputElement)?.value
this.onEmailChange()
}
onEmailConfirmKeyUp(event: KeyboardEvent): void {
this.emailConfirm = (event.target as HTMLInputElement)?.value
this.onEmailChange()
}
onEmailChange(): void {
this.showEmailConfirm = this.currentEmail !== this.newEmail
if (this.showEmailConfirm) {
this.form.get('email_confirm').enable()
if (this.newEmail !== this.emailConfirm) {
if (!this.error) this.error = {}
this.error.email_confirm = $localize`Emails must match`
} else {
delete this.error?.email_confirm
}
} else {
this.form.get('email_confirm').disable()
delete this.error?.email_confirm
}
}
onPasswordKeyUp(event: KeyboardEvent): void {
if ((event.target as HTMLElement).tagName !== 'input') return // toggle button can trigger this handler
this.newPassword = (event.target as HTMLInputElement)?.value
this.onPasswordChange()
}
onPasswordConfirmKeyUp(event: KeyboardEvent): void {
this.passwordConfirm = (event.target as HTMLInputElement)?.value
this.onPasswordChange()
}
onPasswordChange(): void {
this.showPasswordConfirm = this.currentPassword !== this.newPassword
if (this.showPasswordConfirm) {
this.form.get('password_confirm').enable()
if (this.newPassword !== this.passwordConfirm) {
if (!this.error) this.error = {}
this.error.password_confirm = $localize`Passwords must match`
} else {
delete this.error?.password_confirm
}
} else {
this.form.get('password_confirm').disable()
delete this.error?.password_confirm
}
}
save(): void {
const passwordChanged = this.currentPassword !== this.newPassword
const profile = Object.assign({}, this.form.value)
this.networkActive = true
this.profileService
.update(profile)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.toastService.showInfo($localize`Profile updated successfully`)
if (passwordChanged) {
this.toastService.showInfo(
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
}, 2500)
}
this.activeModal.close()
},
error: (error) => {
this.toastService.showError($localize`Error saving profile`, error)
this.networkActive = false
},
})
}
cancel(): void {
this.activeModal.close()
}
generateAuthToken(): void {
this.profileService.generateAuthToken().subscribe({
next: (token: string) => {
this.form.patchValue({ auth_token: token })
},
error: (error) => {
this.toastService.showError(
$localize`Error generating auth token`,
error
)
},
})
}
copyAuthToken(): void {
this.clipboard.copy(this.form.get('auth_token').value)
this.copied = true
setTimeout(() => {
this.copied = false
}, 3000)
}
}

View File

@@ -0,0 +1,7 @@
export interface PaperlessUserProfile {
email?: string
password?: string
first_name?: string
last_name?: string
auth_token?: string
}

View File

@@ -0,0 +1,54 @@
import { TestBed } from '@angular/core/testing'
import { ProfileService } from './profile.service'
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { environment } from 'src/environments/environment'
describe('ProfileService', () => {
let httpTestingController: HttpTestingController
let service: ProfileService
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ProfileService],
imports: [HttpClientTestingModule],
})
httpTestingController = TestBed.inject(HttpTestingController)
service = TestBed.inject(ProfileService)
})
afterEach(() => {
httpTestingController.verify()
})
it('calls get profile endpoint', () => {
service.get().subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}profile/`
)
expect(req.request.method).toEqual('GET')
})
it('calls patch on update', () => {
service.update({ email: 'foo@bar.com' }).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}profile/`
)
expect(req.request.method).toEqual('PATCH')
expect(req.request.body).toEqual({
email: 'foo@bar.com',
})
})
it('supports generating new auth token', () => {
service.generateAuthToken().subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}profile/generate_auth_token/`
)
expect(req.request.method).toEqual('POST')
})
})

View File

@@ -0,0 +1,34 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { PaperlessUserProfile } from '../data/user-profile'
import { environment } from 'src/environments/environment'
@Injectable({
providedIn: 'root',
})
export class ProfileService {
private endpoint = 'profile'
constructor(private http: HttpClient) {}
get(): Observable<PaperlessUserProfile> {
return this.http.get<PaperlessUserProfile>(
`${environment.apiBaseUrl}${this.endpoint}/`
)
}
update(profile: PaperlessUserProfile): Observable<PaperlessUserProfile> {
return this.http.patch<PaperlessUserProfile>(
`${environment.apiBaseUrl}${this.endpoint}/`,
profile
)
}
generateAuthToken(): Observable<string> {
return this.http.post<string>(
`${environment.apiBaseUrl}${this.endpoint}/generate_auth_token/`,
{}
)
}
}