Browse Source

Adds pin and snooze UI

main
Eric Amodio 1 year ago
committed by Keith Daulton
parent
commit
1ba08fa38f
9 changed files with 554 additions and 71 deletions
  1. +1
    -1
      package.json
  2. +59
    -20
      src/plus/focus/focusService.ts
  3. +228
    -34
      src/plus/webviews/focus/focusWebview.ts
  4. +31
    -0
      src/plus/webviews/focus/protocol.ts
  5. +68
    -14
      src/webviews/apps/plus/focus/components/focus-app.ts
  6. +60
    -0
      src/webviews/apps/plus/focus/components/gk-issue-row.ts
  7. +61
    -1
      src/webviews/apps/plus/focus/components/gk-pull-request-row.ts
  8. +45
    -0
      src/webviews/apps/plus/focus/focus.ts
  9. +1
    -1
      yarn.lock

+ 1
- 1
package.json View File

@ -14816,7 +14816,7 @@
},
"dependencies": {
"@gitkraken/gitkraken-components": "10.1.25",
"@gitkraken/shared-web-components": "^0.1.1-rc.6",
"@gitkraken/shared-web-components": "^0.1.1-rc.9",
"@microsoft/fast-element": "1.12.0",
"@microsoft/fast-react-wrapper": "0.3.19",
"@octokit/graphql": "7.0.2",

+ 59
- 20
src/plus/focus/focusService.ts View File

@ -1,21 +1,21 @@
import type { Disposable } from 'vscode';
import type { Container } from '../../container';
import type { GitRemote } from '../../git/models/remote';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
import { log } from '../../system/decorators/log';
import { Logger } from '../../system/logger';
import { getLogScope } from '../../system/logger.scope';
import type { ServerConnection } from '../gk/serverConnection';
export interface FocusItem {
provider: EnrichedItemResponse['provider'];
type: EnrichedItemResponse['entityType'];
id: string;
repositoryName: string;
repositoryOwner: string;
remote?: GitRemote<RichRemoteProvider>;
}
export type EnrichedItem = {
id: string;
userId: string;
userId?: string;
type: EnrichedItemResponse['type'];
provider: EnrichedItemResponse['provider'];
@ -25,28 +25,45 @@ export type EnrichedItem = {
createdAt: number;
updatedAt: number;
} & (
| { repositoryId: string }
| { gitRepositoryId: string }
| {
repositoryName: string;
repositoryOwner: string;
}
);
type EnrichedItemRequest = {
provider: EnrichedItemResponse['provider'];
type: EnrichedItemResponse['entityType'];
id: string;
} & (
| { repositoryId: string }
type GitRepositoryDataRequest =
| {
repositoryName: string;
repositoryOwner: string;
readonly initialCommitSha: string;
readonly remoteUrl?: undefined;
readonly remoteDomain?: undefined;
readonly remotePath?: undefined;
}
);
| ({
readonly initialCommitSha?: string;
readonly remoteUrl: string;
readonly remoteDomain: string;
readonly remotePath: string;
} & (
| { readonly remoteProvider?: undefined }
| {
readonly remoteProvider: string;
readonly remoteProviderRepoDomain: string;
readonly remoteProviderRepoName: string;
readonly remoteProviderRepoOwnerDomain?: string;
}
));
type EnrichedItemRequest = {
provider: EnrichedItemResponse['provider'];
entityType: EnrichedItemResponse['entityType'];
entityId: string;
gitRepoData: GitRepositoryDataRequest;
};
type EnrichedItemResponse = {
id: string;
userId: string;
userId?: string;
type: 'pin' | 'snooze';
provider: 'azure' | 'bitbucket' | 'github' | 'gitlab' | 'gitkraken';
@ -56,7 +73,7 @@ type EnrichedItemResponse = {
createdAt: number;
updatedAt: number;
} & (
| { repositoryId: string }
| { gitRepositoryId: string }
| {
repositoryName: string;
repositoryOwner: string;
@ -120,14 +137,25 @@ export class FocusService implements Disposable {
try {
type Result = { data: EnrichedItemResponse };
const rq: EnrichedItemRequest = {
provider: item.remote!.provider.id as EnrichedItemResponse['provider'],
entityType: item.type,
entityId: item.id,
gitRepoData: {
remoteUrl: item.remote!.url,
remotePath: item.remote!.provider.path,
remoteDomain: item.remote!.provider.domain,
},
};
const rsp = await this.connection.fetchGkDevApi('v1/enrich-items/pin', {
method: 'POST',
body: JSON.stringify(item satisfies EnrichedItemRequest),
body: JSON.stringify(rq),
});
if (!rsp.ok) {
throw new Error(
`Unable to pin item '${item.provider}|${item.repositoryOwner}/${item.repositoryName}#${item.id}': (${rsp.status}) ${rsp.statusText}`,
`Unable to pin item '${rq.provider}|${rq.gitRepoData.remoteDomain}/${rq.gitRepoData.remotePath}#${item.id}': (${rsp.status}) ${rsp.statusText}`,
);
}
@ -152,14 +180,25 @@ export class FocusService implements Disposable {
try {
type Result = { data: EnrichedItemResponse };
const rq: EnrichedItemRequest = {
provider: item.remote!.provider.id as EnrichedItemResponse['provider'],
entityType: item.type,
entityId: item.id,
gitRepoData: {
remoteUrl: item.remote!.url,
remotePath: item.remote!.provider.path,
remoteDomain: item.remote!.provider.domain,
},
};
const rsp = await this.connection.fetchGkDevApi('v1/enrich-items/snooze', {
method: 'POST',
body: JSON.stringify(item satisfies EnrichedItemRequest),
body: JSON.stringify(rq),
});
if (!rsp.ok) {
throw new Error(
`Unable to snooze item '${item.provider}|${item.repositoryOwner}/${item.repositoryName}#${item.id}': (${rsp.status}) ${rsp.statusText}`,
`Unable to snooze item '${rq.provider}|${rq.gitRepoData.remoteDomain}/${rq.gitRepoData.remotePath}#${item.id}': (${rsp.status}) ${rsp.statusText}`,
);
}

+ 228
- 34
src/plus/webviews/focus/focusWebview.ts View File

@ -27,13 +27,27 @@ import { getSettledValue } from '../../../system/promise';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import type { EnrichedItem, FocusItem } from '../../focus/focusService';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type { ShowInCommitGraphCommandArgs } from '../graph/protocol';
import type { OpenBranchParams, OpenWorktreeParams, State, SwitchToBranchParams } from './protocol';
import type {
OpenBranchParams,
OpenWorktreeParams,
PinIssueParams,
PinPrParams,
SnoozeIssueParams,
SnoozePrParams,
State,
SwitchToBranchParams,
} from './protocol';
import {
DidChangeNotificationType,
OpenBranchCommandType,
OpenWorktreeCommandType,
PinIssueCommandType,
PinPrCommandType,
SnoozeIssueCommandType,
SnoozePrCommandType,
SwitchToBranchCommandType,
} from './protocol';
@ -51,15 +65,21 @@ interface SearchedPullRequestWithRemote extends SearchedPullRequest {
isCurrentBranch?: boolean;
hasWorktree?: boolean;
isCurrentWorktree?: boolean;
rank: number;
}
interface SearchedIssueWithRank extends SearchedIssue {
rank: number;
}
export class FocusWebviewProvider implements WebviewProvider<State> {
private _pullRequests: SearchedPullRequestWithRemote[] = [];
private _issues: SearchedIssue[] = [];
private _issues: SearchedIssueWithRank[] = [];
private readonly _disposable: Disposable;
private _etagSubscription?: number;
private _repositoryEventsDisposable?: Disposable;
private _repos?: RepoWithRichRemote[];
private _enrichedItems?: EnrichedItem[];
constructor(
private readonly container: Container,
@ -90,7 +110,115 @@ export class FocusWebviewProvider implements WebviewProvider {
case OpenWorktreeCommandType.method:
onIpc(OpenWorktreeCommandType, e, params => this.onOpenWorktree(params));
break;
case SnoozePrCommandType.method:
onIpc(SnoozePrCommandType, e, params => this.onSnoozePr(params));
break;
case PinPrCommandType.method:
onIpc(PinPrCommandType, e, params => this.onPinPr(params));
break;
case SnoozeIssueCommandType.method:
onIpc(SnoozeIssueCommandType, e, params => this.onSnoozeIssue(params));
break;
case PinIssueCommandType.method:
onIpc(PinIssueCommandType, e, params => this.onPinIssue(params));
break;
}
}
private async onPinIssue({ issue, pin }: PinIssueParams) {
const issueWithRemote = this._issues?.find(r => r.issue.id === issue.id);
if (issueWithRemote == null) return;
if (pin) {
await this.container.focus.unpinItem(issueWithRemote.issue.id);
this._enrichedItems = this._enrichedItems?.filter(e => e.id !== pin);
} else {
const focusItem: FocusItem = {
type: 'issue',
id: issueWithRemote.issue.id,
// remote: issueWithRemote.issue.remote,
};
const enrichedItem = await this.container.focus.pinItem(focusItem);
if (enrichedItem == null) return;
if (this._enrichedItems == null) {
this._enrichedItems = [];
}
this._enrichedItems.push(enrichedItem);
}
void this.notifyDidChangeState();
}
private async onSnoozeIssue({ issue, snooze }: SnoozeIssueParams) {
const issueWithRemote = this._issues?.find(r => r.issue.id === issue.id);
if (issueWithRemote == null) return;
if (snooze) {
await this.container.focus.unsnoozeItem(snooze);
this._enrichedItems = this._enrichedItems?.filter(e => e.id !== snooze);
} else {
const focusItem: FocusItem = {
type: 'issue',
id: issueWithRemote.issue.id,
// remote: issueWithRemote.issue.remote,
};
const enrichedItem = await this.container.focus.snoozeItem(focusItem);
if (enrichedItem == null) return;
if (this._enrichedItems == null) {
this._enrichedItems = [];
}
this._enrichedItems.push(enrichedItem);
}
void this.notifyDidChangeState();
}
private async onPinPr({ pullRequest, pin }: PinPrParams) {
const prWithRemote = this._pullRequests?.find(r => r.pullRequest.id === pullRequest.id);
if (prWithRemote == null) return;
if (pin) {
await this.container.focus.unpinItem(pin);
this._enrichedItems = this._enrichedItems?.filter(e => e.id !== pin);
} else {
const focusItem: FocusItem = {
type: 'pr',
id: prWithRemote.pullRequest.id,
remote: prWithRemote.repoAndRemote.remote,
};
const enrichedItem = await this.container.focus.pinItem(focusItem);
if (enrichedItem == null) return;
if (this._enrichedItems == null) {
this._enrichedItems = [];
}
this._enrichedItems.push(enrichedItem);
}
void this.notifyDidChangeState();
}
private async onSnoozePr({ pullRequest, snooze }: SnoozePrParams) {
const prWithRemote = this._pullRequests?.find(r => r.pullRequest.id === pullRequest.id);
if (prWithRemote == null) return;
if (snooze) {
await this.container.focus.unsnoozeItem(snooze);
this._enrichedItems = this._enrichedItems?.filter(e => e.id !== snooze);
} else {
const focusItem: FocusItem = {
type: 'pr',
id: prWithRemote.pullRequest.id,
remote: prWithRemote.repoAndRemote.remote,
};
const enrichedItem = await this.container.focus.snoozeItem(focusItem);
if (enrichedItem == null) return;
if (this._enrichedItems == null) {
this._enrichedItems = [];
}
this._enrichedItems.push(enrichedItem);
}
void this.notifyDidChangeState();
}
private findSearchedPullRequest(pullRequest: PullRequestShape): SearchedPullRequestWithRemote | undefined {
@ -273,10 +401,11 @@ export class FocusWebviewProvider implements WebviewProvider {
const statePromise = Promise.allSettled([
this.getMyPullRequests(connectedRepos),
this.getMyIssues(connectedRepos),
this.getEnrichedItems(),
]);
async function getStateCore() {
const [prsResult, issuesResult] = await statePromise;
const [prsResult, issuesResult, enrichedItems] = await statePromise;
return {
webviewId: webviewId,
timestamp: Date.now(),
@ -289,10 +418,14 @@ export class FocusWebviewProvider implements WebviewProvider {
isCurrentWorktree: pr.isCurrentWorktree ?? false,
hasWorktree: pr.hasWorktree ?? false,
hasLocalBranch: pr.hasLocalBranch ?? false,
enriched: findEnrichedItem(pr, getSettledValue(enrichedItems)),
rank: pr.rank,
})),
issues: getSettledValue(issuesResult)?.map(issue => ({
issue: serializeIssue(issue.issue),
reasons: issue.reasons,
enriched: findEnrichedItem(issue, getSettledValue(enrichedItems)),
rank: issue.rank,
})),
};
}
@ -374,6 +507,7 @@ export class FocusWebviewProvider implements WebviewProvider {
repoAndRemote: richRepo,
isCurrentWorktree: false,
isCurrentBranch: false,
rank: getPrRank(pr),
};
const remoteBranchName = `${entry.pullRequest.refs!.head.owner}/${entry.pullRequest.refs!.head.branch}`; // TODO@eamodio really need to check for upstream url rather than name
@ -397,36 +531,9 @@ export class FocusWebviewProvider implements WebviewProvider {
}
}
function getScore(pr: SearchedPullRequest) {
let score = 0;
if (pr.reasons.includes('authored')) {
score += 1000;
} else if (pr.reasons.includes('assigned')) {
score += 900;
} else if (pr.reasons.includes('review-requested')) {
score += 800;
} else if (pr.reasons.includes('mentioned')) {
score += 700;
}
if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.Approved) {
if (pr.pullRequest.mergeableState === PullRequestMergeableState.Mergeable) {
score += 100;
} else if (pr.pullRequest.mergeableState === PullRequestMergeableState.Conflicting) {
score += 90;
} else {
score += 80;
}
} else if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.ChangesRequested) {
score += 70;
}
return score;
}
this._pullRequests = allPrs.sort((a, b) => {
const scoreA = getScore(a);
const scoreB = getScore(b);
const scoreA = a.rank;
const scoreB = b.rank;
if (scoreA === scoreB) {
return a.pullRequest.date.getTime() - b.pullRequest.date.getTime();
@ -437,26 +544,113 @@ export class FocusWebviewProvider implements WebviewProvider {
return this._pullRequests;
}
private async getMyIssues(richRepos: RepoWithRichRemote[]): Promise<SearchedIssue[]> {
private async getMyIssues(richRepos: RepoWithRichRemote[]): Promise<SearchedIssueWithRank[]> {
const allIssues = [];
for (const { remote } of richRepos) {
const issues = await this.container.git.getMyIssues(remote);
if (issues == null) {
continue;
}
allIssues.push(...issues.filter(pr => pr.reasons.length > 0));
for (const issue of issues) {
if (issue.reasons.length === 0) {
continue;
}
allIssues.push({
...issue,
rank: 0, // getIssueRank(issue),
});
}
}
// this._issues = allIssues.sort((a, b) => {
// const scoreA = a.rank;
// const scoreB = b.rank;
// if (scoreA === scoreB) {
// return b.issue.updatedDate.getTime() - a.issue.updatedDate.getTime();
// }
// return (scoreB ?? 0) - (scoreA ?? 0);
// });
this._issues = allIssues.sort((a, b) => b.issue.updatedDate.getTime() - a.issue.updatedDate.getTime());
return this._issues;
}
private async getEnrichedItems(): Promise<EnrichedItem[] | undefined> {
// TODO needs cache invalidation
if (this._enrichedItems == null) {
const enrichedItems = await this.container.focus.get();
this._enrichedItems = enrichedItems;
}
return this._enrichedItems;
}
private async notifyDidChangeState(deferState?: boolean) {
void this.host.notify(DidChangeNotificationType, { state: await this.getState(deferState) });
}
}
function findEnrichedItem(item: SearchedPullRequestWithRemote | SearchedIssue, enrichedItems?: EnrichedItem[]) {
if (enrichedItems == null || enrichedItems.length === 0) return;
let result;
// TODO: filter by entity id, type, and gitRepositoryId
if ((item as SearchedPullRequestWithRemote).pullRequest != null) {
result = enrichedItems.find(e => e.entityId === (item as SearchedPullRequestWithRemote).pullRequest.id);
} else {
result = enrichedItems.find(e => e.entityId === (item as SearchedIssue).issue.id);
}
if (result == null) return;
return {
id: result.id,
type: result.type,
};
}
function getPrRank(pr: SearchedPullRequest) {
let score = 0;
if (pr.reasons.includes('authored')) {
score += 1000;
} else if (pr.reasons.includes('assigned')) {
score += 900;
} else if (pr.reasons.includes('review-requested')) {
score += 800;
} else if (pr.reasons.includes('mentioned')) {
score += 700;
}
if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.Approved) {
if (pr.pullRequest.mergeableState === PullRequestMergeableState.Mergeable) {
score += 100;
} else if (pr.pullRequest.mergeableState === PullRequestMergeableState.Conflicting) {
score += 90;
} else {
score += 80;
}
} else if (pr.pullRequest.reviewDecision === PullRequestReviewDecision.ChangesRequested) {
score += 70;
}
return score;
}
// function getIssueRank(issue: SearchedIssue) {
// let score = 0;
// if (issue.reasons.includes('authored')) {
// score += 1000;
// } else if (issue.reasons.includes('assigned')) {
// score += 900;
// } else if (issue.reasons.includes('mentioned')) {
// score += 700;
// }
// return score;
// }
function filterGithubRepos(list: RepoWithRichRemote[]): RepoWithRichRemote[] {
return list.filter(entry => entry.isGitHub);
}

+ 31
- 0
src/plus/webviews/focus/protocol.ts View File

@ -3,6 +3,7 @@ import type { FeatureAccess } from '../../../features';
import type { IssueShape } from '../../../git/models/issue';
import type { PullRequestShape } from '../../../git/models/pullRequest';
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
import type { EnrichedItem } from '../../focus/focusService';
export interface State {
webviewId: WebviewIds | WebviewViewIds;
@ -16,6 +17,12 @@ export interface State {
export interface SearchResultBase {
reasons: string[];
rank?: number;
// TODO: convert to array of EnrichedItem
enriched?: {
id: EnrichedItem['id'];
type: EnrichedItem['type'];
};
}
export interface IssueResult extends SearchResultBase {
@ -53,6 +60,30 @@ export interface SwitchToBranchParams {
}
export const SwitchToBranchCommandType = new IpcCommandType<SwitchToBranchParams>('focus/pr/switchToBranch');
export interface SnoozePrParams {
pullRequest: PullRequestShape;
snooze?: string;
}
export const SnoozePrCommandType = new IpcCommandType<SnoozePrParams>('focus/pr/snooze');
export interface PinPrParams {
pullRequest: PullRequestShape;
pin?: string;
}
export const PinPrCommandType = new IpcCommandType<PinPrParams>('focus/pr/pin');
export interface SnoozeIssueParams {
issue: IssueShape;
snooze?: string;
}
export const SnoozeIssueCommandType = new IpcCommandType<SnoozeIssueParams>('focus/issue/snooze');
export interface PinIssueParams {
issue: IssueShape;
pin?: string;
}
export const PinIssueCommandType = new IpcCommandType<PinIssueParams>('focus/issue/pin');
// Notifications
export interface DidChangeParams {

+ 68
- 14
src/webviews/apps/plus/focus/components/focus-app.ts View File

@ -28,11 +28,12 @@ import './gk-issue-row';
@customElement('gl-focus-app')
export class GlFocusApp extends LitElement {
static override styles = [themeProperties];
private readonly tabFilters = ['prs', 'issues'];
private readonly tabFilters = ['prs', 'issues', 'snoozed'];
private readonly tabFilterOptions = [
{ label: 'All', value: '' },
{ label: 'PRs', value: 'prs' },
{ label: 'Issues', value: 'issues' },
{ label: 'Later', value: 'snoozed' },
];
private readonly mineFilters = ['authored', 'assigned', 'review-requested', 'mentioned'];
private readonly mineFilterOptions = [
@ -98,11 +99,30 @@ export class GlFocusApp extends LitElement {
return [];
}
const items: { isPullrequest: boolean; rank: number; state: Record<string, any>; tags: string[] }[] = [];
const items: {
isPullrequest: boolean;
rank: number;
state: Record<string, any>;
tags: string[];
isPinned: boolean;
isSnoozed: boolean;
enrichedId?: string;
}[] = [];
let rank = 0;
this.state?.pullRequests?.forEach(
({ pullRequest, reasons, isCurrentBranch, isCurrentWorktree, hasWorktree, hasLocalBranch }) => {
({
pullRequest,
reasons,
isCurrentBranch,
isCurrentWorktree,
hasWorktree,
hasLocalBranch,
rank,
enriched,
}) => {
const isPinned = enriched?.type === 'pin';
const isSnoozed = enriched?.type === 'snooze';
items.push({
isPullrequest: true,
state: {
@ -112,19 +132,28 @@ export class GlFocusApp extends LitElement {
hasWorktree: hasWorktree,
hasLocalBranch: hasLocalBranch,
},
rank: ++rank,
rank: rank ?? 0,
tags: reasons,
isPinned: isPinned,
isSnoozed: isSnoozed,
enrichedId: enriched?.id,
});
},
);
this.state?.issues?.forEach(({ issue, reasons }) => {
this.state?.issues?.forEach(({ issue, reasons, rank, enriched }) => {
const isPinned = enriched?.type === 'pin';
const isSnoozed = enriched?.type === 'snooze';
items.push({
isPullrequest: false,
rank: ++rank,
rank: rank ?? 0,
state: {
issue: issue,
},
tags: reasons,
isPinned: isPinned,
isSnoozed: isSnoozed,
enrichedId: enriched?.id,
});
});
@ -135,8 +164,8 @@ export class GlFocusApp extends LitElement {
const counts: Record<string, number> = {};
this.tabFilters.forEach(f => (counts[f] = 0));
this.items.forEach(({ isPullrequest }) => {
const key = isPullrequest ? 'prs' : 'issues';
this.items.forEach(({ isPullrequest, isSnoozed }) => {
const key = isSnoozed ? 'snoozed' : isPullrequest ? 'prs' : 'issues';
if (counts[key] != null) {
counts[key]++;
}
@ -190,6 +219,15 @@ export class GlFocusApp extends LitElement {
});
}
get sortedItems() {
return this.filteredItems.sort((a, b) => {
if (a.isPinned === b.isPinned) {
return a.rank - b.rank;
}
return a.isPinned ? -1 : 1;
});
}
get isLoading() {
return this.state?.pullRequests == null || this.state?.issues == null;
}
@ -207,7 +245,7 @@ export class GlFocusApp extends LitElement {
return this.loadingContent();
}
if (this.filteredItems.length === 0) {
if (this.sortedItems.length === 0) {
return html`
<div class="alert">
<span class="alert__content">None found</span>
@ -217,9 +255,12 @@ export class GlFocusApp extends LitElement {
return html`
${repeat(
this.filteredItems,
item => item.rank,
({ isPullrequest, rank, state }) =>
this.sortedItems,
(item, i) =>
`item-${i}-${
item.isPullrequest ? `pr-${item.state.pullRequest.id}` : `issue-${item.state.issue.id}`
}`,
({ isPullrequest, rank, state, isPinned, isSnoozed }) =>
when(
isPullrequest,
() =>
@ -230,8 +271,18 @@ export class GlFocusApp extends LitElement {
.isCurrentWorktree=${state.isCurrentWorktree}
.hasWorktree=${state.hasWorktree}
.hasLocalBranch=${state.hasLocalBranch}
.pinned=${isPinned}
.snoozed=${isSnoozed}
.enrichedId=${state.enrichedId}
></gk-pull-request-row>`,
() => html`<gk-issue-row .rank=${rank} .issue=${state.issue}></gk-issue-row>`,
() =>
html`<gk-issue-row
.rank=${rank}
.issue=${state.issue}
.pinned=${isPinned}
.snoozed=${isSnoozed}
.enrichedId=${state.enrichedId}
></gk-issue-row>`,
),
)}
`;
@ -341,6 +392,9 @@ export class GlFocusApp extends LitElement {
</header>
<main class="app__main">
<gk-focus-container id="list-focus-items">
<span slot="pin">
<code-icon icon="pinned"></code-icon>
</span>
<span slot="key"><code-icon icon="circle-large-outline"></code-icon></span>
<span slot="date"><code-icon icon="gl-clock"></code-icon></span>
<span slot="repo">Repo / Branch</span>

+ 60
- 0
src/webviews/apps/plus/focus/components/gk-issue-row.ts View File

@ -90,6 +90,21 @@ export class GkIssueRow extends LitElement {
display: inline-block;
min-width: 1.6rem;
}
.pin {
opacity: 0.4;
}
.pin:hover {
opacity: 0.64;
}
gk-focus-row:not(:hover):not(:focus-within) .pin:not(.is-active) {
opacity: 0;
}
.pin.is-active {
opacity: 1;
}
`,
];
@ -99,6 +114,15 @@ export class GkIssueRow extends LitElement {
@property({ type: Object })
public issue?: IssueShape;
@property({ type: Boolean })
public pinned = false;
@property({ type: Boolean })
public snoozed = false;
@property({ attribute: 'enriched-id' })
public enrichedId?: string;
constructor() {
super();
@ -132,6 +156,26 @@ export class GkIssueRow extends LitElement {
return html`
<gk-focus-row>
<span slot="pin">
<gk-tooltip>
<code-icon
class="pin ${this.pinned ? ' is-active' : ''}"
slot="trigger"
icon="pinned"
@click="${this.onPinClick}"
></code-icon>
<span>Pin</span>
</gk-tooltip>
<gk-tooltip>
<code-icon
class="pin ${this.snoozed ? ' is-active' : ''}"
slot="trigger"
icon="bell-slash"
@click="${this.onSnoozeClick}"
></code-icon>
<span>Mark for Later</span>
</gk-tooltip>
</span>
<span slot="key"></span>
<gk-focus-item>
<p>
@ -199,4 +243,20 @@ export class GkIssueRow extends LitElement {
</gk-focus-row>
`;
}
onSnoozeClick(_e: Event) {
this.dispatchEvent(
new CustomEvent('snooze-item', {
detail: { item: this.issue!, snooze: this.snoozed ? this.enrichedId : undefined },
}),
);
}
onPinClick(_e: Event) {
this.dispatchEvent(
new CustomEvent('pin-item', {
detail: { item: this.issue!, pin: this.pinned ? this.enrichedId : undefined },
}),
);
}
}

+ 61
- 1
src/webviews/apps/plus/focus/components/gk-pull-request-row.ts View File

@ -97,7 +97,7 @@ export class GkPullRequestRow extends LitElement {
.row-type {
--gk-badge-outline-padding: 0.3rem 0.8rem;
--gk-badge-font-size: 1.1rem;
opacity: 0.5;
opacity: 0.4;
vertical-align: middle;
}
@ -119,6 +119,21 @@ export class GkPullRequestRow extends LitElement {
display: inline-block;
min-width: 1.6rem;
}
.pin {
opacity: 0.4;
}
.pin:hover {
opacity: 0.64;
}
gk-focus-row:not(:hover):not(:focus-within) .pin:not(.is-active) {
opacity: 0;
}
.pin.is-active {
opacity: 1;
}
`,
];
@ -140,6 +155,15 @@ export class GkPullRequestRow extends LitElement {
@property({ type: Boolean })
public hasLocalBranch = false;
@property({ type: Boolean })
public pinned = false;
@property({ type: Boolean })
public snoozed = false;
@property({ attribute: 'enriched-id' })
public enrichedId?: string;
constructor() {
super();
@ -196,6 +220,26 @@ export class GkPullRequestRow extends LitElement {
return html`
<gk-focus-row>
<span slot="pin">
<gk-tooltip>
<code-icon
class="pin ${this.pinned ? ' is-active' : ''}"
slot="trigger"
icon="pinned"
@click="${this.onPinClick}"
></code-icon>
<span>${this.pinned ? 'Unpinned' : 'Pin'}</span>
</gk-tooltip>
<gk-tooltip>
<code-icon
class="pin ${this.snoozed ? ' is-active' : ''}"
slot="trigger"
icon="${this.snoozed ? 'bell' : 'bell-slash'}"
@click="${this.onSnoozeClick}"
></code-icon>
<span>${this.snoozed ? 'Watch' : 'Mark for Later'}</span>
</gk-tooltip>
</span>
<span slot="key" class="key">
${when(
this.indicator === 'changes',
@ -347,4 +391,20 @@ export class GkPullRequestRow extends LitElement {
}
this.dispatchEvent(new CustomEvent('switch-branch', { detail: this.pullRequest! }));
}
onSnoozeClick(_e: Event) {
this.dispatchEvent(
new CustomEvent('snooze-item', {
detail: { item: this.pullRequest!, snooze: this.snoozed ? this.enrichedId : undefined },
}),
);
}
onPinClick(_e: Event) {
this.dispatchEvent(
new CustomEvent('pin-item', {
detail: { item: this.pullRequest!, pin: this.pinned ? this.enrichedId : undefined },
}),
);
}
}

+ 45
- 0
src/webviews/apps/plus/focus/focus.ts View File

@ -1,9 +1,14 @@
import type { IssueShape } from '../../../../git/models/issue';
import type { PullRequestShape } from '../../../../git/models/pullRequest';
import type { State } from '../../../../plus/webviews/focus/protocol';
import {
DidChangeNotificationType,
OpenBranchCommandType,
OpenWorktreeCommandType,
PinIssueCommandType,
PinPrCommandType,
SnoozeIssueCommandType,
SnoozePrCommandType,
SwitchToBranchCommandType,
} from '../../../../plus/webviews/focus/protocol';
import type { IpcMessage } from '../../../protocol';
@ -11,6 +16,7 @@ import { onIpc } from '../../../protocol';
import { App } from '../../shared/appBase';
import { DOM } from '../../shared/dom';
import type { GlFocusApp } from './components/focus-app';
import type { GkIssueRow } from './components/gk-issue-row';
import type { GkPullRequestRow } from './components/gk-pull-request-row';
import './components/focus-app';
import './focus.scss';
@ -41,6 +47,26 @@ export class FocusApp extends App {
'switch-branch',
(e, target: HTMLElement) => this.onSwitchBranch(e, target),
),
DOM.on<GkPullRequestRow, { item: PullRequestShape | IssueShape; snooze?: string }>(
'gk-pull-request-row',
'snooze-item',
(e, _target: HTMLElement) => this.onSnoozeItem(e, false),
),
DOM.on<GkPullRequestRow, { item: PullRequestShape | IssueShape; pin?: string }>(
'gk-pull-request-row',
'pin-item',
(e, _target: HTMLElement) => this.onPinItem(e, false),
),
DOM.on<GkIssueRow, { item: PullRequestShape | IssueShape; snooze?: string }>(
'gk-issue-row',
'snooze-item',
(e, _target: HTMLElement) => this.onSnoozeItem(e, true),
),
DOM.on<GkIssueRow, { item: PullRequestShape | IssueShape; pin?: string }>(
'gk-issue-row',
'pin-item',
(e, _target: HTMLElement) => this.onPinItem(e, true),
),
);
return disposables;
@ -73,6 +99,25 @@ export class FocusApp extends App {
this.sendCommand(OpenWorktreeCommandType, { pullRequest: e.detail });
}
private onSnoozeItem(e: CustomEvent<{ item: PullRequestShape | IssueShape; snooze?: string }>, isIssue: boolean) {
if (isIssue) {
this.sendCommand(SnoozeIssueCommandType, { issue: e.detail.item as IssueShape, snooze: e.detail.snooze });
} else {
this.sendCommand(SnoozePrCommandType, {
pullRequest: e.detail.item as PullRequestShape,
snooze: e.detail.snooze,
});
}
}
private onPinItem(e: CustomEvent<{ item: PullRequestShape | IssueShape; pin?: string }>, isIssue: boolean) {
if (isIssue) {
this.sendCommand(PinIssueCommandType, { issue: e.detail.item as IssueShape, pin: e.detail.pin });
} else {
this.sendCommand(PinPrCommandType, { pullRequest: e.detail.item as PullRequestShape, pin: e.detail.pin });
}
}
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data as IpcMessage;
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);

+ 1
- 1
yarn.lock View File

@ -237,7 +237,7 @@
react-dragula "1.1.17"
react-onclickoutside "^6.13.0"
"@gitkraken/shared-web-components@^0.1.1-rc.6":
"@gitkraken/shared-web-components@^0.1.1-rc.9":
version "0.1.1-rc.10"
resolved "https://registry.yarnpkg.com/@gitkraken/shared-web-components/-/shared-web-components-0.1.1-rc.10.tgz#bed7021a0e6912ae3196e06078169950f0616bed"
integrity sha512-2GoM9Gg473zbtTL5u5YTiDeU8mzACj2hMQBk+7iruiiVoJLGxvi1bT95MyXHakhv4zk4lI5OVWP6TU4ZAaoDLQ==

Loading…
Cancel
Save