Implement the layout and some details for the "gate column" (#2583)
* progress * progress * Fill in stage and gate name. * Update client-src/elements/chromedash-gate-column.js Co-authored-by: Kyle Ju <kyleju@google.com> * Update client-src/elements/chromedash-gate-column.js Co-authored-by: Kyle Ju <kyleju@google.com> * Implement handleCancel and make as wide as mocks. * Improved indentation Co-authored-by: Kyle Ju <kyleju@google.com>
This commit is contained in:
Родитель
9001b06bdf
Коммит
cb1a07ad25
|
@ -58,6 +58,7 @@ import './elements/chromedash-footer';
|
|||
import './elements/chromedash-form-field';
|
||||
import './elements/chromedash-form-table';
|
||||
import './elements/chromedash-gate-chip';
|
||||
import './elements/chromedash-gate-column';
|
||||
import './elements/chromedash-gantt';
|
||||
import './elements/chromedash-guide-edit-page';
|
||||
import './elements/chromedash-guide-editall-page';
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import {LitElement, css, html, nothing} from 'lit';
|
||||
import {ref, createRef} from 'lit/directives/ref.js';
|
||||
import {showToastMessage} from './utils';
|
||||
import page from 'page';
|
||||
import {SHARED_STYLES} from '../sass/shared-css.js';
|
||||
|
||||
|
||||
class ChromedashApp extends LitElement {
|
||||
gateColumnRef = createRef();
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...SHARED_STYLES,
|
||||
|
@ -51,6 +54,32 @@ class ChromedashApp extends LitElement {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
#content-sidebar-space {
|
||||
position: sticky;
|
||||
flex-shrink: 100;
|
||||
width: var(--sidebar-space);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: var(--sidebar-width);
|
||||
bottom: 0;
|
||||
}
|
||||
#sidebar[hidden] {
|
||||
display: none;
|
||||
}
|
||||
#sidebar-content {
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
height: 85vh;
|
||||
border: var(--sidebar-border);
|
||||
border-radius: var(--sidebar-radius);
|
||||
background: var(--sidebar-bg);
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
#content {
|
||||
margin-left: 0;
|
||||
|
@ -71,6 +100,7 @@ class ChromedashApp extends LitElement {
|
|||
bannerTime: {type: Number},
|
||||
pageComponent: {attribute: false},
|
||||
contextLink: {type: String}, // used for the back button in the feature page
|
||||
sidebarHidden: {type: Boolean},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -85,6 +115,7 @@ class ChromedashApp extends LitElement {
|
|||
this.bannerTime = null;
|
||||
this.pageComponent = null;
|
||||
this.contextLink = '/features';
|
||||
this.sidebarHidden = true;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
@ -205,6 +236,19 @@ class ChromedashApp extends LitElement {
|
|||
this.updateURLParams('q', e.detail.query);
|
||||
}
|
||||
|
||||
showSidebar() {
|
||||
this.sidebarHidden = false;
|
||||
}
|
||||
|
||||
hideSidebar() {
|
||||
this.sidebarHidden = true;
|
||||
}
|
||||
|
||||
showGateColumn(feature, stage, gate) {
|
||||
this.gateColumnRef.value.setContext(feature, stage, gate);
|
||||
this.showSidebar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update window.locaton with new query params.
|
||||
* @param {string} key is the key of the query param.
|
||||
|
@ -248,6 +292,35 @@ class ChromedashApp extends LitElement {
|
|||
return url;
|
||||
}
|
||||
|
||||
|
||||
renderContentAndSidebar() {
|
||||
const wide = (this.pageComponent &&
|
||||
this.pageComponent.tagName == 'CHROMEDASH-ROADMAP-PAGE');
|
||||
if (wide) {
|
||||
return html`
|
||||
<div id="content-component-wrapper" wide>
|
||||
${this.pageComponent}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<div id="content-component-wrapper">
|
||||
${this.pageComponent}
|
||||
</div>
|
||||
<div id="content-sidebar-space">
|
||||
<div id="sidebar" ?hidden=${this.sidebarHidden}>
|
||||
<div id="sidebar-content">
|
||||
<chromedash-gate-column
|
||||
.user=${this.user} ${ref(this.gateColumnRef)}
|
||||
@close=${() => this.hideSidebar()}>
|
||||
</chromedash-gate-column>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
renderRolloutBanner(currentPage) {
|
||||
if (currentPage.startsWith('/newfeatures')) {
|
||||
return html`
|
||||
|
@ -286,11 +359,7 @@ class ChromedashApp extends LitElement {
|
|||
</chromedash-banner>
|
||||
${this.renderRolloutBanner(this.currentPage)}
|
||||
<div id="content-flex-wrapper">
|
||||
<div id="content-component-wrapper"
|
||||
?wide=${this.pageComponent &&
|
||||
this.pageComponent.tagName == 'CHROMEDASH-ROADMAP-PAGE'}>
|
||||
${this.pageComponent}
|
||||
</div>
|
||||
${this.renderContentAndSidebar()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import '@polymer/iron-icon';
|
|||
import './chromedash-activity-log';
|
||||
import './chromedash-callout';
|
||||
import './chromedash-gate-chip';
|
||||
import {autolink} from './utils.js';
|
||||
import {autolink, findProcessStage} from './utils.js';
|
||||
import {SHARED_STYLES} from '../sass/shared-css.js';
|
||||
|
||||
const LONG_TEXT = 60;
|
||||
|
@ -328,17 +328,8 @@ class ChromedashFeatureDetail extends LitElement {
|
|||
`;
|
||||
}
|
||||
|
||||
findProcessStage(feStage) {
|
||||
for (const processStage of this.process.stages) {
|
||||
if (feStage.stage_type == processStage.stage_type) {
|
||||
return processStage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderStageSection(feStage) {
|
||||
const processStage = this.findProcessStage(feStage);
|
||||
const processStage = findProcessStage(feStage, this.process);
|
||||
if (processStage === null) return nothing;
|
||||
const fields = DISPLAY_FIELDS_IN_STAGES[processStage.outgoing_stage];
|
||||
const isActive = (this.feature.intent_stage_int == processStage.outgoing_stage);
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
import {LitElement, css, html, nothing} from 'lit';
|
||||
import './chromedash-activity-log';
|
||||
import {showToastMessage, findProcessStage} from './utils.js';
|
||||
|
||||
import {SHARED_STYLES} from '../sass/shared-css.js';
|
||||
|
||||
|
||||
export const STATE_NAMES = [
|
||||
[7, 'No response'],
|
||||
[1, 'N/a or Ack'],
|
||||
[2, 'Review requested'],
|
||||
[3, 'Review started'],
|
||||
[4, 'Needs work'],
|
||||
[5, 'Approved'],
|
||||
[6, 'Denied'],
|
||||
];
|
||||
|
||||
|
||||
class ChromedashGateColumn extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
...SHARED_STYLES,
|
||||
css`
|
||||
#votes-area {
|
||||
margin: var(--content-padding) 0;
|
||||
}
|
||||
|
||||
#votes-area table {
|
||||
border-spacing: var(--content-padding-half) var(--content-padding);;
|
||||
}
|
||||
|
||||
#votes-area th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#review-status-area {
|
||||
margin: var(--content-padding-half) 0;
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
user: {type: Object},
|
||||
feature: {type: Object},
|
||||
stage: {type: Object},
|
||||
gate: {type: Object},
|
||||
process: {type: Object},
|
||||
votes: {type: Array},
|
||||
comments: {type: Array},
|
||||
loading: {type: Boolean},
|
||||
needsSave: {type: Boolean},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.user = {};
|
||||
this.feature = {};
|
||||
this.stage = {};
|
||||
this.gate = {};
|
||||
this.process = {};
|
||||
this.votes = [];
|
||||
this.comments = [];
|
||||
this.loading = false;
|
||||
this.needsSave = false;
|
||||
}
|
||||
|
||||
setContext(feature, stage, gate) {
|
||||
this.loading = true;
|
||||
this.feature = feature;
|
||||
this.stage = stage;
|
||||
this.gate = gate;
|
||||
const featureId = this.feature.id;
|
||||
Promise.all([
|
||||
window.csClient.getFeatureProcess(featureId),
|
||||
window.csClient.getApprovals(featureId),
|
||||
window.csClient.getComments(featureId),
|
||||
]).then(([process, approvalRes, commentRes]) => {
|
||||
this.process = process;
|
||||
this.votes = approvalRes.approvals.filter((v) =>
|
||||
v.gate_id == this.gate.id);
|
||||
this.comments = commentRes.comments;
|
||||
this.needsSave = false;
|
||||
this.loading = false;
|
||||
}).catch(() => {
|
||||
showToastMessage('Some errors occurred. Please refresh the page or try again later.');
|
||||
this.handleCancel();
|
||||
});
|
||||
}
|
||||
|
||||
_fireEvent(eventName, detail) {
|
||||
const event = new CustomEvent(eventName, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail,
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
this._fireEvent('close', {});
|
||||
}
|
||||
|
||||
renderHeadingsSkeleton() {
|
||||
return html`
|
||||
<h3 class="sl-skeleton-header-container">
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
</h3>
|
||||
<h2 class="sl-skeleton-header-container">
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
</h2>
|
||||
`;
|
||||
}
|
||||
|
||||
renderHeadings() {
|
||||
const processStage = findProcessStage(this.stage, this.process);
|
||||
const processStageName = processStage ? processStage.name : nothing;
|
||||
return html`
|
||||
<h3>${processStageName}</h3>
|
||||
<h2>${this.gate.team_name}</h2>
|
||||
`;
|
||||
}
|
||||
|
||||
renderReviewStatusSkeleton() {
|
||||
return html`
|
||||
<h3 class="sl-skeleton-header-container">
|
||||
Status: <sl-skeleton effect="sheen"></sl-skeleton>
|
||||
</h3>
|
||||
`;
|
||||
}
|
||||
|
||||
renderReviewStatus() {
|
||||
// TODO(jrobbins): display gate state name and requested_on or reviewed_on.
|
||||
return html`
|
||||
<div>
|
||||
Review requested on YYYY-MM-DD
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderVotesSkeleton() {
|
||||
return html`
|
||||
<table>
|
||||
<tr><th>Reviewer</th><th>Review status</th></tr>
|
||||
<tr>
|
||||
<td><sl-skeleton effect="sheen"></sl-skeleton></td>
|
||||
<td><sl-skeleton effect="sheen"></sl-skeleton></td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
findStateName(state) {
|
||||
for (const item of STATE_NAMES) {
|
||||
if (item[0] == state) {
|
||||
return item[1];
|
||||
}
|
||||
}
|
||||
// This should not normally be seen by users, but it will help us
|
||||
// cope with data migration.
|
||||
return `State ${state}`;
|
||||
}
|
||||
|
||||
renderVoteReadOnly(vote) {
|
||||
// TODO(jrobbins): background colors
|
||||
return this.findStateName(vote.state);
|
||||
}
|
||||
|
||||
renderVoteMenu(state) {
|
||||
// hoist is needed when <sl-select> is in overflow:hidden context.
|
||||
return html`
|
||||
<sl-select name="${this.gate.id}"
|
||||
value="${state}"
|
||||
@sl-change=${this.handleSelectChanged}
|
||||
hoist size="small">
|
||||
${STATE_NAMES.map((valName) => html`
|
||||
<sl-menu-item value="${valName[0]}">${valName[1]}</sl-menu-item>`,
|
||||
)}
|
||||
</sl-select>
|
||||
`;
|
||||
}
|
||||
|
||||
renderVoteRow(vote) {
|
||||
const voteCell = (vote.set_by == this.user.email) ?
|
||||
this.renderVoteMenu(vote.state) :
|
||||
this.renderVoteReadOnly(vote);
|
||||
return html`
|
||||
<tr>
|
||||
<td>${vote.set_by}</td>
|
||||
<td>${voteCell}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
renderVotes() {
|
||||
const canVote = true; // TODO(jrobbins): permission checks.
|
||||
const myVoteExists = this.votes.some((v) => v.set_by == this.user.email);
|
||||
const addVoteRow = (canVote && !myVoteExists) ?
|
||||
this.renderVoteRow({set_by: this.user.email, state: 7}) :
|
||||
nothing;
|
||||
|
||||
return html`
|
||||
<table>
|
||||
<tr><th>Reviewer</th><th>Review status</th></tr>
|
||||
${this.votes.map((v) => this.renderVoteRow(v))}
|
||||
${addVoteRow}
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
renderQuestionnaireSkeleton() {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
renderQuestionnaire() {
|
||||
// TODO(jrobbins): Implement questionnaires later.
|
||||
return nothing;
|
||||
}
|
||||
|
||||
renderCommentsSkeleton() {
|
||||
return html`
|
||||
<h2>Comments & Activity</h2>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
`;
|
||||
}
|
||||
|
||||
renderComments() {
|
||||
return html`
|
||||
<h2>Comments & Activity</h2>
|
||||
TODO(jrobbins): Comments go here
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.loading ?
|
||||
this.renderHeadingsSkeleton() :
|
||||
this.renderHeadings()}
|
||||
|
||||
<div id="review-status-area">
|
||||
${this.loading ?
|
||||
this.renderReviewStatusSkeleton() :
|
||||
this.renderReviewStatus()}
|
||||
</div>
|
||||
|
||||
<div id="votes-area">
|
||||
${this.loading ?
|
||||
this.renderVotesSkeleton() :
|
||||
this.renderVotes()}
|
||||
</div>
|
||||
|
||||
${this.loading ?
|
||||
this.renderQuestionnaireSkeleton() :
|
||||
this.renderQuestionnaire()}
|
||||
|
||||
${this.loading ?
|
||||
this.renderCommentsSkeleton() :
|
||||
this.renderComments()}
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-gate-column', ChromedashGateColumn);
|
|
@ -32,3 +32,14 @@ export function slotAssignedElements(component, slotName) {
|
|||
export function clamp(val, lowerBound, upperBound) {
|
||||
return Math.max(lowerBound, Math.min(upperBound, val));
|
||||
}
|
||||
|
||||
|
||||
/* Given a feature entry stage entity, look up the related process stage. */
|
||||
export function findProcessStage(feStage, process) {
|
||||
for (const processStage of process.stages) {
|
||||
if (feStage.stage_type == processStage.stage_type) {
|
||||
return processStage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,12 @@
|
|||
--leftnav-selected-color: var(--md-blue-900);
|
||||
--leftnav-divider-border: 1px solid var(--md-gray-100-alpha);
|
||||
|
||||
--sidebar-space: 410px;
|
||||
--sidebar-width: 400px;
|
||||
--sidebar-bg: white;
|
||||
--sidebar-border: 2px solid hsl(0, 0%, 85%);
|
||||
--sidebar-radius: var(--large-border-radius);
|
||||
|
||||
--button-background: inherit;
|
||||
--button-color: var(--md-gray-900-alpha);
|
||||
--button-font-size: 10pt;
|
||||
|
|
Загрузка…
Ссылка в новой задаче