ソースを参照

feat: init invitation wiring

Co-authored-by: Andrew Bastin <[email protected]>
liyasthomas 4 年 前
コミット
3bdf2baf97

+ 1 - 1
packages/hoppscotch-app/components/collections/SaveRequest.vue

@@ -21,7 +21,7 @@
             {{ $t("request.name") }}
           </label>
         </div>
-        <label class="px-4 pt-4 pb-4">
+        <label class="p-4">
           {{ $t("collection.select_location") }}
         </label>
         <CollectionsGraphql

+ 1 - 1
packages/hoppscotch-app/components/http/CodegenModal.vue

@@ -34,7 +34,7 @@
           />
         </tippy>
         <div class="flex flex-1 justify-between">
-          <label for="generatedCode" class="px-4 pt-4 pb-4">
+          <label for="generatedCode" class="p-4">
             {{ t("request.generated_code") }}
           </label>
         </div>

+ 2 - 1
packages/hoppscotch-app/components/teams/Edit.vue

@@ -17,7 +17,7 @@
             {{ $t("action.label") }}
           </label>
         </div>
-        <div class="flex flex-1 justify-between items-center">
+        <div class="flex pt-4 flex-1 justify-between items-center">
           <label for="memberList" class="p-4">
             {{ $t("team.members") }}
           </label>
@@ -25,6 +25,7 @@
             <ButtonSecondary
               svg="user-plus"
               :label="$t('team.invite')"
+              filled
               @click.native="
                 () => {
                   $emit('invite-team')

+ 91 - 12
packages/hoppscotch-app/components/teams/Invite.vue

@@ -3,7 +3,7 @@
     <template #body>
       <div class="flex flex-col px-2">
         <div class="flex flex-1 justify-between items-center">
-          <label for="memberList" class="p-4">
+          <label for="memberList" class="pb-4 px-4">
             {{ $t("team.pending_invites") }}
           </label>
         </div>
@@ -93,7 +93,7 @@
             </div>
           </div>
         </div>
-        <div class="flex flex-1 justify-between items-center">
+        <div class="flex pt-4 flex-1 justify-between items-center">
           <label for="memberList" class="p-4">
             {{ $t("team.invite_tooltip") }}
           </label>
@@ -101,6 +101,7 @@
             <ButtonSecondary
               svg="plus"
               :label="$t('add.new')"
+              filled
               @click.native="addNewInvitee"
             />
           </div>
@@ -182,9 +183,9 @@
               justify-center
             "
           >
-            <SmartIcon class="opacity-75 pb-2" name="users" />
+            <SmartIcon class="opacity-75 pb-2" name="user-plus" />
             <span class="text-center pb-4">
-              {{ $t("empty.members") }}
+              {{ $t("empty.invites") }}
             </span>
             <ButtonSecondary
               :label="$t('add.new')"
@@ -208,8 +209,18 @@
 </template>
 
 <script setup lang="ts">
-import { watch, ref, reactive } from "@nuxtjs/composition-api"
+import { watch, ref, reactive, useContext } from "@nuxtjs/composition-api"
+import * as T from "fp-ts/Task"
 import * as E from "fp-ts/Either"
+import * as A from "fp-ts/Array"
+import * as O from "fp-ts/Option"
+import { flow, pipe } from "fp-ts/function"
+import { Email, EmailCodec } from "../../helpers/backend/types/Email"
+import { TeamMemberRole } from "../../helpers/backend/graphql"
+import {
+  createTeamInvitation,
+  revokeTeamInvitation,
+} from "../../helpers/backend/mutations/TeamInvitation"
 import { useGQLQuery } from "~/helpers/backend/GQLClient"
 import {
   GetPendingInvitesDocument,
@@ -217,6 +228,12 @@ import {
   GetPendingInvitesQueryVariables,
 } from "~/helpers/backend/graphql"
 
+const {
+  $toast,
+  app: { i18n },
+} = useContext()
+const t = i18n.t.bind(i18n)
+
 const props = defineProps({
   show: Boolean,
   editingteamID: { type: String, default: null },
@@ -256,20 +273,29 @@ watch(
   }
 )
 
-const removeInvitee = (id: string) => {
-  console.log(id)
+const removeInvitee = async (id: string) => {
+  const result = await revokeTeamInvitation(id)()
+  if (E.isLeft(result)) {
+    $toast.error(`${t("error.something_went_wrong")}`, {
+      icon: "error_outline",
+    })
+  } else {
+    $toast.success(`${t("team.member_removed")}`, {
+      icon: "person",
+    })
+  }
 }
 
-const newInvites = ref([])
+const newInvites = ref<Array<{ key: string; value: TeamMemberRole }>>([])
 
 const addNewInvitee = () => {
   newInvites.value.push({
     key: "",
-    value: "",
+    value: TeamMemberRole.Viewer,
   })
 }
 
-const updateNewInviteeRole = (index: number, role: string) => {
+const updateNewInviteeRole = (index: number, role: TeamMemberRole) => {
   newInvites.value[index].value = role
 }
 
@@ -277,8 +303,61 @@ const removeNewInvitee = (id: number) => {
   newInvites.value.splice(id, 1)
 }
 
-const sendInvites = () => {
-  console.log(newInvites.value)
+const result = ref<
+  Array<{
+    email: Email
+    status: "error" | "success"
+  }>
+>([])
+
+const sendInvites = async () => {
+  const validationResult = pipe(
+    newInvites.value,
+    O.fromPredicate(
+      (invites): invites is Array<{ key: Email; value: TeamMemberRole }> =>
+        pipe(
+          invites,
+          A.every((invitee) => EmailCodec.is(invitee.key))
+        )
+    ),
+    O.map(
+      A.map((invitee) =>
+        createTeamInvitation(invitee.key, invitee.value, props.editingteamID)
+      )
+    )
+  )
+
+  if (O.isNone(validationResult)) {
+    // Error handling for no validation
+    $toast.error(`${t("error.incorrect_email")}`, {
+      icon: "error_outline",
+    })
+    return
+  }
+
+  result.value = await pipe(
+    A.sequence(T.task)(validationResult.value),
+    T.chain(
+      flow(
+        A.mapWithIndex((i, el) =>
+          pipe(
+            el,
+            E.foldW(
+              () => ({
+                status: "error" as const,
+                email: newInvites.value[i].key as Email,
+              }),
+              () => ({
+                status: "success" as const,
+                email: newInvites.value[i].key as Email,
+              })
+            )
+          )
+        ),
+        T.of
+      )
+    )
+  )()
 }
 
 const hideModal = () => {

+ 1 - 1
packages/hoppscotch-app/components/teams/Team.vue

@@ -28,7 +28,7 @@
         </div>
       </div>
     </div>
-    <div v-if="!compact" class="flex items-center justify-between">
+    <div v-if="!compact" class="flex flex-shrink-0 items-end justify-between">
       <span>
         <ButtonSecondary
           v-if="team.myRole === 'OWNER'"

+ 46 - 32
packages/hoppscotch-app/helpers/backend/mutations/TeamInvitation.ts

@@ -1,32 +1,52 @@
-import { runMutation } from "../GQLClient";
-import { AcceptTeamInvitationDocument, AcceptTeamInvitationMutation, AcceptTeamInvitationMutationVariables, CreateTeamInvitationDocument, CreateTeamInvitationMutation, CreateTeamInvitationMutationVariables, RevokeTeamInvitationDocument, RevokeTeamInvitationMutation, RevokeTeamInvitationMutationVariables, TeamMemberRole } from "../graphql";
-import { Email } from "../types/Email";
-import { pipe } from "fp-ts/function";
-import * as TE from "fp-ts/TaskEither";
+import { pipe } from "fp-ts/function"
+import * as TE from "fp-ts/TaskEither"
+import { runMutation } from "../GQLClient"
+import {
+  AcceptTeamInvitationDocument,
+  AcceptTeamInvitationMutation,
+  AcceptTeamInvitationMutationVariables,
+  CreateTeamInvitationDocument,
+  CreateTeamInvitationMutation,
+  CreateTeamInvitationMutationVariables,
+  RevokeTeamInvitationDocument,
+  RevokeTeamInvitationMutation,
+  RevokeTeamInvitationMutationVariables,
+  TeamMemberRole,
+} from "../graphql"
+import { Email } from "../types/Email"
 
-type CreateTeamInvitationErrors
-  = "invalid/email" | "team/invalid_id" | "team/member_not_found" | "team_invite/already_member" | "team_invite/member_has_invite" 
+type CreateTeamInvitationErrors =
+  | "invalid/email"
+  | "team/invalid_id"
+  | "team/member_not_found"
+  | "team_invite/already_member"
+  | "team_invite/member_has_invite"
 
-type RevokeTeamInvitationErrors
-  = "team/not_required_role" | "team_invite/no_invite_found"
+type RevokeTeamInvitationErrors =
+  | "team/not_required_role"
+  | "team_invite/no_invite_found"
 
-type AcceptTeamInvitationErrors
-  = "team_invite/no_invite_found" | "team_invitee/not_invitee" | "team_invite/already_member" | "team_invite/email_do_not_match"
+type AcceptTeamInvitationErrors =
+  | "team_invite/no_invite_found"
+  | "team_invitee/not_invitee"
+  | "team_invite/already_member"
+  | "team_invite/email_do_not_match"
 
-export const createTeamInvitation = (inviteeEmail: Email, inviteeRole: TeamMemberRole, teamID: string) =>
+export const createTeamInvitation = (
+  inviteeEmail: Email,
+  inviteeRole: TeamMemberRole,
+  teamID: string
+) =>
   pipe(
     runMutation<
       CreateTeamInvitationMutation,
       CreateTeamInvitationMutationVariables,
       CreateTeamInvitationErrors
-    >(
-      CreateTeamInvitationDocument,
-      {
-        inviteeEmail,
-        inviteeRole,
-        teamID
-      }
-    ),
+    >(CreateTeamInvitationDocument, {
+      inviteeEmail,
+      inviteeRole,
+      teamID,
+    }),
     TE.map((x) => x.createTeamInvitation)
   )
 
@@ -35,21 +55,15 @@ export const revokeTeamInvitation = (inviteID: string) =>
     RevokeTeamInvitationMutation,
     RevokeTeamInvitationMutationVariables,
     RevokeTeamInvitationErrors
-  >(
-    RevokeTeamInvitationDocument,
-    {
-      inviteID
-    }
-  )
+  >(RevokeTeamInvitationDocument, {
+    inviteID,
+  })
 
 export const acceptTeamInvitation = (inviteID: string) =>
   runMutation<
     AcceptTeamInvitationMutation,
     AcceptTeamInvitationMutationVariables,
     AcceptTeamInvitationErrors
-  >(
-    AcceptTeamInvitationDocument,
-    {
-      inviteID
-    }
-  )
+  >(AcceptTeamInvitationDocument, {
+    inviteID,
+  })

+ 3 - 2
packages/hoppscotch-app/helpers/backend/types/Email.ts

@@ -1,6 +1,7 @@
 import * as t from "io-ts"
 
-const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 
+const emailRegex =
+  /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
 
 interface EmailBrand {
   readonly Email: unique symbol
@@ -12,4 +13,4 @@ export const EmailCodec = t.brand(
   "Email"
 )
 
-export type Email = t.TypeOf<typeof EmailCodec>
+export type Email = t.TypeOf<typeof EmailCodec>

+ 2 - 0
packages/hoppscotch-app/locales/en.json

@@ -141,6 +141,7 @@
     "folder": "Folder is empty",
     "headers": "This request does not have any headers",
     "history": "History is empty",
+    "invites": "Invite list is empty",
     "members": "Team is empty",
     "parameters": "This request does not have any parameters",
     "pending_invites": "There are no pending invites for this team",
@@ -167,6 +168,7 @@
     "empty_req_name": "Empty Request Name",
     "f12_details": "(F12 for details)",
     "gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
+    "incorrect_email": "Incorrect email",
     "json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
     "network_fail": "Could not send request",
     "no_duration": "No duration",