canonrs-client 0.1.0

CanonRS client-side runtime
use leptos::prelude::*;
use canonrs_core::DataTableToolbarPrimitive;
use super::types::{DataTableRequest, DataTableResponse, ColumnDef};
use super::PinPosition;
use super::state::DataTableState;
use super::data_table_header::DataTableHeader;
use super::data_table_body::DataTableBody;
use super::data_table_pagination::DataTablePagination;

fn col_width_px(col: &ColumnDef<impl Clone>, widths: &std::collections::HashMap<String, u32>) -> String {
    widths.get(&col.id)
        .map(|w| format!("{}px", w))
        .or_else(|| col.width.clone())
        .unwrap_or_else(|| "150px".to_string())
}

#[component]
pub fn DataTableInteractive<T, F>(
    columns: Signal<Vec<ColumnDef<T>>>,
    fetch_data: F,
    #[prop(default = 20)] page_size: usize,
    #[prop(into, optional)] id: Option<String>,
    #[prop(optional)] density: Option<Signal<String>>,
    #[prop(optional)] zebra: Option<Signal<bool>>,
    #[prop(optional)] row_hover: Option<Signal<bool>>,
    #[prop(optional)] sticky_header: Option<Signal<bool>>,
    #[prop(optional)] draggable: Option<Signal<bool>>,
    #[prop(optional)] resizable: Option<Signal<bool>>,
    #[prop(optional)] pinnable: Option<Signal<bool>>,
    #[prop(optional)] column_widths: Option<RwSignal<std::collections::HashMap<String, u32>>>,
    #[prop(optional)] pinned_columns: Option<RwSignal<std::collections::HashMap<String, PinPosition>>>,
) -> impl IntoView
where
    T: Clone + PartialEq + Send + Sync + 'static,
    F: Fn(DataTableRequest) -> Result<DataTableResponse<T>, String> + 'static,
{
    let state: DataTableState<T> = DataTableState::new();

    let column_widths_signal  = column_widths.unwrap_or(state.column_widths);
    let pinned_columns_signal = pinned_columns.unwrap_or(state.pinned_columns);

    let cols_left = Signal::derive(move || {
        columns.with(|cols| {
            pinned_columns_signal.with(|pins| {
                cols.iter().filter(|c| pins.get(&c.id) == Some(&PinPosition::Left)).cloned().collect::<Vec<_>>()
            })
        })
    });
    let cols_center = Signal::derive(move || {
        columns.with(|cols| {
            pinned_columns_signal.with(|pins| {
                cols.iter().filter(|c| !matches!(pins.get(&c.id), Some(PinPosition::Left) | Some(PinPosition::Right))).cloned().collect::<Vec<_>>()
            })
        })
    });
    let cols_right = Signal::derive(move || {
        columns.with(|cols| {
            pinned_columns_signal.with(|pins| {
                cols.iter().filter(|c| pins.get(&c.id) == Some(&PinPosition::Right)).cloned().collect::<Vec<_>>()
            })
        })
    });

    let left_width = Signal::derive(move || {
        cols_left.with(|cols| {
            column_widths_signal.with(|widths| {
                cols.iter().map(|c| widths.get(&c.id).copied().unwrap_or(150)).sum::<u32>()
            })
        })
    });
    let right_width = Signal::derive(move || {
        cols_right.with(|cols| {
            column_widths_signal.with(|widths| {
                cols.iter().map(|c| widths.get(&c.id).copied().unwrap_or(150)).sum::<u32>()
            })
        })
    });

    let fetch_data = std::sync::Arc::new(fetch_data);
    let fetch_data_effect = fetch_data.clone();
    Effect::new(move |_| {
        columns.track();
        state.current_page.track();
        state.sort_column.track();
        state.sort_ascending.track();
        state.filter_query.track();
        state.loading.set(true);
        state.error.set(None);
        let request = DataTableRequest {
            page:           state.current_page.get(),
            page_size,
            sort_column:    state.sort_column.get(),
            sort_ascending: state.sort_ascending.get(),
            filter_query:   state.filter_query.get(),
        };
        match fetch_data_effect(request) {
            Ok(r)  => { state.data.set(r.data); state.total.set(r.total); state.loading.set(false); }
            Err(e) => { state.error.set(Some(e)); state.loading.set(false); }
        }
    });

    let table_id       = id.unwrap_or_else(|| "datatable-interactive".to_string());
    let table_id_clone = table_id.clone();

    let density_signal   = density.unwrap_or_else(|| Signal::derive(|| "comfortable".to_string()));
    let zebra_signal     = zebra.unwrap_or_else(|| Signal::derive(|| false));
    let hover_signal     = row_hover.unwrap_or_else(|| Signal::derive(|| true));
    let sticky_signal    = sticky_header.unwrap_or_else(|| Signal::derive(|| false));
    let draggable_signal = draggable.unwrap_or_else(|| Signal::derive(|| false));
    let resizable_signal = resizable.unwrap_or_else(|| Signal::derive(|| false));
    let pinnable_signal  = pinnable.unwrap_or_else(|| Signal::derive(|| false));

    use super::data_table_column_resize::use_column_resize;
    use super::data_table_column_pin::use_column_pin;
    use super::data_table_column_reorder::use_column_reorder;

    use_column_resize(format!("{}-center-resize", table_id), resizable_signal, {
        let widths = column_widths_signal;
        move |col_id, width| { widths.update(|m| { m.insert(col_id, width); }); }
    });

    Effect::new(move |_| {
        if !pinnable_signal.get() {
            pinned_columns_signal.update(|m| m.clear());
        }
    });

    use_column_pin(table_id.clone(), pinnable_signal, {
        let pins = pinned_columns_signal;
        move |col_id, position| { pins.update(|m| { m.insert(col_id, position); }); }
    });
    use_column_reorder(table_id.clone(), draggable_signal, move |from, to| {
        leptos::logging::log!("reorder: {} -> {}", from, to);
    });

    view! {
        <div
            data-rs-datatable=""
            data-rs-density=move || density_signal.get()
            data-rs-zebra=move || zebra_signal.get().to_string()
            data-rs-row-hover=move || hover_signal.get().to_string()
            data-rs-sticky-header=move || sticky_signal.get().to_string()
            data-rs-resizable=move || resizable_signal.get().to_string()
            data-rs-pinnable=move || pinnable_signal.get().to_string()
            data-rs-draggable=move || draggable_signal.get().to_string()
            id=table_id
        >
            <DataTableToolbarPrimitive>
                <input
                    type="text"
                    placeholder="Search..."
                    class="px-3 py-2 border rounded w-64"
                    on:input=move |ev| {
                        state.filter_query.set(event_target_value(&ev));
                        state.current_page.set(1);
                    }
                />
            </DataTableToolbarPrimitive>

            <div data-rs-datatable-scroll="" style="display: flex; overflow: hidden; border: var(--data-table-border-width, 1px) solid var(--data-table-border-color); border-radius: var(--data-table-radius);">

                <div
                    data-rs-datatable-panel="left"
                    style=move || {
                        let w = left_width.get();
                        if w == 0 { "display: none".to_string() }
                        else { format!("width: {}px; flex-shrink: 0; overflow: hidden; border-right: 2px solid var(--data-table-border-color);", w) }
                    }
                >
                    <table data-rs-datatable-table="" role="table" style="table-layout: fixed; border-collapse: collapse; width: 100%;">
                        <colgroup>
                            {move || cols_left.with(|cols| {
                                column_widths_signal.with(|widths| {
                                    cols.iter().map(|col| {
                                        let w = col_width_px(col, widths);
                                        view! { <col style=format!("width: {}", w) /> }
                                    }).collect_view()
                                })
                            })}
                        </colgroup>
                        <DataTableHeader<T>
                            columns=cols_left
                            sort_column=state.sort_column
                            sort_ascending=state.sort_ascending
                            current_page=state.current_page
                            draggable=Signal::derive(|| false)
                            resizable=Signal::derive(|| false)
                            pinnable=pinnable_signal
                            table_id=format!("{}-left", table_id_clone)
                            column_widths=column_widths_signal
                            panel="left"
                        />
                        <DataTableBody<T>
                            columns=cols_left
                            data=state.data
                            loading=state.loading
                            error=state.error
                        />
                    </table>
                </div>

                <div
                    data-rs-datatable-panel="center"
                    style=move || {
                        let max_h = if sticky_signal.get() { "max-height: 400px; ".to_string() } else { String::new() };
                        format!("{}flex: 1; overflow-x: auto; overflow-y: auto;", max_h)
                    }
                >
                    <table data-rs-datatable-table="" role="table" style="table-layout: fixed; border-collapse: collapse;">
                        <colgroup>
                            {move || cols_center.with(|cols| {
                                column_widths_signal.with(|widths| {
                                    cols.iter().map(|col| {
                                        let w = col_width_px(col, widths);
                                        view! { <col style=format!("width: {}", w) /> }
                                    }).collect_view()
                                })
                            })}
                        </colgroup>
                        <DataTableHeader<T>
                            columns=cols_center
                            sort_column=state.sort_column
                            sort_ascending=state.sort_ascending
                            current_page=state.current_page
                            draggable=draggable_signal
                            resizable=resizable_signal
                            pinnable=pinnable_signal
                            table_id=format!("{}-center", table_id_clone)
                            column_widths=column_widths_signal
                            panel="center"
                        />
                        <DataTableBody<T>
                            columns=cols_center
                            data=state.data
                            loading=state.loading
                            error=state.error
                        />
                    </table>
                </div>

                <div
                    data-rs-datatable-panel="right"
                    style=move || {
                        let w = right_width.get();
                        if w == 0 { "display: none".to_string() }
                        else { format!("width: {}px; flex-shrink: 0; overflow: hidden; border-left: 2px solid var(--data-table-border-color);", w) }
                    }
                >
                    <table data-rs-datatable-table="" role="table" style="table-layout: fixed; border-collapse: collapse; width: 100%;">
                        <colgroup>
                            {move || cols_right.with(|cols| {
                                column_widths_signal.with(|widths| {
                                    cols.iter().map(|col| {
                                        let w = col_width_px(col, widths);
                                        view! { <col style=format!("width: {}", w) /> }
                                    }).collect_view()
                                })
                            })}
                        </colgroup>
                        <DataTableHeader<T>
                            columns=cols_right
                            sort_column=state.sort_column
                            sort_ascending=state.sort_ascending
                            current_page=state.current_page
                            draggable=Signal::derive(|| false)
                            resizable=Signal::derive(|| false)
                            pinnable=pinnable_signal
                            table_id=format!("{}-right", table_id_clone)
                            column_widths=column_widths_signal
                            panel="right"
                        />
                        <DataTableBody<T>
                            columns=cols_right
                            data=state.data
                            loading=state.loading
                            error=state.error
                        />
                    </table>
                </div>

            </div>

            <DataTablePagination
                current_page=state.current_page
                total=state.total
                page_size=page_size
            />
        </div>
    }
}