Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions app/components/board-collaborator-list/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ const props = withDefaults(
*/
ownerFirst?: boolean;
/**
* If provided, the list will render at most `limit` collaborators. If
* there are more than `limit` collaborators, the `limit`-th element on the
* list will be a placeholder for the number of remaining collaborators.
* If provided, the list will render at most `limit` collaborators. If there
* are more than `limit` collaborators, the `limit`-th element on the list
* will be a placeholder for the number of remaining collaborators.
*
* If this number is less than 1, the list will render all collaborators.
*
*/
limit?: number;
}>(),
Expand Down Expand Up @@ -64,6 +63,8 @@ const collapsedCollaboratorsText = computed(() =>
.join("\n")
: "",
);

const { toggleAssignee, filters } = useBoardFilters();
</script>

<template>
Expand Down Expand Up @@ -99,8 +100,14 @@ const collapsedCollaboratorsText = computed(() =>
>
<UniversalUserAvatar
:user="collaborator"
class="avatar border border-foreground"
class="avatar cursor-pointer transition-[border] transition-duration-[300ms]"
:class="{
'border-blue-500 border-3': filters.assignees?.includes(
collaborator.id,
),
}"
:data-testid="`user-avatar-${collaborator.id}`"
@click="() => toggleAssignee(collaborator.id)"
/>
</li>
</template>
Expand Down
7 changes: 6 additions & 1 deletion app/components/mini-task.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const props = defineProps<{
onDragStart?: (event: DragEvent, task: Task) => void;
}>();

const route = useRoute();
const { labels } = inject(BOARD_DATA_KEY)!;

const isDragging = ref(false);
Expand Down Expand Up @@ -36,6 +37,10 @@ const labelMap = computed(() => {
}, {}) ?? {}
);
});

const toLink = computed(() => {
return { query: { ...route.query, "view-task": props.task.id } };
});
</script>

<template>
Expand All @@ -46,7 +51,7 @@ const labelMap = computed(() => {
@dragstart="onDragStart"
@dragend="onDragEnd"
>
<NuxtLink :to="{ query: { 'view-task': task.id } }">
<NuxtLink :to="toLink">
<CardHeader class="p-4 gap-4">
<div
v-if="task.labels.length > 0"
Expand Down
15 changes: 13 additions & 2 deletions app/components/status-column.vue
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,17 @@ const doDeleteColumn = async () => {

await deleteColumn(props.column);
};

const { filters } = useBoardFilters();
const filteredTasks = computed(() => {
return filters.value.assignees
? tasks.value?.filter((task) =>
task.assignees.some((assignee) =>
filters.value.assignees!.includes(assignee.userId),
),
)
: tasks.value;
});
</script>

<template>
Expand Down Expand Up @@ -270,7 +281,7 @@ const doDeleteColumn = async () => {
v-if="isPending"
class="inline-block w-[1em] h-[3em]"
/>
<span v-else-if="tasks">{{ tasks!.length }} tasks</span>
<span v-else-if="tasks">{{ filteredTasks!.length }} tasks</span>
</span>
</CardTitle>
<EasyTooltip :tooltip="collapsed ? 'Expand Column' : 'Collapse Column'">
Expand Down Expand Up @@ -334,7 +345,7 @@ const doDeleteColumn = async () => {
<CardContent class="px-2 overflow-y-auto expanded-only">
<ol v-if="tasks" ref="tasksRef" class="flex flex-col gap-2">
<MiniTask
v-for="task in tasks"
v-for="task in filteredTasks"
:key="task.id"
:task
draggable="true"
Expand Down
5 changes: 4 additions & 1 deletion app/components/ui/drawer/Drawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ const props = withDefaults(defineProps<DrawerRootProps>(), {

const emits = defineEmits<DrawerRootEmits>();

const forwarded = useForwardPropsEmits(props, emits);
const forwarded: ComputedRef<Record<string, unknown>> = useForwardPropsEmits(
props,
emits,
);
</script>

<template>
Expand Down
43 changes: 43 additions & 0 deletions app/components/workspace/plan-settings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang="ts" setup>
import { getFetchErrorMessage } from "~/lib/utils";
import type { Workspace } from "~~/server/db/schema";

const props = defineProps<{ workspace: Workspace }>();

const {
data: plan,
error,
isPending,
suspense,
} = useWorkspacePlan(() => props.workspace.id);

if (import.meta.env.SSR) {
await suspense();
}
</script>

<template>
<div class="space-y-lg mt-6">
<section>
<div class="flex flex-row items-center gap-2">
<SheetTitle>Plan & Billing</SheetTitle>
<ActivityIndicator v-if="isPending" />
</div>
<SheetDescription>
Manage your workspace plan and payment methods.
</SheetDescription>
</section>

<template v-if="!isPending">
<div v-if="plan">Plan: {{ plan.name }}</div>

<div v-else-if="error">
<span class="text-red-500 font-semibold">{{
getFetchErrorMessage(error, "Failed to fetch plan")
}}</span>
</div>

<div v-else>You are on the free plan</div>
</template>
</div>
</template>
4 changes: 3 additions & 1 deletion app/components/workspace/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
LazyWorkspaceGeneralSettings,
LazyWorkspaceMemberSettings,
LazyWorkspacePlanSettings,
LazyWorkspaceRuleSettings,
LazyWorkspaceUsage,
} from "#components";
Expand All @@ -11,14 +12,15 @@ defineProps<{
workspace: Workspace;
}>();

const validPages = ["general", "members", "rules", "usage"] as const;
const validPages = ["general", "members", "rules", "usage", "billing"] as const;
type ValidPage = (typeof validPages)[number];

const components = {
general: LazyWorkspaceGeneralSettings,
members: LazyWorkspaceMemberSettings,
rules: LazyWorkspaceRuleSettings,
usage: LazyWorkspaceUsage,
billing: LazyWorkspacePlanSettings,
};

const page = useQueryParam<ValidPage>("settings", {
Expand Down
67 changes: 67 additions & 0 deletions app/composables/useBoardFilters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { z } from "zod";

const boardFiltersSchema = z.object({
assignees: z
.string()
.transform((str) => (str === "" ? [] : str.split(",")))
.pipe(z.array(z.string().uuid()))
.optional(),
});

type BoardFilters = z.infer<typeof boardFiltersSchema>;

export const useBoardFilters = () => {
const route = useRoute();
const router = useRouter();

const validatedFilters = ref<BoardFilters>({});

watch(
() => route.query,
() => {
const result = boardFiltersSchema.safeParse(route.query);

if (result.success) {
validatedFilters.value = result.data;
} else {
console.error("Invalid board filters:", result.error);
validatedFilters.value = {};
}
},
{ immediate: true },
);

const updateQuery = (newQuery: Record<string, string>) => {
router.replace({
query: {
...route.query,
...newQuery,
},
});
};

const setAssignees = (assignees: string[] | null) => {
if (assignees === null || assignees.length === 0) {
const { assignees: _, ...restQuery } = route.query;
router.replace({ query: restQuery });
} else {
updateQuery({ assignees: assignees.join(",") });
}
};

const toggleAssignee = (assignee: string) => {
const currentAssignees = validatedFilters.value.assignees || [];
const index = currentAssignees.indexOf(assignee);

console.log("Toggling assignee:", assignee, "Index:", index);

if (index !== -1) {
const filtered = currentAssignees.filter((a) => a !== assignee);
setAssignees(filtered.length > 0 ? filtered : null);
} else {
setAssignees([...currentAssignees, assignee]);
}
};

return { filters: validatedFilters, setAssignees, toggleAssignee };
};
7 changes: 2 additions & 5 deletions app/composables/useWorkspace.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
queryOptions,
type UndefinedInitialQueryOptions,
} from "@tanstack/vue-query";
import { queryOptions, type UseQueryOptions } from "@tanstack/vue-query";
import { getWorkspacesOptions } from "~/composables/useWorkspaces";
import { normalizeDates } from "~/lib/utils";
import type {
Expand All @@ -15,7 +12,7 @@ export const getWorkspaceOptions = (id: MaybeRefOrGetter<string>) =>
queryKey: ["workspace", id],
});

export const useWorkspace = <T extends UndefinedInitialQueryOptions>(
export const useWorkspace = <T extends UseQueryOptions>(
id: MaybeRefOrGetter<string>,
extraOptions: Partial<T> = {},
) => {
Expand Down
24 changes: 24 additions & 0 deletions app/composables/useWorkspacePlan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { queryOptions } from "@tanstack/vue-query";
import type { Plan } from "~~/server/db/schema";

export const getWorkspacePlanOptions = (
workspaceId: MaybeRefOrGetter<string>,
) =>
queryOptions<Plan | null>({
queryKey: ["workspace", workspaceId, "plan"],
});

export const useWorkspacePlan = (workspaceId: MaybeRefOrGetter<string>) => {
const client = useQueryClient();

return useQuery<Plan | null>(
{
queryKey: getWorkspacePlanOptions(workspaceId).queryKey,
queryFn: async () =>
useRequestFetch()(`/api/workspace/${toValue(workspaceId)}/plan`).then(
(response) => response.plan,
),
},
client,
);
};
8 changes: 8 additions & 0 deletions drizzle/0025_plain_gwen_stacy.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE "plans" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(255) NOT NULL,
"subscription_id" varchar(255) NOT NULL,
"workspace_id" uuid NOT NULL
);
--> statement-breakpoint
ALTER TABLE "plans" ADD CONSTRAINT "plans_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;
1 change: 1 addition & 0 deletions drizzle/0026_foamy_micromacro.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "workspaces" ADD COLUMN "customer_id" varchar(255);
Loading