Newer
Older
NocodeToolOnRDL / src / components / sidebar / tableList.tsx
@Sakoda2269 Sakoda2269 10 days ago 25 KB useMemoを撤廃
import type { TablesActions, TablesState } from "../../hooks/useTables";
import { DataType,  translateDataType } from "../../types/table";
import type { Column, Table } from "../../types/table";
import { useState } from "react"
import { FiX } from "react-icons/fi";
import { MdDeleteOutline, MdOutlineDelete, MdOutlineEdit } from "react-icons/md";

interface TalbeListProps {
    tableState: TablesState,
    tableActions: TablesActions
}

export function TableList({ tableState, tableActions }: TalbeListProps) {

    const [showAddModal, setShowAddModal] = useState<boolean>(false);
    const [editTableIndex, setEditTableIndex] = useState<number>(-1);

    const handleAddTable = () => {
        setShowAddModal(true);
    }

    const handleDeleteTable = (e: React.MouseEvent, id: string) => {
        e.stopPropagation();
        tableActions.deleteTable(id)
    }

    return (
        <div className="p-4 bg-white">
            <div className="flex items-center justify-between mb-4">
                <h3 className="text-lg font-semibold text-gray-900">テーブル</h3>
            </div>

            <div className="space-y-2">
                {tableState.tables.map((table, idx) =>
                    <div
                        key={`table${idx}`}
                        onClick={() => { setEditTableIndex(idx); setShowAddModal(true) }}
                        className={`group relative rounded-xl border transition-all duration-200 hover:border-gray-300 hover:shadow-sm bg-sky-100 border-gray-200 bg-white border-gray-200`}
                    >
                        <div className="flex items-center p-3 cursor-pointer">
                            <div className="flex-1 min-w-0">
                                <div className="font-medium text-sm truncate text-gray-900">
                                    {table.name}
                                </div>
                            </div>
                            <button
                                onClick={(e) => handleDeleteTable(e, table.id)}
                                className="p-1.5 rounded-lg transition-all duration-200 text-gray-400 hover:text-red-600 hover:bg-gray-100 group-hover:opacity-100"
                            >
                                <MdDeleteOutline size={20} />
                            </button>
                        </div>
                    </div>
                )}
            </div>

            <div className="mt-4 pt-4 border-t border-gray-100">
                <button
                    onClick={handleAddTable}
                    className="w-full flex items-center justify-center p-3 text-sm font-medium text-blue-600 bg-blue-50 hover:bg-blue-100 rounded-xl transition-all duration-200 border border-blue-200 hover:border-blue-300"
                >
                    <span className="mr-2 text-lg">+</span>
                    新しいテーブルを追加
                </button>
            </div>
            {showAddModal && <AddTableModal
                close={() => { setEditTableIndex(-1); setShowAddModal(false) }}
                tables={tableState.tables}
                editTable={editTableIndex === -1 ? null : tableState.tables[editTableIndex]}
                addTable={tableActions.addTable}
                updateTable={tableActions.updateTable}
            />}
        </div>
    )
}

interface AddTableModalProps {
    close: () => void
    tables: Table[]
    editTable: Table | null
    addTable: (table: Table) => void
    updateTable: (updatedTable: Table) => void
}

function AddTableModal({ close, tables, editTable, addTable, updateTable }: AddTableModalProps) {

    const [pageNum, setPageNum] = useState<number>(0);
    const [tableName, setTableName] = useState<string>(editTable === null ? "" : editTable.name);
    const [columns, setColumns] = useState<Column[]>(editTable === null ? [] : editTable.columns);
    const [editColumnIndex, setEditColumnIndex] = useState<number>(-1);

    const handleCancel = () => {
        setPageNum(0);
        setTableName("");
        close();
    }

    const handleNext = () => {
        setPageNum(cur => cur + 1);
    }

    const handlePrev = () => {
        setPageNum(cur => cur - 1);
    }

    const handleConfirm = () => {
        if (editTable === null) {
            const newTable: Table = {
                name: tableName,
                columns: columns,
                id: "t" + self.crypto.randomUUID().replace(/-/g, ""),
            }
            addTable(newTable);
        } else {
            const newTable: Table = {
                name: tableName,
                columns: columns,
                id: editTable.id
            }
            updateTable(newTable)
        }
        setPageNum(0);
        setTableName("");
        setColumns([]);
        setEditColumnIndex(-1);
        close();
    }

    const setColumn = (column: Column) => {
        if (editColumnIndex === -1) {
            setColumns(cur => [...cur, column]);
        } else {
            setColumns(cur => cur.map((col, idx) => idx === editColumnIndex ? column : col))
        }
    }

    const editColumn = (index: number, isForeingColumn: boolean) => {
        setEditColumnIndex(index)
        setPageNum(isForeingColumn ? 3 : 2);
    }

    const deleteColumn = (index: number) => {
        setColumns(cur => cur.filter((_, idx) => index != idx))
    }

    return (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
            <div className="bg-white rounded-xl shadow-xl p-6 w-[80vw]  max-w-[90vw]">
                <div className="flex items-center justify-between mb-4">
                    <h3 className="text-lg font-semibold text-gray-900">{editTable === null ? "新しいテーブルを作成" : "テーブルを編集"}</h3>
                    <button
                        className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
                        onClick={handleCancel}
                    >
                        <FiX size={20} />
                    </button>
                </div>

                {pageNum == 0 && <div>
                    <label className="block text-sm font-medium text-gray-700 mb-2">
                        テーブル名
                    </label>
                    <input
                        type="text"
                        value={tableName}
                        onChange={(e) => setTableName(e.target.value)}
                        placeholder="スクリーン名を入力してください"
                        className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
                        autoFocus
                    />
                    <div className="mt-6 flex gap-3 justify-end">
                        <button
                            onClick={handleCancel}
                            className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
                        >
                            キャンセル
                        </button>
                        <button
                            onClick={handleNext}
                            disabled={!tableName.trim()}
                            className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-lg transition-colors"
                        >
                            次へ
                        </button>
                    </div>
                </div>}

                {pageNum == 1 && <div className="mt-6 flex-col">
                    <ColumnList
                        columns={columns}
                        tables={tables}
                        deleteColumn={deleteColumn}
                        editColumn={editColumn}
                    />
                    <div className="flex gap-3 justify-center mt-6">
                        <button
                            onClick={() => { setPageNum(2) }}
                            className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors"
                        >カラムを追加</button>
                        <button
                            onClick={() => { setPageNum(3) }}
                            className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors"
                        >外部テーブルから追加</button>
                    </div>

                    <div className="flex gap-3 justify-end mt-6">
                        <button
                            onClick={handlePrev}
                            className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
                        >
                            戻る
                        </button>
                        <button
                            onClick={handleConfirm}
                            disabled={columns.length === 0}
                            className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-lg transition-colors"
                        >
                            作成
                        </button>
                    </div>
                </div>}

                {pageNum == 2 && <div>
                    <CreateColumnScreen
                        setColumn={setColumn}
                        close={() => { setEditColumnIndex(-1); setPageNum(1) }}
                        column={editColumnIndex === -1 ? null : columns[editColumnIndex]}
                    />
                </div>}

                {pageNum === 3 && <div>
                    <CreateForeignColumnScreen
                        setColumn={setColumn}
                        close={() => { setEditColumnIndex(-1); setPageNum(1) }}
                        column={editColumnIndex === -1 ? null : columns[editColumnIndex]}
                        tables={tables}
                        tableId={editTable !== null ? editTable.id : ""}
                    />
                </div>}

            </div>
        </div>
    )
}

interface ColumnListProps {
    columns: Column[]
    tables: Table[]
    deleteColumn: (index: number) => void
    editColumn: (index: number, isForeingColumn: boolean) => void
}

function ColumnList({ columns, tables, deleteColumn, editColumn }: ColumnListProps) {

    const getForeignColType = (refId: string, names: string[]): DataType[] => {
        const cols = tables.find(table => table.name === refId)?.columns;
        if (!cols) {
            return [];
        }
        return names.map(name => cols.find(col => col.name === name)?.type as DataType);
    }

    return (
        <div>
            {columns.length === 0 ?
                <div>
                    まだカラムがありません
                </div> :
                <div className="flex overflow-x-auto">
                    <div className="flex-col">
                        <div className="h-16 w-32 flex items-center justify-center border">
                            主キー
                        </div>
                        <div className="h-32 w-32 flex items-center justify-center border border-t-0">
                            名前
                        </div>
                        <div className="h-16 w-32 flex items-center justify-center border border-t-0">
                            型
                        </div>
                        <div className="h-16 w-32 flex items-center justify-center border border-t-0">
                            参照テーブル
                        </div>
                        <div className="h-16 w-32 flex items-center justify-center border border-t-0">
                            その他
                        </div>
                    </div>
                    {columns.map((col, idx) => {
                        if (!col.foreignTableName) {
                            return (
                                <div key={idx} className="flex-col">
                                    <div className="h-16 min-w-32 flex items-center justify-center border border-l-0">
                                        <input type="checkbox" readOnly checked={col.isPrimary} />
                                    </div>
                                    <div className="h-32 min-w-32 flex items-center justify-center border border-l-0 border-t-0">
                                        {col.name}
                                    </div>
                                    <div className="h-16 min-w-32 flex items-center justify-center border border-l-0 border-t-0">
                                        {col.type != "foreign" ? translateDataType(col.type) : ""}
                                    </div>
                                    <div className="h-16 min-w-32 flex items-center justify-center border border-l-0 border-t-0">
                                    </div>
                                    <div className="h-16 min-w-32 flex items-center justify-around border border-l-0 border-t-0">
                                        <button
                                            onClick={() => editColumn(idx, false)}
                                        >
                                            <MdOutlineEdit
                                                className="hover:text-green-600 transition-colors"
                                                size="24"
                                            />
                                        </button>
                                        <button
                                            onClick={() => deleteColumn(idx)}
                                        >
                                            <MdOutlineDelete
                                                className="hover:text-red-600 transition-colors"
                                                size="24"
                                            />
                                        </button>
                                    </div>
                                </div>
                            )
                        }
                        return (
                            <div key={idx} className="flex-col">
                                <div className="h-16 min-w-32 flex items-center justify-center border border-l-0">
                                    <input type="checkbox" readOnly checked={col.isPrimary} />
                                </div>
                                <div className="h-16 min-w-32 flex items-center justify-center border border-l-0 border-t-0">
                                    {col.name}
                                </div>
                                <div className="h-16 flex items-center justify-center border border-l-0 border-t-0">
                                    {col.foreignTableColumns?.map((col, idx) =>
                                        <div
                                            key={`fcol${idx}`}
                                            className={`h-full flex flex-1 justify-center items-center w-20 border border-t-0 border-b-0 border-r-0 ${idx === 0 ? "border-l-0" : ""} `}
                                        >
                                            {col}
                                        </div>
                                    )}
                                </div>
                                <div className="h-16 min-w-32 flex items-center justify-center border border-l-0 border-t-0">
                                    {getForeignColType(col.foreignTableName, col.foreignTableColumns!).map((type, idx) =>
                                        <div
                                            key={`fcol${idx}`}
                                            className={`h-full flex flex-1 justify-center items-center w-20 border border-t-0 border-b-0 border-r-0 ${idx === 0 ? "border-l-0" : ""} `}
                                        >
                                            {translateDataType(type)}
                                        </div>
                                    )}
                                </div>
                                <div className="h-16 min-w-32 flex items-center justify-center border border-l-0 border-t-0">
                                    {tables.find(table => table.name === col.foreignTableName)?.name ?? "error"}
                                </div>
                                <div className="h-16 min-w-32 flex items-center justify-around border border-l-0 border-t-0">
                                    <button
                                        onClick={() => editColumn(idx, true)}
                                    >
                                        <MdOutlineEdit
                                            className="hover:text-green-600 transition-colors"
                                            size="24"
                                        />
                                    </button>
                                    <button
                                        onClick={() => deleteColumn(idx)}
                                    >
                                        <MdOutlineDelete
                                            className="hover:text-red-600 transition-colors"
                                            size="24"
                                        />
                                    </button>
                                </div>
                            </div>
                        )
                    })}
                </div>
            }
        </div>
    )
}

interface CreateColumnScreenProps {
    setColumn: (col: Column) => void
    close: () => void
    column: Column | null
}

function CreateColumnScreen({ setColumn, close, column }: CreateColumnScreenProps) {

    const [name, setName] = useState<string>(column === null ? "" : column.name);
    const [isPrimary, setIsPrimary] = useState<boolean>(column === null ? false : column.isPrimary);
    const [type, setType] = useState<DataType>(column === null ? DataType.String : column.type as DataType);

    const validate = (): boolean => {
        if (!name.trim()) {
            return false;
        }
        return true;
    }

    const handleConfirm = () => {
        const column: Column = {
            name: name,
            type: type,
            isPrimary: isPrimary
        };
        setColumn(column);
        close();
    }

    const handleCancel = () => {
        close();
    }

    return (
        <div className="flex-col">
            <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                    カラム名
                </label>
                <input
                    type="text"
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                    placeholder="カラム名を入力してください"
                    className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
                    autoFocus
                />
            </div>
            <div className="mt-3">
                <label className="block text-sm font-medium text-gray-700 mb-1">
                    主キー
                </label>
                <input
                    type="checkbox"
                    checked={isPrimary}
                    onChange={(e) => setIsPrimary(e.target.checked)}
                />
            </div>
            <div className="mt-3">
                <label className="block text-sm font-medium text-gray-700 mb-1">
                    型
                </label>
                <select
                    className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
                    value={type}
                    onChange={(e) => setType(e.target.value as DataType)}
                >
                    {Object.entries(DataType).map(([idx, value]) =>
                        <option key={idx} value={value}>
                            {translateDataType(value)}
                        </option>
                    )}
                </select>
            </div>
            <div className="flex gap-3 justify-end mt-6">
                <button
                    onClick={handleCancel}
                    className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
                >
                    戻る
                </button>
                <button
                    onClick={handleConfirm}
                    disabled={!validate()}
                    className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-lg transition-colors"
                >
                    追加
                </button>
            </div>
        </div>
    )
}

interface CreateForeignColumnScreenProps {
    setColumn: (col: Column) => void
    close: () => void
    tables: Table[]
    tableId: string
    column: Column | null
}

function CreateForeignColumnScreen({ setColumn, close, tables, tableId, column }: CreateForeignColumnScreenProps) {

    const [name, setName] = useState<string>(column?.name ?? "")
    const [referencedTableId, setReferencedTableId] = useState<string>(column?.foreignTableName ?? "");
    const referencedTableColumns = tables.find((table) => table.name === referencedTableId)?.columns ?? [];
    const [selectedColumns, setSelectedColumns] = useState<string[]>(column?.foreignTableColumns ?? []);

    const selectRefTable = (tableId: string) => {
        setReferencedTableId(tableId);
        setSelectedColumns([]);
    }

    const validate = (): boolean => {
        if (!name.trim()) return false;
        if (selectedColumns.length === 0) return false;
        return true;
    }

    const handleConfirm = () => {
        const newColumn: Column = {
            name: name,
            isPrimary: false,
            type: "foreign",
            foreignTableName: referencedTableId,
            foreignTableColumns: selectedColumns,
        }
        setColumn(newColumn);
        setReferencedTableId("");
        setSelectedColumns([]);
        setName("");
        close();
    }

    const handleCancel = () => {
        setReferencedTableId("");
        setSelectedColumns([]);
        setName("");
        close();
    }

    const selectColumn = (name: string) => {
        setSelectedColumns(cur => cur.includes(name) ? cur.filter(col => col !== name) : [...cur, name]);
    }

    return (
        <div className="flex-col">
            <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                    カラム名
                </label>
                <input
                    type="text"
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                    placeholder="カラム名を入力してください"
                    className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
                    autoFocus
                />
            </div>
            <div className="mt-3">
                <label className="block text-sm font-medium text-gray-700 mb-1">
                    参照するテーブル
                </label>
                <select
                    className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
                    value={referencedTableId}
                    onChange={(e) => selectRefTable(e.target.value)}
                >
                    <option value="">選択してください...</option>
                    {tables.map((table, idx) => {
                        if (table.id !== tableId)
                            return <option key={`foreignCol${idx}`} value={table.name}>
                                {table.name}
                            </option>
                    })}
                </select>
            </div>
            <div className="mt-3">
                <label className="block text-sm font-medium text-gray-700 mb-1">
                    参照するカラム
                </label>
                <div className="flex ml-4">
                    {referencedTableColumns.map((col, idx) =>
                        <div key={`foreingColumns${idx}`} className={`${idx === 0 ? "" : "ml-32"}`}>
                            <label>
                                <input
                                    type="checkbox"
                                    checked={selectedColumns.includes(col.name)}
                                    onChange={() => selectColumn(col.name)}
                                />
                                {col.name}
                            </label>
                        </div>
                    )}
                </div>
            </div>
            <div className="flex gap-3 justify-end mt-6">
                <button
                    onClick={handleCancel}
                    className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
                >
                    戻る
                </button>
                <button
                    onClick={handleConfirm}
                    disabled={!validate()}
                    className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-lg transition-colors"
                >
                    追加
                </button>
            </div>
        </div>
    )
}