From 9a9e70d3d45083a43e97c1d7bb2983a5b3236af7 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 27 Nov 2023 17:16:10 -0500 Subject: [PATCH] Adds timed snooze menu to Focus UI --- src/plus/webviews/focus/protocol.ts | 2 + .../apps/plus/focus/components/common.css.ts | 21 ++++- .../apps/plus/focus/components/gk-issue-row.ts | 36 ++++---- .../plus/focus/components/gk-pull-request-row.ts | 32 +++---- .../apps/plus/focus/components/gk-theme.css.ts | 11 ++- src/webviews/apps/plus/focus/components/snooze.ts | 98 ++++++++++++++++++++++ src/webviews/apps/plus/focus/focus.ts | 20 +++-- 7 files changed, 178 insertions(+), 42 deletions(-) create mode 100644 src/webviews/apps/plus/focus/components/snooze.ts diff --git a/src/plus/webviews/focus/protocol.ts b/src/plus/webviews/focus/protocol.ts index 526353e..e7fe3ce 100644 --- a/src/plus/webviews/focus/protocol.ts +++ b/src/plus/webviews/focus/protocol.ts @@ -60,6 +60,7 @@ export const SwitchToBranchCommandType = new IpcCommandType('focus/pr/snooze'); @@ -72,6 +73,7 @@ export const PinPrCommandType = new IpcCommandType('focus/pr/pin'); export interface SnoozeIssueParams { issue: IssueShape; + expiresAt?: string; snooze?: string; } export const SnoozeIssueCommandType = new IpcCommandType('focus/issue/snooze'); diff --git a/src/webviews/apps/plus/focus/components/common.css.ts b/src/webviews/apps/plus/focus/components/common.css.ts index bc2a94d..b72f504 100644 --- a/src/webviews/apps/plus/focus/components/common.css.ts +++ b/src/webviews/apps/plus/focus/components/common.css.ts @@ -94,6 +94,13 @@ export const rowBaseStyles = css` line-height: 2.4rem; } + gk-focus-row:not(:hover):not(:focus-within) gl-snooze:not([snoozed]), + gk-focus-row:not(:hover):not(:focus-within) .pin:not(.is-active) { + opacity: 0; + } +`; + +export const pinStyles = css` .icon { box-sizing: border-box; display: inline-flex; @@ -109,16 +116,26 @@ export const rowBaseStyles = css` cursor: pointer; opacity: 0.4; } + .pin:hover { opacity: 0.64; text-decoration: none; } - gk-focus-row:not(:hover):not(:focus-within) .pin:not(.is-active) { - opacity: 0; + .pin:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; } .pin.is-active { opacity: 1; } + + .pin-menu { + width: max-content; + } + + gk-tooltip gk-menu { + z-index: 10; + } `; diff --git a/src/webviews/apps/plus/focus/components/gk-issue-row.ts b/src/webviews/apps/plus/focus/components/gk-issue-row.ts index 6e1c873..34ff13e 100644 --- a/src/webviews/apps/plus/focus/components/gk-issue-row.ts +++ b/src/webviews/apps/plus/focus/components/gk-issue-row.ts @@ -15,14 +15,23 @@ import { when } from 'lit/directives/when.js'; import type { IssueMember, IssueShape } from '../../../../../git/models/issue'; import { elementBase } from '../../../shared/components/styles/lit/base.css'; import { repoBranchStyles } from './branch-tag.css'; -import { rowBaseStyles } from './common.css'; +import { pinStyles, rowBaseStyles } from './common.css'; import { dateAgeStyles } from './date-styles.css'; import { themeProperties } from './gk-theme.css'; import { fromDateRange } from './helpers'; +import './snooze'; @customElement('gk-issue-row') export class GkIssueRow extends LitElement { - static override styles = [themeProperties, elementBase, dateAgeStyles, repoBranchStyles, rowBaseStyles, css``]; + static override styles = [ + themeProperties, + elementBase, + dateAgeStyles, + repoBranchStyles, + pinStyles, + rowBaseStyles, + css``, + ]; @property({ type: Number }) public rank?: number; @@ -31,10 +40,10 @@ export class GkIssueRow extends LitElement { public issue?: IssueShape; @property() - public pinned = false; + public pinned?: string; @property() - public snoozed = false; + public snoozed?: string; constructor() { super(); @@ -80,16 +89,7 @@ export class GkIssueRow extends LitElement { > ${this.pinned ? 'Unpin' : 'Pin'} - - - ${this.snoozed ? 'Unsnooze' : 'Snooze'} - + @@ -159,11 +159,15 @@ export class GkIssueRow extends LitElement { `; } - onSnoozeClick(e: Event) { + onSnoozeAction(e: CustomEvent<{ expiresAt: never; snooze: string } | { expiresAt?: string; snooze: never }>) { e.preventDefault(); this.dispatchEvent( new CustomEvent('snooze-item', { - detail: { item: this.issue!, snooze: this.snoozed }, + detail: { + item: this.issue!, + expiresAt: e.detail.expiresAt, + snooze: this.snoozed, + }, }), ); } diff --git a/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts b/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts index 61a39f2..0f35cc3 100644 --- a/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts +++ b/src/webviews/apps/plus/focus/components/gk-pull-request-row.ts @@ -16,14 +16,23 @@ import { when } from 'lit/directives/when.js'; import type { PullRequestMember, PullRequestShape } from '../../../../../git/models/pullRequest'; import { elementBase } from '../../../shared/components/styles/lit/base.css'; import { repoBranchStyles } from './branch-tag.css'; -import { rowBaseStyles } from './common.css'; +import { pinStyles, rowBaseStyles } from './common.css'; import { dateAgeStyles } from './date-styles.css'; import { themeProperties } from './gk-theme.css'; import { fromDateRange } from './helpers'; +import './snooze'; @customElement('gk-pull-request-row') export class GkPullRequestRow extends LitElement { - static override styles = [themeProperties, elementBase, dateAgeStyles, repoBranchStyles, rowBaseStyles, css``]; + static override styles = [ + themeProperties, + elementBase, + dateAgeStyles, + repoBranchStyles, + pinStyles, + rowBaseStyles, + css``, + ]; @property({ type: Number }) public rank?: number; @@ -142,16 +151,7 @@ export class GkPullRequestRow extends LitElement { > ${this.pinned ? 'Unpin' : 'Pin'} - - - ${this.snoozed ? 'Unsnooze' : 'Snooze'} - + @@ -305,11 +305,15 @@ export class GkPullRequestRow extends LitElement { this.dispatchEvent(new CustomEvent('switch-branch', { detail: this.pullRequest! })); } - onSnoozeClick(e: Event) { + onSnoozeAction(e: CustomEvent<{ expiresAt: never; snooze: string } | { expiresAt?: string; snooze: never }>) { e.preventDefault(); this.dispatchEvent( new CustomEvent('snooze-item', { - detail: { item: this.pullRequest!, snooze: this.snoozed }, + detail: { + item: this.pullRequest!, + expiresAt: e.detail.expiresAt, + snooze: this.snoozed, + }, }), ); } diff --git a/src/webviews/apps/plus/focus/components/gk-theme.css.ts b/src/webviews/apps/plus/focus/components/gk-theme.css.ts index e37685f..237f071 100644 --- a/src/webviews/apps/plus/focus/components/gk-theme.css.ts +++ b/src/webviews/apps/plus/focus/components/gk-theme.css.ts @@ -2,6 +2,9 @@ import { css } from 'lit'; export const themeProperties = css` :host { + --focus-color: var(--vscode-focusBorder); + --gk-focus-border-color: var(--focus-color); + --gk-additions-color: var(--vscode-gitDecoration-addedResourceForeground); --gk-deletions-color: var(--vscode-gitDecoration-deletedResourceForeground); @@ -9,10 +12,10 @@ export const themeProperties = css` --gk-tag-background-color: var(--background-10); --gk-text-secondary-color: var(--color-foreground--85); - --gk-menu-border-color: var(--background-30); - --gk-menu-background-color: var(--background-10); - --gk-menu-item-background-color-hover: var(--background-15); - --gk-menu-item-font-color-disabled: var(--color-foreground--50); + --gk-menu-border-color: var(--vscode-menu-border); + --gk-menu-background-color: var(--vscode-menu-background); + --gk-menu-item-background-color-hover: var(--vscode-menu-selectionBackground); + --gk-menu-item-background-color-active: var(--vscode-menu-background); --gk-button-ghost-color: var(--color-foreground); --gk-button-ghost-color-active: var(--color-foreground--85); diff --git a/src/webviews/apps/plus/focus/components/snooze.ts b/src/webviews/apps/plus/focus/components/snooze.ts new file mode 100644 index 0000000..5212de3 --- /dev/null +++ b/src/webviews/apps/plus/focus/components/snooze.ts @@ -0,0 +1,98 @@ +import { defineGkElement, Menu, MenuItem, Popover, Tooltip } from '@gitkraken/shared-web-components'; +import { html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { pinStyles } from './common.css'; +import { themeProperties } from './gk-theme.css'; + +const HOUR = 60 * 60 * 1000; + +@customElement('gl-snooze') +class GlSnooze extends LitElement { + static override styles = [themeProperties, pinStyles]; + + @property({ reflect: true }) + public snoozed?: string; + + constructor() { + super(); + + defineGkElement(Menu, MenuItem, Popover, Tooltip); + } + + override render() { + if (this.snoozed) { + return html` + + + Unsnooze + + `; + } + + return html` + + + + Snooze + Snooze for 1 hour + Snooze for 4 hours + Snooze until tomorrow at 9:00 AM + + + `; + } + + private onSnoozeActionCore(expiresAt?: string) { + this.dispatchEvent( + new CustomEvent('gl-snooze-action', { + detail: { expiresAt: expiresAt, snooze: this.snoozed }, + }), + ); + } + + onUnsnoozeClick(e: Event) { + e.preventDefault(); + this.onSnoozeActionCore(); + } + + onSelectDuration(e: CustomEvent<{ target: MenuItem }>) { + e.preventDefault(); + const duration = e.detail.target.dataset.value; + if (!duration) return; + + if (duration === 'unlimited') { + this.onSnoozeActionCore(); + return; + } + + const now = new Date(); + let nowTime = now.getTime(); + switch (duration) { + case '1hr': + nowTime += HOUR; + break; + case '4hr': + nowTime += HOUR * 4; + break; + case 'tomorrow-9am': + now.setDate(now.getDate() + 1); + now.setHours(9, 0, 0, 0); + nowTime = now.getTime(); + break; + } + + this.onSnoozeActionCore(new Date(nowTime).toISOString()); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'gl-snooze': GlSnooze; + } + + interface HTMLElementEventMap { + 'gl-snooze-action': CustomEvent<{ expiresAt: never; snooze: string } | { expiresAt?: string; snooze: never }>; + } +} diff --git a/src/webviews/apps/plus/focus/focus.ts b/src/webviews/apps/plus/focus/focus.ts index 6a9b2bd..c9bd998 100644 --- a/src/webviews/apps/plus/focus/focus.ts +++ b/src/webviews/apps/plus/focus/focus.ts @@ -47,22 +47,22 @@ export class FocusApp extends App { 'switch-branch', (e, target: HTMLElement) => this.onSwitchBranch(e, target), ), - DOM.on( + DOM.on( 'gk-pull-request-row', 'snooze-item', (e, _target: HTMLElement) => this.onSnoozeItem(e, false), ), - DOM.on( + DOM.on( 'gk-pull-request-row', 'pin-item', (e, _target: HTMLElement) => this.onPinItem(e, false), ), - DOM.on( + DOM.on( 'gk-issue-row', 'snooze-item', (e, _target: HTMLElement) => this.onSnoozeItem(e, true), ), - DOM.on( + DOM.on( 'gk-issue-row', 'pin-item', (e, _target: HTMLElement) => this.onPinItem(e, true), @@ -99,12 +99,20 @@ export class FocusApp extends App { this.sendCommand(OpenWorktreeCommandType, { pullRequest: e.detail }); } - private onSnoozeItem(e: CustomEvent<{ item: PullRequestShape | IssueShape; snooze?: string }>, isIssue: boolean) { + private onSnoozeItem( + e: CustomEvent<{ item: PullRequestShape | IssueShape; expiresAt?: string; snooze?: string }>, + isIssue: boolean, + ) { if (isIssue) { - this.sendCommand(SnoozeIssueCommandType, { issue: e.detail.item as IssueShape, snooze: e.detail.snooze }); + this.sendCommand(SnoozeIssueCommandType, { + issue: e.detail.item as IssueShape, + expiresAt: e.detail.expiresAt, + snooze: e.detail.snooze, + }); } else { this.sendCommand(SnoozePrCommandType, { pullRequest: e.detail.item as PullRequestShape, + expiresAt: e.detail.expiresAt, snooze: e.detail.snooze, }); }