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

feat/cell editing #1805

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,9 @@ export class NestRecordSqliteRepository extends RecordSqliteRepository {
restoreOneById(table: Table, id: string): Promise<void> {
return super.restoreOneById(table, id)
}

@UseRequestContext()
updateManyByIds(table: Table, updates: { id: string; spec: IRecordSpec }[]): Promise<void> {
return super.updateManyByIds(table, updates)
}
}
8 changes: 7 additions & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,11 @@
"xlsx": "^0.18.5",
"zod": "^3.22.4"
},
"type": "module"
"type": "module",
"dependencies": {
"@zag-js/color-picker": "^0.38.1",
"@zag-js/combobox": "^0.38.1",
"@zag-js/number-input": "^0.38.1",
"@zag-js/types": "^0.38.1"
}
}
44 changes: 44 additions & 0 deletions apps/frontend/src/lib/cell/CellEditors/base-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Edition, RevoGrid } from '@revolist/revogrid/dist/types/interfaces'
import type { VNode } from '@revolist/revogrid/dist/types/stencil-public-runtime'
import type { BaseField } from '@undb/core'
import { isEmpty } from 'lodash-es'
import { toast } from 'svelte-sonner'

export type SaveCallback = (value: any, preventFocus: boolean) => void

export abstract class BaseEditor<E extends Element, T extends BaseField> implements Edition.EditorBase {
element?: E | null | undefined
editCell?: Edition.EditCell | undefined

componentDidRender(): void {}
disconnectedCallback(): void {}
abstract render(createElement?: RevoGrid.HyperFunc<VNode> | undefined): string | void | VNode | VNode[]

constructor(
public column: RevoGrid.ColumnRegular,
protected saveCallback: SaveCallback,
) {}

protected get field(): T {
return this.column.field as T
}

// TODO: describe type
onChange<V = unknown>(value: V) {
// TODO: better check updates
if (value === this.editCell?.model[this.editCell.prop]) {
return
}

if (this.field.required && isEmpty(value)) {
return
}

const result = this.field.valueSchema.safeParse(value)
if (result.success) {
this.saveCallback(result.data, false)
} else {
toast.warning(result.error.flatten((i) => i.message).formErrors.join('\n'))
}
}
}
82 changes: 82 additions & 0 deletions apps/frontend/src/lib/cell/CellEditors/color-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { RevoGrid } from '@revolist/revogrid/dist/types/interfaces'
import type { VNode } from '@revolist/revogrid/dist/types/stencil-public-runtime'
import type { ColorField } from '@undb/core'
import * as colorPicker from '@zag-js/color-picker'
import delay from 'delay'
import htm from 'htm'
import { BaseEditor, type SaveCallback } from './base-editor'
import { normalizer } from './normalizer'

export class ColorEditor extends BaseEditor<HTMLDivElement, ColorField> {
private api: colorPicker.Api
private service: ReturnType<typeof colorPicker.machine>

constructor(
public column: RevoGrid.ColumnRegular,
protected saveCallback: SaveCallback,
) {
super(column, saveCallback)

const service = colorPicker.machine({
id: this.column.prop as string,
open: true,
positioning: {
strategy: 'fixed',
placement: 'bottom-start',
},
})
this.service = service
const machine = service.start()

const api = colorPicker.connect(machine.state, machine.send, normalizer)
this.api = api
}

private initElement() {
const editCell = this.editCell
if (!editCell) return

const value = editCell.model[editCell.prop] as string
if (value) {
const parsed = colorPicker.parse(value)
this.api.setValue(parsed)
}
}

async componentDidRender() {
await delay(0)
this.initElement()
}

disconnectedCallback(): void {
this.service.stop()
}

render(createComponent: RevoGrid.HyperFunc<VNode>) {
const html = htm.bind(createComponent)
const api = this.api

return html`
<div ...${api.rootProps}>
<input ...${api.hiddenInputProps} />

<div ...${api.controlProps}>
<button class="w-5 h-5" ...${api.triggerProps}>
<div ...${api.getTransparencyGridProps({ size: '10px' })} />
<div ...${api.getSwatchProps({ value: api.value })} />
</button>
<input ...${api.getChannelInputProps({ channel: 'hex' })} />
</div>

<div ...${api.positionerProps}>
<div ...${api.contentProps}>
<div ...${api.getAreaProps()}>
<div ...${api.getAreaBackgroundProps()} />
<div ...${api.getAreaThumbProps()} />
</div>
</div>
</div>
</div>
`
}
}
43 changes: 22 additions & 21 deletions apps/frontend/src/lib/cell/CellEditors/date-editor.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import type { Edition, RevoGrid } from '@revolist/revogrid/dist/types/interfaces'
import type { RevoGrid } from '@revolist/revogrid/dist/types/interfaces'
import type { VNode } from '@revolist/revogrid/dist/types/stencil-public-runtime'
import type { DateField } from '@undb/core'
import delay from 'delay'
import htm from 'htm'
import { BaseEditor } from './base-editor'

export type SaveCallback = (value: Edition.SaveData, preventFocus: boolean) => void
export class DateEditor extends BaseEditor<HTMLInputElement, DateField> {
private initElement() {
const element = this.element
if (!element) return

export class DateEditor implements Edition.EditorBase {
public element: HTMLInputElement | null = null
public editCell: Edition.EditCell | undefined = undefined
element.focus()

constructor(
public column: RevoGrid.ColumnRegular,
private saveCallback: SaveCallback,
) {}
const editCell = this.editCell
if (!editCell) return

element.value = editCell.model[editCell.prop] as string
}
async componentDidRender() {
await delay(0)
this.element?.focus()
}

private onChange(e: Event) {
this.element?.blur()
this.saveCallback((e.target as HTMLInputElement).valueAsDate?.toISOString() ?? '', false)
this.initElement()
}

render(createComponent: RevoGrid.HyperFunc<VNode>) {
return createComponent('input', {
type: 'date',
onchange: (e: Event) => this.onChange(e),
class:
'border-2 border-primary-300 rounded-none text-gray-900 text-sm focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5',
})
const html = htm.bind(createComponent)
return html`
<input
type="date"
onchange=${(e: Event) => this.onChange((e.target as HTMLInputElement).valueAsDate?.toISOString() ?? '')}
class="border-2 border-primary-300 rounded-none text-gray-900 text-sm focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5"
/>
`
}
}
9 changes: 9 additions & 0 deletions apps/frontend/src/lib/cell/CellEditors/editors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { Components } from '@revolist/revogrid'
import { ColorEditor } from './color-editor'
import { DateEditor } from './date-editor'
import { EmailEditor } from './email-editor'
import { NumberEditor } from './number-editor'
import { SelectEditor } from './select-editor'
import { StringEditor } from './string-editor'

export const editors: Components.RevoGrid['editors'] = {
string: StringEditor,
date: DateEditor,
email: EmailEditor,
number: NumberEditor,
currency: NumberEditor,
color: ColorEditor,
select: SelectEditor,
}
39 changes: 39 additions & 0 deletions apps/frontend/src/lib/cell/CellEditors/email-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Edition, RevoGrid } from '@revolist/revogrid/dist/types/interfaces'
import type { VNode } from '@revolist/revogrid/dist/types/stencil-public-runtime'
import type { EmailField } from '@undb/core'
import delay from 'delay'
import htm from 'htm'
import { BaseEditor } from './base-editor'

export class EmailEditor extends BaseEditor<HTMLInputElement, EmailField> {
public element: HTMLInputElement | null = null
public editCell: Edition.EditCell | undefined = undefined

private initElement() {
const element = this.element
if (!element) return

element.focus()

const editCell = this.editCell
if (!editCell) return

element.value = editCell.model[editCell.prop] as string
}

async componentDidRender() {
await delay(0)
this.initElement()
}

render(createComponent: RevoGrid.HyperFunc<VNode>) {
const html = htm.bind(createComponent)
return html`
<input
onchange=${(e: Event) => this.onChange((e.target as HTMLInputElement).value)}
placeholder="example@email.com"
class="border-2 border-primary-300 rounded-none text-gray-900 text-sm focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5"
/>
`
}
}
3 changes: 3 additions & 0 deletions apps/frontend/src/lib/cell/CellEditors/normalizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createNormalizer } from '@zag-js/types'

export const normalizer = createNormalizer((v) => v)
67 changes: 67 additions & 0 deletions apps/frontend/src/lib/cell/CellEditors/number-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { RevoGrid } from '@revolist/revogrid/dist/types/interfaces'
import type { VNode } from '@revolist/revogrid/dist/types/stencil-public-runtime'
import type { CurrencyField, NumberField } from '@undb/core'
import * as numberInput from '@zag-js/number-input'
import delay from 'delay'
import htm from 'htm'
import { BaseEditor, type SaveCallback } from './base-editor'
import { normalizer } from './normalizer'

export class NumberEditor extends BaseEditor<HTMLDivElement, NumberField | CurrencyField> {
private api: numberInput.Api
private service: ReturnType<typeof numberInput.machine>

constructor(
public column: RevoGrid.ColumnRegular,
protected saveCallback: SaveCallback,
) {
super(column, saveCallback)

const service = numberInput.machine({
id: this.column.prop as string,
})
this.service = service
const machine = service.start()

const api = numberInput.connect(machine.state, machine.send, normalizer)

this.api = api
}

private initElement() {
const editCell = this.editCell
if (!editCell) return

const value = editCell.model[editCell.prop] as string
this.api.setValue(Number(value))
this.api.focus()
}

async componentDidRender() {
await delay(0)
this.initElement()
}

disconnectedCallback(): void {
this.service.stop()
}

render(createComponent: RevoGrid.HyperFunc<VNode>) {
const html = htm.bind(createComponent)
const api = this.api

return html`
<div class="flex items-center" ...${api.rootProps}>
<input
type="number"
...${api.inputProps}
onchange=${(e: Event) => {
const value = (e.target as HTMLInputElement).value
return this.onChange(parseInt(value.replace(/,/g, ''), 10))
}}
class="border-2 border-primary-300 rounded-none text-gray-900 text-sm focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5"
/>
</div>
`
}
}