Skip to content

Commit

Permalink
feat: add record use modal ui (#618)
Browse files Browse the repository at this point in the history
  • Loading branch information
boris-w committed May 23, 2024
1 parent b987080 commit 26ae386
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 65 deletions.
160 changes: 160 additions & 0 deletions apps/nextjs-app/src/features/app/blocks/view/AddRecordModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useMutation } from '@tanstack/react-query';
import { FieldKeyType } from '@teable/core';
import { createRecords } from '@teable/openapi';
import { RecordEditor } from '@teable/sdk/components/expand-record/RecordEditor';
import { useFields, useTableId, useViewId } from '@teable/sdk/hooks';
import type { IFieldInstance, Record } from '@teable/sdk/model';
import { createRecordInstance, recordInstanceFieldMap } from '@teable/sdk/model';
import { Spin } from '@teable/ui-lib/base';
import { Button, Dialog, DialogContent, DialogTrigger } from '@teable/ui-lib/shadcn';
import { isEqual } from 'lodash';
import { useTranslation } from 'next-i18next';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCounter } from 'react-use';

interface IAddRecordModalProps {
children?: React.ReactNode;
callback?: (recordId: string) => void;
}

export const AddRecordModal = (props: IAddRecordModalProps) => {
const { children, callback } = props;
const tableId = useTableId();
const viewId = useViewId();
const showFields = useFields();
const [open, setOpen] = useState(false);
const [version, updateVersion] = useCounter(0);
const { t } = useTranslation('common');
const allFields = useFields({ withHidden: true, withDenied: true });
const [record, setRecord] = useState<Record | undefined>(undefined);

const { mutate: createRecord, isLoading } = useMutation({
mutationFn: (fields: { [fieldId: string]: unknown }) =>
createRecords(tableId!, {
records: [{ fields }],
fieldKeyType: FieldKeyType.Id,
}),
onSuccess: (data) => {
setOpen(false);
callback?.(data.data.records[0].id);
},
});

const newRecord = useCallback(
(version: number = 0) => {
setRecord((preRecord) => {
const record = createRecordInstance({
id: '',
fields: version > 0 && preRecord?.fields ? preRecord.fields : {},
});
record.updateCell = (fieldId: string, newValue: unknown) => {
record.fields[fieldId] = newValue;
updateVersion.inc();
return Promise.resolve();
};
return record;
});
},
[updateVersion]
);

useEffect(() => {
if (!open) {
updateVersion.reset();
newRecord();
}
}, [newRecord, open, updateVersion]);

useEffect(() => {
// init record
newRecord();
}, [newRecord]);

useEffect(() => {
if (version > 0) {
newRecord(version);
}
}, [version, newRecord]);

useEffect(() => {
if (!allFields.length) {
return;
}
setRecord((record) =>
record
? recordInstanceFieldMap(
record,
allFields.reduce(
(acc, field) => {
acc[field.id] = field;
return acc;
},
{} as { [fieldId: string]: IFieldInstance }
)
)
: record
);
}, [allFields, record]);

const showFieldsId = useMemo(() => new Set(showFields.map((field) => field.id)), [showFields]);

const fields = useMemo(
() => (viewId ? allFields.filter((field) => showFieldsId.has(field.id)) : []),
[allFields, showFieldsId, viewId]
);

const hiddenFields = useMemo(
() => (viewId ? allFields.filter((field) => !showFieldsId.has(field.id)) : []),
[allFields, showFieldsId, viewId]
);

const onChange = (newValue: unknown, fieldId: string) => {
if (isEqual(record?.getCellValue(fieldId), newValue)) {
return;
}
record?.updateCell(fieldId, newValue);
};

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
closeable={false}
className="flex h-full max-w-3xl flex-col p-0 pt-6"
style={{ width: 'calc(100% - 40px)', height: 'calc(100% - 100px)' }}
onMouseDown={(e) => e.stopPropagation()}
onInteractOutside={(e) => e.preventDefault()}
onKeyDown={(e) => e.stopPropagation()}
>
<div className="flex-1 overflow-y-auto p-10 pt-4">
<RecordEditor
record={record}
fields={fields}
hiddenFields={hiddenFields}
onChange={onChange}
/>
</div>
<div className="flex justify-end gap-4 border-t px-10 py-3">
<Button variant={'outline'} size={'sm'} onClick={() => setOpen(false)}>
{t('actions.cancel')}
</Button>
<Button
className="relative overflow-hidden"
size={'sm'}
disabled={!tableId || isLoading}
onClick={() => {
createRecord(record?.fields ?? {});
}}
>
{isLoading && (
<div className="absolute flex size-full items-center justify-center">
<Spin className="mr-2" />
</div>
)}
{t('actions.create')}
</Button>
</div>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
import { Draggable } from '@hello-pangea/dnd';
import { FieldKeyType } from '@teable/core';
import { Plus } from '@teable/icons';
import { createRecords } from '@teable/openapi';
import { generateLocalId } from '@teable/sdk/components';
import { useTableId, useViewId } from '@teable/sdk/hooks';
import type { Record } from '@teable/sdk/model';
import { Button, cn } from '@teable/ui-lib';
import { useRef, useState } from 'react';
import type { VirtuosoHandle } from 'react-virtuoso';
import { AddRecordModal } from '../../AddRecordModal';
import { UNCATEGORIZED_STACK_ID } from '../constant';
import type { IKanbanContext } from '../context';
import { useInView, useKanban } from '../hooks';
import { useKanbanStackCollapsedStore } from '../store';
import type { IStackData } from '../type';
import { getCellValueByStack } from '../utils';
import type { ICardMap } from './interface';
import { KanbanStack } from './KanbanStack';
import { KanbanStackHeader } from './KanbanStackHeader';
Expand All @@ -34,34 +32,17 @@ export const KanbanStackContainer = (props: IKanbanStackContainerProps) => {
const tableId = useTableId();
const viewId = useViewId();
const { collapsedStackMap, setCollapsedStackMap } = useKanbanStackCollapsedStore();
const { permission, stackField, setExpandRecordId } = useKanban() as Required<IKanbanContext>;
const { permission } = useKanban() as Required<IKanbanContext>;
const [ref, isInView] = useInView();
const [editMode, setEditMode] = useState(false);
const virtuosoRef = useRef<VirtuosoHandle>(null);

const { id: stackId } = stack;
const { id: fieldId, type: fieldType } = stackField;
const { stackDraggable, cardCreatable } = permission;
const isUncategorized = stackId === UNCATEGORIZED_STACK_ID;
const draggable = stackDraggable && !disabled && !editMode && !isUncategorized;

const onAppend = async () => {
if (tableId == null) return;
const cellValue = getCellValueByStack(fieldType, stack);
const res = await createRecords(tableId, {
fieldKeyType: FieldKeyType.Id,
records: [
{
fields: { [fieldId]: cellValue },
},
],
});
const record = res.data.records[0];

if (record != null) {
setExpandRecordId(record.id);
}

const onAppendCallback = () => {
setTimeout(() => {
virtuosoRef.current?.scrollToIndex({
index: 'LAST',
Expand Down Expand Up @@ -130,11 +111,13 @@ export const KanbanStackContainer = (props: IKanbanStackContainerProps) => {
</div>

{cardCreatable && (
<div className="flex items-center justify-center rounded-b-md bg-slate-50 px-3 py-2 dark:bg-slate-900">
<Button variant="outline" className="w-full shadow-none" onClick={onAppend}>
<Plus className="size-5" />
</Button>
</div>
<AddRecordModal callback={onAppendCallback}>
<div className="flex items-center justify-center rounded-b-md bg-slate-50 px-3 py-2 dark:bg-slate-900">
<Button variant="outline" className="w-full shadow-none">
<Plus className="size-5" />
</Button>
</div>
</AddRecordModal>
)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,25 @@
import { Plus } from '@teable/icons';
import { useTable, useTablePermission } from '@teable/sdk/hooks';
import { useTablePermission } from '@teable/sdk/hooks';
import { Button } from '@teable/ui-lib/shadcn/ui/button';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { AddRecordModal } from '../AddRecordModal';
import { GridViewOperators } from './components';
import { Others } from './Others';

export const GridToolBar: React.FC = () => {
const table = useTable();
const router = useRouter();
const permission = useTablePermission();

const addRecord = useCallback(async () => {
if (!table) {
return;
}
await table.createRecord({}).then((res) => {
const record = res.data.records[0];

if (record == null) return;

const recordId = record.id;

router.push(
{
pathname: router.pathname,
query: { ...router.query, recordId },
},
undefined,
{
shallow: true,
}
);
});
}, [router, table]);

return (
<div className="flex items-center gap-2 border-t px-4 py-2 @container/toolbar">
<Button
className="size-6 shrink-0 rounded-full p-0 font-normal"
size={'xs'}
variant={'outline'}
onClick={addRecord}
disabled={!permission['record|create']}
>
<Plus className="size-4" />
</Button>
<AddRecordModal>
<Button
className="size-6 shrink-0 rounded-full p-0 font-normal"
size={'xs'}
variant={'outline'}
disabled={!permission['record|create']}
>
<Plus className="size-4" />
</Button>
</AddRecordModal>
<div className="mx-2 h-4 w-px shrink-0 bg-slate-200"></div>
<div className="flex flex-1 justify-between">
<GridViewOperators disabled={!permission['view|update']} />
Expand Down

0 comments on commit 26ae386

Please sign in to comment.