Skip to content

Commit

Permalink
Sinc
Browse files Browse the repository at this point in the history
  • Loading branch information
raggesilver committed Dec 13, 2024
1 parent b6fc13d commit 155a684
Show file tree
Hide file tree
Showing 23 changed files with 1,763 additions and 68 deletions.
3 changes: 3 additions & 0 deletions app/components/activity-indicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<Icon name="lucide:loader" class="animate-spin" />
</template>
94 changes: 90 additions & 4 deletions app/components/workspace/member-settings.vue
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>
26 changes: 26 additions & 0 deletions app/composables/useWorkspaceCollaborators.ts
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,
);
};
79 changes: 79 additions & 0 deletions app/composables/useWorkspaceInvitation.ts
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,
);
}
},
});
};
10 changes: 4 additions & 6 deletions app/layouts/legal.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
<script setup lang="ts">
const production = useRequestURL().hostname.includes("localhost");
const production = computed(() => useRequestURL().hostname.includes("fly.dev"));
</script>

<template>
<div
v-if="!production"
class="text-background bg-foreground py-4 sticky top-0 left-0 z-50"
>
<div v-if="!production" class="text-background bg-foreground py-4">
<p class="max-w-[65ch] w-full text-justify mx-auto px-4 md:px-0">
You are on a preview deployment. The Terms of Service and Privacy Policy
are drafts and may never become official.
</p>
</div>
<NavBarLanding />
<main v-once :class="{ production }">
<slot />
</main>
<app-footer />
</template>

<style>
<style scoped>
main {
position: relative;
}
Expand Down
23 changes: 16 additions & 7 deletions app/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,23 @@ export const publicUserSchema = z.object({
profilePictureUrl: z.string().url().optional(),
});

export const createInvitationSchema = z.object({
boardId: z.string().uuid(),
});
export const createInvitationSchema = z
.object({
boardId: z.string().uuid(),
})
.or(z.object({ workspaceId: z.string().uuid() }));

export const deactivateInvitationSchema = z.object({
invitationId: z.string().uuid(),
boardId: z.string().uuid(),
});
export const deactivateInvitationSchema = z
.object({
invitationId: z.string().uuid(),
boardId: z.string().uuid(),
})
.or(
z.object({
invitationId: z.string().uuid(),
workspaceId: z.string().uuid(),
}),
);

export const addAssigneeSchema = z.object({
userId: z.string().uuid(),
Expand Down
15 changes: 15 additions & 0 deletions drizzle/0024_dark_gorgon.sql
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)
));
Loading

0 comments on commit 155a684

Please sign in to comment.