reflex-dev / reflex

🕸️ Web apps in pure Python 🐍
https://reflex.dev
Apache License 2.0
20.48k stars 1.18k forks source link

RX callbacks for google picker API #4435

Open alieralex opened 3 days ago

alieralex commented 3 days ago

We are trying to implement a callback to reflex to both update the frontend state and make a request to the backend when a user successfully completes a google file picker. Our flow is as follows:

  1. We load in javascript code using rx.script for the handling of a google docs file picker component
  2. A user clicks a button to open a google docs file picker.
  3. The button calls a function in state called show_folder_picker
  4. show_folder_picker calls a javascript function we had loaded in to open the google picker using rx.call_script
  5. rx.call_script uses the callback parameter to try to trigger a function to update the frontend / backend (self.picker_callback)-> This is the issue. The callback is called when the picker in opened, not when a user picks a file
  6. The user selects the folder using the google docs file picker
  7. The user saves their selection, closing the picker and calling a callback method we have defined in our javascript.

Ideally we could pass self.picker_callback into the javascript code so that it can be called when the google picker succeeds. We can't find a way to do that and as far as we know there is no way to tie a reflex state function to our script.

Specifics (please complete the following information):

We have tried a lot of paths to solve this including reflex EventHandler.. no luck so far.

linear[bot] commented 3 days ago

ENG-4143 RX callbacks for google picker API

masenf commented 3 days ago

i don't have an API key set up to really test it, but the generated code is looking alright; see if this approach is workable

import reflex as rx
from reflex.components.props import PropsBase
from reflex.utils.imports import ImportVar

def _drive_picker_callback_signature(data: rx.Var[dict]) -> tuple[rx.Var[dict]]:
    return (data,)

class DrivePicker(PropsBase):
    client_id: str | rx.Var[str]
    developer_key: str | rx.Var[str]
    view_id: str | rx.Var[str]
    callback_function: rx.EventChain | rx.Var[rx.EventChain]

    def __init__(self, **kwargs):
        callback_function = kwargs.pop("callback_function", None)
        if callback_function is not None:
            kwargs["callback_function"] = rx.EventChain(
                events=[
                    rx.event.call_event_handler(
                        callback_function, _drive_picker_callback_signature
                    ),
                ],
                args_spec=_drive_picker_callback_signature,
            )
        return super().__init__(**kwargs)

    @property
    def open_picker(self) -> rx.Var:
        return rx.Var(
            f"() => openPicker({rx.Var.create(self)})",
            _var_type=rx.EventChain,
            _var_data=rx.vars.VarData(
                imports={
                    "react-google-drive-picker": ImportVar(
                        tag="useDrivePicker", is_default=True
                    )
                },
                hooks=[
                    "const [openPicker, authResponse] = useDrivePicker();",
                ],
            ),
        )

class State(rx.State):
    """The app state."""

    def callback(self, data):
        print(data)

def index() -> rx.Component:
    picker = DrivePicker(
        client_id="foo",
        developer_key="bar",
        view_id="DOCS",
        callback_function=State.callback,
    )
    return rx.container(
        rx.color_mode.button(position="top-right"),
        rx.vstack(
            rx.button("Open Picker", on_click=picker.open_picker),
        ),
        rx.logo(),
    )

app = rx.App()
app.add_page(index)

(yes, some of this is undocumented)

Generates code that sort of looks like the example usage on npm

export function Button_76aae5a36aab9ba3fddbfcfafe8ab3f8() {
  const [openPicker, authResponse] = useDrivePicker();
  const [addEvents, connectErrors] = useContext(EventLoopContext);

  const on_click_53f7d2f543ac6d4f299618cdf5a3cccd = useCallback(
    () =>
      openPicker({
        ["clientId"]: "foo",
        ["developerKey"]: "bar",
        ["viewId"]: "DOCS",
        ["callbackFunction"]: (_data) =>
          addEvents(
            [
              Event(
                "reflex___state____state.repro_picker___repro_picker____state.callback",
                { ["data"]: _data },
                {}
              ),
            ],
            [_data],
            {}
          ),
      }),
    [addEvents, Event, openPicker, authResponse]
  );

  return (
    <RadixThemesButton onClick={on_click_53f7d2f543ac6d4f299618cdf5a3cccd}>
      {"Open Picker"}
    </RadixThemesButton>
  );
}
alieralex commented 3 days ago

Thanks Masen,

I’ll try it a little later today!

Alex Osborne Supercog.ai 510-592-4578

On Mon, Nov 25, 2024 at 12:51 PM Masen Furer @.***> wrote:

i don't have an API key set up to really test it, but the generated code is looking alright; see if this approach is workable

import reflex as rxfrom reflex.components.props import PropsBasefrom reflex.utils.imports import ImportVar

def _drive_picker_callback_signature(data: rx.Var[dict]) -> tuple[rx.Var[dict]]: return (data,)

class DrivePicker(PropsBase): client_id: str | rx.Var[str] developer_key: str | rx.Var[str] view_id: str | rx.Var[str] callback_function: rx.EventChain | rx.Var[rx.EventChain]

def __init__(self, **kwargs):
    callback_function = kwargs.pop("callback_function", None)
    if callback_function is not None:
        kwargs["callback_function"] = rx.EventChain(
            events=[
                rx.event.call_event_handler(
                    callback_function, _drive_picker_callback_signature
                ),
            ],
            args_spec=_drive_picker_callback_signature,
        )
    return super().__init__(**kwargs)

@property
def open_picker(self) -> rx.Var:
    return rx.Var(
        f"() => openPicker({rx.Var.create(self)})",
        _var_type=rx.EventChain,
        _var_data=rx.vars.VarData(
            imports={
                "react-google-drive-picker": ImportVar(
                    tag="useDrivePicker", is_default=True
                )
            },
            hooks=[
                "const [openPicker, authResponse] = useDrivePicker();",
            ],
        ),
    )

class State(rx.State): """The app state."""

def callback(self, data):
    print(data)

def index() -> rx.Component: picker = DrivePicker( client_id="foo", developer_key="bar", view_id="DOCS", callback_function=State.callback, ) return rx.container( rx.color_mode.button(position="top-right"), rx.vstack( rx.button("Open Picker", on_click=picker.open_picker), ), rx.logo(), )

app = rx.App()app.add_page(index)

(yes, some of this is undocumented)

Generates code that sort of looks like the example usage on npm

export function Button_76aae5a36aab9ba3fddbfcfafe8ab3f8() { const [openPicker, authResponse] = useDrivePicker(); const [addEvents, connectErrors] = useContext(EventLoopContext);

const on_click_53f7d2f543ac6d4f299618cdf5a3cccd = useCallback( () => openPicker({

    ["developerKey"]: "bar",
    ["viewId"]: "DOCS",
    ["callbackFunction"]: (_data) =>
      addEvents(
        [
          Event(
            "reflex___state____state.repro_picker___repro_picker____state.callback",
            { ["data"]: _data },
            {}
          ),
        ],
        [_data],
        {}
      ),
  }),
[addEvents, Event, openPicker, authResponse]

);

return (

{"Open Picker"}

);}

— Reply to this email directly, view it on GitHub https://github.com/reflex-dev/reflex/issues/4435#issuecomment-2499006752, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABZEWVQONXA6RB4R5NPWKCT2COEURAVCNFSM6AAAAABSOVMQPKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDIOJZGAYDMNZVGI . You are receiving this because you authored the thread.Message ID: @.***>

alieralex commented 11 hours ago

Hi Masen,

Again thanks for your reply and approach. Your approach uses a google picker react component, and we have spent significant time developing our own for complete flexibility with the Google Picker API. I was wondering if you would be willing to support that approach?

I explored sending my own events but ran into problems. We are running reflex 0.5.9. But it looks like maybe you could help us there. In my current implementation I have created a hidden button, and in my javascript picker callback I am finding this button and calling button.click(). This works for the first time after a reset, but fails on subsequent times.

But if there is a way in my javascript code, running reflex 0.5.9 that I could send an event with data to my state function that will handle the picker data, that would be preferable. I just haven't been able to get that to work. Alternatively if you can suggest how I can get handlerButton.click(); to wor consistently then that is all tested and in place.

Here is javascript where I pass data in local storage and click the button:

            const eventDetail = {{
                type:      eventType,
                payload:   payload,
                timestamp: new Date().toISOString()
            }};
            localStorage.setItem('picker_event',

JSON.stringify(eventDetail));

            const handlerButton =

document.getElementById('folder_select_handler'); if (handlerButton) {{ // Add small delay to ensure event handler is bound await new Promise(resolve => setTimeout(resolve, 100)); handlerButton.click(); }} else {{ this.error('Could not find handler button'); }}

I hope you had a happy thanksgiving and look forward to your response.

Thanks,

Alex Osborne Cofounder Supercog.ai 510-592-4578

On Mon, Nov 25, 2024 at 12:51 PM Masen Furer @.***> wrote:

i don't have an API key set up to really test it, but the generated code is looking alright; see if this approach is workable

import reflex as rxfrom reflex.components.props import PropsBasefrom reflex.utils.imports import ImportVar

def _drive_picker_callback_signature(data: rx.Var[dict]) -> tuple[rx.Var[dict]]: return (data,)

class DrivePicker(PropsBase): client_id: str | rx.Var[str] developer_key: str | rx.Var[str] view_id: str | rx.Var[str] callback_function: rx.EventChain | rx.Var[rx.EventChain]

def __init__(self, **kwargs):
    callback_function = kwargs.pop("callback_function", None)
    if callback_function is not None:
        kwargs["callback_function"] = rx.EventChain(
            events=[
                rx.event.call_event_handler(
                    callback_function, _drive_picker_callback_signature
                ),
            ],
            args_spec=_drive_picker_callback_signature,
        )
    return super().__init__(**kwargs)

@property
def open_picker(self) -> rx.Var:
    return rx.Var(
        f"() => openPicker({rx.Var.create(self)})",
        _var_type=rx.EventChain,
        _var_data=rx.vars.VarData(
            imports={
                "react-google-drive-picker": ImportVar(
                    tag="useDrivePicker", is_default=True
                )
            },
            hooks=[
                "const [openPicker, authResponse] = useDrivePicker();",
            ],
        ),
    )

class State(rx.State): """The app state."""

def callback(self, data):
    print(data)

def index() -> rx.Component: picker = DrivePicker( client_id="foo", developer_key="bar", view_id="DOCS", callback_function=State.callback, ) return rx.container( rx.color_mode.button(position="top-right"), rx.vstack( rx.button("Open Picker", on_click=picker.open_picker), ), rx.logo(), )

app = rx.App()app.add_page(index)

(yes, some of this is undocumented)

Generates code that sort of looks like the example usage on npm

export function Button_76aae5a36aab9ba3fddbfcfafe8ab3f8() { const [openPicker, authResponse] = useDrivePicker(); const [addEvents, connectErrors] = useContext(EventLoopContext);

const on_click_53f7d2f543ac6d4f299618cdf5a3cccd = useCallback( () => openPicker({

    ["developerKey"]: "bar",
    ["viewId"]: "DOCS",
    ["callbackFunction"]: (_data) =>
      addEvents(
        [
          Event(
            "reflex___state____state.repro_picker___repro_picker____state.callback",
            { ["data"]: _data },
            {}
          ),
        ],
        [_data],
        {}
      ),
  }),
[addEvents, Event, openPicker, authResponse]

);

return (

{"Open Picker"}

);}

— Reply to this email directly, view it on GitHub https://c58RN04.na1.hs-sales-engage.com/Ctc/T5+23284/c58RN04/Jll2-6qcW7Y8-PT6lZ3nFW7XGty718fxV4VQD-JS7Pby8gW8jQSb-6c1HV1W2_1JyP18Z2m6W26XRD34pJzq5W2jDmTr98DNt3N8DV1CxZZf3lW6h7ZDl6GcfXLW4pSGKF1sT2vCW47yRsH2hzYDcVmCgfd47cNScW1p7Mm85c61v6W3dvXcg9k7mrPW2fM0Dz8c3cXCW7wsjHn2qdKq4W1c7dFW5JtymHV1DP145Rhmw9VFB-TR3Pl-S-W8XkFT48MK5XnN6slRmpW1nPgW1YZzDv7MrpdXW8ktPgP95tl_hW3Z6plg7WB6b1W2mqbdX5LzYPSW42q-Jy7yRSdPW74ZdK64DxrMpf6M-tBl04, or unsubscribe https://c58RN04.na1.hs-sales-engage.com/Ctc/T5+23284/c58RN04/Jks4YGXpW69t95C6lZ3pPN5qK_8nfxSXlW5wTv2N7HByDtW8l81xF5XWrbZW2XxCJb4sPRXnW6dZMjk3Nb1CMW3H0WYV6KxrKwVTQSLQ7fbl9JW1Q3rym8G_d_YW2PPGTs3f-wkhVZqQ-g3FVMmtW25p4hj6nwthmW8Yx3r93N4j0gVrjmkL1GdnqdM3TBkzrxn0FW10d4rw5SF1dbW5dJWcS1Mfj-nW9m3bmp5RxK5cW2sBKJm3Zb6KcW2KtcSm92Q0mBW65JxTn8bzfTJW68dttR8sS2LyW1QmSck8Brbz9N5-bktKfLK8BVQncd68L5m3XW8W5pff7rlT7yN63569H-h4C2W4TCV7L5THWFrW1_KX7R6tZM1LW8Ygtth8WxMW-V48GMg5tJpNJW4jbBc2103TwWN3kTNRkPQNqBW4HYGK-1cVJf8VBr7hL1FvjjtW4zkC1X3K7JKqW6ZHgZw3pjDTcf89kzgR04 . You are receiving this because you authored the thread.Message ID: @.***>

-- Alex Osborne 510-592-4578