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

feature: gantt view #1275

Merged
merged 16 commits into from
Jul 6, 2023
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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
- 🔧 Deploy with variant adapters
- 🎮 Developer friendly. Provide openapi / webhooks / realtime subscriptions / sdk(soon) / erd preview and more
- :sparkles: Multiple built-in field types and variants
- :city_sunset: Different types of views, including grid, kanban, tree, calendar and more
- :city_sunset: Different types of views, including grid, kanban, gantt, tree, calendar and more

## 📚 Tech Stack

Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/core/table/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { SetFieldSortCommandHandler } from './set-field-sort.command.handler.js'
import { SetFieldVisibilityCommandHandler } from './set-field-visibility.command.handler.js'
import { SetFieldWidthCommandHandler } from './set-field-width.command.handler.js'
import { SetFiltersCommandHandler } from './set-filters.command.handler.js'
import { SetGanttFieldCommandHandler } from './set-gantt-field.command.handler.js'
import { SetKanbanFieldCommandHandler } from './set-kanban-field.command.handler.js'
import { SetPinnedFieldsCommandHandler } from './set-pinned-fields.command.handler.js'
import { SetRowHeightCommandHandler } from './set-row-height.command.handler.js'
Expand Down Expand Up @@ -86,4 +87,5 @@ export const commandHandlers = [
DeleteWidgetCommandHandler,
DuplicateFieldCommandHandler,
ExportGridCommandHandler,
SetGanttFieldCommandHandler,
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ICommandHandler } from '@nestjs/cqrs'
import { CommandHandler } from '@nestjs/cqrs'
import { type ITableRepository } from '@undb/core'
import { SetGanttFieldCommandHandler as DomainHandler, SetGanttFieldCommand } from '@undb/cqrs'
import { InjectTableRepository } from '../adapters/index.js'

@CommandHandler(SetGanttFieldCommand)
export class SetGanttFieldCommandHandler extends DomainHandler implements ICommandHandler<SetGanttFieldCommand> {
constructor(
@InjectTableRepository()
protected readonly repo: ITableRepository,
) {
super(repo)
}
}
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"svelte-check": "^3.4.4",
"svelte-copy": "^1.4.1",
"svelte-dnd-action": "^0.9.22",
"svelte-gantt": "4.0.9-beta",
"svelte-grid": "^5.1.1",
"svelte-i18next": "^2.0.0",
"svelte-jsoneditor": "^0.17.8",
Expand Down
7 changes: 7 additions & 0 deletions apps/frontend/src/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

.root,
#root,
#docs-root {
--primary-color: #fff;
--secondary-color: #000;
}
77 changes: 77 additions & 0 deletions apps/frontend/src/lib/gantt/GanttConfig.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script lang="ts">
import { getTable, getView } from '$lib/store/table'
import { Button, Hr, Radio } from 'flowbite-svelte'
import FieldIcon from '$lib/field/FieldIcon.svelte'
import { trpc } from '$lib/trpc/client'
import { writable } from 'svelte/store'
import { configViewModal, createFieldInitial, createFieldModal } from '$lib/store/modal'
import { t } from '$lib/i18n'
import { invalidate } from '$app/navigation'
import { FieldId } from '@undb/core'

const table = getTable()
const view = getView()
$: ganttFields = $table.schema.ganttFields

const ganttField = writable($view.ganttFieldIdString)
const setField = trpc().table.view.gantt.setField.mutation({
async onSuccess(data, variables, context) {
await invalidate(`table:${$table.id.value}`)
$view.ganttFieldIdString = $ganttField
configViewModal.close()
},
})
const onChange = async () => {
if (ganttField) {
$setField.mutate({
tableId: $table.id.value,
viewId: $view.id.value,
field: $ganttField,
})
}
}
</script>

<div class="flex flex-col space-y-2">
{#each ganttFields as field}
<Radio bind:group={$ganttField} name="ganttFieldId" value={field.id.value} on:change={onChange} class="space-x-1">
<FieldIcon type={field.type} />
<span>{field.name.value}</span>
</Radio>
{/each}
</div>

{#if ganttFields.length}
<div class="my-4">
<Hr>
<span class="text-gray-400 text-sm font-normal">{$t('or', { ns: 'common' })}</span>
</Hr>
</div>
{/if}

<div class="flex flex-col justify-center gap-2">
<Button
size="xs"
color="light"
class="flex gap-2"
on:click={() => {
const id = FieldId.createId()
$createFieldInitial = {
id,
type: 'date-range',
}

createFieldModal.open(async () => {
$setField.mutate({
tableId: $table.id.value,
viewId: $view.id.value,
field: id,
})
})
}}
>
<i class="ti ti-plus" />
<span>{$t('Create New Date Range Field')}</span>
<FieldIcon type="select" />
</Button>
</div>
23 changes: 23 additions & 0 deletions apps/frontend/src/lib/gantt/GanttIndex.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { getTable, getView } from '$lib/store/table'
import { Card } from 'flowbite-svelte'
import type { DateRangeField } from '@undb/core'
import GanttConfig from './GanttConfig.svelte'
import GanttView from './GanttView.svelte'

const table = getTable()
const view = getView()

$: fieldId = $view.ganttFieldIdString
$: field = fieldId ? ($table.schema.getFieldById(fieldId).into() as DateRangeField | undefined) : undefined
</script>

{#if field}
<GanttView {field} />
{:else}
<div class="flex items-center justify-center h-screen w-full bg-gray-100 dark:bg-slate-800/80">
<Card class="flex-1">
<GanttConfig />
</Card>
</div>
{/if}
9 changes: 9 additions & 0 deletions apps/frontend/src/lib/gantt/GanttToolbar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import ShareViewButton from '$lib/share/ShareViewButton.svelte'
import CreateRecordButton from '$lib/table/CreateRecordButton.svelte'
import FilterMenu from '$lib/table/FilterMenu.svelte'
</script>

<CreateRecordButton />
<FilterMenu />
<ShareViewButton />
180 changes: 180 additions & 0 deletions apps/frontend/src/lib/gantt/GanttView.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<script lang="ts">
import { SvelteGantt, SvelteGanttDependencies, SvelteGanttTable } from 'svelte-gantt'
import type { SvelteGanttComponent, SvelteGanttOptions } from 'svelte-gantt/types/gantt'
import { addDays, endOfWeek, startOfWeek, subDays } from 'date-fns'
import { currentRecordId, getTable, listRecordFn, readonly, recordHash } from '$lib/store/table'
import { RecordFactory, type DateRangeField } from '@undb/core'
import type { RowModel } from 'svelte-gantt/types/core/row'
import type { TaskModel } from 'svelte-gantt/types/core/task'
import { onMount } from 'svelte'
import { t } from '$lib/i18n'
import { trpc } from '$lib/trpc/client'

const table = getTable()
export let field: DateRangeField

let currentStart = startOfWeek(new Date())
let currentEnd = endOfWeek(new Date())

const previous = () => {
currentStart = subDays(currentStart, 7)
currentEnd = subDays(currentEnd, 7)
}

const next = () => {
currentStart = addDays(currentStart, 7)
currentEnd = addDays(currentEnd, 7)
}

$: listRecords = $listRecordFn(
[
{
path: field.id.value,
type: field.type,
operator: '$between',
value: [currentStart.toISOString(), currentEnd.toISOString()],
},
],
{
queryHash: $recordHash + '_gantt',
},
)

const updateRecord = trpc().record.update.mutation({
async onSuccess(data, variables, context) {
await $listRecords.refetch()
},
})

$: records = RecordFactory.fromQueryRecords($listRecords?.data?.records ?? [], $table.schema.toIdMap()) ?? []
$: rows = records.map<RowModel>((r) => ({
id: r.id.value,
label: r.getDisplayFieldsValue($table),
height: 52,
classes: 'bg-gray-100 dark:!bg-gray-300 dark:text-white',
}))
$: tasks = records.map<TaskModel>((r) => {
const value = r.valuesJSON?.[field.id.value]
const [from, to] = value
const fromTimeStamp = new Date(from).getTime()
const toTimeStampe = new Date(to).getTime()

return {
id: r.id.value as any as number,
resourceId: r.id.value as any as number,
label: r.getDisplayFieldsValue($table),
from: fromTimeStamp,
to: toTimeStampe,
classes: '!bg-blue-400 hover:!bg-blue-500',
enableDragging: !$readonly,
}
})

$: options = {
rows,
tasks,
dependencies: [],
timeRanges: [],
columnOffset: 15,
magnetOffset: 15,
rowHeight: 52,
rowPadding: 6,
headers: [{ unit: 'day', format: 'MMMM Do' }],
fitWidth: true,
minWidth: 800,
from: currentStart.getTime(),
to: currentEnd.getTime(),
tableHeaders: [{ title: $t('Label', { ns: 'common' }), property: 'label', width: 140 }],
tableWidth: 240,
ganttTableModules: [SvelteGanttTable],
ganttBodyModules: [SvelteGanttDependencies],
} satisfies SvelteGanttOptions

let ele: HTMLElement | undefined
let gantt: SvelteGanttComponent
onMount(() => {
if (ele) {
gantt = new SvelteGantt({ target: ele, props: options })
// @ts-expect-error
gantt.api.tasks.on.dblclicked((event, b) => {
const [model] = event
if (!model) return
const recordId = model.id
$currentRecordId = recordId
})
// @ts-expect-error
gantt.api.tasks.on.changed((event) => {
const [model] = event
if (!model) return
if (model.sourceRow.model.id !== model.targetRow.model.id) return

const task = model.task
const recordId = task.model.resourceId
const newFrom = new Date(task.model.from)
const newTo = new Date(task.model.to)

$updateRecord.mutate({
tableId: $table.id.value,
id: recordId,
values: {
[field.id.value]: [newFrom.toISOString(), newTo.toISOString()],
},
})
})
}
})

$: if (gantt) gantt.$set(options)
</script>

<div class="w-full">
<div class="p-2 text-gray-500">
<div class="flex justify-end gap-2">
<button
on:click={previous}
class="p-1 hover:bg-gray-100 w-6 h-6 inline-flex items-center justify-center transition"
>
<i class="ti ti-chevron-left" />
</button>
<button on:click={next} class="p-1 hover:bg-gray-100 w-6 h-6 inline-flex items-center justify-center transition">
<i class="ti ti-chevron-right" />
</button>
</div>
</div>
<div class="border-t" bind:this={ele} id="undb-gantt" />
</div>

<style>
#undb-gantt {
flex-grow: 1;
overflow: auto;
}

#undb-gantt :global(.sg-hover) {
background-color: #00000008;
}

#undb-gantt :global(.sg-hover .sg-table-body-cell) {
background-color: #00000008;
}

:global(.dark .sg-gantt .column-header-cell) {
color: white;
}

:global(.dark .sg-gantt .column-header-cell:hover) {
color: #374151;
background-color: #f7f7f7;
}

:global(.dark .sg-gantt .sg-table-body-cell) {
color: white;
background-color: #374151;
border: none;
}

:global(.dark .sg-gantt .sg-table-header-cell) {
color: white;
background-color: #374151;
}
</style>
2 changes: 1 addition & 1 deletion apps/frontend/src/lib/store/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export const listRecordFn: Readable<
'share.view',
() => (filter?: IRootFilter, options?: ListRecordQueryOptions) =>
trpc().share.viewRecords.query(
{ viewId: $view.id.value },
{ viewId: $view.id.value, q: $q, filter },
{ refetchOnMount: false, refetchOnWindowFocus: true, queryHash: $recordHash, ...options },
),
)
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/lib/table/TableIndex.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import CalendarIndex from '$lib/calendar/CalendarIndex.svelte'
import DashboardIndex from '$lib/dashboard/DashboardIndex.svelte'
import TreeIndex from '$lib/tree/TreeIndex.svelte'
import GanttIndex from '$lib/gantt/GanttIndex.svelte'

const view = getView()

Expand All @@ -15,6 +16,7 @@
const map: Partial<Record<IViewDisplayType, ComponentType>> = {
grid: TableView,
kanban: KanbanIndex,
gantt: GanttIndex,
tree: TreeIndex,
calendar: CalendarIndex,
dashboard: DashboardIndex,
Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/src/lib/table/TableView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,14 @@
}

:global(.dark .hovered) {
background-color: #374151;
background-color: var(--primary-color) !important;
}

:global(.dark revogr-data .rgRow.focused-rgRow) {
background-color: #374151 !important;
}

:global(.dark revo-grid[theme=compact] revogr-header .rgHeaderCell.focused-cell){
background-color: #374151 !important
:global(.dark revo-grid[theme='compact'] revogr-header .rgHeaderCell.focused-cell) {
background-color: #374151 !important;
}
</style>
Loading