Skip to content

Commit

Permalink
Merge pull request #2014 from undb-io/release/v1.0.0-67
Browse files Browse the repository at this point in the history
Release version v1.0.0-67
  • Loading branch information
nichenqin authored Sep 10, 2024
2 parents f86a3b6 + f63e067 commit fa1497c
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 81 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog


## v1.0.0-67

## v1.0.0-66

## v1.0.0-65
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import type { IOption } from "@undb/table"
import Option from "../option/option.svelte"
import { ChevronRightIcon } from "lucide-svelte"
import { kanbanStore } from "$lib/store/kanban.store"
export let option: IOption
export let viewId: string
</script>

<div class="flex h-full w-full flex-1 flex-col items-center">
<button on:click={() => kanbanStore.toggleLane(viewId, option.id)}>
<ChevronRightIcon class="text-muted-foreground h-4 w-4" />
</button>
<Option class="translate-y-8 -rotate-90 text-nowrap" {option} />
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@
import { getTable } from "$lib/store/table.store"
import KanbanCard from "./kanban-card.svelte"
import Button from "$lib/components/ui/button/button.svelte"
import { EllipsisIcon, GripVerticalIcon, LoaderCircleIcon, PencilIcon, PlusIcon, TrashIcon } from "lucide-svelte"
import {
EllipsisIcon,
GripVerticalIcon,
LoaderCircleIcon,
PencilIcon,
PlusIcon,
TrashIcon,
Maximize2Icon,
} from "lucide-svelte"
import { CREATE_RECORD_MODAL, toggleModal } from "$lib/store/modal.store"
import { defaultRecordValues } from "$lib/store/records.store"
import { getRecordsStore } from "$lib/store/records.store"
Expand All @@ -32,6 +40,8 @@
import * as AlertDialog from "$lib/components/ui/alert-dialog"
import { match } from "ts-pattern"
import { cn } from "$lib/utils"
import { kanbanStore } from "$lib/store/kanban.store"
import SelectKanbanCollapsedLane from "./select-kanban-collapsed-lane.svelte"
const table = getTable()
const recordsStore = getRecordsStore()
Expand Down Expand Up @@ -73,7 +83,9 @@
})
}
let isInView: boolean
let getIsLaneCollapsed = kanbanStore.getIsLaneCollapsed
$: isLaneCollapsed = $getIsLaneCollapsed($viewId, option?.id ?? "") ?? false
const query = createInfiniteQuery(
derived([table, viewId], ([$table, $viewId]) => {
const view = $table.views.getViewById($viewId)
Expand Down Expand Up @@ -149,7 +161,7 @@
}
onMount(() => {
if (!shareId && !readonly) {
if (!shareId && !readonly && laneElement) {
new Sortable(laneElement, {
group: "shared",
animation: 150,
Expand Down Expand Up @@ -222,24 +234,31 @@

<div
data-option-id={option?.id ?? null}
class={cn("kanban-lane flex w-[350px] shrink-0 flex-col space-y-2 rounded-sm px-2 pt-2 transition-all")}
class={cn(
"kanban-lane flex shrink-0 flex-col rounded-sm transition-all",
isLaneCollapsed ? "w-10 rounded-md border shadow-sm" : "w-[350px]",
)}
>
<div class="flex w-full items-center justify-between gap-1">
<div class="flex items-center gap-1">
{#if !shareId && option && !readonly}
<div class="lane-handle cursor-move">
<GripVerticalIcon class="text-muted-foreground h-4 w-4" />
</div>
{/if}

{#if option}
<Option {option} />
{:else}
<Option option={{ id: "", name: "No Option", color: "gray" }} />
{/if}
{#if isLaneCollapsed}
<div class="mr-2 w-full pt-2" bind:this={laneElement} data-option-id={option?.id ?? null}>
<SelectKanbanCollapsedLane option={option ?? { id: "", name: "No Option", color: "gray" }} viewId={$viewId} />
</div>
{:else}
<div class="flex w-full items-center justify-between gap-1">
<div class="flex items-center gap-1">
{#if !shareId && option && !readonly}
<div class="lane-handle cursor-move">
<GripVerticalIcon class="text-muted-foreground h-4 w-4" />
</div>
{/if}

{#if option}
<Option {option} />
{:else}
<Option option={{ id: "", name: "No Option", color: "gray" }} />
{/if}
</div>

{#if !shareId && !readonly && option}
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button size="xs" variant="ghost" builders={[builder]}>
Expand All @@ -248,74 +267,102 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-48">
<DropdownMenu.Group>
<DropdownMenu.Item class="text-muted-foreground text-xs" on:click={() => (updateOptionDialogOpen = true)}>
<PencilIcon class="mr-2 h-3 w-3" />
Update option
</DropdownMenu.Item>
{#if !shareId && !readonly && option}
<DropdownMenu.Item class="text-muted-foreground text-xs" on:click={() => (updateOptionDialogOpen = true)}>
<PencilIcon class="mr-2 h-3 w-3" />
Update option
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={field.options.length <= 1}
class="text-muted-foreground text-xs"
on:click={() => (deleteOptionDialogOpen = true)}
>
<TrashIcon class="mr-2 h-3 w-3" />
Delete option
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item
disabled={field.options.length <= 1}
class="text-muted-foreground text-xs"
on:click={() => (deleteOptionDialogOpen = true)}
on:click={() => {
kanbanStore.toggleLane($viewId, option?.id ?? "")
}}
>
<TrashIcon class="mr-2 h-3 w-3" />
Delete option
<Maximize2Icon class="mr-2 h-3 w-3" />
Collapse Lane
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
</div>
<div class="max-w-[350px] flex-1 space-y-2 overflow-hidden" data-option-id={option?.id ?? null}>
<div
bind:this={laneElement}
data-option-id={option?.id ?? null}
class={cn(
"h-full flex-1 space-y-2 overflow-y-auto rounded-lg border bg-gray-100 p-2",
getKanbanBgColor(option?.color ?? "gray"),
)}
>
{#if !readonly && $hasPermission("record:create")}
<Button on:click={onCreateRecord} variant="outline" size="sm" class="w-full">
<PlusIcon class="text-muted-foreground mr-2 h-4 w-4 font-semibold" />
</Button>
{/if}
{#if $query.isLoading}
<KanbanSkeleton />
{:else if $query.isError}
<p>error: {$query.error.message}</p>
{:else}
{#each recordDos as record (record.id.value)}
<KanbanCard {readonly} {record} {fields} />
{/each}
{#if $query.hasNextPage && $query.isFetchedAfterMount}
<Button
disabled={$query.isFetching}
variant="secondary"
size="sm"
class="w-full"
on:click={() => $query.fetchNextPage()}
>
{#if $query.isFetching}
<LoaderCircleIcon class="mr-2 h-3 w-3 animate-spin" />
</div>
<div class="mt-2 max-w-[350px] flex-1 space-y-2 overflow-hidden" data-option-id={option?.id ?? null}>
<div
bind:this={laneElement}
data-option-id={option?.id ?? null}
class={cn(
"h-full flex-1 space-y-2 overflow-y-auto rounded-lg border bg-gray-100 p-2",
getKanbanBgColor(option?.color ?? "gray"),
)}
>
{#if !readonly && $hasPermission("record:create")}
{#if $query.isFetchedAfterMount}
{#if $query.data?.pages[0]?.total > 0}
<Button on:click={onCreateRecord} variant="outline" size="sm" class="w-full">
<PlusIcon class="text-muted-foreground mr-2 h-4 w-4 font-semibold" />
</Button>
{:else}
<div class="flex h-full w-full flex-col items-center justify-center space-y-3">
<p class="text-sm font-semibold">No records</p>
<p class="text-muted-foreground text-xs">
Create a new record of this option <Option
option={option ?? { id: "", name: "No Option", color: "gray" }}
/>
</p>
<Button on:click={onCreateRecord} variant="outline" size="sm">
<PlusIcon class="text-muted-foreground mr-2 h-4 w-4 font-semibold" />
New Record
</Button>
</div>
{/if}
Load more
</Button>
{/if}
{/if}
{/if}
{#if $query.isLoading}
<KanbanSkeleton />
{:else if $query.isError}
<p>error: {$query.error.message}</p>
{:else}
{#each recordDos as record (record.id.value)}
<KanbanCard {readonly} {record} {fields} />
{/each}
{#if $query.hasNextPage && $query.isFetchedAfterMount}
<Button
disabled={$query.isFetching}
variant="secondary"
size="sm"
class="w-full"
on:click={() => $query.fetchNextPage()}
>
{#if $query.isFetching}
<LoaderCircleIcon class="mr-2 h-3 w-3 animate-spin" />
{/if}
Load more
</Button>
{/if}
{/if}
</div>
</div>
</div>
<div class="flex w-full items-center justify-between px-2 py-0.5">
<Button variant="outline" size="xs" on:click={onCreateRecord}>
<PlusIcon class="text-muted-foreground mr-2 h-3 w-3 font-semibold" />
New Record
</Button>
<div class="mt-2 flex w-full items-center justify-between px-2 py-0.5">
<Button variant="outline" size="xs" on:click={onCreateRecord}>
<PlusIcon class="text-muted-foreground mr-2 h-3 w-3 font-semibold" />
New Record
</Button>

{#if $query.isFetchedAfterMount}
<p class="text-muted-foreground text-xs">
{$query.data?.pages.flatMap((r) => r.records).length} / {$query.data?.pages[0]?.total} records
</p>
{/if}
</div>
{#if $query.isFetchedAfterMount}
<p class="text-muted-foreground text-xs">
{$query.data?.pages.flatMap((r) => r.records).length} / {$query.data?.pages[0]?.total} records
</p>
{/if}
</div>
{/if}
</div>

{#if option && $hasPermission("field:update")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
export let shareId: string | undefined = undefined
let fieldId = view.field.unwrapUnchecked()!
$: field = $table.schema.getFieldById(new FieldIdVo(fieldId)).into(undefined) as SelectField
let field = $table.schema.getFieldById(new FieldIdVo(fieldId)).into(undefined) as SelectField
let lanesContainer: HTMLElement
$: options = field.options ?? []
Expand All @@ -52,12 +52,12 @@
if (!shareId) {
new Sortable(lanesContainer, {
animation: 150,
ghostClass: "bg-gray-200",
ghostClass: "bg-gray-100",
handle: ".lane-handle", // 添加一个句柄类,用于拖拽
onEnd: (evt) => {
const { oldIndex, newIndex } = evt
if (oldIndex !== newIndex && isNumber(oldIndex) && isNumber(newIndex)) {
options = arrayMoveImmutable(options, oldIndex, newIndex)
options = arrayMoveImmutable(options, oldIndex - 1, newIndex - 1)
$updateFieldMudation.mutate({
tableId: $table.id.value,
field: {
Expand All @@ -77,7 +77,7 @@
})
let name: string
let color: IColors
let color = field.getNextColor()
const createOption = () => {
if (shareId) {
Expand All @@ -102,7 +102,7 @@
</script>

<div class="flex-1 overflow-x-auto overflow-y-hidden p-4">
<div bind:this={lanesContainer} class="flex h-full overflow-y-hidden pr-4">
<div bind:this={lanesContainer} class="flex h-full space-x-2 overflow-y-hidden pr-2">
<SelectKanbanLane {field} {readonly} tableId={$table.id.value} {viewId} {fieldId} option={null} {shareId} />
{#each options as option (option.id)}
<SelectKanbanLane {field} {readonly} tableId={$table.id.value} {viewId} {fieldId} {option} {shareId} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
getTextColor(option.color),
getRingColor(option.color),
getBorderColor(option.color),
$$restProps.class,
)}
>
{option.name}
Expand Down
42 changes: 42 additions & 0 deletions apps/frontend/src/lib/store/kanban.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { persisted } from "svelte-persisted-store"
import { derived } from "svelte/store"

interface ICollapsedLanes {
[key: string]: {
[key: string]: boolean
}
}

export const createKanbanStore = () => {
const store = persisted("undb_kanban", {
collapsedLanes: {} as ICollapsedLanes,
})

const { subscribe, set, update } = store

const toggleLane = (viewId: string, laneId: string) => {
update((state) => {
if (!state.collapsedLanes[viewId]) {
state.collapsedLanes[viewId] = {}
}
state.collapsedLanes[viewId][laneId] = !state.collapsedLanes[viewId][laneId]
return state
})
}

const getIsLaneCollapsed = derived(
store,
($store) => (viewId: string, laneId: string) => $store.collapsedLanes[viewId]?.[laneId] ?? false,
)

return {
subscribe,
set,
update,

getIsLaneCollapsed,
toggleLane,
}
}

export const kanbanStore = createKanbanStore()
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "undb",
"version": "1.0.0-66",
"version": "1.0.0-67",
"private": true,
"scripts": {
"build": "NODE_ENV=production bun --bun turbo build",
Expand Down
4 changes: 4 additions & 0 deletions packages/table/src/modules/schema/fields/option/options.vo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ export class Options extends ValueObject<Option[]> {
toJSON() {
return this.props.map((o) => o.toJSON())
}

getNextColor() {
return new ColorsVO().next(this.props[this.props.length - 1]?.value.color)
}
}
Loading

0 comments on commit fa1497c

Please sign in to comment.