diff --git a/share-api.cabal b/share-api.cabal index 989c26e4..5154f5c8 100644 --- a/share-api.cabal +++ b/share-api.cabal @@ -148,7 +148,8 @@ library Share.Web.Share.DefinitionSearch Share.Web.Share.Diffs.Impl Share.Web.Share.Diffs.Types - Share.Web.Share.DisplayInfo + Share.Web.Share.DisplayInfo.Queries + Share.Web.Share.DisplayInfo.Types Share.Web.Share.Impl Share.Web.Share.Orgs.API Share.Web.Share.Orgs.Impl diff --git a/src/Share/BackgroundJobs/Webhooks/Types.hs b/src/Share/BackgroundJobs/Webhooks/Types.hs index 16e3b7ef..905bfa0d 100644 --- a/src/Share/BackgroundJobs/Webhooks/Types.hs +++ b/src/Share/BackgroundJobs/Webhooks/Types.hs @@ -15,7 +15,7 @@ import Data.Text qualified as Text import Share.Contribution (ContributionStatus) import Share.IDs import Share.Prelude -import Share.Web.Share.DisplayInfo (UserDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo) data BranchPayload = BranchPayload { branchId :: BranchId, diff --git a/src/Share/Postgres/Users/Queries.hs b/src/Share/Postgres/Users/Queries.hs index 8fb14a08..34dd7905 100644 --- a/src/Share/Postgres/Users/Queries.hs +++ b/src/Share/Postgres/Users/Queries.hs @@ -15,6 +15,7 @@ module Share.Postgres.Users.Queries userByGithubUserId, userByHandle, createFromGithubUser, + joinOrgIdsToUserIdsOf, NewOrPreExisting (..), getNewOrPreExisting, isNew, @@ -48,7 +49,7 @@ import Share.Utils.Postgres import Share.Utils.URI (URIParam (..)) import Share.Web.Authorization.Types qualified as AuthZ import Share.Web.Errors (EntityMissing (EntityMissing), ErrorID (..), ToServerError (..)) -import Share.Web.Share.DisplayInfo (UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo (..), UserLike (..)) -- | Efficiently resolve User Display Info for UserIds within a structure. userDisplayInfoOf :: (PG.QueryA m) => Traversal s t UserId UserDisplayInfo -> s -> m t @@ -274,12 +275,12 @@ findOrCreateGithubUser authZReceipt ghu@(GithubUser _login githubUserId _avatarU Nothing -> do New <$> createFromGithubUser authZReceipt ghu primaryEmail userHandle -searchUsersByNameOrHandlePrefix :: Query -> Limit -> PG.Transaction e [(User, Maybe OrgId)] +searchUsersByNameOrHandlePrefix :: Query -> Limit -> PG.Transaction e [(UserLike UserId team OrgId)] searchUsersByNameOrHandlePrefix (Query prefix) (Limit limit) = do let q = likeEscape prefix <> "%" - PG.queryListRows @(User PG.:. (PG.Only (Maybe OrgId))) + PG.queryListRows @(UserId, Maybe OrgId) [PG.sql| - SELECT u.id, u.name, u.primary_email, u.avatar_url, u.handle, u.private, org.id + SELECT u.id, org.id FROM users u LEFT JOIN orgs org ON org.user_id = u.id WHERE (u.handle ILIKE #{q} @@ -287,7 +288,30 @@ searchUsersByNameOrHandlePrefix (Query prefix) (Limit limit) = do ) AND NOT u.private LIMIT #{limit} |] - <&> fmap \(user PG.:. PG.Only mayOrgId) -> (user, mayOrgId) + <&> fmap \(userId, mayOrgId) -> case mayOrgId of + Just orgId -> UnifiedOrg orgId + Nothing -> UnifiedUser userId + +joinOrgIdsToUserIdsOf :: Traversal s t UserId (UserId, Maybe OrgId) -> s -> PG.Transaction e t +joinOrgIdsToUserIdsOf trav s = do + s + & unsafePartsOf trav %%~ \userIds -> do + let usersTable = zip [0 :: Int32 ..] userIds + PG.queryListRows @(UserId, Maybe OrgId) + [PG.sql| + WITH values(ord, user_id) AS ( + SELECT * FROM ^{PG.toTable usersTable} + ) + SELECT u.id, org.id + FROM values + LEFT JOIN orgs org ON org.user_id = values.user_id + JOIN users u ON u.id = values.user_id + ORDER BY ord + |] + <&> fmap \(userId, mayOrgId) -> + if length userIds /= length userIds + then error "joinOrgIdsToUserIdsOf: Missing user ids." + else (userId, mayOrgId) data UserCreationError = UserHandleTaken UserHandle diff --git a/src/Share/Web/Authorization/Types.hs b/src/Share/Web/Authorization/Types.hs index 5be6cf0d..c7898fb7 100644 --- a/src/Share/Web/Authorization/Types.hs +++ b/src/Share/Web/Authorization/Types.hs @@ -48,7 +48,7 @@ import Share.IDs import Share.Postgres qualified as PG import Share.Prelude import Share.Utils.API (AtKey (..)) -import Share.Web.Share.DisplayInfo +import Share.Web.Share.DisplayInfo.Types data SubjectKind = UserSubjectKind | OrgSubjectKind | TeamSubjectKind deriving (Show) diff --git a/src/Share/Web/Share/Branches/Types.hs b/src/Share/Web/Share/Branches/Types.hs index 17110269..41072f39 100644 --- a/src/Share/Web/Share/Branches/Types.hs +++ b/src/Share/Web/Share/Branches/Types.hs @@ -14,7 +14,7 @@ import Share.IDs import Share.IDs qualified as IDs import Share.Postgres.IDs import Share.Web.Share.Contributions.Types (ShareContribution) -import Share.Web.Share.DisplayInfo (UserDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo) import Share.Web.Share.Projects.Types branchToShareBranch :: BranchShortHand -> Branch CausalHash -> APIProject -> [ShareContribution UserDisplayInfo] -> ShareBranch diff --git a/src/Share/Web/Share/Comments/API.hs b/src/Share/Web/Share/Comments/API.hs index 590115e0..0d1d565e 100644 --- a/src/Share/Web/Share/Comments/API.hs +++ b/src/Share/Web/Share/Comments/API.hs @@ -7,7 +7,7 @@ import Servant import Share.IDs import Share.Web.Share.Comments import Share.Web.Share.Comments.Types -import Share.Web.Share.DisplayInfo (UserDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo) type CommentResourceServer = UpdateComment :<|> DeleteComment diff --git a/src/Share/Web/Share/Comments/Impl.hs b/src/Share/Web/Share/Comments/Impl.hs index cc992740..3779c288 100644 --- a/src/Share/Web/Share/Comments/Impl.hs +++ b/src/Share/Web/Share/Comments/Impl.hs @@ -21,7 +21,7 @@ import Share.Web.Authorization qualified as AuthZ import Share.Web.Errors import Share.Web.Share.Comments import Share.Web.Share.Comments.Types -import Share.Web.Share.DisplayInfo (UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo (..)) createCommentEndpoint :: Maybe Session -> diff --git a/src/Share/Web/Share/Comments/Types.hs b/src/Share/Web/Share/Comments/Types.hs index c64323d8..d6b431ce 100644 --- a/src/Share/Web/Share/Comments/Types.hs +++ b/src/Share/Web/Share/Comments/Types.hs @@ -9,7 +9,7 @@ module Share.Web.Share.Comments.Types where import Data.Aeson import Share.Prelude import Share.Web.Share.Comments (CommentEvent (..), RevisionNumber) -import Share.Web.Share.DisplayInfo (UserDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo) data CreateCommentRequest = CreateCommentRequest { content :: Text diff --git a/src/Share/Web/Share/Contributions/API.hs b/src/Share/Web/Share/Contributions/API.hs index 7b7b7296..d062cdbf 100644 --- a/src/Share/Web/Share/Contributions/API.hs +++ b/src/Share/Web/Share/Contributions/API.hs @@ -14,7 +14,7 @@ import Share.Utils.Servant (RequiredQueryParam) import Share.Web.Share.Comments.API qualified as Comments import Share.Web.Share.Contributions.Types import Share.Web.Share.Diffs.Types (ShareNamespaceDiffResponse, ShareTermDiffResponse, ShareTypeDiffResponse) -import Share.Web.Share.DisplayInfo (UserDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo) import Unison.Name (Name) type ContributionsByUserAPI = ListContributionsByUserEndpoint diff --git a/src/Share/Web/Share/Contributions/Impl.hs b/src/Share/Web/Share/Contributions/Impl.hs index 84b088c2..d8a95883 100644 --- a/src/Share/Web/Share/Contributions/Impl.hs +++ b/src/Share/Web/Share/Contributions/Impl.hs @@ -56,7 +56,7 @@ import Share.Web.Share.Contributions.MergeDetection qualified as MergeDetection import Share.Web.Share.Contributions.Types import Share.Web.Share.Diffs.Impl qualified as Diffs import Share.Web.Share.Diffs.Types (ShareNamespaceDiffResponse (..), ShareNamespaceDiffStatus (..), ShareTermDiffResponse (..), ShareTypeDiffResponse (..)) -import Share.Web.Share.DisplayInfo (UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo (..)) import Unison.Name (Name) import Unison.Server.Types import Unison.Syntax.Name qualified as Name diff --git a/src/Share/Web/Share/Contributions/Types.hs b/src/Share/Web/Share/Contributions/Types.hs index cfa02778..5cc4be9c 100644 --- a/src/Share/Web/Share/Contributions/Types.hs +++ b/src/Share/Web/Share/Contributions/Types.hs @@ -20,7 +20,7 @@ import Share.Utils.API (NullableUpdate, parseNullableUpdate) import Share.Utils.Logging qualified as Logging import Share.Web.Errors qualified as Err import Share.Web.Share.Comments (CommentEvent (..), commentEventTimestamp) -import Share.Web.Share.DisplayInfo (UserDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo) import U.Codebase.HashTags (CausalHash (..)) import Unison.Hash qualified as Hash import Web.HttpApiData (ToHttpApiData (..)) diff --git a/src/Share/Web/Share/DisplayInfo.hs b/src/Share/Web/Share/DisplayInfo.hs deleted file mode 100644 index 1afbf87a..00000000 --- a/src/Share/Web/Share/DisplayInfo.hs +++ /dev/null @@ -1,75 +0,0 @@ --- Standard ways of displaying core Share concepts. --- --- This was consolidated to this module mostly to avoid circular imports. -module Share.Web.Share.DisplayInfo - ( UserDisplayInfo (..), - OrgDisplayInfo (..), - TeamDisplayInfo (..), - ) -where - -import Data.Aeson (ToJSON (..)) -import Data.Aeson qualified as Aeson -import Data.Aeson.Types (FromJSON) -import Network.URI (URI) -import Share.IDs -import Share.Prelude -import Share.Utils.URI (URIParam (..)) - --- | Common type for displaying a user. -data UserDisplayInfo = UserDisplayInfo - { handle :: UserHandle, - name :: Maybe Text, - avatarUrl :: Maybe URI, - userId :: UserId - } - deriving (Show, Eq, Ord) - -instance ToJSON UserDisplayInfo where - toJSON UserDisplayInfo {handle, name, avatarUrl, userId} = - Aeson.object - [ "handle" Aeson..= handle, - "name" Aeson..= name, - "avatarUrl" Aeson..= (URIParam <$> avatarUrl), - "userId" Aeson..= userId - ] - -instance FromJSON UserDisplayInfo where - parseJSON = - Aeson.withObject "UserDisplayInfo" $ \o -> do - handle <- o Aeson..: "handle" - name <- o Aeson..:? "name" - avatarUrl <- fmap unpackURI <$> o Aeson..:? "avatarUrl" - userId <- o Aeson..: "userId" - pure UserDisplayInfo {handle, name, avatarUrl, userId} - --- | Common type for displaying an Org. -data OrgDisplayInfo = OrgDisplayInfo - { user :: UserDisplayInfo, - orgId :: OrgId, - isCommercial :: Bool - } - deriving (Show, Eq, Ord) - -instance ToJSON OrgDisplayInfo where - toJSON OrgDisplayInfo {user, orgId, isCommercial} = - Aeson.object - [ "user" Aeson..= user, - "orgId" Aeson..= orgId, - "isCommercial" Aeson..= isCommercial - ] - -data TeamDisplayInfo = TeamDisplayInfo - { teamId :: TeamId, - name :: Text, - avatarUrl :: Maybe URI - } - deriving (Show, Eq, Ord) - -instance ToJSON TeamDisplayInfo where - toJSON TeamDisplayInfo {teamId, name, avatarUrl} = - Aeson.object - [ "teamId" Aeson..= teamId, - "name" Aeson..= name, - "avatarUrl" Aeson..= (URIParam <$> avatarUrl) - ] diff --git a/src/Share/Web/Share/DisplayInfo/Queries.hs b/src/Share/Web/Share/DisplayInfo/Queries.hs new file mode 100644 index 00000000..00146844 --- /dev/null +++ b/src/Share/Web/Share/DisplayInfo/Queries.hs @@ -0,0 +1,28 @@ +module Share.Web.Share.DisplayInfo.Queries (userLikeDisplayInfoOf, unifiedDisplayInfoForUserOf) where + +import Control.Lens +import Share.IDs +import Share.Postgres (Transaction) +import Share.Postgres.Users.Queries qualified as UserQ +import Share.Postgres.Users.Queries qualified as UsersQ +import Share.Web.Share.DisplayInfo.Types +import Share.Web.Share.Orgs.Queries qualified as OrgsQ +import Share.Web.Share.Teams.Queries qualified as TeamsQ + +userLikeDisplayInfoOf :: Traversal s t UserLikeIds UnifiedDisplayInfo -> s -> Transaction e t +userLikeDisplayInfoOf trav s = do + s & unsafePartsOf trav \userLikeIds -> do + withUsers <- userLikeIds & UsersQ.userDisplayInfoOf (traversed . unifiedUser_) + withTeams <- withUsers & TeamsQ.teamDisplayInfoOf (traversed . unifiedTeam_) + withOrgs <- withTeams & OrgsQ.orgDisplayInfoOf (traversed . unifiedOrg_) + pure withOrgs + +unifiedDisplayInfoForUserOf :: Traversal s t UserId UnifiedDisplayInfo -> s -> Transaction e t +unifiedDisplayInfoForUserOf trav s = do + s & unsafePartsOf trav \userLikeIds -> do + userLikes <- + UserQ.joinOrgIdsToUserIdsOf traversed userLikeIds + <&> fmap \case + (_userId, Just orgId) -> UnifiedOrg orgId + (userId, Nothing) -> UnifiedUser userId + userLikes & userLikeDisplayInfoOf traversed diff --git a/src/Share/Web/Share/DisplayInfo/Types.hs b/src/Share/Web/Share/DisplayInfo/Types.hs new file mode 100644 index 00000000..ad56900f --- /dev/null +++ b/src/Share/Web/Share/DisplayInfo/Types.hs @@ -0,0 +1,128 @@ +-- Standard ways of displaying core Share concepts. +-- +-- This was consolidated to this module mostly to avoid circular imports. +module Share.Web.Share.DisplayInfo.Types + ( UserDisplayInfo (..), + OrgDisplayInfo (..), + TeamDisplayInfo (..), + UserLike (..), + UnifiedDisplayInfo, + UserLikeIds, + unifiedUser_, + unifiedOrg_, + unifiedTeam_, + ) +where + +import Control.Lens +import Data.Aeson (ToJSON (..)) +import Data.Aeson qualified as Aeson +import Data.Aeson.Types (FromJSON) +import Network.URI (URI) +import Share.IDs +import Share.Prelude +import Share.Utils.URI (URIParam (..)) + +-- | A single unified type for anywhere the frontend may need to display a user-like +-- thing; whether org, team, or user. +data UserLike user team org + = UnifiedUser user + | UnifiedOrg org + | UnifiedTeam team + deriving (Show, Eq, Ord) + +instance (ToJSON user, ToJSON team, ToJSON org) => ToJSON (UserLike user team org) where + toJSON = \case + UnifiedUser u -> Aeson.object ["kind" Aeson..= ("user" :: Text), "info" Aeson..= u] + UnifiedOrg o -> Aeson.object ["kind" Aeson..= ("org" :: Text), "info" Aeson..= o] + UnifiedTeam t -> Aeson.object ["kind" Aeson..= ("team" :: Text), "info" Aeson..= t] + +instance (FromJSON user, FromJSON team, FromJSON org) => FromJSON (UserLike user team org) where + parseJSON = + Aeson.withObject "UserLike" $ \o -> do + kind <- o Aeson..: "kind" + case kind of + ("user" :: Text) -> UnifiedUser <$> o Aeson..: "info" + ("org" :: Text) -> UnifiedOrg <$> o Aeson..: "info" + ("team" :: Text) -> UnifiedTeam <$> o Aeson..: "info" + _ -> fail $ "Unknown UserLike kind: " <> show kind + +type UnifiedDisplayInfo = UserLike UserDisplayInfo TeamDisplayInfo OrgDisplayInfo + +type UserLikeIds = UserLike UserId TeamId OrgId + +unifiedUser_ :: Traversal (UserLike user team org) (UserLike user' team org) user user' +unifiedUser_ f = \case + (UnifiedUser u) -> UnifiedUser <$> f u + (UnifiedOrg o) -> pure $ UnifiedOrg o + (UnifiedTeam t) -> pure $ UnifiedTeam t + +unifiedOrg_ :: Traversal (UserLike user team org) (UserLike user team org') org org' +unifiedOrg_ f = \case + (UnifiedUser u) -> pure $ UnifiedUser u + (UnifiedOrg o) -> UnifiedOrg <$> f o + (UnifiedTeam t) -> pure $ UnifiedTeam t + +unifiedTeam_ :: Traversal (UserLike user team org) (UserLike user team' org) team team' +unifiedTeam_ f = \case + (UnifiedUser u) -> pure $ UnifiedUser u + (UnifiedOrg o) -> pure $ UnifiedOrg o + (UnifiedTeam t) -> UnifiedTeam <$> f t + +-- | Common type for displaying a user. +data UserDisplayInfo = UserDisplayInfo + { handle :: UserHandle, + name :: Maybe Text, + avatarUrl :: Maybe URI, + userId :: UserId + } + deriving (Show, Eq, Ord) + +instance ToJSON UserDisplayInfo where + toJSON UserDisplayInfo {handle, name, avatarUrl, userId} = + Aeson.object + [ "handle" Aeson..= handle, + "name" Aeson..= name, + "avatarUrl" Aeson..= (URIParam <$> avatarUrl), + "userId" Aeson..= userId + ] + +instance FromJSON UserDisplayInfo where + parseJSON = + Aeson.withObject "UserDisplayInfo" $ \o -> do + handle <- o Aeson..: "handle" + name <- o Aeson..:? "name" + avatarUrl <- fmap unpackURI <$> o Aeson..:? "avatarUrl" + userId <- o Aeson..: "userId" + pure UserDisplayInfo {handle, name, avatarUrl, userId} + +-- | Common type for displaying an Org. +data OrgDisplayInfo = OrgDisplayInfo + { user :: UserDisplayInfo, + orgId :: OrgId, + isCommercial :: Bool + } + deriving (Show, Eq, Ord) + +instance ToJSON OrgDisplayInfo where + toJSON OrgDisplayInfo {user, orgId, isCommercial} = + Aeson.object + [ "user" Aeson..= user, + "orgId" Aeson..= orgId, + "isCommercial" Aeson..= isCommercial + ] + +data TeamDisplayInfo = TeamDisplayInfo + { teamId :: TeamId, + name :: Text, + avatarUrl :: Maybe URI + } + deriving (Show, Eq, Ord) + +instance ToJSON TeamDisplayInfo where + toJSON TeamDisplayInfo {teamId, name, avatarUrl} = + Aeson.object + [ "teamId" Aeson..= teamId, + "name" Aeson..= name, + "avatarUrl" Aeson..= (URIParam <$> avatarUrl) + ] diff --git a/src/Share/Web/Share/Impl.hs b/src/Share/Web/Share/Impl.hs index 616314eb..f20ad6d4 100644 --- a/src/Share/Web/Share/Impl.hs +++ b/src/Share/Web/Share/Impl.hs @@ -38,7 +38,6 @@ import Share.Utils.API import Share.Utils.Caching import Share.Utils.Logging qualified as Logging import Share.Utils.Servant.Cookies qualified as Cookies -import Share.Utils.URI (URIParam (..)) import Share.Web.App import Share.Web.Authentication qualified as AuthN import Share.Web.Authorization qualified as AuthZ @@ -48,7 +47,7 @@ import Share.Web.Share.Branches.Impl qualified as Branches import Share.Web.Share.CodeBrowsing.API (CodeBrowseAPI) import Share.Web.Share.Contributions.Impl qualified as Contributions import Share.Web.Share.DefinitionSearch qualified as DefinitionSearch -import Share.Web.Share.DisplayInfo (OrgDisplayInfo (..), UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Queries qualified as DisplayInfoQ import Share.Web.Share.Orgs.Queries qualified as OrgQ import Share.Web.Share.Orgs.Types (Org (..)) import Share.Web.Share.Projects.Impl qualified as Projects @@ -364,17 +363,12 @@ searchEndpoint (MaybeAuthedUserID callerUserId) (Query query) (fromMaybe (Limit -- We don't have a great way to order users and projects together, so we just limit to a max -- of 5 users (who match the query as a prefix), then return the rest of the results from -- projects. - (users, projects) <- PG.runTransaction $ do - users <- UserQ.searchUsersByNameOrHandlePrefix userQuery (Limit 5) + (userLikes, projects) <- PG.runTransaction $ do + userLikes <- UserQ.searchUsersByNameOrHandlePrefix userQuery (Limit 5) + userLikesWithInfo <- DisplayInfoQ.userLikeDisplayInfoOf traversed userLikes projects <- Q.searchProjects callerUserId projectUserFilter projectQuery limit - userResultsWithOrgInfo <- OrgQ.orgsByIdsOf (traversed . _2 . _Just) users - pure (userResultsWithOrgInfo, projects) - let userResults = - users - <&> \(User {user_name = name, avatar_url = avatarUrl, handle, user_id = userId}, mayOrgInfo) -> - case mayOrgInfo of - Just (Org {orgId, isCommercial}) -> SearchResultOrg (OrgDisplayInfo {user = UserDisplayInfo {handle, name, avatarUrl = unpackURI <$> avatarUrl, userId}, orgId, isCommercial}) - Nothing -> SearchResultUser (UserDisplayInfo {handle, name, avatarUrl = unpackURI <$> avatarUrl, userId}) + pure (userLikesWithInfo, projects) + let userResults = SearchResultUserLike <$> userLikes let projectResults = projects <&> \(Project {slug, summary, visibility}, ownerHandle) -> @@ -487,22 +481,23 @@ searchDefinitionsEndpoint callerUserId (Query query) mayLimit userFilter project accountInfoEndpoint :: Session -> WebApp UserAccountInfo accountInfoEndpoint Session {sessionUserId} = do User {user_name, avatar_url, user_email, handle, user_id} <- PGO.expectUserById sessionUserId - (completedTours, organizationMemberships, isSuperadmin) <- PG.runTransaction $ do - tours <- Q.getCompletedToursForUser user_id - memberships <- Q.organizationMemberships user_id + PG.runTransaction $ do + completedTours <- Q.getCompletedToursForUser user_id + organizationMemberships <- Q.organizationMemberships user_id isSuperadmin <- AuthZQ.isSuperadmin user_id - pure (tours, memberships, isSuperadmin) - pure $ - UserAccountInfo - { handle = handle, - name = user_name, - avatarUrl = avatar_url, - userId = user_id, - primaryEmail = user_email, - completedTours, - organizationMemberships, - isSuperadmin - } + displayInfo <- DisplayInfoQ.unifiedDisplayInfoForUserOf id user_id + pure $ + UserAccountInfo + { handle = handle, + name = user_name, + avatarUrl = avatar_url, + userId = user_id, + primaryEmail = user_email, + completedTours, + organizationMemberships, + isSuperadmin, + displayInfo + } completeToursEndpoint :: Session -> NonEmpty TourId -> WebApp NoContent completeToursEndpoint Session {sessionUserId} flows = do diff --git a/src/Share/Web/Share/Orgs/API.hs b/src/Share/Web/Share/Orgs/API.hs index 30bcd260..7d33829b 100644 --- a/src/Share/Web/Share/Orgs/API.hs +++ b/src/Share/Web/Share/Orgs/API.hs @@ -14,7 +14,7 @@ import Servant import Share.IDs import Share.OAuth.Session (AuthenticatedUserId) import Share.Web.Authorization.Types (AddRolesRequest, ListRolesResponse, RemoveRolesRequest) -import Share.Web.Share.DisplayInfo (OrgDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (OrgDisplayInfo) import Share.Web.Share.Orgs.Types type API = diff --git a/src/Share/Web/Share/Orgs/Impl.hs b/src/Share/Web/Share/Orgs/Impl.hs index bfcef9d8..514ed9ec 100644 --- a/src/Share/Web/Share/Orgs/Impl.hs +++ b/src/Share/Web/Share/Orgs/Impl.hs @@ -17,7 +17,7 @@ import Share.Web.App import Share.Web.Authorization qualified as AuthZ import Share.Web.Authorization.Types import Share.Web.Errors -import Share.Web.Share.DisplayInfo (OrgDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (OrgDisplayInfo) import Share.Web.Share.Orgs.API as API import Share.Web.Share.Orgs.Operations qualified as OrgOps import Share.Web.Share.Orgs.Queries qualified as OrgQ diff --git a/src/Share/Web/Share/Orgs/Queries.hs b/src/Share/Web/Share/Orgs/Queries.hs index 684779ba..24102b2d 100644 --- a/src/Share/Web/Share/Orgs/Queries.hs +++ b/src/Share/Web/Share/Orgs/Queries.hs @@ -23,7 +23,7 @@ import Share.Postgres import Share.Prelude import Share.Utils.URI import Share.Web.Authorization.Types -import Share.Web.Share.DisplayInfo (OrgDisplayInfo (..), UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (OrgDisplayInfo (..), UserDisplayInfo (..)) import Share.Web.Share.Orgs.Types (Org (..)) orgByUserId :: UserId -> Transaction e (Maybe Org) diff --git a/src/Share/Web/Share/Orgs/Types.hs b/src/Share/Web/Share/Orgs/Types.hs index e7e08aba..3953f3a7 100644 --- a/src/Share/Web/Share/Orgs/Types.hs +++ b/src/Share/Web/Share/Orgs/Types.hs @@ -14,7 +14,7 @@ import Data.Text (Text) import Share.IDs import Share.Postgres (DecodeRow (..), decodeField) import Share.Utils.URI (URIParam) -import Share.Web.Share.DisplayInfo (UserDisplayInfo) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo) data Org = Org {orgId :: OrgId, isCommercial :: Bool} deriving (Show, Eq, Ord) diff --git a/src/Share/Web/Share/Roles.hs b/src/Share/Web/Share/Roles.hs index 3160fc79..7e068cbb 100644 --- a/src/Share/Web/Share/Roles.hs +++ b/src/Share/Web/Share/Roles.hs @@ -6,7 +6,7 @@ where import Data.List qualified as List import Share.IDs qualified as IDs import Share.Web.Authorization.Types -import Share.Web.Share.DisplayInfo (OrgDisplayInfo (..), TeamDisplayInfo (..), UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (OrgDisplayInfo (..), TeamDisplayInfo (..), UserDisplayInfo (..)) -- | The ordering isn't necessary logic, it just makes transcript tests much easier. canonicalRoleAssignmentOrdering :: [RoleAssignment DisplayAuthSubject] -> [RoleAssignment DisplayAuthSubject] diff --git a/src/Share/Web/Share/Teams/Queries.hs b/src/Share/Web/Share/Teams/Queries.hs index 89f9a496..cfc928bc 100644 --- a/src/Share/Web/Share/Teams/Queries.hs +++ b/src/Share/Web/Share/Teams/Queries.hs @@ -5,7 +5,7 @@ import Share.IDs import Share.Postgres import Share.Prelude import Share.Utils.URI (URIParam (..)) -import Share.Web.Share.DisplayInfo (TeamDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (TeamDisplayInfo (..)) -- | Efficiently resolve Team Display Info for TeamIds within a structure. teamDisplayInfoOf :: (QueryA m) => Traversal s t TeamId TeamDisplayInfo -> s -> m t diff --git a/src/Share/Web/Share/Tickets/API.hs b/src/Share/Web/Share/Tickets/API.hs index 8b14fe4b..35770bd1 100644 --- a/src/Share/Web/Share/Tickets/API.hs +++ b/src/Share/Web/Share/Tickets/API.hs @@ -9,7 +9,7 @@ import Share.IDs import Share.Ticket import Share.Utils.API import Share.Web.Share.Comments.API qualified as Comments -import Share.Web.Share.DisplayInfo (UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo (..)) import Share.Web.Share.Tickets.Types type TicketsByUserAPI = ListTicketsByUserEndpoint diff --git a/src/Share/Web/Share/Tickets/Impl.hs b/src/Share/Web/Share/Tickets/Impl.hs index 3950e691..49c692b1 100644 --- a/src/Share/Web/Share/Tickets/Impl.hs +++ b/src/Share/Web/Share/Tickets/Impl.hs @@ -31,7 +31,7 @@ import Share.Web.Errors import Share.Web.Share.Comments import Share.Web.Share.Comments.Impl qualified as Comments import Share.Web.Share.Comments.Types -import Share.Web.Share.DisplayInfo (UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo (..)) import Share.Web.Share.Tickets.API import Share.Web.Share.Tickets.API qualified as API import Share.Web.Share.Tickets.Types diff --git a/src/Share/Web/Share/Tickets/Types.hs b/src/Share/Web/Share/Tickets/Types.hs index b7e30200..c33880d0 100644 --- a/src/Share/Web/Share/Tickets/Types.hs +++ b/src/Share/Web/Share/Tickets/Types.hs @@ -14,7 +14,7 @@ import Share.Prelude import Share.Ticket (TicketStatus) import Share.Utils.API (NullableUpdate, parseNullableUpdate) import Share.Web.Share.Comments -import Share.Web.Share.DisplayInfo (UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (UserDisplayInfo (..)) data ShareTicket user = ShareTicket { ticketId :: TicketId, diff --git a/src/Share/Web/Share/Types.hs b/src/Share/Web/Share/Types.hs index 803c57f1..752b6a39 100644 --- a/src/Share/Web/Share/Types.hs +++ b/src/Share/Web/Share/Types.hs @@ -14,7 +14,7 @@ import Share.Project (ProjectVisibility) import Share.Utils.API (NullableUpdate, parseNullableUpdate) import Share.Utils.URI import Share.Web.Authorization.Types (RolePermission) -import Share.Web.Share.DisplayInfo (OrgDisplayInfo (..), UserDisplayInfo (..)) +import Share.Web.Share.DisplayInfo.Types (OrgDisplayInfo (..), UnifiedDisplayInfo, UserDisplayInfo (..)) import Unison.Name (Name) import Unison.Server.Doc (Doc) import Unison.Server.Share.DefinitionSummary.Types (TermSummary (..), TypeSummary (..)) @@ -102,7 +102,7 @@ instance ToJSON DocResponse where ] data SearchResult - = SearchResultUser UserDisplayInfo + = SearchResultUserLike UnifiedDisplayInfo | -- | shorthand summary visibility SearchResultProject ProjectShortHand (Maybe Text) ProjectVisibility | SearchResultOrg OrgDisplayInfo @@ -110,13 +110,10 @@ data SearchResult instance ToJSON SearchResult where toJSON = \case - SearchResultUser (UserDisplayInfo {handle, name, avatarUrl, userId}) -> + SearchResultUserLike userLike -> Aeson.object - [ "handle" .= fromId @UserHandle @Text handle, - "name" .= name, - "avatarUrl" .= avatarUrl, - "userId" .= userId, - "tag" .= ("User" :: Text) + [ "displayInfo" .= userLike, + "tag" .= ("UserLike" :: Text) ] SearchResultOrg (OrgDisplayInfo {user = UserDisplayInfo {handle, name, avatarUrl, userId}, orgId}) -> Aeson.object @@ -144,7 +141,8 @@ data UserAccountInfo = UserAccountInfo -- List of tours which the user has completed. completedTours :: [TourId], organizationMemberships :: [UserHandle], - isSuperadmin :: Bool + isSuperadmin :: Bool, + displayInfo :: UnifiedDisplayInfo } deriving (Show) @@ -158,7 +156,8 @@ instance ToJSON UserAccountInfo where "userId" .= userId, "completedTours" .= completedTours, "organizationMemberships" .= organizationMemberships, - "isSuperadmin" .= isSuperadmin + "isSuperadmin" .= isSuperadmin, + "displayInfo" .= displayInfo ] type PathSegment = Text diff --git a/transcripts/share-apis/code-browse/account.json b/transcripts/share-apis/code-browse/account.json index 432f1da5..a559bd9d 100644 --- a/transcripts/share-apis/code-browse/account.json +++ b/transcripts/share-apis/code-browse/account.json @@ -2,6 +2,15 @@ "body": { "avatarUrl": "https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", "completedTours": [], + "displayInfo": { + "info": { + "avatarUrl": "https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", + "handle": "transcripts", + "name": "Transcript User", + "userId": "U-" + }, + "kind": "user" + }, "handle": "transcripts", "isSuperadmin": false, "name": "Transcript User", diff --git a/transcripts/share-apis/code-browse/search.json b/transcripts/share-apis/code-browse/search.json index 9ff75130..2f813171 100644 --- a/transcripts/share-apis/code-browse/search.json +++ b/transcripts/share-apis/code-browse/search.json @@ -1,11 +1,16 @@ { "body": [ { - "avatarUrl": null, - "handle": "test", - "name": null, - "tag": "User", - "userId": "U-" + "displayInfo": { + "info": { + "avatarUrl": null, + "handle": "test", + "name": null, + "userId": "U-" + }, + "kind": "user" + }, + "tag": "UserLike" }, { "projectRef": "@test/publictestproject", diff --git a/transcripts/share-apis/projects-flow/project-search.json b/transcripts/share-apis/projects-flow/project-search.json index 9ff75130..2f813171 100644 --- a/transcripts/share-apis/projects-flow/project-search.json +++ b/transcripts/share-apis/projects-flow/project-search.json @@ -1,11 +1,16 @@ { "body": [ { - "avatarUrl": null, - "handle": "test", - "name": null, - "tag": "User", - "userId": "U-" + "displayInfo": { + "info": { + "avatarUrl": null, + "handle": "test", + "name": null, + "userId": "U-" + }, + "kind": "user" + }, + "tag": "UserLike" }, { "projectRef": "@test/publictestproject", diff --git a/transcripts/share-apis/search/omni-search-orgs.json b/transcripts/share-apis/search/omni-search-orgs.json index 5eb800f1..31fd805a 100644 --- a/transcripts/share-apis/search/omni-search-orgs.json +++ b/transcripts/share-apis/search/omni-search-orgs.json @@ -1,12 +1,20 @@ { "body": [ { - "avatarUrl": "https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", - "handle": "unison", - "name": "Unison Org", - "orgId": "ORG-", - "tag": "Org", - "userId": "U-" + "displayInfo": { + "info": { + "isCommercial": false, + "orgId": "ORG-", + "user": { + "avatarUrl": "https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", + "handle": "unison", + "name": "Unison Org", + "userId": "U-" + } + }, + "kind": "org" + }, + "tag": "UserLike" }, { "projectRef": "@unison/privateorgproject", diff --git a/transcripts/share-apis/search/omni-search-users.json b/transcripts/share-apis/search/omni-search-users.json index 9ff75130..2f813171 100644 --- a/transcripts/share-apis/search/omni-search-users.json +++ b/transcripts/share-apis/search/omni-search-users.json @@ -1,11 +1,16 @@ { "body": [ { - "avatarUrl": null, - "handle": "test", - "name": null, - "tag": "User", - "userId": "U-" + "displayInfo": { + "info": { + "avatarUrl": null, + "handle": "test", + "name": null, + "userId": "U-" + }, + "kind": "user" + }, + "tag": "UserLike" }, { "projectRef": "@test/publictestproject", diff --git a/transcripts/share-apis/user-creation/new-user-profile.json b/transcripts/share-apis/user-creation/new-user-profile.json index 79139537..fed9e2ae 100644 --- a/transcripts/share-apis/user-creation/new-user-profile.json +++ b/transcripts/share-apis/user-creation/new-user-profile.json @@ -2,6 +2,15 @@ "body": { "avatarUrl": "https://avatars.githubusercontent.com/u/0?v=4", "completedTours": [], + "displayInfo": { + "info": { + "avatarUrl": "https://avatars.githubusercontent.com/u/0?v=4", + "handle": "localgithubuser", + "name": "Local Github User", + "userId": "U-" + }, + "kind": "user" + }, "handle": "localgithubuser", "isSuperadmin": false, "name": "Local Github User",