-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b6fc13d
commit 155a684
Showing
23 changed files
with
1,763 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<template> | ||
<Icon name="lucide:loader" class="animate-spin" /> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,108 @@ | ||
<script lang="ts" setup> | ||
import { toast } from "vue-sonner"; | ||
import type { Workspace } from "~~/server/db/schema"; | ||
defineProps<{ | ||
const props = defineProps<{ | ||
workspace: Workspace; | ||
}>(); | ||
const { | ||
data: collaborators, | ||
isPending, | ||
suspense, | ||
} = useWorkspaceCollaborators(() => props.workspace.id); | ||
const { | ||
data: invitationLink, | ||
isPending: isInvitationLinkPending, | ||
suspense: invitationLinkSuspense, | ||
} = useWorkspaceInvitationLink(() => props.workspace.id); | ||
const { mutateAsync: createInvitationLink, isPending: isCreatingLink } = | ||
useCreateWorkspaceInvitationLinkMutation(() => props.workspace.id); | ||
const { mutateAsync: disableInvitationLink, isPending: isDisablingLink } = | ||
useDeactivateWorkspaceInvitationLinkMutation(() => props.workspace.id); | ||
if (import.meta.env.SSR) { | ||
await Promise.all([suspense(), invitationLinkSuspense()]); | ||
} | ||
const generateAndCopyLink = async (id: string) => { | ||
const _url = useRequestURL(); | ||
const url = new URL("/api/invitation/accept", _url.origin); | ||
url.searchParams.set("token", id); | ||
return navigator.clipboard | ||
.writeText(url.toString()) | ||
.then(() => toast.success("Link copied to clipboard.")) | ||
.catch((err) => { | ||
console.error(err); | ||
toast.error("Failed to copy link to clipboard."); | ||
}); | ||
}; | ||
const onInviteWithLink = async () => { | ||
if (isCreatingLink.value) return; | ||
if (invitationLink.value) { | ||
// invitationLink.value.id | ||
await generateAndCopyLink(invitationLink.value.id); | ||
return; | ||
} | ||
const invitation = await createInvitationLink(); | ||
console.log({ invitation }); | ||
await generateAndCopyLink(invitation.id); | ||
}; | ||
const onDisableLink = async () => { | ||
if (isDisablingLink.value || !invitationLink.value) return; | ||
await disableInvitationLink(invitationLink.value.id) | ||
.then(() => toast.success("Invitation link disabled.")) | ||
.catch(() => toast.error("Failed to disable invitation link.")); | ||
}; | ||
</script> | ||
|
||
<template> | ||
<div class="space-y-lg mt-6"> | ||
<section> | ||
<SheetTitle>Member Settings</SheetTitle> | ||
<SheetTitle class="flex items-center gap-2"> | ||
Member Settings | ||
<LazyActivityIndicator v-if="isPending || isInvitationLinkPending" /> | ||
</SheetTitle> | ||
<SheetDescription> | ||
Add or remove members from your workspace. | ||
</SheetDescription> | ||
</section> | ||
|
||
<div class="text-muted-foreground text-center"> | ||
<ComingSoon /> | ||
<p>Invitation link: {{ JSON.stringify(invitationLink, null, 2) }}</p> | ||
|
||
<section> | ||
<h3 class="font-semibold">Invite Collaborators</h3> | ||
<div class="flex flex-col md:flex-row gap-x-4 gap-y-2"> | ||
<p class="text-sm text-muted-foreground md:w-2/3"> | ||
Anyone with an invite link can join this free Workspace. You can also | ||
disable and create a new invite link for this Workspace at any time. | ||
Pending invitations count toward the 10 collaborator limit. | ||
</p> | ||
<div class="flex flex-col gap-2 flex-1"> | ||
<Button @click="onInviteWithLink"> | ||
Invite with link <LazyActivityIndicator v-if="isCreatingLink" /> | ||
<Icon v-else name="lucide:link" class="ml-1" /> | ||
</Button> | ||
<Button variant="secondary" @click="onDisableLink"> | ||
Disable link | ||
<LazyActivityIndicator v-if="isDisablingLink" /> | ||
</Button> | ||
</div> | ||
</div> | ||
</section> | ||
|
||
<div v-if="!isPending"> | ||
{{ collaborators }} | ||
</div> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { queryOptions } from "@tanstack/vue-query"; | ||
import type { WorkspaceCollaborator } from "~~/server/db/schema"; | ||
|
||
export const getWorkspaceCollaboratorsOptions = ( | ||
workspaceId: MaybeRefOrGetter<string>, | ||
) => | ||
queryOptions<WorkspaceCollaborator[]>({ | ||
queryKey: ["workspaceCollaborators", workspaceId], | ||
}); | ||
|
||
export const useWorkspaceCollaborators = ( | ||
workspaceId: MaybeRefOrGetter<string>, | ||
) => { | ||
const client = useQueryClient(); | ||
|
||
return useQuery( | ||
{ | ||
queryKey: getWorkspaceCollaboratorsOptions(workspaceId).queryKey, | ||
queryFn: () => | ||
useRequestFetch()( | ||
`/api/workspace/${toValue(workspaceId)}/collaborators`, | ||
), | ||
}, | ||
client, | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { queryOptions } from "@tanstack/vue-query"; | ||
import { normalizeDates } from "~/lib/utils"; | ||
import type { InvitationLink } from "~~/server/db/schema"; | ||
|
||
export const getWorkspaceInvitationLinkOptions = ( | ||
workspaceId: MaybeRefOrGetter<string>, | ||
) => | ||
queryOptions<InvitationLink | null>({ | ||
queryKey: ["workspace", workspaceId, "active-invitation-link"], | ||
}); | ||
|
||
export const useWorkspaceInvitationLink = ( | ||
workspaceId: MaybeRefOrGetter<string>, | ||
) => { | ||
const client = useQueryClient(); | ||
|
||
return useQuery( | ||
{ | ||
queryKey: getWorkspaceInvitationLinkOptions(workspaceId).queryKey, | ||
queryFn: () => | ||
useRequestFetch()( | ||
`/api/workspace/${toValue(workspaceId)}/active-invitation`, | ||
).then((response) => | ||
response ? normalizeDates<InvitationLink>(response) : null, | ||
), | ||
}, | ||
client, | ||
); | ||
}; | ||
|
||
export const useCreateWorkspaceInvitationLinkMutation = ( | ||
workspaceId: MaybeRefOrGetter<string>, | ||
) => { | ||
const client = useQueryClient(); | ||
|
||
return useMutation({ | ||
mutationFn: () => | ||
useRequestFetch()(`/api/invitation`, { | ||
method: "POST", | ||
body: { workspaceId: toValue(workspaceId) }, | ||
}).then((response) => normalizeDates<InvitationLink>(response)), | ||
onSuccess: (result) => { | ||
client.setQueryData( | ||
getWorkspaceInvitationLinkOptions(workspaceId).queryKey, | ||
result, | ||
); | ||
}, | ||
}); | ||
}; | ||
|
||
export const useDeactivateWorkspaceInvitationLinkMutation = ( | ||
workspaceId: MaybeRefOrGetter<string>, | ||
) => { | ||
const client = useQueryClient(); | ||
|
||
return useMutation({ | ||
mutationFn: (invitationLinkId: string) => | ||
useRequestFetch()<InvitationLink>( | ||
`/api/invitation/${invitationLinkId}/deactivate`, | ||
{ | ||
method: "POST", | ||
}, | ||
), | ||
onSuccess: async (result) => { | ||
const activeWorkspaceInvitationLink = client.getQueryData( | ||
getWorkspaceInvitationLinkOptions(workspaceId).queryKey, | ||
); | ||
|
||
console.log({ result, activeWorkspaceInvitationLink }); | ||
|
||
if (activeWorkspaceInvitationLink?.id === result.id) { | ||
client.setQueryData( | ||
getWorkspaceInvitationLinkOptions(workspaceId).queryKey, | ||
null, | ||
); | ||
} | ||
}, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
DROP INDEX IF EXISTS "invitation_links_board_id_active_index";--> statement-breakpoint | ||
ALTER TABLE "invitation_links" ALTER COLUMN "board_id" DROP NOT NULL;--> statement-breakpoint | ||
ALTER TABLE "invitation_links" ADD COLUMN "workspace_id" uuid;--> statement-breakpoint | ||
DO $$ BEGIN | ||
ALTER TABLE "invitation_links" ADD CONSTRAINT "invitation_links_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action; | ||
EXCEPTION | ||
WHEN duplicate_object THEN null; | ||
END $$; | ||
--> statement-breakpoint | ||
CREATE UNIQUE INDEX IF NOT EXISTS "invitation_links_workspace_id_active_index" ON "invitation_links" USING btree ("workspace_id","active") WHERE "invitation_links"."active" = true AND "invitation_links"."workspace_id" IS NOT NULL;--> statement-breakpoint | ||
CREATE UNIQUE INDEX IF NOT EXISTS "invitation_links_board_id_active_index" ON "invitation_links" USING btree ("board_id","active") WHERE "invitation_links"."active" = true AND "invitation_links"."board_id" IS NOT NULL;--> statement-breakpoint | ||
ALTER TABLE "invitation_links" ADD CONSTRAINT "valid_invitation_target" CHECK (( | ||
("invitation_links"."workspace_id" IS NULL AND "invitation_links"."board_id" IS NOT NULL) OR | ||
("invitation_links"."workspace_id" IS NOT NULL AND "invitation_links"."board_id" IS NULL) | ||
)); |
Oops, something went wrong.