Skip to content

Commit

Permalink
Workspaces (#129)
Browse files Browse the repository at this point in the history
Rename workspaces to boards, which is what they are. Also, introduce real workspaces plus the UI, API, and authorization logic around them.
  • Loading branch information
raggesilver authored Jan 8, 2025
1 parent 2638ebd commit ebf8fac
Show file tree
Hide file tree
Showing 116 changed files with 16,243 additions and 5,701 deletions.
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,13 @@ logs
.env.*
!.env.example

/appdata
/db-data
/cert

*.toml
*.md
LICENSE
.prettier*
.eslint*
.editorconfig
22 changes: 22 additions & 0 deletions .github/workflows/fly-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Fly Deploy (Staging)
on:
push:
branches:
- staging

env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
concurrency: deploy-group-staging
environment:
name: staging
url: https://tasksapp-staging.fly.dev/
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl config save --app tasksapp-staging --config fly-staging.toml
- run: flyctl deploy --config fly-staging.toml --remote-only
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ logs
.env.*
!.env.example

/db-data
/appdata
/cert
/coverage
1 change: 1 addition & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"trailingComma": "all",
"plugins": [
"prettier-plugin-organize-imports",
"prettier-plugin-jsdoc",
"prettier-plugin-sql"
],
"language": "postgresql"
Expand Down
5 changes: 5 additions & 0 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,15 @@
* {
@apply border-border;
}

body {
@apply bg-background text-foreground;
}

.standard-grid {
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr;
}

.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
Expand Down
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>
8 changes: 5 additions & 3 deletions app/components/app-breadcrumbs/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
defineProps<{
entries: { title: string; link: string }[];
entries: { title: string; link: string; style?: string }[];
}>();
</script>

Expand All @@ -11,11 +11,13 @@ defineProps<{
<BreadcrumbSeparator v-if="i > 0" />
<BreadcrumbItem>
<BreadcrumbLink v-if="i < entries.length - 1" as-child>
<NuxtLink :to="entry.link">
<NuxtLink :to="entry.link" :style="entry.style">
{{ entry.title }}
</NuxtLink>
</BreadcrumbLink>
<BreadcrumbPage v-else>{{ entry.title }}</BreadcrumbPage>
<BreadcrumbPage v-else :style="entry.style">{{
entry.title
}}</BreadcrumbPage>
</BreadcrumbItem>
</template>
</BreadcrumbList>
Expand Down
6 changes: 5 additions & 1 deletion app/components/attachment/preview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ const iconForFileType = computed(() => {
</EasyTooltip>
<EasyTooltip tooltip="Open in new tab">
<Button size="micro" variant="outline" as-child>
<NuxtLink :to="`/api/attachment/${attachment.id}`" external>
<NuxtLink
:to="`/api/attachment/${attachment.id}`"
external
target="_blank"
>
<Icon name="lucide:external-link" class="w-3 h-3" />
</NuxtLink>
</Button>
Expand Down
21 changes: 21 additions & 0 deletions app/components/coming-soon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts" setup>
defineProps<{
withIcon?: boolean;
large?: boolean;
vertical?: boolean;
}>();
</script>

<template>
<span
:class="{ 'text-xl': large, 'flex-col': vertical }"
class="flex gap-x-2 gap-y-0 items-center"
>
<Icon
v-if="withIcon"
:class="large ? 'text-[2em]' : 'text-[1.4em]'"
name="lucide:hard-hat"
/>
<span>Coming soon.</span>
</span>
</template>
13 changes: 13 additions & 0 deletions app/components/content/privacy-release-date.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
const lastReleaseDate = "2025-01-08";
const production = ref(process.env.NODE_ENV === "production");
const releaseDate = computed(() =>
production.value ? lastReleaseDate : "RELEASE_DATE",
);
</script>

<template>
<span>{{ releaseDate }}</span>
</template>
13 changes: 13 additions & 0 deletions app/components/content/tos-release-date.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
const lastReleaseDate = "2025-01-08";
const production = ref(process.env.NODE_ENV === "production");
const releaseDate = computed(() =>
production.value ? lastReleaseDate : "RELEASE_DATE",
);
</script>

<template>
<span>{{ releaseDate }}</span>
</template>
25 changes: 22 additions & 3 deletions app/components/create-board/form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ import { useForm } from "vee-validate";
import { toast } from "vue-sonner";
import type { z } from "zod";
import { createBoardSchema } from "~/lib/validation";
import type { Board } from "~~/server/db/schema";
import type { Board, Workspace } from "~~/server/db/schema";
const props = defineProps<{
workspace: Workspace;
}>();
const emit = defineEmits(["dismiss"]);
const schema = toTypedSchema(createBoardSchema);
const form = useForm({
validationSchema: schema,
initialValues: {
workspaceId: props.workspace.id,
name: "",
},
});
const queryClient = useQueryClient();
Expand Down Expand Up @@ -66,20 +74,31 @@ const onSubmit = form.handleSubmit((values) => {

<template>
<form class="p-4 pb-2 sm:p-0" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="workspaceId">
<FormItem>
<FormControl>
<Input type="hidden" v-bind="componentField" disabled />
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>Board Name</FormLabel>
<FormControl>
<Input
type="text"
placeholder="JavaScript 101"
v-bind="componentField"
/>
</FormControl>
<FormDescription>This is the name of your board.</FormDescription>
<FormMessage />
</FormItem>
</FormField>

<p class="mt-4 text-sm text-muted-foreground text-left">
The new board will inherit settings from the workspace, meaning workspace
members will have access to it.
</p>
<p v-if="formError" class="mt-4 text-[0.8rem] font-medium text-destructive">
{{ formError }}
</p>
Expand Down
17 changes: 12 additions & 5 deletions app/components/create-board/index.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
<script lang="ts" setup>
import { useMediaQuery } from "@vueuse/core";
import type { Workspace } from "~~/server/db/schema";
import Form from "./form.vue";
const props = defineProps<{
workspace: Workspace;
}>();
const isDesktop = useMediaQuery("(min-width: 640px)");
const isOpen = ref(false);
const title = "Create Board";
const description = "Create a new board to organize your tasks.";
const title = "Create a new board";
const description = computed(
() => `Create a new board in the ${props.workspace.name} workspace.`,
);
</script>

<template>
Expand All @@ -19,7 +26,7 @@ const description = "Create a new board to organize your tasks.";
<DialogTitle>{{ title }}</DialogTitle>
<DialogDescription>{{ description }}</DialogDescription>
</DialogHeader>
<Form @dismiss="isOpen = false" />
<Form :workspace @dismiss="isOpen = false" />
</DialogContent>
</Dialog>
<!-- And an iOS-like bottom sheet on mobile -->
Expand All @@ -29,7 +36,7 @@ const description = "Create a new board to organize your tasks.";
<DrawerTitle>{{ title }}</DrawerTitle>
<DrawerDescription>{{ description }}</DrawerDescription>
</DrawerHeader>
<Form @dismiss="isOpen = false" />
<Form :workspace @dismiss="isOpen = false" />
<DrawerFooter class="pt-2">
<DrawerClose as-child>
<Button variant="outline">Cancel</Button>
Expand All @@ -40,7 +47,7 @@ const description = "Create a new board to organize your tasks.";
</ClientOnly>

<Button
variant="outline"
variant="secondary"
size="sm"
class="flex items-center gap-2"
@click="isOpen = true"
Expand Down
20 changes: 13 additions & 7 deletions app/components/create-column/form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,31 @@ import { useForm } from "vee-validate";
import { toast } from "vue-sonner";
import type { z } from "zod";
import { createStatusColumnSchema } from "~/lib/validation";
import type { StatusColumn } from "~~/server/db/schema";
import type { Board, StatusColumn } from "~~/server/db/schema";
const localSchema = createStatusColumnSchema.pick({ name: true });
type SchemaType = z.infer<typeof localSchema>;
const props = defineProps<{
board: Board;
}>();
type SchemaType = z.infer<typeof createStatusColumnSchema>;
const boardId = useRouteParamSafe("id") as Ref<string>;
const emit = defineEmits(["dismiss"]);
const schema = toTypedSchema(localSchema);
const schema = toTypedSchema(createStatusColumnSchema);
const form = useForm({
validationSchema: schema,
initialValues: {
name: "",
workspaceId: props.board.workspaceId,
},
});
const queryClient = useQueryClient();
const { mutateAsync } = useMutation({
mutationFn: (data: SchemaType) =>
$fetch(`/api/column/${boardId.value}`, {
$fetch(`/api/column/${props.board.id}`, {
method: "POST",
body: data,
}),
Expand All @@ -38,7 +44,7 @@ const { mutateAsync } = useMutation({
normalized,
);
queryClient.setQueryData<StatusColumn[]>(
["board-columns", boardId],
["board-columns", props.board.id],
(old) => {
if (old) {
return [...old, normalized];
Expand Down
13 changes: 9 additions & 4 deletions app/components/create-column/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<script lang="ts" setup>
import { useMediaQuery } from "@vueuse/core";
import type { Board } from "~~/server/db/schema";
import Form from "./form.vue";
defineProps<{
board: Board;
}>();
const isDesktop = useMediaQuery("(min-width: 640px)");
const isOpen = ref(false);
Expand All @@ -19,7 +24,7 @@ const description = "Create a new status column to organize your tasks.";
<DialogTitle>{{ title }}</DialogTitle>
<DialogDescription>{{ description }}</DialogDescription>
</DialogHeader>
<Form @dismiss="isOpen = false" />
<Form :board @dismiss="isOpen = false" />
</DialogContent>
</Dialog>
<!-- And an iOS-like bottom sheet on mobile -->
Expand All @@ -29,7 +34,7 @@ const description = "Create a new status column to organize your tasks.";
<DrawerTitle>{{ title }}</DrawerTitle>
<DrawerDescription>{{ description }}</DrawerDescription>
</DrawerHeader>
<Form @dismiss="isOpen = false" />
<Form :board @dismiss="isOpen = false" />
<DrawerFooter class="pt-2">
<DrawerClose as-child>
<Button variant="outline">Cancel</Button>
Expand All @@ -39,7 +44,7 @@ const description = "Create a new status column to organize your tasks.";
</Drawer>
</ClientOnly>

<Button variant="outline" @click="isOpen = true">
New Column <Icon name="lucide:plus" />
<Button variant="secondary" size="sm" @click="isOpen = true">
New Column <Icon name="lucide:plus" class="h-[1em] ml-1" />
</Button>
</template>
Loading

0 comments on commit ebf8fac

Please sign in to comment.