import type { useTablesActions, useTablesData } from "../../hooks/useTables";
import { DataType, translateDataType } from "../../types/table";
import type { Column, Table } from "../../types/table";
import { useMemo, useState } from "react"
import { FiX } from "react-icons/fi";
import { MdDeleteOutline, MdOutlineDelete, MdOutlineEdit } from "react-icons/md";
interface TalbeListProps {
tableData: useTablesData,
tableActions: useTablesActions
}
export function TableList({ tableData, 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">
{tableData.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={tableData.tables}
editTable={editTableIndex === -1 ? null : tableData.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 = useMemo(
() => tables.find((table) => table.name === referencedTableId)?.columns ?? [],
[referencedTableId, tables]
)
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>
)
}