feat: notifications in the app and, optionally, by e-mail
Derived from events and kept per person: whoever a ticket is assigned to, whoever reports or works on it when it moves, fails QA or gets a comment, and whoever joins a project. Never about what you did yourself, once per event. They arrive live through a subscription to a bell in the header, and are copied by e-mail to whoever turns it on when SMTP_URL is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,17 @@ export type LinkIssuesInput = {
|
||||
type: IssueLinkType;
|
||||
};
|
||||
|
||||
/** Why someone is told */
|
||||
export type NotificationKind =
|
||||
| 'ADDED_TO_PROJECT'
|
||||
| 'ASSIGNED'
|
||||
| 'COMMENTED'
|
||||
| 'MENTIONED'
|
||||
| 'QA_FAILED'
|
||||
| 'REMINDER'
|
||||
| 'STATUS_CHANGED'
|
||||
| 'WATCHED';
|
||||
|
||||
export type ProjectMemberInput = {
|
||||
projectId: string;
|
||||
role?: ProjectRole;
|
||||
@@ -524,6 +535,83 @@ export type MyIssuesQuery = {
|
||||
};
|
||||
};
|
||||
|
||||
export type NotificationFieldsFragment = {
|
||||
id: string;
|
||||
kind: NotificationKind;
|
||||
projectId: string;
|
||||
projectKey: string;
|
||||
issueId: string | null;
|
||||
issueKey: string | null;
|
||||
summary: string;
|
||||
readAt: string | null;
|
||||
createdAt: string;
|
||||
actor: { id: string; name: string };
|
||||
};
|
||||
|
||||
export type MyNotificationsQueryVariables = Exact<{
|
||||
unreadOnly: boolean;
|
||||
limit: number;
|
||||
}>;
|
||||
|
||||
export type MyNotificationsQuery = {
|
||||
myNotifications: {
|
||||
unread: number;
|
||||
items: Array<{
|
||||
id: string;
|
||||
kind: NotificationKind;
|
||||
projectId: string;
|
||||
projectKey: string;
|
||||
issueId: string | null;
|
||||
issueKey: string | null;
|
||||
summary: string;
|
||||
readAt: string | null;
|
||||
createdAt: string;
|
||||
actor: { id: string; name: string };
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type NotificationSettingsQueryVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type NotificationSettingsQuery = {
|
||||
notificationSettings: { email: boolean; emailAvailable: boolean };
|
||||
};
|
||||
|
||||
export type MarkNotificationsReadMutationVariables = Exact<{
|
||||
ids?: Array<string> | string | null | undefined;
|
||||
}>;
|
||||
|
||||
export type MarkNotificationsReadMutation = { markNotificationsRead: boolean };
|
||||
|
||||
export type UpdateNotificationSettingsMutationVariables = Exact<{
|
||||
email: boolean;
|
||||
}>;
|
||||
|
||||
export type UpdateNotificationSettingsMutation = {
|
||||
updateNotificationSettings: { email: boolean; emailAvailable: boolean };
|
||||
};
|
||||
|
||||
export type NotificationAddedSubscriptionVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type NotificationAddedSubscription = {
|
||||
notificationAdded: {
|
||||
id: string;
|
||||
kind: NotificationKind;
|
||||
projectId: string;
|
||||
projectKey: string;
|
||||
issueId: string | null;
|
||||
issueKey: string | null;
|
||||
summary: string;
|
||||
readAt: string | null;
|
||||
createdAt: string;
|
||||
actor: { id: string; name: string };
|
||||
};
|
||||
};
|
||||
|
||||
export type MyProjectsQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type MyProjectsQuery = {
|
||||
@@ -1559,6 +1647,61 @@ export const CommandResultFieldsFragmentDoc = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<CommandResultFieldsFragment, unknown>;
|
||||
export const NotificationFieldsFragmentDoc = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'NotificationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'Notification' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'kind' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectKey' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'issueKey' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'summary' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'readAt' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'actor' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'UserRefFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'UserRefFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'UserRef' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<NotificationFieldsFragment, unknown>;
|
||||
export const BoardDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
@@ -3155,6 +3298,358 @@ export const MyIssuesDocument = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<MyIssuesQuery, MyIssuesQueryVariables>;
|
||||
export const MyNotificationsDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'MyNotifications' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'unreadOnly' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'Boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'limit' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'myNotifications' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'unreadOnly' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'unreadOnly' },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'limit' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'limit' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'unread' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'items' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'NotificationFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'UserRefFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'UserRef' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'NotificationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'Notification' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'kind' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectKey' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'issueKey' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'summary' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'readAt' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'actor' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'UserRefFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
MyNotificationsQuery,
|
||||
MyNotificationsQueryVariables
|
||||
>;
|
||||
export const NotificationSettingsDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'NotificationSettings' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'notificationSettings' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'email' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'emailAvailable' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
NotificationSettingsQuery,
|
||||
NotificationSettingsQueryVariables
|
||||
>;
|
||||
export const MarkNotificationsReadDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'MarkNotificationsRead' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: { kind: 'Variable', name: { kind: 'Name', value: 'ids' } },
|
||||
type: {
|
||||
kind: 'ListType',
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'markNotificationsRead' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'ids' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'ids' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
MarkNotificationsReadMutation,
|
||||
MarkNotificationsReadMutationVariables
|
||||
>;
|
||||
export const UpdateNotificationSettingsDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'UpdateNotificationSettings' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'email' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'Boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'updateNotificationSettings' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'email' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'email' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'email' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'emailAvailable' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
UpdateNotificationSettingsMutation,
|
||||
UpdateNotificationSettingsMutationVariables
|
||||
>;
|
||||
export const NotificationAddedDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'subscription',
|
||||
name: { kind: 'Name', value: 'NotificationAdded' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'notificationAdded' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'NotificationFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'UserRefFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'UserRef' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'FragmentDefinition',
|
||||
name: { kind: 'Name', value: 'NotificationFields' },
|
||||
typeCondition: {
|
||||
kind: 'NamedType',
|
||||
name: { kind: 'Name', value: 'Notification' },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'kind' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'projectKey' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'issueKey' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'summary' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'readAt' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'actor' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'UserRefFields' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
NotificationAddedSubscription,
|
||||
NotificationAddedSubscriptionVariables
|
||||
>;
|
||||
export const MyProjectsDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
fragment NotificationFields on Notification {
|
||||
id
|
||||
kind
|
||||
projectId
|
||||
projectKey
|
||||
issueId
|
||||
issueKey
|
||||
summary
|
||||
readAt
|
||||
createdAt
|
||||
actor {
|
||||
...UserRefFields
|
||||
}
|
||||
}
|
||||
|
||||
query MyNotifications($unreadOnly: Boolean!, $limit: Int!) {
|
||||
myNotifications(unreadOnly: $unreadOnly, limit: $limit) {
|
||||
unread
|
||||
items {
|
||||
...NotificationFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query NotificationSettings {
|
||||
notificationSettings {
|
||||
email
|
||||
emailAvailable
|
||||
}
|
||||
}
|
||||
|
||||
mutation MarkNotificationsRead($ids: [ID!]) {
|
||||
markNotificationsRead(ids: $ids)
|
||||
}
|
||||
|
||||
mutation UpdateNotificationSettings($email: Boolean!) {
|
||||
updateNotificationSettings(email: $email) {
|
||||
email
|
||||
emailAvailable
|
||||
}
|
||||
}
|
||||
|
||||
subscription NotificationAdded {
|
||||
notificationAdded {
|
||||
...NotificationFields
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user