todoist-sdk-0.1.2.1: src/Web/Todoist/Domain/Project.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE UndecidableInstances #-}
{- |
Module : Web.Todoist.Domain.Project
Description : Project API types and operations for Todoist REST API
Copyright : (c) 2025 Sam S. Almahri
License : MIT
Maintainer : sam.salmahri@gmail.com
This module provides types and operations for working with Todoist projects.
Projects are used to organize tasks into groups, support hierarchical nesting,
and can be displayed in list, board, or calendar view.
= Usage Example
@
import Web.Todoist.Domain.Project
import Web.Todoist.Util.Builder
import Web.Todoist.Runner
main :: IO ()
main = do
let config = newTodoistConfig "your-api-token"
-- Create a new project
let newProj = runBuilder (createProjectBuilder "My Project")
(withDescription "Project description" <> withViewStyle Board)
project <- todoist config (addProject newProj)
-- Get all projects
projects <- todoist config getAllProjects
-- Update a project
let update = runBuilder updateProjectBuilder (withName "Updated Name")
updated <- todoist config (updateProject update projectId)
@
For more details, see: <https://developer.todoist.com/rest/v2/#projects>
-}
module Web.Todoist.Domain.Project
( TodoistProjectM (..)
, Project (..)
, Collaborator (..)
, ProjectCreate
, ProjectUpdate (..)
, PaginationParam (..)
, createProjectBuilder
, updateProjectBuilder
, paginationParamBuilder
, IsShared (..)
, IsArchived (..)
, CanAssignTasks (..)
-- * Lenses
, projectId
, name
, description
, order
, color
, isCollapsed
, isShared
, isFavorite
, isArchived
, canAssignTasks
, viewStyle
, createdAt
, updatedAt
, collaboratorId
, collaboratorName
, collaboratorEmail
-- * ProjectCreate Lenses
, projectCreateName
, projectCreateDescription
, projectCreateParentId
, projectCreateColor
, projectCreateIsFavorite
, projectCreateViewStyle
, projectCreateWorkspaceId
-- * ProjectUpdate Lenses
, projectUpdateName
, projectUpdateDescription
, projectUpdateColor
, projectUpdateIsFavorite
, projectUpdateViewStyle
) where
import Web.Todoist.Domain.Types
( Color (..)
, Description (..)
, IsCollapsed (..)
, IsFavorite (..)
, Name (..)
, Order (..)
, ProjectId (..)
, ViewStyle (..)
)
import Web.Todoist.Internal.Types (Params, ProjectPermissions)
import Web.Todoist.Util.Builder
( HasCursor (..)
, HasColor (..)
, HasDescription (..)
, HasIsFavorite (..)
, HasLimit (..)
, HasName (..)
, HasParentId (..)
, HasViewStyle (..)
, HasWorkspaceId (..)
, Initial
, seed
)
import Web.Todoist.Util.QueryParam (QueryParam (..))
import Control.Applicative ((<|>))
import Control.Monad (Monad)
import Data.Aeson
( FromJSON (parseJSON)
, ToJSON (toJSON)
, Value
, defaultOptions
, fieldLabelModifier
, genericParseJSON
, genericToJSON
, omitNothingFields
)
import Data.Aeson.Types (Parser)
import Data.Bool (Bool (False, True))
import Data.Eq (Eq)
import Data.Function (($))
import Data.Functor ((<$>))
import Data.Int (Int)
import qualified Data.List as L
import Data.Maybe (Maybe (Just, Nothing), maybe)
import Data.Monoid ((<>))
import Data.String (String)
import Data.Text (Text)
import qualified Data.Text
import GHC.Generics (Generic)
import Lens.Micro (to)
import Text.Show (Show)
import Web.Todoist.Lens (Getter)
{- | Project domain type representing a Todoist project
Contains all project metadata including name, color, view style, and hierarchical
information. Projects can be nested using parent_id and organized visually
using order and view_style.
-}
data Project = Project
{ _id :: ProjectId
, _name :: Name
, _description :: Description
, _order :: Order
, _color :: Color
, _is_collapsed :: IsCollapsed
, _is_shared :: IsShared
, _is_favorite :: IsFavorite
, _is_archived :: IsArchived
, _can_assign_tasks :: CanAssignTasks
, _view_style :: ViewStyle
, _created_at :: Maybe Text
, _updated_at :: Maybe Text
}
deriving (Show, Eq)
-- | Lenses for Project
projectId :: Getter Project ProjectId
projectId = to (\(Project {_id = x}) -> x)
{-# INLINE projectId #-}
name :: Getter Project Name
name = to (\(Project {_name = x}) -> x)
{-# INLINE name #-}
description :: Getter Project Description
description = to (\(Project {_description = x}) -> x)
{-# INLINE description #-}
order :: Getter Project Order
order = to (\(Project {_order = x}) -> x)
{-# INLINE order #-}
color :: Getter Project Color
color = to (\(Project {_color = x}) -> x)
{-# INLINE color #-}
isCollapsed :: Getter Project IsCollapsed
isCollapsed = to (\(Project {_is_collapsed = x}) -> x)
{-# INLINE isCollapsed #-}
isShared :: Getter Project IsShared
isShared = to (\(Project {_is_shared = x}) -> x)
{-# INLINE isShared #-}
isFavorite :: Getter Project IsFavorite
isFavorite = to (\(Project {_is_favorite = x}) -> x)
{-# INLINE isFavorite #-}
isArchived :: Getter Project IsArchived
isArchived = to (\(Project {_is_archived = x}) -> x)
{-# INLINE isArchived #-}
canAssignTasks :: Getter Project CanAssignTasks
canAssignTasks = to (\(Project {_can_assign_tasks = x}) -> x)
{-# INLINE canAssignTasks #-}
viewStyle :: Getter Project ViewStyle
viewStyle = to (\(Project {_view_style = x}) -> x)
{-# INLINE viewStyle #-}
createdAt :: Getter Project (Maybe Text)
createdAt = to (\(Project {_created_at = x}) -> x)
{-# INLINE createdAt #-}
updatedAt :: Getter Project (Maybe Text)
updatedAt = to (\(Project {_updated_at = x}) -> x)
{-# INLINE updatedAt #-}
{- | Request body for creating a new project
All fields except name are optional. Use the builder pattern with 'createProjectBuilder'
for ergonomic construction.
-}
data ProjectCreate = ProjectCreate
{ _name :: Name
, _description :: Maybe Description
, _parent_id :: Maybe ParentId
, _color :: Maybe Color -- Default: {"name":"charcoal","hex":"#808080","database_index":47}
, _is_favorite :: IsFavorite
, _view_style :: Maybe ViewStyle
, _workspace_id :: Maybe Int
}
deriving (Show, Generic)
instance ToJSON ProjectCreate where
toJSON :: ProjectCreate -> Value
toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1, omitNothingFields = True}
instance FromJSON ProjectCreate where
parseJSON :: Value -> Parser ProjectCreate
parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}
instance HasDescription ProjectCreate where
hasDescription :: Text -> ProjectCreate -> ProjectCreate
hasDescription desc ProjectCreate {..} = ProjectCreate {_description = Just (Description desc), ..}
instance HasParentId ProjectCreate where
hasParentId :: Text -> ProjectCreate -> ProjectCreate
hasParentId pid ProjectCreate {..} = ProjectCreate {_parent_id = Just (ParentIdStr (Data.Text.unpack pid)), ..}
instance HasViewStyle ProjectCreate where
hasViewStyle :: ViewStyle -> ProjectCreate -> ProjectCreate
hasViewStyle style ProjectCreate {..} = ProjectCreate {_view_style = Just style, ..}
instance HasWorkspaceId ProjectCreate where
hasWorkspaceId :: Int -> ProjectCreate -> ProjectCreate
hasWorkspaceId wid ProjectCreate {..} = ProjectCreate {_workspace_id = Just wid, ..}
-- | Lenses for ProjectCreate
projectCreateName :: Getter ProjectCreate Name
projectCreateName = to (\(ProjectCreate {_name = x}) -> x)
{-# INLINE projectCreateName #-}
projectCreateDescription :: Getter ProjectCreate (Maybe Description)
projectCreateDescription = to (\(ProjectCreate {_description = x}) -> x)
{-# INLINE projectCreateDescription #-}
projectCreateParentId :: Getter ProjectCreate (Maybe ParentId)
projectCreateParentId = to (\(ProjectCreate {_parent_id = x}) -> x)
{-# INLINE projectCreateParentId #-}
projectCreateColor :: Getter ProjectCreate (Maybe Color)
projectCreateColor = to (\(ProjectCreate {_color = x}) -> x)
{-# INLINE projectCreateColor #-}
projectCreateIsFavorite :: Getter ProjectCreate IsFavorite
projectCreateIsFavorite = to (\(ProjectCreate {_is_favorite = x}) -> x)
{-# INLINE projectCreateIsFavorite #-}
projectCreateViewStyle :: Getter ProjectCreate (Maybe ViewStyle)
projectCreateViewStyle = to (\(ProjectCreate {_view_style = x}) -> x)
{-# INLINE projectCreateViewStyle #-}
projectCreateWorkspaceId :: Getter ProjectCreate (Maybe Int)
projectCreateWorkspaceId = to (\(ProjectCreate {_workspace_id = x}) -> x)
{-# INLINE projectCreateWorkspaceId #-}
{- | Request body type for updating an existing project
All fields are optional to support partial updates
-}
data ProjectUpdate = ProjectUpdate
{ _name :: Maybe Name
, _description :: Maybe Description
, _color :: Maybe Color -- Note: API accepts string or integer, using Text for now
, _is_favorite :: Maybe IsFavorite
, _view_style :: Maybe ViewStyle
}
deriving (Show, Eq, Generic)
instance ToJSON ProjectUpdate where
toJSON :: ProjectUpdate -> Value
toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1, omitNothingFields = True}
instance FromJSON ProjectUpdate where
parseJSON :: Value -> Parser ProjectUpdate
parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}
instance HasName ProjectUpdate where
hasName :: Text -> ProjectUpdate -> ProjectUpdate
hasName name ProjectUpdate {..} = ProjectUpdate {_name = Just (Name name), ..}
instance HasDescription ProjectUpdate where
hasDescription :: Text -> ProjectUpdate -> ProjectUpdate
hasDescription desc ProjectUpdate {..} = ProjectUpdate {_description = Just (Description desc), ..}
instance HasViewStyle ProjectUpdate where
hasViewStyle :: ViewStyle -> ProjectUpdate -> ProjectUpdate
hasViewStyle style ProjectUpdate {..} = ProjectUpdate {_view_style = Just style, ..}
instance HasIsFavorite ProjectUpdate where
hasIsFavorite :: Bool -> ProjectUpdate -> ProjectUpdate
hasIsFavorite fav ProjectUpdate {..} = ProjectUpdate {_is_favorite = Just (IsFavorite fav), ..}
instance HasColor ProjectUpdate where
hasColor :: Text -> ProjectUpdate -> ProjectUpdate
hasColor col ProjectUpdate {..} = ProjectUpdate {_color = Just (Color col), ..}
-- | Lenses for ProjectUpdate
projectUpdateName :: Getter ProjectUpdate (Maybe Name)
projectUpdateName = to (\(ProjectUpdate {_name = x}) -> x)
{-# INLINE projectUpdateName #-}
projectUpdateDescription :: Getter ProjectUpdate (Maybe Description)
projectUpdateDescription = to (\(ProjectUpdate {_description = x}) -> x)
{-# INLINE projectUpdateDescription #-}
projectUpdateColor :: Getter ProjectUpdate (Maybe Color)
projectUpdateColor = to (\(ProjectUpdate {_color = x}) -> x)
{-# INLINE projectUpdateColor #-}
projectUpdateIsFavorite :: Getter ProjectUpdate (Maybe IsFavorite)
projectUpdateIsFavorite = to (\(ProjectUpdate {_is_favorite = x}) -> x)
{-# INLINE projectUpdateIsFavorite #-}
projectUpdateViewStyle :: Getter ProjectUpdate (Maybe ViewStyle)
projectUpdateViewStyle = to (\(ProjectUpdate {_view_style = x}) -> x)
{-# INLINE projectUpdateViewStyle #-}
-- projects
createProjectBuilder :: Text -> Initial ProjectCreate
createProjectBuilder name =
seed
ProjectCreate
{ _name = Name name
, _description = Nothing
, _parent_id = Nothing
, _color = Nothing
, _is_favorite = IsFavorite False
, _view_style = Nothing
, _workspace_id = Nothing
}
{- | Create an empty ProjectUpdate (for use with Builder combinators)
Use with runBuilder: runBuilder updateProjectBuilder (withName "New Name" <> withDescription "desc")
-}
updateProjectBuilder :: Initial ProjectUpdate
updateProjectBuilder =
seed
ProjectUpdate
{ _name = Nothing
, _description = Nothing
, _color = Nothing
, _is_favorite = Nothing
, _view_style = Nothing
}
data Collaborator = Collaborator
{ _id :: Text
, _name :: Name
, _email :: Text
}
deriving (Show, Eq, Generic)
instance ToJSON Collaborator where
toJSON :: Collaborator -> Value
toJSON = genericToJSON defaultOptions {fieldLabelModifier = L.drop 1}
instance FromJSON Collaborator where
parseJSON :: Value -> Parser Collaborator
parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = L.drop 1}
-- | Lenses for Collaborator
collaboratorId :: Getter Collaborator Text
collaboratorId = to (\(Collaborator {_id = x}) -> x)
{-# INLINE collaboratorId #-}
collaboratorName :: Getter Collaborator Name
collaboratorName = to (\(Collaborator {_name = x}) -> x)
{-# INLINE collaboratorName #-}
collaboratorEmail :: Getter Collaborator Text
collaboratorEmail = to (\(Collaborator {_email = x}) -> x)
{-# INLINE collaboratorEmail #-}
{- | Query parameters for paginated requests
Used for projects and collaborators endpoints
-}
data PaginationParam = PaginationParam
{ cursor :: Maybe Text
, limit :: Maybe Int
}
deriving (Show, Eq)
instance QueryParam PaginationParam where
toQueryParam :: PaginationParam -> Params
toQueryParam PaginationParam {..} =
maybe [] (\c -> [("cursor", c)]) cursor
<> maybe [] (\l -> [("limit", Data.Text.show l)]) limit
-- | Create new PaginationParam for use with builder pattern
paginationParamBuilder :: Initial PaginationParam
paginationParamBuilder = seed $ PaginationParam {cursor = Nothing, limit = Nothing}
-- HasX instances for builder pattern
instance HasCursor PaginationParam where
hasCursor :: Text -> PaginationParam -> PaginationParam
hasCursor c PaginationParam {..} = PaginationParam {cursor = Just c, ..}
instance HasLimit PaginationParam where
hasLimit :: Int -> PaginationParam -> PaginationParam
hasLimit l PaginationParam {..} = PaginationParam {limit = Just l, ..}
data ParentId = ParentIdStr String | ParentIdInt Int deriving (Show, Generic)
instance ToJSON ParentId where
toJSON :: ParentId -> Value
toJSON (ParentIdStr s) = toJSON s
toJSON (ParentIdInt i) = toJSON i
instance FromJSON ParentId where
parseJSON :: Value -> Parser ParentId
parseJSON v = (ParentIdStr <$> parseJSON v) <|> (ParentIdInt <$> parseJSON v)
class (Monad m) => TodoistProjectM m where
-- | Get all projects (automatically fetches all pages)
getAllProjects :: m [Project]
-- | Get a single project by ID
getProject :: ProjectId -> m Project
-- | Get all collaborators for a project (automatically fetches all pages)
getProjectCollaborators :: ProjectId -> m [Collaborator]
-- | Create a new project
addProject :: ProjectCreate -> m ProjectId
-- | Delete a project and all its tasks
deleteProject :: ProjectId -> m ()
-- | Archive a project (hides from active view but preserves data)
archiveProject :: ProjectId -> m ProjectId
-- | Unarchive a previously archived project
unarchiveProject :: ProjectId -> m ProjectId
getProjectPermissions :: m ProjectPermissions
-- | Update an existing project
updateProject :: ProjectUpdate -> ProjectId -> m Project
{- | Get projects with manual pagination control
Returns a tuple of (results, next_cursor) for the requested page
-}
getAllProjectsPaginated :: PaginationParam -> m ([Project], Maybe Text)
{- | Get project collaborators with manual pagination control
Returns a tuple of (results, next_cursor) for the requested page
-}
getProjectCollaboratorsPaginated :: PaginationParam -> ProjectId -> m ([Collaborator], Maybe Text)
-- | Get all projects with custom page size (fetches all pages automatically)
getAllProjectsWithLimit :: Int -> m [Project]
-- | Get all project collaborators with custom page size (fetches all pages automatically)
getProjectCollaboratorsWithLimit :: Int -> ProjectId -> m [Collaborator]
newtype IsShared = IsShared {getIsShared :: Bool} deriving (Show, Eq, Generic)
instance FromJSON IsShared where
parseJSON :: Value -> Parser IsShared
parseJSON v = IsShared <$> parseJSON v
instance ToJSON IsShared where
toJSON :: IsShared -> Value
toJSON (IsShared txt) = toJSON txt
newtype IsArchived = IsArchived {getIsArchived :: Bool} deriving (Show, Eq, Generic)
instance FromJSON IsArchived where
parseJSON :: Value -> Parser IsArchived
parseJSON v = IsArchived <$> parseJSON v
instance ToJSON IsArchived where
toJSON :: IsArchived -> Value
toJSON (IsArchived txt) = toJSON txt
newtype CanAssignTasks = CanAssignTasks {getCanAssignTasks :: Bool} deriving (Show, Eq, Generic)
instance FromJSON CanAssignTasks where
parseJSON :: Value -> Parser CanAssignTasks
parseJSON v = CanAssignTasks <$> parseJSON v
instance ToJSON CanAssignTasks where
toJSON :: CanAssignTasks -> Value
toJSON (CanAssignTasks txt) = toJSON txt
newtype Email = Email {getEmail :: Text} deriving (Show, Eq, Generic)
instance FromJSON Email where
parseJSON :: Value -> Parser Email
parseJSON v = Email <$> parseJSON v
instance ToJSON Email where
toJSON :: Email -> Value
toJSON (Email txt) = toJSON txt