feat: saved views of my work, across my projects

A person saves named views of «my work»: projects, assigned to me, that I
follow, status categories, priorities, bugs only, overdue and text. They
are private (Mongo my_views, no history), listed and deleted by their
owner (GraphQL myViews / saveMyView / deleteMyView, REST /api/me/views),
and run as the cross-project search, which gains project, priority,
bugsOnly and overdue filters (also in MCP my_issues). The home page shows
them as a bar above «my work», with a drawer to save a new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 14:22:20 +02:00
parent e14f445e6b
commit 5b0396bc10
46 changed files with 2648 additions and 35 deletions

View File

@@ -102,6 +102,12 @@ export class GraphqlIssuesGateway extends IssuesGateway {
categories: filter.categories as never,
limit: filter.limit,
search: filter.search?.trim() || null,
projectIds: filter.projectIds?.length ? filter.projectIds : null,
priorities: filter.priorities?.length
? (filter.priorities as never)
: null,
bugsOnly: filter.bugsOnly ?? null,
overdue: filter.overdue ?? null,
})
.pipe(
map((d) => ({

View File

@@ -1,11 +1,16 @@
import { inject, Injectable } from '@angular/core';
import {
MyView,
MyViewCriteria,
SavedFilter,
SavedFilterCriteria,
SavedFiltersGateway,
} from '@task-stream/web/tracker/domain';
import { map, Observable } from 'rxjs';
import {
DeleteMyViewDocument,
MyViewsDocument,
SaveMyViewDocument,
DeleteSavedFilterDocument,
SavedFiltersDocument,
SaveFilterDocument,
@@ -38,4 +43,25 @@ export class GraphqlSavedFiltersGateway extends SavedFiltersGateway {
.mutate(DeleteSavedFilterDocument, { id })
.pipe(map(() => undefined));
}
myViews(): Observable<MyView[]> {
return this.gql
.query(MyViewsDocument, {})
.pipe(map((d) => d.myViews as MyView[]));
}
saveMyView(input: {
name: string;
criteria: MyViewCriteria;
}): Observable<string> {
return this.gql
.mutate(SaveMyViewDocument, { input: input as never })
.pipe(map((d) => d.saveMyView));
}
removeMyView(id: string): Observable<void> {
return this.gql
.mutate(DeleteMyViewDocument, { id })
.pipe(map(() => undefined));
}
}

View File

@@ -236,6 +236,18 @@ export type LinkIssuesInput = {
type: IssueLinkType;
};
export type MyViewCriteriaInput = {
assignedToMe?: boolean | null | undefined;
bugsOnly?: boolean | null | undefined;
categories?: Array<StatusCategory> | null | undefined;
overdue?: boolean | null | undefined;
priorities?: Array<IssuePriority> | null | undefined;
/** Empty: all my projects */
projectIds?: Array<string> | null | undefined;
search?: string | null | undefined;
watching?: boolean | null | undefined;
};
/** Why someone is told */
export type NotificationKind =
| 'ADDED_TO_PROJECT'
@@ -293,6 +305,11 @@ export type SaveFilterInput = {
shared?: boolean;
};
export type SaveMyViewInput = {
criteria: MyViewCriteriaInput;
name: string;
};
export type SavedFilterCriteriaInput = {
areas?: Array<string> | null | undefined;
assigneeId?: string | null | undefined;
@@ -1023,6 +1040,10 @@ export type MyIssuesQueryVariables = Exact<{
categories?: Array<StatusCategory> | StatusCategory | null | undefined;
limit: number;
search?: string | null | undefined;
projectIds?: Array<string> | string | null | undefined;
priorities?: Array<IssuePriority> | IssuePriority | null | undefined;
bugsOnly?: boolean | null | undefined;
overdue?: boolean | null | undefined;
}>;
export type MyIssuesQuery = {
@@ -2117,6 +2138,38 @@ export type DeleteSavedFilterMutationVariables = Exact<{
export type DeleteSavedFilterMutation = { deleteSavedFilter: boolean };
export type MyViewsQueryVariables = Exact<{ [key: string]: never }>;
export type MyViewsQuery = {
myViews: Array<{
id: string;
name: string;
createdAt: string;
criteria: {
projectIds: Array<string> | null;
assignedToMe: boolean | null;
watching: boolean | null;
categories: Array<StatusCategory> | null;
priorities: Array<IssuePriority> | null;
bugsOnly: boolean | null;
overdue: boolean | null;
search: string | null;
};
}>;
};
export type SaveMyViewMutationVariables = Exact<{
input: SaveMyViewInput;
}>;
export type SaveMyViewMutation = { saveMyView: string };
export type DeleteMyViewMutationVariables = Exact<{
id: string;
}>;
export type DeleteMyViewMutation = { deleteMyView: boolean };
export type SprintFieldsFragment = {
id: string;
projectId: string;
@@ -6604,6 +6657,53 @@ export const MyIssuesDocument = {
},
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } },
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'projectIds' },
},
type: {
kind: 'ListType',
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'priorities' },
},
type: {
kind: 'ListType',
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'IssuePriority' },
},
},
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'bugsOnly' },
},
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Boolean' } },
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'overdue' },
},
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Boolean' } },
},
],
selectionSet: {
kind: 'SelectionSet',
@@ -6652,6 +6752,38 @@ export const MyIssuesDocument = {
name: { kind: 'Name', value: 'search' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'projectIds' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'projectIds' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'priorities' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'priorities' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'bugsOnly' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'bugsOnly' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'overdue' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'overdue' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
@@ -12637,6 +12769,161 @@ export const DeleteSavedFilterDocument = {
DeleteSavedFilterMutation,
DeleteSavedFilterMutationVariables
>;
export const MyViewsDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'MyViews' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'myViews' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'criteria' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'projectIds' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'assignedToMe' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'watching' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'categories' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'priorities' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'bugsOnly' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'overdue' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'search' },
},
],
},
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<MyViewsQuery, MyViewsQueryVariables>;
export const SaveMyViewDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'SaveMyView' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'input' },
},
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'SaveMyViewInput' },
},
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'saveMyView' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'input' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'input' },
},
},
],
},
],
},
},
],
} as unknown as DocumentNode<SaveMyViewMutation, SaveMyViewMutationVariables>;
export const DeleteMyViewDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'DeleteMyView' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'deleteMyView' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'id' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'id' },
},
},
],
},
],
},
},
],
} as unknown as DocumentNode<
DeleteMyViewMutation,
DeleteMyViewMutationVariables
>;
export const SprintsDocument = {
kind: 'Document',
definitions: [

View File

@@ -136,6 +136,10 @@ query MyIssues(
$categories: [StatusCategory!]
$limit: Int!
$search: String
$projectIds: [ID!]
$priorities: [IssuePriority!]
$bugsOnly: Boolean
$overdue: Boolean
) {
myIssues(
assignedToMe: $assignedToMe
@@ -143,6 +147,10 @@ query MyIssues(
categories: $categories
limit: $limit
search: $search
projectIds: $projectIds
priorities: $priorities
bugsOnly: $bugsOnly
overdue: $overdue
) {
total
items {

View File

@@ -32,3 +32,29 @@ mutation SaveFilter($input: SaveFilterInput!) {
mutation DeleteSavedFilter($id: ID!) {
deleteSavedFilter(id: $id)
}
query MyViews {
myViews {
id
name
createdAt
criteria {
projectIds
assignedToMe
watching
categories
priorities
bugsOnly
overdue
search
}
}
}
mutation SaveMyView($input: SaveMyViewInput!) {
saveMyView(input: $input)
}
mutation DeleteMyView($id: ID!) {
deleteMyView(id: $id)
}