Newer
Older
NocodeToolOnRDL / src / components / sidebar / screenList.tsx
@Sakoda2269 Sakoda2269 10 days ago 20 KB useMemoを撤廃
import type { ComponentActions } from "../../hooks/useComponent";
import type { ScreenActions, ScreenState } from "../../hooks/useScreen";
import type { TablesState } from "../../hooks/useTables";
import { capitalizeFirst } from "../../lib/stringUtils";
import { ComponentType } from "../../types/component"
import type { ButtonComponent, Component, TextComponent, TextFieldComponent } from "../../types/component";
import type { Screen } from "../../types/screen"
import { useState } from "react";
import { FiEdit3, FiMoreHorizontal, FiTrash2, FiX } from "react-icons/fi"

interface ScreenListPorps {
    screenState: ScreenState,
    screenActions: ScreenActions,
    componentActions: ComponentActions,
    tableState: TablesState
    selectComponent: (index: number) => void
}

export function ScreenList({ screenState, screenActions, componentActions, tableState, selectComponent }: ScreenListPorps) {

    const [showAddModal, setShowAddModel] = useState(false);
    const [editIndex, setEditIndex] = useState(-1);
    const [editName, setEditName] = useState("");
    const [activeDropdown, setActiveDropdown] = useState(-1);
    const [showTemplateModal, setShowTemplateModal] = useState(false);

    const handleShowAddModal = () => {
        setEditIndex(-1)
        setEditName("");
        setShowAddModel(true);
    }

    const closeDropdown = () => {
        setActiveDropdown(-1);
    }

    const startEdit = (index: number, curName: string) => {
        setEditName(curName);
        setEditIndex(index);
        setShowAddModel(true);
    }


    return (
        <div className="p-4 bg-white w-full">
            <div className="flex items-center justify-between mb-4 w-full">
                <h3 className="text-lg font-semibold text-gray-900">スクリーン</h3>
            </div>

            <div className="space-y-2">
                {screenState.screens.map((screen, index) => (
                    <ScreenCard
                        key={index}
                        screen={screen}
                        index={index}
                        selected={index === screenState.curScreenIndex}
                        setCurScreenIndex={screenActions.setCurScreenIndex}
                        activeDropdown={activeDropdown}
                        setActiveDropdown={setActiveDropdown}
                        isLast={screenState.screens.length === 1}
                        closeDropdown={closeDropdown}
                        deleteScreen={screenActions.deleteScreen}
                        startEdit={startEdit}
                        selectComponent={selectComponent}
                    />
                ))}
            </div>

            <div className="mt-4 pt-4 border-t border-gray-100">
                <button
                    onClick={handleShowAddModal}
                    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>
                <button
                    onClick={() => setShowTemplateModal(true)}
                    className="mt-4 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>
            {/* Click outside to close dropdown */}
            {activeDropdown !== -1 && (
                <div
                    className="fixed inset-0 z-5"
                    onClick={() => setActiveDropdown(-1)}
                />
            )}
            {showAddModal && <AddScreenModal
                close={() => setShowAddModel(false)}
                addScreen={screenActions.addScreen}
                editIndex={editIndex}
                editName={editName}
                selectComponent={selectComponent}
                updateScreen={screenActions.updateScreen}
            />}
            {showTemplateModal && <AddTemplateModal
                componenetActions={componentActions}
                tableState={tableState}
                screenState={screenState}
                screenActions={screenActions}
                close={() => setShowTemplateModal(false)}
            />}
        </div>
    )
}

interface ScreenCardProps {
    screen: Screen,
    selected: boolean,
    setCurScreenIndex: (index: number) => void
    index: number,
    activeDropdown: number,
    setActiveDropdown: (index: number) => void,
    isLast: boolean,
    closeDropdown: () => void,
    deleteScreen: (index: number) => void,
    startEdit: (index: number, curName: string) => void
    selectComponent: (index: number) => void
}

function ScreenCard({
    screen,
    selected,
    setCurScreenIndex,
    index,
    activeDropdown,
    setActiveDropdown,
    isLast,
    closeDropdown,
    deleteScreen,
    startEdit,
    selectComponent
}: ScreenCardProps) {

    const handleSelectScreen = (index: number) => {
        selectComponent(-1);
        setCurScreenIndex(index)
    }

    const handleEditAction = (index: number, name: string) => {
        startEdit(index, name)
        closeDropdown()
    };
    const handleDeleteAction = (index: number) => {
        deleteScreen(index);
        closeDropdown()
    };


    return (
        <div
            className={`group relative rounded-xl border transition-all duration-200 hover:border-gray-300 hover:shadow-sm bg-sky-100 border-gray-200 ${selected ? "bg-sky-100 border-gray-400" : "bg-white border-gray-200"}`}
        >
            <div onClick={() => { handleSelectScreen(index) }} 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">
                        {screen.name}
                    </div>
                </div>
                <button
                    onClick={() => { setActiveDropdown(index) }}
                    className="p-1.5 rounded-lg transition-all duration-200 text-gray-400 hover:text-gray-600 hover:bg-gray-100 opacity-0 group-hover:opacity-100"
                >
                    <FiMoreHorizontal size={14} />
                </button>
            </div>

            {activeDropdown === index && (
                <div className="absolute right-2 top-12 z-10 bg-white rounded-lg shadow-lg border border-gray-200 py-1 min-w-[140px]">
                    <button
                        onClick={() => handleEditAction(index, screen.name)}
                        className="flex items-center w-full px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
                    >
                        <FiEdit3 size={14} className="mr-2" />
                        編集
                    </button>
                    {!isLast && (
                        <>
                            <hr className="my-1 border-gray-100" />
                            <button
                                onClick={() => handleDeleteAction(index)}
                                className="flex items-center w-full px-3 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
                            >
                                <FiTrash2 size={14} className="mr-2" />
                                削除
                            </button>
                        </>
                    )}
                </div>
            )}

        </div>
    )
}

interface AddScreenModalProps {
    close: () => void,
    addScreen: (name: string) => void,
    editIndex: number,
    editName: string,
    updateScreen: (index: number, name: string) => void
    selectComponent: (index: number) => void
}

function AddScreenModal({ close, addScreen, editName, editIndex, updateScreen, selectComponent }: AddScreenModalProps) {

    const [newScreenName, setNewScreenName] = useState<string>(editIndex !== -1 ? editName : "");

    const handleCancelCreate = () => {
        setNewScreenName("");
        close();
    }

    const handleCreateConfirm = () => {
        selectComponent(-1);
        if (editIndex === -1) {
            addScreen(newScreenName);
        } else {
            updateScreen(editIndex, newScreenName);
        }
        setNewScreenName("");
        close();
    }

    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-96 max-w-[90vw]">
                <div className="flex items-center justify-between mb-4">
                    <h3 className="text-lg font-semibold text-gray-900">新しいスクリーンを作成</h3>
                    <button
                        onClick={handleCancelCreate}
                        className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
                    >
                        <FiX size={20} />
                    </button>
                </div>

                <div className="mb-6">
                    <label className="block text-sm font-medium text-gray-700 mb-2">
                        スクリーン名
                    </label>
                    <input
                        type="text"
                        value={newScreenName}
                        onChange={(e) => setNewScreenName(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="flex gap-3 justify-end">
                    <button
                        onClick={handleCancelCreate}
                        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={handleCreateConfirm}
                        disabled={!newScreenName.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>
        </div>
    )
}

interface AddTemplateModalProps {
    close: () => void,
    tableState: TablesState,
    screenState: ScreenState
    screenActions: ScreenActions
    componenetActions: ComponentActions
}

function AddTemplateModal({ close, tableState, screenState, screenActions, componenetActions }: AddTemplateModalProps) {

    const [type, setType] = useState<"add" | "list" | "delete" | "update" | "search" | "">("");
    const [screenName, setScreenName] = useState("");

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

    const handleTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        const selectedType = e.target.value as "add" | "list" | "delete" | "update" | "search" | "";
        setType(selectedType);
    }


    return (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50" onClick={handleOutsideClick}>
            <div className="w-full bg-white rounded-xl shadow-xl p-6 w-96 max-w-[70vw]" onClick={(e) => { e.stopPropagation() }}>
                <div className="flex items-center justify-between mb-4">
                    <h3 className="text-lg font-semibold text-gray-900">テンプレートを選択</h3>
                    <button
                        onClick={() => close()}
                        className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
                    >
                        <FiX size={20} />
                    </button>
                </div>
                <div className="flex w-full mb-4">
                    <label className="mr-4 mt-2 flex-1">画面名</label>
                    <input
                        type="text"
                        className="border rounded flex-10 px-3"
                        onChange={(e) => setScreenName(e.target.value)}
                        value={screenName}
                    />
                </div>
                <div className="flex w-full mb-4">
                    <label className="mr-4 mt-2 flex-1">
                        タイプを選択
                    </label>
                    <select
                        className="border px-3 py-2 rounded flex-10"
                        onChange={handleTypeChange}
                    >
                        <option value={""}>
                            選択してください...
                        </option>
                        <option value={"add"}>
                            追加画面
                        </option>
                        <option value={"list"}>
                            一覧画面
                        </option>
                        <option value={"update"}>
                            編集画面
                        </option>
                        <option value={"delete"}>
                            削除画面
                        </option>
                        <option value={"search"}>
                            検索画面
                        </option>
                    </select>
                </div>
                {type === "add" && <AddTemplate
                    tableState={tableState}
                    componenetActions={componenetActions}
                    screenName={screenName}
                    screenActions={screenActions}
                    screenState={screenState}
                    close={close}
                />}
            </div>
        </div>
    )
}

interface AddTemplateProps {
    tableState: TablesState,
    componenetActions: ComponentActions,
    screenState: ScreenState,
    screenActions: ScreenActions
    screenName: string
    close: () => void
}

function AddTemplate({ tableState, componenetActions, screenName, screenActions, screenState, close }: AddTemplateProps) {

    const tables = tableState.tables;
    const [selectedTableId, setSelectedTableId] = useState("");
    const [successScreen, setSuccessScreen] = useState("");

    const confirm = async () => {
        const MARGIN_X = 50;
        const COLUMN_MARGIN_Y = 20;
        const LABEL_MARGIN_Y = 25;
        const TEXT_WIDTH = 650;
        const TEXT_HEIGHT = 25;
        const INPUT_WIDTH = 650;
        const INPUT_HEIGHT = 40;

        const screenId = await screenActions.addScreen(screenName);
        componenetActions.selectComponent(-1);
        const table = tables.find((table) => table.id === selectedTableId);
        const actionData: { [key: string]: string } = {};
        const components: Component[] = [];
        for (let i = 0; i < (table?.columns ?? []).length; i++) {
            const column = table?.columns[i];
            let columnName;
            if (!(column?.foreignTableName)) {
                columnName = column?.name;
            } else {
                const foreignTable = tables.find((table) => table.name === column.foreignTableName);
                columnName = `${foreignTable?.columns.find(col => col.isPrimary)?.name}Of${capitalizeFirst(foreignTable?.name)}`;
            }
            const label: TextComponent = {
                id: "w" + self.crypto.randomUUID().replace(/-/g, ""),
                screenId: screenId,
                index: i * 2,
                name: `${table?.name}${capitalizeFirst(columnName)}Label`,
                type: ComponentType.Text,
                uiData: {
                    posX: MARGIN_X * (i % 2 + 1) + INPUT_WIDTH * (i % 2),
                    posY: COLUMN_MARGIN_Y * (Math.floor(i / 2) + 1) + (TEXT_HEIGHT + INPUT_HEIGHT + LABEL_MARGIN_Y) * (Math.floor(i / 2)),
                    width: TEXT_WIDTH,
                    height: TEXT_HEIGHT
                },
                generalData: {
                    text: `${columnName}`
                },
                style: {}
            }
            const field: TextFieldComponent = {
                id: "w" + self.crypto.randomUUID().replace(/-/g, ""),
                screenId: screenId,
                index: i * 2 + 1,
                name: `${table?.name}${capitalizeFirst(columnName)}Input`,
                type: ComponentType.TextField,
                uiData: {
                    posX: MARGIN_X * (i % 2 + 1) + INPUT_WIDTH * (i % 2),
                    posY: (COLUMN_MARGIN_Y + LABEL_MARGIN_Y) * (Math.floor(i / 2) + 1) + (TEXT_HEIGHT + INPUT_HEIGHT) * (Math.floor(i / 2)),
                    width: INPUT_WIDTH,
                    height: INPUT_HEIGHT
                },
                generalData: {
                    text: ""
                },
                style: {}
            }
            actionData[columnName ?? ""] = `\${${table?.name}${capitalizeFirst(columnName)}Input}`
            components.push(label);
            components.push(field);
        }
        const confirmButton: ButtonComponent = {
            id: "w" + self.crypto.randomUUID().replace(/-/g, ""),
            name: `add${capitalizeFirst(table?.name)}ConfirmButton`,
            screenId: screenId,
            index: (table?.columns.length ?? 0) * 2,
            type: ComponentType.Button,
            uiData: {
                posX: 1200,
                posY: 750,
                width: 150,
                height: 40
            },
            generalData: {
                text: "登録"
            },
            style: {},
            action: {
                type: "addData",
                action: {
                    tableName: table?.name ?? "",
                    newData: actionData,
                    successScreenName: successScreen,
                    failScreenName: ""
                }
            }
        }
        const backButton: ButtonComponent = {
            id: "w" + self.crypto.randomUUID().replace(/-/g, ""),
            name: `add${capitalizeFirst(table?.name)}BackButton`,
            screenId: screenId,
            index: (table?.columns.length ?? 0) * 2 + 1,
            type: ComponentType.Button,
            uiData: {
                posX: 200,
                posY: 750,
                width: 150,
                height: 40
            },
            generalData: {
                text: "戻る"
            },
            style: {},
            action: {
                type: "navigate",
                action: {
                    nextScreenName: "_back"
                }
            }
        }
        components.push(confirmButton);
        components.push(backButton)
        componenetActions.addComponents(components, screenId);
        close();
    }

    return (
        <div className="flex flex-col gap-4">
            <div className="flex">
                <label className="mr-4 mt-2 flex-1">対象テーブル</label>
                <select
                    className="border px-3 py-2 rounded flex-10"
                    value={selectedTableId}
                    onChange={(e) => { setSelectedTableId(e.target.value) }}
                >
                    <option value={""}>選択してください...</option>
                    {tables.map((table, idx) => (
                        <option key={idx} value={table.id}>
                            {table.name}
                        </option>
                    ))}
                </select>
            </div>
            <div className="flex">
                <label className="mr-4 mt-2 flex-1">成功時遷移先</label>
                <select
                    className="border px-3 py-2 rounded flex-10"
                    value={successScreen}
                    onChange={(e) => { setSuccessScreen(e.target.value) }}
                >
                    <option value={""}>選択してください...</option>
                    <option value="_back">戻る</option>
                    {screenState.screens.map((screen, idx) => (
                        <option key={idx} value={screen.name}>
                            {screen.name}
                        </option>
                    ))}
                </select>
            </div>
            <div className="flex justify-end">
                <button className="px-4 py-2 bg-blue-600 text-white rounded-lg 
                    hover:bg-blue-700 active:scale-95 
                    transition duration-200 disabled:bg-gray-400 disabled:cursor-not-allowed disabled:opacity-70"
                    onClick={confirm}
                >
                    追加
                </button>
            </div>
        </div>
    )
}