Skip to content

Commit

Permalink
feat: calendar view
Browse files Browse the repository at this point in the history
  • Loading branch information
nichenqin committed Nov 7, 2024
1 parent d26d26e commit aeaab9d
Show file tree
Hide file tree
Showing 53 changed files with 1,647 additions and 46 deletions.
2 changes: 2 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
},
"type": "module",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
"@codemirror/commands": "^6.7.1",
"@codemirror/language": "^6.10.3",
"@codemirror/state": "^6.4.1",
Expand All @@ -90,6 +91,7 @@
"@internationalized/date": "^3.5.6",
"@svelte-put/clickoutside": "^3.0.2",
"@tanstack/svelte-query": "^5.59.13",
"@tanstack/svelte-virtual": "^3.10.8",
"@tiptap/core": "^2.8.0",
"@tiptap/pm": "^2.8.0",
"@tiptap/starter-kit": "^2.8.0",
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type Base {
tables: [Table]!
}

type CalendarOption {
field: String
}

type Dashboard {
baseId: ID!
description: String
Expand Down Expand Up @@ -268,6 +272,7 @@ type User {

type View {
aggregate: JSON
calendar: CalendarOption
color: JSON
fields: JSON
filter: JSON
Expand All @@ -294,6 +299,7 @@ type ViewOption {
}

enum ViewType {
calendar
gallery
grid
kanban
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script lang="ts">
import { Trash2Icon } from "lucide-svelte"
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
import { type RecordDO, CalendarView, DateFieldValue, FieldIdVo } from "@undb/table"
import { getRecordsStore } from "$lib/store/records.store"
import { getTable } from "$lib/store/table.store"
import { trpc } from "$lib/trpc/client"
import { createMutation } from "@tanstack/svelte-query"
import { useQueryClient } from "@tanstack/svelte-query"
import { cn } from "$lib/utils"
import { monthStore } from "$lib/store/calendar.store"
export let view: CalendarView
const store = getRecordsStore()
const t = getTable()
let fieldId = view.field.into(undefined)
let field = fieldId ? $t.schema.getFieldById(new FieldIdVo(fieldId)).into(undefined) : undefined
const updateRecord = createMutation({
mutationFn: trpc.record.update.mutate,
})
const client = useQueryClient()
let isDraggedOver = false
function setupDropTarget(node: HTMLElement) {
dropTargetForElements({
element: node,
getData(e) {
return {
type: "calendar-date-drop",
}
},
onDragEnter(e) {
isDraggedOver = true
},
onDragLeave(e) {
isDraggedOver = false
},
async onDrop(args) {
if (!field) return
const data = args.source.data
const type = data.type
if (type !== "calendar-date-drag") {
return
}
const record = data.record as RecordDO
record.values.setValue(field.id, new DateFieldValue(null))
store.setRecord(record)
await $updateRecord.mutateAsync({
tableId: $t.id.value,
id: record.id.value,
values: {
[field.id.value]: null,
},
})
await client.invalidateQueries({
queryKey: ["records", $t?.id.value, view.id.value],
})
},
})
}
</script>

<button use:setupDropTarget class={cn("hidden transition-opacity", $monthStore.isDragging && "block")}>
<Trash2Icon class={cn("size-10 text-gray-600", isDraggedOver && "text-red-600")} />
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js"
import { getTable } from "$lib/store/table.store"
import { updateCalendarViewDTO, type CalendarView } from "@undb/table"
import FieldPicker from "../field-picker/field-picker.svelte"
import { defaults, superForm } from "sveltekit-superforms"
import { zodClient } from "sveltekit-superforms/adapters"
import CreateFieldButton from "../create-field/create-field-button.svelte"
import { trpc } from "$lib/trpc/client"
import { createMutation } from "@tanstack/svelte-query"
import { toast } from "svelte-sonner"
import { invalidate } from "$app/navigation"
import { hasPermission } from "$lib/store/space-member.store"
import { CircleCheckBigIcon } from "lucide-svelte"
export let readonly = false
const table = getTable()
$: fields = $table.schema.getCalendarFields()
export let view: CalendarView
const form = superForm(
defaults(
{
tableId: $table.id.value,
viewId: view.id.value,
type: "calendar",
name: view.name.value,
calendar: view.calendar.unwrapOrElse(() => ({ field: undefined })),
},
zodClient(updateCalendarViewDTO),
),
{
SPA: true,
dataType: "json",
validators: zodClient(updateCalendarViewDTO),
resetForm: false,
invalidateAll: false,
onUpdate(event) {
if (!event.form.valid) return
$updateViewMutation.mutate(event.form.data)
},
},
)
const { enhance, form: formData } = form
const updateViewMutation = createMutation({
mutationFn: trpc.table.view.update.mutate,
mutationKey: ["updateView"],
async onSuccess(data, variables, context) {
toast.success("View updated")
await invalidate(`undb:table:${$table.id.value}`)
},
})
</script>

<div class="space-y-2">
<form id="select-calendar-field-form" class="space-y-2" use:enhance>
<div class="grid w-full items-center gap-4">
<div class="flex flex-col space-y-1.5">
<FieldPicker
disabled={readonly}
placeholder="Select a select type field to group calendar lanes"
value={$formData.calendar?.field}
onValueChange={(field) => {
if ($formData.calendar) {
$formData.calendar.field = field
} else {
$formData.calendar = { field }
}
}}
filter={(f) => fields.map((f) => f.id.value).includes(f.id)}
/>
</div>
</div>
</form>

{#if !readonly && $hasPermission("field:update")}
<CreateFieldButton class="w-full" variant="secondary" />
{/if}

{#if !readonly}
<div class="flex w-full justify-end">
<Button
variant="outline"
size="sm"
class="w-full"
type="submit"
form="select-calendar-field-form"
disabled={readonly}
>
<CircleCheckBigIcon class="mr-2 size-4" />
Confirm
</Button>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button"
import { CalendarIcon } from "lucide-svelte"
import * as Dropdown from "$lib/components/ui/dropdown-menu"
import { FieldIdVo, type CalendarView } from "@undb/table"
import CalendarFieldForm from "./calendar-field-form.svelte"
import { getTable } from "$lib/store/table.store"
import FieldIcon from "../field-icon/field-icon.svelte"
const table = getTable()
export let view: CalendarView
export let readonly = false
let fieldId = view.field.into(undefined)
let field = fieldId ? $table.schema.getFieldById(new FieldIdVo(fieldId)).into(undefined) : undefined
</script>

<Dropdown.Root>
<Dropdown.Trigger asChild let:builder>
<Button variant="ghost" size="sm" class="gap-1" builders={[builder]} {...$$restProps}>
<CalendarIcon class="text-muted-foreground mr-2 h-4 w-4 font-semibold" />
Calendar
{#if field}
<span>by</span>
<span class="inline-flex items-center gap-1 rounded-sm bg-gray-100 px-1.5 py-0.5">
<FieldIcon type={field.type} {field} class="size-3 text-gray-700" />
{field.name.value}
</span>
{/if}
</Button>
</Dropdown.Trigger>
<Dropdown.Content class="w-[400px] p-2">
<Dropdown.Label>
{#if !readonly}
Update calendar view
{:else}
Calendar view
{/if}
</Dropdown.Label>
<CalendarFieldForm {view} {readonly} />
</Dropdown.Content>
</Dropdown.Root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import Button from "$lib/components/ui/button/button.svelte"
import * as Popover from "$lib/components/ui/popover"
import { monthStore } from "$lib/store/calendar.store"
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-svelte"
import CalendarViewMiniMonth from "./calendar-view-mini-month.svelte"
import { format } from "date-fns"
</script>

<Button variant="secondary" size="xs" on:click={() => monthStore.prevMonth()}>
<ChevronLeftIcon class="size-3 font-semibold text-gray-500" />
</Button>
<Popover.Root>
<Popover.Trigger>
<Button variant="secondary" size="xs">
{format($monthStore.currentDate, "yyyy-MM")}
</Button>
</Popover.Trigger>
<Popover.Content class="px-0 py-2">
<CalendarViewMiniMonth />
</Popover.Content>
</Popover.Root>
<Button variant="secondary" size="xs" on:click={() => monthStore.nextMonth()}>
<ChevronRightIcon class="size-3 font-semibold text-gray-500" />
</Button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button"
import { monthStore } from "$lib/store/calendar.store"
import { cn } from "$lib/utils"
import { format } from "date-fns"
import { getMonth } from "date-fns/getMonth"
import { isSameMonth } from "date-fns/isSameMonth"
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-svelte"
const currentYear = monthStore.currentYear
const currentMonth = monthStore.currentMonth
</script>

<div class="flex w-full flex-col">
<div class="flex items-center px-2 py-1">
<Button variant="ghost" size="icon" on:click={() => monthStore.prevYear()}>
<ChevronLeftIcon class="size-3 text-gray-500" />
</Button>
<span class="flex-1 text-center text-sm font-medium">{$currentYear}</span>
<Button variant="ghost" size="icon" on:click={() => monthStore.nextYear()}>
<ChevronRightIcon class="size-3 text-gray-500" />
</Button>
</div>
<div class="grid grid-cols-4 gap-1.5 px-1.5 py-1">
{#each Array.from({ length: 12 }).map((_, index) => index + 1) as month}
{@const date = new Date($currentYear, month - 1, 1)}
{@const isCurrentMonth = $currentMonth === getMonth(date) + 1}
{@const isNow = isSameMonth(date, new Date())}
<button
class={cn(
"p-2 text-sm hover:bg-gray-100",
isCurrentMonth && "bg-gray-200 font-semibold",
isNow && !isCurrentMonth && "font-semibold text-blue-500",
)}
on:click={() => monthStore.setMonth(date)}
>
{format(date, "MMM")}
</button>
{/each}
</div>
</div>
Loading

0 comments on commit aeaab9d

Please sign in to comment.