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