blob: c317d393f98aa9a37fe42e4d9fb387f41d4ba971 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import debounce from 'debounce';
6import {LitElement, html, css} from 'lit-element';
7
8import {userV3ToRef} from 'shared/convertersV0.js';
9
10import {store, connectStore} from 'reducers/base.js';
11import {hotlists} from 'reducers/hotlists.js';
12import * as sitewide from 'reducers/sitewide.js';
13import * as users from 'reducers/users.js';
14
15import 'elements/framework/links/mr-user-link/mr-user-link.js';
16import 'elements/hotlist/mr-hotlist-header/mr-hotlist-header.js';
17
18/** Hotlist People page */
19class _MrHotlistPeoplePage extends LitElement {
20 /** @override */
21 static get styles() {
22 return css`
23 :host {
24 display: block;
25 }
26 section {
27 margin: 16px 24px;
28 }
29 h2 {
30 font-weight: normal;
31 }
32
33 ul {
34 padding: 0;
35 }
36 li {
37 list-style-type: none;
38 }
39 p, li, form {
40 display: flex;
41 }
42 p, ul, li, form {
43 margin: 12px 0;
44 }
45
46 input {
47 margin-left: -6px;
48 padding: 4px;
49 width: 320px;
50 }
51
52 button {
53 align-items: center;
54 background-color: transparent;
55 border: 0;
56 cursor: pointer;
57 display: inline-flex;
58 margin: 0 4px;
59 padding: 0;
60 }
61 .material-icons {
62 font-size: 18px;
63 }
64
65 .placeholder::before {
66 animation: pulse 1s infinite ease-in-out;
67 border-radius: 3px;
68 content: " ";
69 height: 10px;
70 margin: 4px 0;
71 width: 200px;
72 }
73 @keyframes pulse {
74 0% {background-color: var(--chops-blue-50);}
75 50% {background-color: var(--chops-blue-75);}
76 100% {background-color: var(--chops-blue-50);}
77 }
78 `;
79 }
80
81 /** @override */
82 render() {
83 return html`
84 <mr-hotlist-header selected=1></mr-hotlist-header>
85 ${this._renderPage()}
86 `;
87 }
88
89 /**
90 * @return {TemplateResult}
91 */
92 _renderPage() {
93 if (this._fetchError) {
94 return html`<section>${this._fetchError.description}</section>`;
95 }
96
97 return html`
98 <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
99
100 <section>
101 <h2>Owner</h2>
102 ${this._renderOwner(this._owner)}
103 </section>
104
105 <section>
106 <h2>Editors</h2>
107 ${this._renderEditors(this._editors)}
108
109 ${this._permissions.includes(hotlists.ADMINISTER) ? html`
110 <form @submit=${this._onAddEditors}>
111 <input id="add" placeholder="List of email addresses"></input>
112 <button><i class="material-icons">add</i></button>
113 </form>
114 ` : html``}
115 </section>
116 `;
117 }
118
119 /**
120 * @param {?User} owner
121 * @return {TemplateResult}
122 */
123 _renderOwner(owner) {
124 if (!owner) return html`<p class="placeholder"></p>`;
125 return html`
126 <p><mr-user-link .userRef=${userV3ToRef(owner)}></mr-user-link></p>
127 `;
128 }
129
130 /**
131 * @param {?Array<User>} editors
132 * @return {TemplateResult}
133 */
134 _renderEditors(editors) {
135 if (!editors) return html`<p class="placeholder"></p>`;
136 if (!editors.length) return html`<p>No editors.</p>`;
137
138 return html`
139 <ul>${editors.map((editor) => this._renderEditor(editor))}</ul>
140 `;
141 }
142
143 /**
144 * @param {?User} editor
145 * @return {TemplateResult}
146 */
147 _renderEditor(editor) {
148 if (!editor) return html`<li class="placeholder"></li>`;
149
150 const canRemove = this._permissions.includes(hotlists.ADMINISTER) ||
151 editor.name === this._currentUserName;
152
153 return html`
154 <li>
155 <mr-user-link .userRef=${userV3ToRef(editor)}></mr-user-link>
156 ${canRemove ? html`
157 <button @click=${this._removeEditor.bind(this, editor.name)}>
158 <i class="material-icons">clear</i>
159 </button>
160 ` : html``}
161 </li>
162 `;
163 }
164
165 /** @override */
166 static get properties() {
167 return {
168 // Populated from Redux.
169 _hotlist: {type: Object},
170 _owner: {type: Object},
171 _editors: {type: Array},
172 _permissions: {type: Array},
173 _currentUserName: {type: String},
174 _fetchError: {type: Object},
175 };
176 }
177
178 /** @override */
179 constructor() {
180 super();
181
182 // Populated from Redux.
183 /** @type {?Hotlist} */ this._hotlist = null;
184 /** @type {?User} */ this._owner = null;
185 /** @type {Array<User>} */ this._editors = null;
186 /** @type {Array<Permission>} */ this._permissions = [];
187 /** @type {?String} */ this._currentUserName = null;
188 /** @type {?Error} */ this._fetchError = null;
189
190 this._debouncedAddEditors = debounce(this._addEditors, 400, true);
191 }
192
193 /** Adds hotlist editors.
194 * @param {Event} event
195 */
196 async _onAddEditors(event) {
197 event.preventDefault();
198
199 const input =
200 /** @type {HTMLInputElement} */ (this.shadowRoot.getElementById('add'));
201 const emails = input.value.split(/[\s,;]/).filter((e) => e);
202 if (!emails.length) return;
203 const editors = emails.map((email) => 'users/' + email);
204 try {
205 await this._debouncedAddEditors(editors);
206 input.value = '';
207 } catch (error) {
208 // The `hotlists.update()` call shows a snackbar on errors.
209 }
210 }
211
212 /** Adds hotlist editors.
213 * @param {Array<string>} editors An Array of User resource names.
214 */
215 async _addEditors(editors) {}
216
217 /**
218 * Removes a hotlist editor.
219 * @param {string} name A User resource name.
220 */
221 async _removeEditor(name) {}
222};
223
224/** Redux-connected version of _MrHotlistPeoplePage. */
225export class MrHotlistPeoplePage extends connectStore(_MrHotlistPeoplePage) {
226 /** @override */
227 stateChanged(state) {
228 this._hotlist = hotlists.viewedHotlist(state);
229 this._owner = hotlists.viewedHotlistOwner(state);
230 this._editors = hotlists.viewedHotlistEditors(state);
231 this._permissions = hotlists.viewedHotlistPermissions(state);
232 this._currentUserName = users.currentUserName(state);
233 this._fetchError = hotlists.requests(state).fetch.error;
234 }
235
236 /** @override */
237 updated(changedProperties) {
238 super.updated(changedProperties);
239
240 if (changedProperties.has('_hotlist') && this._hotlist) {
241 const pageTitle = 'People - ' + this._hotlist.displayName;
242 store.dispatch(sitewide.setPageTitle(pageTitle));
243 const headerTitle = 'Hotlist ' + this._hotlist.displayName;
244 store.dispatch(sitewide.setHeaderTitle(headerTitle));
245 }
246 }
247
248 /** @override */
249 async _addEditors(editors) {
250 await store.dispatch(hotlists.update(this._hotlist.name, {editors}));
251 }
252
253 /** @override */
254 async _removeEditor(name) {
255 await store.dispatch(hotlists.removeEditors(this._hotlist.name, [name]));
256 }
257}
258
259customElements.define('mr-hotlist-people-page-base', _MrHotlistPeoplePage);
260customElements.define('mr-hotlist-people-page', MrHotlistPeoplePage);