Skip to content

Commit

Permalink
Manifold TODO (#3211)
Browse files Browse the repository at this point in the history
* Basic layout

* Tweaks

* create market from task

* Add droplet sound

* ui

* Assign tasks to users

* Show/hide completed tasks

* Tweak

* Fix categories and tap outside

* Use mobile hook

* Remove public read policies

* Add todo to sitemap

* Only admins can assign tasks

* Set priorities

* Priority styles
  • Loading branch information
IanPhilips authored Dec 12, 2024
1 parent 63350e6 commit 61c3d21
Show file tree
Hide file tree
Showing 17 changed files with 1,127 additions and 3 deletions.
18 changes: 18 additions & 0 deletions backend/api/src/create-category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { APIHandler } from './helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const createCategory: APIHandler<'create-category'> = async (props, auth) => {
const { name, color } = props
const pg = createSupabaseDirectClient()

console.log('Creating category', { userId: auth.uid, name, color })

const result = await pg.one(
`insert into categories (user_id, name, color)
values ($1, $2, $3)
returning id`,
[auth.uid, name, color]
)

return { id: result.id }
}
29 changes: 29 additions & 0 deletions backend/api/src/create-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { APIHandler } from './helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const createTask: APIHandler<'create-task'> = async (props, auth) => {
const {
text,
category_id: categoryId,
priority,
assignee_id: assigneeId,
} = props
const pg = createSupabaseDirectClient()

console.log('Creating task', {
userId: auth.uid,
text,
categoryId,
priority,
assigneeId,
})

const result = await pg.one(
`insert into tasks (creator_id, assignee_id, text, category_id, priority)
values ($1, $2, $3, $4, $5)
returning *`,
[auth.uid, assigneeId || auth.uid, text, categoryId, priority]
)

return result
}
20 changes: 20 additions & 0 deletions backend/api/src/get-categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { APIHandler } from './helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const getCategories: APIHandler<'get-categories'> = async (_, auth) => {
const pg = createSupabaseDirectClient()

console.log('Getting categories for user', auth.uid)

const categories = await pg.manyOrNone(
`SELECT DISTINCT ON (c.id) c.*
FROM categories c
LEFT JOIN tasks t ON c.id = t.category_id
WHERE c.user_id = $1
OR t.assignee_id = $1
ORDER BY c.id, c.display_order, c.created_time`,
[auth.uid]
)

return { categories }
}
18 changes: 18 additions & 0 deletions backend/api/src/get-tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { APIHandler } from './helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const getTasks: APIHandler<'get-tasks'> = async (_, auth) => {
const pg = createSupabaseDirectClient()

console.log('Getting tasks for user', auth.uid)

const tasks = await pg.manyOrNone(
`select *
from tasks
where creator_id = $1 or assignee_id = $1
order by priority, created_time desc`,
[auth.uid]
)

return { tasks }
}
15 changes: 15 additions & 0 deletions backend/api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,17 @@ import { generateAIAnswers } from './generate-ai-answers'
import { getmonthlybets2024 } from './get-monthly-bets-2024'
import { getmaxminprofit2024 } from './get-max-min-profit-2024'
import { getNextLoanAmount } from './get-next-loan-amount'

import { createTask } from './create-task'
import { updateTask } from './update-task'
import { createCategory } from './create-category'
import { getCategories } from './get-categories'
import { updateCategory } from './update-category'
import { getTasks } from './get-tasks'

import { getSiteActivity } from './get-site-activity'


// we define the handlers in this object in order to typecheck that every API has a handler
export const handlers: { [k in APIPath]: APIHandler<k> } = {
'refresh-all-clients': refreshAllClients,
Expand Down Expand Up @@ -298,5 +307,11 @@ export const handlers: { [k in APIPath]: APIHandler<k> } = {
'get-monthly-bets-2024': getmonthlybets2024,
'get-max-min-profit-2024': getmaxminprofit2024,
'get-next-loan-amount': getNextLoanAmount,
'create-task': createTask,
'update-task': updateTask,
'create-category': createCategory,
'get-categories': getCategories,
'update-category': updateCategory,
'get-tasks': getTasks,
'get-site-activity': getSiteActivity,
}
38 changes: 38 additions & 0 deletions backend/api/src/update-category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { APIHandler } from './helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const updateCategory: APIHandler<'update-category'> = async (
props,
auth
) => {
const { categoryId, name, color, displayOrder, archived } = props
const pg = createSupabaseDirectClient()

console.log('Updating category', {
categoryId,
name,
color,
displayOrder,
archived,
userId: auth.uid,
})

const updates: { [key: string]: any } = {}
if (name !== undefined) updates.name = name
if (color !== undefined) updates.color = color
if (displayOrder !== undefined) updates.display_order = displayOrder
if (archived !== undefined) updates.archived = archived

const setClauses = Object.entries(updates)
.map(([key], i) => `${key} = $${i + 3}`)
.join(', ')

await pg.none(
`update categories
set ${setClauses}
where id = $1 and user_id = $2`,
[categoryId, auth.uid, ...Object.values(updates)]
)

return { success: true }
}
37 changes: 37 additions & 0 deletions backend/api/src/update-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { log } from 'shared/utils'
import { APIError, APIHandler } from './helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'

export const updateTask: APIHandler<'update-task'> = async (props, auth) => {
const { id, text, completed, priority, category_id, archived, assignee_id } =
props
const pg = createSupabaseDirectClient()
log('Updating task', props)

// Build update fields dynamically
const updates: { [key: string]: any } = {}
if (completed !== undefined) updates.completed = completed
if (priority !== undefined) updates.priority = priority
if (category_id !== undefined) updates.category_id = category_id
if (text !== undefined) updates.text = text
if (archived !== undefined) updates.archived = archived
if (assignee_id !== undefined) updates.assignee_id = assignee_id
const setClauses = Object.entries(updates)
.map(([key], i) => `${key} = $${i + 3}`)
.join(', ')

const result = await pg.oneOrNone(
`update tasks
set ${setClauses}
where id = $1 and (creator_id = $2 or assignee_id = $2)
returning id, priority, category_id, text, completed, assignee_id
`,
[id, auth.uid, ...Object.values(updates)]
)

if (!result) {
throw new APIError(404, 'Task not found or unauthorized')
}

return { success: true }
}
16 changes: 16 additions & 0 deletions backend/supabase/categories.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
create table if not exists
categories (
id bigint primary key generated always as identity not null,
user_id text not null,
name text not null,
color text,
display_order integer default 0 not null,
archived boolean default false not null,
created_time timestamp with time zone default now() not null
);

-- Row Level Security
alter table categories enable row level security;

-- Indexes
create index categories_user_id_idx on categories (user_id);
22 changes: 22 additions & 0 deletions backend/supabase/tasks.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
create table if not exists
tasks (
id bigint primary key generated always as identity not null,
creator_id text not null references users (id),
assignee_id text not null references users (id),
category_id bigint not null,
text text not null,
completed boolean default false not null,
priority integer default 0 not null,
archived boolean default false not null,
created_time timestamp with time zone default now() not null
);

-- Row Level Security
alter table tasks enable row level security;

-- Indexes
create index tasks_creator_id_idx on tasks (creator_id);

create index tasks_assignee_id_idx on tasks (assignee_id);

create index tasks_category_id_idx on tasks (category_id);
76 changes: 76 additions & 0 deletions common/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { NON_POINTS_BETS_LIMIT } from 'common/supabase/bets'
import { ContractMetric } from 'common/contract-metric'

import { JSONContent } from '@tiptap/core'
import { Task, TaskCategory } from 'common/todo'
// mqp: very unscientific, just balancing our willingness to accept load
// with user willingness to put up with stale data
export const DEFAULT_CACHE_STRATEGY =
Expand Down Expand Up @@ -1913,6 +1914,80 @@ export const API = (_apiTypeCheck = {
userId: z.string(),
}),
},

'create-task': {
method: 'POST',
visibility: 'public',
authed: true,
returns: {} as Task,
props: z
.object({
text: z.string(),
category_id: z.number().optional(),
priority: z.number().default(0),
assignee_id: z.string().optional(),
})
.strict(),
},
'update-task': {
method: 'POST',
visibility: 'public',
authed: true,
returns: {} as { success: boolean },
props: z
.object({
id: z.number(),
text: z.string().optional(),
completed: z.boolean().optional(),
priority: z.number().optional(),
category_id: z.number().optional(),
archived: z.boolean().optional(),
assignee_id: z.string().optional(),
})
.strict(),
},
'create-category': {
method: 'POST',
visibility: 'public',
authed: true,
returns: {} as { id: number },
props: z
.object({
name: z.string(),
color: z.string().optional(),
displayOrder: z.number().optional(),
})
.strict(),
},
'get-categories': {
method: 'GET',
visibility: 'public',
authed: true,
returns: {} as { categories: TaskCategory[] },
props: z.object({}).strict(),
},
'update-category': {
method: 'POST',
visibility: 'public',
authed: true,
returns: {} as { success: boolean },
props: z
.object({
categoryId: z.number(),
name: z.string().optional(),
color: z.string().optional(),
displayOrder: z.number().optional(),
archived: z.boolean().optional(),
})
.strict(),
},
'get-tasks': {
method: 'GET',
visibility: 'public',
authed: true,
returns: {} as { tasks: Task[] },
props: z.object({}).strict(),

'get-site-activity': {
method: 'GET',
visibility: 'public',
Expand All @@ -1929,6 +2004,7 @@ export const API = (_apiTypeCheck = {
blockedGroupSlugs: z.array(z.string()).optional(),
blockedContractIds: z.array(z.string()).optional(),
}).strict(),

},
} as const)

Expand Down
19 changes: 19 additions & 0 deletions common/src/todo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type TaskCategory = {
id: number
name: string
color?: string
displayOrder: number
archived?: boolean
}

export type Task = {
id: number
creator_id: string
assignee_id: string
text: string
completed: boolean
category_id: number // -1 for inbox
created_time: number
priority: number
archived: boolean
}
Loading

0 comments on commit 61c3d21

Please sign in to comment.