Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release version v1.0.0-67 #2014

Merged
merged 5 commits into from
Sep 10, 2024
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
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
Loading