Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions frontend/src/api/labels.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Label } from '@/models/label'
import { Request } from '../utils/api'
import { transport } from './transport'

type LabelsResponse = {
labels: Label[]
Expand All @@ -10,9 +11,25 @@ type SingleLabelResponse = {
}

export const CreateLabel = async (label: Omit<Label, 'id'>) =>
await Request<SingleLabelResponse>(`/labels`, 'POST', label)
export const GetLabels = async () => await Request<LabelsResponse>(`/labels`)
await transport({
http: () => Request<SingleLabelResponse>(`/labels`, 'POST', label),
ws: (ws) => ws.request('create_label', label),
})

export const GetLabels = async () =>
await transport({
http: () => Request<LabelsResponse>(`/labels`),
ws: (ws) => ws.request('get_user_labels'),
})

export const UpdateLabel = async (label: Label) =>
await Request<SingleLabelResponse>(`/labels`, 'PUT', label)
await transport({
http: () => Request<SingleLabelResponse>(`/labels`, 'PUT', label),
ws: (ws) => ws.request('update_label', label),
})

export const DeleteLabel = async (id: number) =>
await Request<void>(`/labels/${id}`, 'DELETE')
await transport({
http: () => Request<void>(`/labels/${id}`, 'DELETE'),
ws: (ws) => ws.request('delete_label', id),
})
54 changes: 44 additions & 10 deletions frontend/src/api/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Task } from '@/models/task'
import { Request } from '../utils/api'
import { HistoryEntry } from '@/models/history'
import { MarshallLabels } from '@/utils/marshalling'
import { transport } from './transport'

type TaskIdResponse = {
task: number
Expand All @@ -20,33 +21,66 @@ type TaskHistoryResponse = {
}

export const GetTasks = async (): Promise<TasksResponse> =>
await Request<TasksResponse>(`/tasks/`)
await transport({
http: () => Request<TasksResponse>(`/tasks/`),
ws: (ws) => ws.request('get_tasks'),
})

export const GetCompletedTasks = async (): Promise<TasksResponse> =>
await Request<TasksResponse>(`/tasks/completed`)
await transport({
http: () => Request<TasksResponse>(`/tasks/completed`),
// WS handler requires data to be present (at least {}).
ws: (ws) => ws.request('get_completed_tasks', {}),
})

export const MarkTaskComplete = async (id: number, endRecurrence: boolean): Promise<SingleTaskResponse> =>
await Request<SingleTaskResponse>(`/tasks/${id}/do?endRecurrence=${endRecurrence}`, 'POST')
await transport({
http: () => Request<SingleTaskResponse>(`/tasks/${id}/do?endRecurrence=${endRecurrence}`, 'POST'),
ws: (ws) => ws.request('complete_task', { id, endRecurrence }),
})

export const SkipTask = async (id: number): Promise<SingleTaskResponse> =>
await Request<SingleTaskResponse>(`/tasks/${id}/skip`, 'POST')
await transport({
http: () => Request<SingleTaskResponse>(`/tasks/${id}/skip`, 'POST'),
ws: (ws) => ws.request('skip_task', id),
})

export const CreateTask = async (task: Omit<Task, 'id'>) =>
await Request<TaskIdResponse>(`/tasks/`, 'POST', MarshallLabels(task as Task))
await transport({
http: () => Request<TaskIdResponse>(`/tasks/`, 'POST', MarshallLabels(task)),
ws: (ws) => ws.request('create_task', MarshallLabels(task)),
})

export const DeleteTask = async (id: number) =>
await Request<void>(`/tasks/${id}`, 'DELETE')
await transport({
http: () => Request<void>(`/tasks/${id}`, 'DELETE'),
ws: (ws) => ws.request('delete_task', id),
})

export const SaveTask = async (task: Task) =>
await Request<void>(`/tasks/`, 'PUT', MarshallLabels(task))
await transport({
http: () => Request<void>(`/tasks/`, 'PUT', MarshallLabels(task)),
ws: (ws) => ws.request('update_task', MarshallLabels(task)),
})

export const GetTaskHistory = async (taskId: number) =>
await Request<TaskHistoryResponse>(`/tasks/${taskId}/history`)
await transport({
http: () => Request<TaskHistoryResponse>(`/tasks/${taskId}/history`),
ws: (ws) => ws.request('get_task_history', taskId),
})

export const UpdateDueDate = async (
id: number,
due_date: string,
): Promise<SingleTaskResponse> =>
await Request<SingleTaskResponse>(`/tasks/${id}/dueDate`, 'PUT', {
due_date
await transport({
http: () =>
Request<SingleTaskResponse>(`/tasks/${id}/dueDate`, 'PUT', {
due_date,
}),
ws: (ws) =>
ws.request('update_due_date', {
id,
due_date,
}),
})
25 changes: 18 additions & 7 deletions frontend/src/api/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { APIToken, ApiTokenScope } from '@/models/token'
import { Request } from '@/utils/api'
import { transport } from './transport'

export type SingleAPITokenResponse = {
token: APIToken
Expand All @@ -14,14 +15,24 @@ export const CreateLongLivedToken = async (
scopes: ApiTokenScope[],
expiration: number,
) =>
await Request<SingleAPITokenResponse>(`/users/tokens`, 'POST', {
name,
scopes,
expiration,
await transport({
http: () =>
Request<SingleAPITokenResponse>(`/users/tokens`, 'POST', {
name,
scopes,
expiration,
}),
ws: (ws) => ws.request('create_app_token', { name, scopes, expiration }),
})

export const DeleteLongLivedToken = async (id: string) =>
await Request<void>(`/users/tokens/${id}`, 'DELETE')
export const DeleteLongLivedToken = async (id: number) =>
await transport({
http: () => Request<void>(`/users/tokens/${id}`, 'DELETE'),
ws: (ws) => ws.request('delete_app_token', id),
})

export const GetLongLivedTokens = async () =>
await Request<TokensResponse>(`/users/tokens`)
await transport({
http: () => Request<TokensResponse>(`/users/tokens`),
ws: (ws) => ws.request('get_app_tokens'),
})
18 changes: 18 additions & 0 deletions frontend/src/api/transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { store } from '@/store/store'
import WebSocketManager from '@/utils/websocket'

export async function transport<T>(opts: {
http: () => Promise<T>
ws: (mgr: WebSocketManager) => Promise<T>
}): Promise<T> {
const state = store.getState()
const enabled = Boolean(state.featureFlags.sendViaWebsocket)
if (enabled) {
const mgr = WebSocketManager.getInstance()
if (mgr.isConnected()) {
return opts.ws(mgr)
}
}

return opts.http()
}
11 changes: 8 additions & 3 deletions frontend/src/api/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
NotificationTriggerOptions,
NotificationType,
} from '@/models/notifications'
import { transport } from './transport'

type UserResponse = {
user: User
Expand All @@ -21,7 +22,11 @@ export const UpdateNotificationSettings = async (
provider: NotificationType,
triggers: NotificationTriggerOptions,
) =>
await Request<void>(`/users/notifications`, 'PUT', {
provider,
triggers,
await transport({
http: () =>
Request<void>(`/users/notifications`, 'PUT', {
provider,
triggers,
}),
ws: (ws) => ws.request('update_notification_settings', { provider, triggers }),
})
7 changes: 6 additions & 1 deletion frontend/src/constants/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { retrieveValue, storeValue } from '@/utils/storage'

export type FeatureFlag = 'useWebsockets' | 'refreshStaleData'
export type FeatureFlag = 'useWebsockets' | 'sendViaWebsocket' | 'refreshStaleData'

export interface FeatureFlagDefinition {
name: FeatureFlag
Expand All @@ -14,6 +14,11 @@ export const featureFlagDefinitions: FeatureFlagDefinition[] = [
description: 'Use websockets',
defaultValue: false,
},
{
name: 'sendViaWebsocket',
description: 'Send requests via WebSocket (requires websockets)',
defaultValue: false,
},
{
name: 'refreshStaleData',
description: 'Refresh stale data when tab becomes visible',
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/models/token.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export interface APIToken {
id: string
id: number
name: string
token: string
expires_at: string
scopes?: string[]
}

export type ApiTokenScope =
Expand Down
54 changes: 51 additions & 3 deletions frontend/src/models/websocket.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Label } from './label'
import { APIToken } from './token'
import { APIToken, ApiTokenScope } from './token'
import { NotificationTrigger, NotificationTriggerOptions, NotificationType } from './notifications'
import { Task } from './task'

Expand All @@ -8,12 +8,60 @@ export type WSAction =
| 'create_label'
| 'update_label'
| 'delete_label'
| 'get_app_tokens'
| 'create_app_token'
| 'delete_app_token'
| 'update_notification_settings'
| 'get_tasks'
| 'get_completed_tasks'
| 'get_task'
| 'create_task'
| 'update_task'
| 'delete_task'
| 'skip_task'
| 'update_due_date'
| 'complete_task'
| 'uncomplete_task'
| 'get_task_history'

export interface WSActionPayloads {
get_user_labels: void
create_label: Omit<Label, 'id'>
update_label: Label
delete_label: { id: number }
delete_label: number

get_app_tokens: void
create_app_token: {
name: string
scopes: ApiTokenScope[]
expiration: number
}
delete_app_token: number
update_notification_settings: {
provider: NotificationType
triggers: NotificationTriggerOptions
}

get_tasks: void
get_completed_tasks: {
limit?: number
page?: number
}
get_task: number
create_task: Omit<Omit<Task, 'id'>, 'labels'> & { labels: number[] }
update_task: Omit<Task, 'labels'> & { labels: number[] }
delete_task: number
skip_task: number
update_due_date: {
id: number
due_date: string
}
complete_task: {
id: number
endRecurrence: boolean
}
uncomplete_task: number
get_task_history: number
}

export type WSEvent =
Expand All @@ -36,7 +84,7 @@ export interface WSEventPayloads {
label_updated: { label: Label }
label_deleted: { id: number }
app_token_created: APIToken
app_token_deleted: { id: string }
app_token_deleted: { id: number }
notification_settings_updated: {
provider: NotificationType
triggers: NotificationTriggerOptions
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/store/tokensSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const createToken = createAsyncThunk(

export const deleteToken = createAsyncThunk(
'tokens/deleteToken',
async (tokenId: string) => await DeleteLongLivedToken(tokenId)
async (tokenId: number) => await DeleteLongLivedToken(tokenId)
)

const tokensSlice = createSlice({
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/utils/marshalling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,16 @@ export const MakeTask = (taskUI: Omit<TaskUI, 'id'>): Omit<Task, 'id'> => {
end_date: MarshallDate(taskUI.end_date),
}
}
type HasLabels = {
labels: Array<{ id: number }>
}

export const MarshallLabels = (task: Task): Omit<Task, 'labels'> & {labels: number[] } => {
export const MarshallLabels = <T extends HasLabels>(
value: T,
): Omit<T, 'labels'> & { labels: number[] } => {
return {
...task,
labels: task.labels.map(label => label.id),
...value,
labels: value.labels.map(label => label.id),
}
}

Expand Down
Loading
Loading