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

Rename column functionality #595

Merged
merged 10 commits into from
Oct 17, 2024
4 changes: 4 additions & 0 deletions client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export class Client {
return await this.request<{ columns: types.IColumn[] }>('/column/list')
}

async columnRename(props: { path: string; oldName: string; newName: string }) {
return await this.request<Record<string, never>>('/column/rename', props)
}

// Config

async configRead(props: Record<string, never> = {}) {
Expand Down
1 change: 1 addition & 0 deletions client/components/Controllers/Table/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default function Editor() {
onEditStop={store.stopTableEditing}
defaultCellSelection={cellSelection}
onCellSelectionChange={setCellSelection}
onColumnRename={store.renameColumn}
handle={(ref) => store.setRefs({ grid: ref })}
/>
</Box>
Expand Down
6 changes: 4 additions & 2 deletions client/components/Editors/Base/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import HeadingBox from './Heading/Box'
export interface EditorListProps {
kind: string
query?: string
onAddClick: () => void
onAddClick?: () => void
// We accept search as a prop otherwise it loses focus
SearchInput?: React.ReactNode
}

export default function EditorList(props: React.PropsWithChildren<EditorListProps>) {
const AddButton = () => {
if (!props.onAddClick) return null

return (
<Button title={`Add ${startCase(props.kind)}`} onClick={() => props.onAddClick()}>
<Button title={`Add ${startCase(props.kind)}`} onClick={() => props.onAddClick?.()}>
Add {startCase(props.kind)}
</Button>
)
Expand Down
36 changes: 23 additions & 13 deletions client/components/Editors/Base/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,37 @@ interface EditorListItemProps {

export default function EditorListItem(props: EditorListItemProps) {
const theme = useTheme()

const RemoveButton = () => {
if (!props.onRemoveClick) return null

return (
<Button
size="small"
color="warning"
component="span"
title={`Remove ${capitalize(props.kind)}`}
sx={{ marginLeft: 2, textDecoration: 'underline' }}
onClick={(ev) => {
ev.stopPropagation()
props.onRemoveClick?.()
}}
>
Remove
</Button>
)
}

const EndIcon = () => {
const label = (props.type || 'item').toUpperCase()
return (
<Box>
<Typography component="span">{label}</Typography>
<Button
size="small"
color="warning"
component="span"
title={`Remove ${capitalize(props.kind)}`}
sx={{ marginLeft: 2, textDecoration: 'underline' }}
onClick={(ev) => {
ev.stopPropagation()
props.onRemoveClick && props.onRemoveClick()
}}
>
Remove
</Button>
<RemoveButton />
</Box>
)
}

return (
<Button
size="large"
Expand Down
8 changes: 3 additions & 5 deletions client/components/Editors/Schema/Sections/Fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,11 @@ function FieldList() {
const query = useStore((state) => state.fieldState.query)
const fieldItems = useStore(selectors.fieldItems)
const updateFieldState = useStore((state) => state.updateFieldState)
const addField = useStore((state) => state.addField)
const removeField = useStore((state) => state.removeField)

return (
<EditorList
kind="field"
query={query}
onAddClick={() => addField()}
SearchInput={
<EditorSearch
value={query || ''}
Expand All @@ -46,10 +44,9 @@ function FieldList() {
<EditorListItem
key={index}
kind="field"
name={field.title || field.name}
name={field.name}
type={field.type}
onClick={() => updateFieldState({ index })}
onRemoveClick={() => removeField(index)}
/>
))}
</EditorList>
Expand Down Expand Up @@ -116,6 +113,7 @@ function Name() {

return (
<InputField
disabled
label="Name"
value={value}
error={!value}
Expand Down
2 changes: 1 addition & 1 deletion client/components/Editors/Table/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function createColumns(

const renderHeader: IColumn['header'] = () => {
const firstError = labelErrors?.[0]
const label = firstError ? firstError.label : field.title ?? field.name
const label = firstError ? firstError.label : field.name

if (firstError) {
return (
Expand Down
118 changes: 96 additions & 22 deletions client/components/Editors/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '@inovua/reactdatagrid-community/index.css'
import * as React from 'react'
import SpinnerCard from '../../Parts/Cards/Spinner'
import Box from '@mui/material/Box'
import InputDialog from '../../Parts/Dialogs/Input'
import Typography from '@mui/material/Typography'
import InovuaDatagrid from '@inovua/reactdatagrid-community'
import { TypeDataGridProps } from '@inovua/reactdatagrid-community/types'
Expand All @@ -20,10 +21,12 @@ export interface TableEditorProps extends Partial<TypeDataGridProps> {
report?: types.IReport
history?: types.IHistory
selection?: types.ITableSelection
onColumnRename?: (props: { index: number; oldName: string; newName: string }) => void
}

export default function TableEditor(props: TableEditorProps) {
const { source, schema, report, history, selection, ...others } = props
const { source, schema, report, history, selection, onColumnRename, ...others } = props
const [dialog, setDialog] = React.useState<IDialog | undefined>()

const theme = useTheme()
const colorPalette = theme.palette
Expand Down Expand Up @@ -53,11 +56,24 @@ export default function TableEditor(props: TableEditorProps) {
})

const renderColumnContextMenu = React.useCallback(
(menuProps: { items: any[] }): React.ReactNode => {
(menuProps: { items: any[] }, context: any) => {
menuProps.items = menuProps.items.filter((x) => x.label !== 'Columns' && x !== '-')
return
menuProps.items.push({
itemId: 'rename',
label: 'Rename',
disabled: history?.changes.length,
onClick: () => {
setDialog({
type: 'columnRename',
name: context.cellProps.name,
// first column is the row number column
index: context.cellProps.columnIndex - 1,
})
},
})
return undefined
},
[]
[history?.changes.length]
)

function resizeTable() {
Expand All @@ -72,24 +88,82 @@ export default function TableEditor(props: TableEditorProps) {
}

return (
<InovuaDatagrid
onReady={resizeTable}
idProperty="_rowNumber"
dataSource={source}
columns={columns}
pagination={true}
loadingText={<Typography>Loading...</Typography>}
renderLoadMask={LoadMask}
defaultActiveCell={settings.DEFAULT_ACTIVE_CELL}
style={{ height: '100%', border: 'none' }}
limit={rowsPerPage}
onLimitChange={setRowsPerPage}
rowHeight={rowHeight}
showColumnMenuLockOptions={false}
showColumnMenuGroupOptions={false}
enableColumnAutosize={false}
renderColumnContextMenu={renderColumnContextMenu}
{...others}
<>
<ColumnRenameDialog
dialog={dialog}
schema={schema}
onClose={() => setDialog(undefined)}
onColumnRename={onColumnRename}
/>
<InovuaDatagrid
onReady={resizeTable}
idProperty="_rowNumber"
dataSource={source}
columns={columns}
pagination={true}
loadingText={<Typography>Loading...</Typography>}
renderLoadMask={LoadMask}
defaultActiveCell={settings.DEFAULT_ACTIVE_CELL}
style={{ height: '100%', border: 'none' }}
limit={rowsPerPage}
onLimitChange={setRowsPerPage}
rowHeight={rowHeight}
showColumnMenuLockOptions={false}
showColumnMenuGroupOptions={false}
enableColumnAutosize={false}
renderColumnContextMenu={renderColumnContextMenu}
{...others}
/>
</>
)
}

type IDialog = IColumnRenameDialog

type IColumnRenameDialog = {
type: 'columnRename'
name: string
index: number
}

function ColumnRenameDialog(props: {
dialog?: IDialog
onClose: () => void
schema: types.ISchema
onColumnRename: React.ComponentProps<typeof TableEditor>['onColumnRename']
}) {
if (props.dialog?.type !== 'columnRename') return null

const [name, setName] = React.useState(props.dialog.name)

const isUpdated = name !== props.dialog.name
const isUnique = !props.schema.fields.map((field) => field.name).includes(name)

let errorMessage = ''
if (!name) {
errorMessage = 'Name must not be blank'
} else if (isUpdated && !isUnique) {
errorMessage = 'Name must be unique'
}

return (
<InputDialog
open={true}
value={name}
description="Enter a new column name:"
onChange={setName}
title={`Rename Column "${props.dialog.name}"`}
onCancel={props.onClose}
disabled={!isUpdated || !!errorMessage}
errorMessage={errorMessage}
onConfirm={() => {
props.onColumnRename?.({
index: props.dialog!.index,
oldName: props.dialog!.name,
newName: name,
})
props.onClose()
}}
/>
)
}
Expand Down
11 changes: 7 additions & 4 deletions client/components/Parts/Dialogs/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ export interface InputDialogProps extends Omit<ConfirmDialogProps, 'onConfirm'>

export default function InputDialog(props: InputDialogProps) {
const { prefix, placholder, spellcheck, onConfirm, errorMessage, ...rest } = props
const [value, setValue] = React.useState('')
const [value, setValue] = React.useState(props.value || '')

const handleConfirm = () => onConfirm && onConfirm(value)
return (
<ConfirmDialog {...rest} onConfirm={handleConfirm} disabled={!value}>
<ConfirmDialog
{...rest}
onConfirm={handleConfirm}
disabled={props.disabled || !value}
>
<TextField
error={!!errorMessage}
helperText={errorMessage}
helperText={errorMessage || ' '}
autoFocus
fullWidth
size="small"
Expand All @@ -38,7 +42,6 @@ export default function InputDialog(props: InputDialogProps) {
<InputAdornment position="start">{prefix}</InputAdornment>
) : undefined,
}}
sx={{ marginBottom: 1 }}
/>
{props.children}
</ConfirmDialog>
Expand Down
23 changes: 23 additions & 0 deletions client/store/actions/table.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { client } from '@client/client'
import invariant from 'tiny-invariant'
import { mapValues, isNull } from 'lodash'
import { onFileCreated, onFileUpdated } from './file'
import { cloneDeep } from 'lodash'
Expand Down Expand Up @@ -275,6 +276,28 @@ export async function deleteMultipleCells(cells: types.ICellSelection) {
helpers.applyTableHistory({ changes: [change] }, grid.data)
}

export async function renameColumn(props: {
index: number
oldName: string
newName: string
}) {
const { grid } = getRefs()
const { path } = store.getState()
invariant(grid)
invariant(path)

const result = await client.columnRename({ ...props, path })

if (result instanceof client.Error) {
return store.setState('rename-column-error', (state) => {
state.error = result
})
}

await onFileUpdated([path])
grid.reload()
}

// Loaders

export const tableLoader: types.ITableLoader = async ({ skip, limit, sortInfo }) => {
Expand Down
2 changes: 1 addition & 1 deletion server/endpoints/column/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Register modules
from . import list
from . import list, rename
Loading
Loading