blob: 1124ef0c694c8c67288dad8470aad186ab26b0d8 [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 {LitElement, html, css} from 'lit-element';
6import {store, connectStore} from 'reducers/base.js';
7import {SHARED_STYLES} from 'shared/shared-styles.js';
8import 'elements/framework/mr-star/mr-project-star.js';
9import 'shared/typedef.js';
10import 'elements/chops/chops-chip/chops-chip.js';
11
12import * as projects from 'reducers/projects.js';
13import {users} from 'reducers/users.js';
14import {stars} from 'reducers/stars.js';
15import {computeRoleByProjectName} from './helpers.js';
16
17
18/**
19 * `<mr-projects-page>`
20 *
21 * Displays list of all projects.
22 *
23 */
24export class MrProjectsPage extends connectStore(LitElement) {
25 /** @override */
26 static get styles() {
27 return [
28 SHARED_STYLES,
29 css`
30 :host {
31 box-sizing: border-box;
32 display: block;
33 padding: 1em 8px;
34 padding-left: 40px; /** 32px + 8px */
35 margin: auto;
36 max-width: 1280px;
37 width: 100%;
38 }
39 :host::after {
40 content: "";
41 background-image: url('/static/images/chromium.svg');
42 background-repeat: no-repeat;
43 background-position: right -100px bottom -150px;
44 background-size: 700px;
45 opacity: 0.09;
46 width: 100%;
47 height: 100%;
48 bottom: 0;
49 right: 0;
50 position: fixed;
51 z-index: -1;
52 }
53 h2 {
54 font-size: 20px;
55 letter-spacing: 0.1px;
56 font-weight: 500;
57 margin-top: 1em;
58 }
59 .project-header {
60 display: flex;
61 align-items: flex-start;
62 flex-direction: row;
63 justify-content: space-between;
64 font-size: 16px;
65 line-height: 24px;
66 margin: 0;
67 margin-bottom: 16px;
68 padding-top: 0.1em;
69 padding-bottom: 16px;
70 letter-spacing: 0.1px;
71 font-weight: 500;
72 width: 100%;
73 border-bottom: var(--chops-normal-border);
74 border-color: var(--chops-gray-400);
75 }
76 .project-title {
77 display: flex;
78 flex-direction: column;
79 }
80 h3 {
81 margin: 0;
82 padding: 0;
83 font-weight: inherit;
84 font-size: inherit;
85 transition: color var(--chops-transition-time) ease-in-out;
86 }
87 h3:hover {
88 color: var(--chops-link-color);
89 }
90 .subtitle {
91 color: var(--chops-gray-700);
92 font-size: var(--chops-main-font-size);
93 line-height: 100%;
94 font-weight: normal;
95 }
96 .project-container {
97 display: flex;
98 align-items: stretch;
99 flex-wrap: wrap;
100 width: 100%;
101 padding: 0.5em 0;
102 margin-bottom: 3em;
103 }
104 .project {
105 background: var(--chops-white);
106 width: 220px;
107 margin-right: 32px;
108 margin-bottom: 32px;
109 display: flex;
110 flex-direction: column;
111 align-items: flex-start;
112 justify-content: flex-start;
113 border-radius: 4px;
114 border: var(--chops-normal-border);
115 padding: 16px;
116 color: var(--chops-primary-font-color);
117 font-weight: normal;
118 line-height: 16px;
119 transition: all var(--chops-transition-time) ease-in-out;
120 }
121 .project:hover {
122 text-decoration: none;
123 cursor: pointer;
124 box-shadow: 0 2px 6px hsla(0,0%,0%,0.12),
125 0 1px 3px hsla(0,0%,0%,0.24);
126 }
127 .project > p {
128 margin: 0;
129 margin-bottom: 32px;
130 flex-grow: 1;
131 }
132 .view-project-link {
133 text-transform: uppercase;
134 margin: 0;
135 font-weight: 600;
136 flex-grow: 0;
137 }
138 .view-project-link:hover {
139 text-decoration: underline;
140 }
141 `,
142 ];
143 }
144
145 /** @override */
146 render() {
147 const myProjects = this.myProjects;
148 const otherProjects = this.otherProjects;
149 const noProjects = !myProjects.length && !otherProjects.length;
150
151 if (this._isFetchingProjects && noProjects) {
152 return html`Loading...`;
153 }
154
155 if (noProjects) {
156 return html`No projects found.`;
157 }
158
159 if (!myProjects.length) {
160 // Skip sorting projects into different sections if the user
161 // has no projects.
162 return html`
163 <h2>All projects</h2>
164 <div class="project-container all-projects">
165 ${otherProjects.map((project) => this._renderProject(project))}
166 </div>
167 `;
168 }
169
170 const myProjectsTemplate = myProjects.map((project) => this._renderProject(
171 project, this._roleByProjectName[project.name]));
172
173 return html`
174 <h2>My projects</h2>
175 <div class="project-container my-projects">
176 ${myProjectsTemplate}
177 </div>
178
179 <h2>Other projects</h2>
180 <div class="project-container other-projects">
181 ${otherProjects.map((project) => this._renderProject(project))}
182 </div>
183 `;
184 }
185
186 /** @override */
187 static get properties() {
188 return {
189 _projects: {type: Array},
190 _isFetchingProjects: {type: Boolean},
191 _currentUser: {type: String},
192 _roleByProjectName: {type: Array},
193 };
194 }
195
196 /** @override */
197 constructor() {
198 super();
199 /**
200 * @type {Array<Project>}
201 */
202 this._projects = [];
203 /**
204 * @type {boolean}
205 */
206 this._isFetchingProjects = false;
207 /**
208 * @type {string}
209 */
210 this._currentUser = undefined;
211 /**
212 * @type {Object<ProjectName, string>}
213 */
214 this._roleByProjectName = {};
215 }
216
217 /** @override */
218 connectedCallback() {
219 super.connectedCallback();
220 store.dispatch(projects.list());
221 }
222
223 /** @override */
224 updated(changedProperties) {
225 if (changedProperties.has('_currentUser') && this._currentUser) {
226 const userName = this._currentUser;
227 store.dispatch(users.gatherProjectMemberships(userName));
228 store.dispatch(stars.listProjects(userName));
229 }
230 }
231
232 /** @override */
233 stateChanged(state) {
234 this._projects = projects.all(state);
235 this._isFetchingProjects = projects.requests(state).list.requesting;
236 this._currentUser = users.currentUserName(state);
237 const allProjectMemberships = users.projectMemberships(state);
238 this._roleByProjectName = computeRoleByProjectName(
239 allProjectMemberships[this._currentUser]);
240 }
241
242 /**
243 * @param {Project} project
244 * @param {string=} role
245 * @return {TemplateResult}
246 */
247 _renderProject(project, role) {
248 return html`
249 <a href="/p/${project.displayName}/issues/list" class="project">
250 <div class="project-header">
251 <span class="project-title">
252 <h3>${project.displayName}</h3>
253 <span class="subtitle" ?hidden=${!role} title="My role: ${role}">
254 Role: ${role}
255 </span>
256 </span>
257
258 <mr-project-star .name=${project.name}></mr-project-star>
259 </div>
260 <p>
261 ${project.summary}
262 </p>
263 <button class="view-project-link linkify">
264 View project
265 </button>
266 </a>
267 `;
268 }
269
270 /**
271 * Projects the currently logged in user is a member of.
272 * @return {Array<Project>}
273 */
274 get myProjects() {
275 return this._projects.filter(
276 ({name}) => this._userIsMemberOfProject(name));
277 }
278
279 /**
280 * Projects the currently logged in user is not a member of.
281 * @return {Array<Project>}
282 */
283 get otherProjects() {
284 return this._projects.filter(
285 ({name}) => !this._userIsMemberOfProject(name));
286 }
287
288 /**
289 * Helper to check if a user is a member of a project.
290 * @param {ProjectName} project Resource name of a project.
291 * @return {boolean} Whether the user a member of the given project.
292 */
293 _userIsMemberOfProject(project) {
294 return project in this._roleByProjectName;
295 }
296}
297customElements.define('mr-projects-page', MrProjectsPage);