Skip to content

Commit

Permalink
feat: Make ui.command to support path and download props #2224 (#2262)
Browse files Browse the repository at this point in the history
  • Loading branch information
marek-mihok authored Feb 9, 2024
1 parent 31c8957 commit cd469fc
Show file tree
Hide file tree
Showing 16 changed files with 398 additions and 50 deletions.
2 changes: 1 addition & 1 deletion py/examples/toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async def serve(q: Q):
),
ui.command(name='upload', label='Upload', icon='Upload'),
ui.command(name='share', label='Share', icon='Share'),
ui.command(name='download', label='Download', icon='Download'),
ui.command(name='download', label='Download', icon='Download', path='https://wave.h2o.ai/img/logo.svg', download=True),
],
secondary_items=[
ui.command(name='tile', caption='Grid View', icon='Tiles'),
Expand Down
20 changes: 20 additions & 0 deletions py/h2o_lightwave/h2o_lightwave/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,17 @@ def __init__(
icon: Optional[str] = None,
items: Optional[List['Command']] = None,
value: Optional[str] = None,
path: Optional[str] = None,
download: Optional[bool] = None,
):
_guard_scalar('Command.name', name, (str,), True, False, False)
_guard_scalar('Command.label', label, (str,), False, True, False)
_guard_scalar('Command.caption', caption, (str,), False, True, False)
_guard_scalar('Command.icon', icon, (str,), False, True, False)
_guard_vector('Command.items', items, (Command,), False, True, False)
_guard_scalar('Command.value', value, (str,), False, True, False)
_guard_scalar('Command.path', path, (str,), False, True, False)
_guard_scalar('Command.download', download, (bool,), False, True, False)
self.name = name
"""An identifying name for this component. If the name is prefixed with a '#', the command sets the location hash to the name when executed."""
self.label = label
Expand All @@ -179,6 +183,10 @@ def __init__(
"""Sub-commands, if any"""
self.value = value
"""Data associated with this command, if any."""
self.path = path
"""The path or URL to link to. The 'items' and 'value' props are ignored when specified."""
self.download = download
"""True if the link should prompt the user to save the linked URL instead of navigating to it."""

def dump(self) -> Dict:
"""Returns the contents of this object as a dict."""
Expand All @@ -188,13 +196,17 @@ def dump(self) -> Dict:
_guard_scalar('Command.icon', self.icon, (str,), False, True, False)
_guard_vector('Command.items', self.items, (Command,), False, True, False)
_guard_scalar('Command.value', self.value, (str,), False, True, False)
_guard_scalar('Command.path', self.path, (str,), False, True, False)
_guard_scalar('Command.download', self.download, (bool,), False, True, False)
return _dump(
name=self.name,
label=self.label,
caption=self.caption,
icon=self.icon,
items=None if self.items is None else [__e.dump() for __e in self.items],
value=self.value,
path=self.path,
download=self.download,
)

@staticmethod
Expand All @@ -212,19 +224,27 @@ def load(__d: Dict) -> 'Command':
_guard_vector('Command.items', __d_items, (dict,), False, True, False)
__d_value: Any = __d.get('value')
_guard_scalar('Command.value', __d_value, (str,), False, True, False)
__d_path: Any = __d.get('path')
_guard_scalar('Command.path', __d_path, (str,), False, True, False)
__d_download: Any = __d.get('download')
_guard_scalar('Command.download', __d_download, (bool,), False, True, False)
name: str = __d_name
label: Optional[str] = __d_label
caption: Optional[str] = __d_caption
icon: Optional[str] = __d_icon
items: Optional[List['Command']] = None if __d_items is None else [Command.load(__e) for __e in __d_items]
value: Optional[str] = __d_value
path: Optional[str] = __d_path
download: Optional[bool] = __d_download
return Command(
name,
label,
caption,
icon,
items,
value,
path,
download,
)


Expand Down
6 changes: 6 additions & 0 deletions py/h2o_lightwave/h2o_lightwave/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def command(
icon: Optional[str] = None,
items: Optional[List[Command]] = None,
value: Optional[str] = None,
path: Optional[str] = None,
download: Optional[bool] = None,
) -> Command:
"""Create a command.
Expand All @@ -70,6 +72,8 @@ def command(
icon: The icon to be displayed for this command.
items: Sub-commands, if any
value: Data associated with this command, if any.
path: The path or URL to link to. The 'items' and 'value' props are ignored when specified.
download: True if the link should prompt the user to save the linked URL instead of navigating to it.
Returns:
A `h2o_wave.types.Command` instance.
"""
Expand All @@ -80,6 +84,8 @@ def command(
icon,
items,
value,
path,
download,
)


Expand Down
20 changes: 20 additions & 0 deletions py/h2o_wave/h2o_wave/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,17 @@ def __init__(
icon: Optional[str] = None,
items: Optional[List['Command']] = None,
value: Optional[str] = None,
path: Optional[str] = None,
download: Optional[bool] = None,
):
_guard_scalar('Command.name', name, (str,), True, False, False)
_guard_scalar('Command.label', label, (str,), False, True, False)
_guard_scalar('Command.caption', caption, (str,), False, True, False)
_guard_scalar('Command.icon', icon, (str,), False, True, False)
_guard_vector('Command.items', items, (Command,), False, True, False)
_guard_scalar('Command.value', value, (str,), False, True, False)
_guard_scalar('Command.path', path, (str,), False, True, False)
_guard_scalar('Command.download', download, (bool,), False, True, False)
self.name = name
"""An identifying name for this component. If the name is prefixed with a '#', the command sets the location hash to the name when executed."""
self.label = label
Expand All @@ -179,6 +183,10 @@ def __init__(
"""Sub-commands, if any"""
self.value = value
"""Data associated with this command, if any."""
self.path = path
"""The path or URL to link to. The 'items' and 'value' props are ignored when specified."""
self.download = download
"""True if the link should prompt the user to save the linked URL instead of navigating to it."""

def dump(self) -> Dict:
"""Returns the contents of this object as a dict."""
Expand All @@ -188,13 +196,17 @@ def dump(self) -> Dict:
_guard_scalar('Command.icon', self.icon, (str,), False, True, False)
_guard_vector('Command.items', self.items, (Command,), False, True, False)
_guard_scalar('Command.value', self.value, (str,), False, True, False)
_guard_scalar('Command.path', self.path, (str,), False, True, False)
_guard_scalar('Command.download', self.download, (bool,), False, True, False)
return _dump(
name=self.name,
label=self.label,
caption=self.caption,
icon=self.icon,
items=None if self.items is None else [__e.dump() for __e in self.items],
value=self.value,
path=self.path,
download=self.download,
)

@staticmethod
Expand All @@ -212,19 +224,27 @@ def load(__d: Dict) -> 'Command':
_guard_vector('Command.items', __d_items, (dict,), False, True, False)
__d_value: Any = __d.get('value')
_guard_scalar('Command.value', __d_value, (str,), False, True, False)
__d_path: Any = __d.get('path')
_guard_scalar('Command.path', __d_path, (str,), False, True, False)
__d_download: Any = __d.get('download')
_guard_scalar('Command.download', __d_download, (bool,), False, True, False)
name: str = __d_name
label: Optional[str] = __d_label
caption: Optional[str] = __d_caption
icon: Optional[str] = __d_icon
items: Optional[List['Command']] = None if __d_items is None else [Command.load(__e) for __e in __d_items]
value: Optional[str] = __d_value
path: Optional[str] = __d_path
download: Optional[bool] = __d_download
return Command(
name,
label,
caption,
icon,
items,
value,
path,
download,
)


Expand Down
6 changes: 6 additions & 0 deletions py/h2o_wave/h2o_wave/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def command(
icon: Optional[str] = None,
items: Optional[List[Command]] = None,
value: Optional[str] = None,
path: Optional[str] = None,
download: Optional[bool] = None,
) -> Command:
"""Create a command.
Expand All @@ -70,6 +72,8 @@ def command(
icon: The icon to be displayed for this command.
items: Sub-commands, if any
value: Data associated with this command, if any.
path: The path or URL to link to. The 'items' and 'value' props are ignored when specified.
download: True if the link should prompt the user to save the linked URL instead of navigating to it.
Returns:
A `h2o_wave.types.Command` instance.
"""
Expand All @@ -80,6 +84,8 @@ def command(
icon,
items,
value,
path,
download,
)


Expand Down
12 changes: 10 additions & 2 deletions r/R/ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ ui_text <- function(
#' @param icon The icon to be displayed for this command.
#' @param items Sub-commands, if any
#' @param value Data associated with this command, if any.
#' @param path The path or URL to link to. The 'items' and 'value' props are ignored when specified.
#' @param download True if the link should prompt the user to save the linked URL instead of navigating to it.
#' @return A Command instance.
#' @export
ui_command <- function(
Expand All @@ -111,20 +113,26 @@ ui_command <- function(
caption = NULL,
icon = NULL,
items = NULL,
value = NULL) {
value = NULL,
path = NULL,
download = NULL) {
.guard_scalar("name", "character", name)
.guard_scalar("label", "character", label)
.guard_scalar("caption", "character", caption)
.guard_scalar("icon", "character", icon)
.guard_vector("items", "WaveCommand", items)
.guard_scalar("value", "character", value)
.guard_scalar("path", "character", path)
.guard_scalar("download", "logical", download)
.o <- list(
name=name,
label=label,
caption=caption,
icon=icon,
items=items,
value=value)
value=value,
path=path,
download=download)
class(.o) <- append(class(.o), c(.wave_obj, "WaveCommand"))
return(.o)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2680,12 +2680,14 @@
<option name="Python" value="true"/>
</context>
</template>
<template name="w_full_command" value="ui.command(name='$name$',label='$label$',caption='$caption$',icon='$icon$',value='$value$',items=[&#10; $items$ &#10;]),$END$" description="Create Wave Command with full attributes." toReformat="true" toShortenFQNames="true">
<template name="w_full_command" value="ui.command(name='$name$',label='$label$',caption='$caption$',icon='$icon$',value='$value$',path='$path$',download=$download$,items=[&#10; $items$ &#10;]),$END$" description="Create Wave Command with full attributes." toReformat="true" toShortenFQNames="true">
<variable name="name" expression="" defaultValue="" alwaysStopAt="true"/>
<variable name="label" expression="" defaultValue="" alwaysStopAt="true"/>
<variable name="caption" expression="" defaultValue="" alwaysStopAt="true"/>
<variable name="icon" expression="" defaultValue="" alwaysStopAt="true"/>
<variable name="value" expression="" defaultValue="" alwaysStopAt="true"/>
<variable name="path" expression="" defaultValue="" alwaysStopAt="true"/>
<variable name="download" expression="" defaultValue="&quot;False&quot;" alwaysStopAt="true"/>
<variable name="items" expression="" defaultValue="" alwaysStopAt="true"/>
<context>
<option name="Python" value="true"/>
Expand Down
2 changes: 1 addition & 1 deletion tools/vscode-extension/component-snippets.json
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,7 @@
"Wave Full Command": {
"prefix": "w_full_command",
"body": [
"ui.command(name='$1', label='$2', caption='$3', icon='$4', value='$5', items=[\n\t\t$6\t\t\n]),$0"
"ui.command(name='$1', label='$2', caption='$3', icon='$4', value='$5', path='$6', download=${7:False}, items=[\n\t\t$8\t\t\n]),$0"
],
"description": "Create a full Wave Command."
},
Expand Down
89 changes: 89 additions & 0 deletions ui/src/button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ describe('Button.tsx', () => {
})

describe('Commands button', () => {
const path = 'https://wave.h2o.ai/img/logo.svg'
const buttonProps = {
items: [{
button: {
Expand Down Expand Up @@ -254,5 +255,93 @@ describe('Button.tsx', () => {
expect(getByTestId(name)).toHaveClass('ms-Link')
expect(container.querySelector('i[data-icon-name="ChevronDown"]') as HTMLLIElement).not.toBeInTheDocument()
})

it('Does not set args or calls sync on click when command has download link specified', () => {
const
btnCommandDownloadProps: Buttons = {
items: [{
button: {
name,
label: name,
commands: [
{ name: 'command1', label: 'Command 1' },
{ name: 'command2', label: 'Command 2', path, download: true },
]
}
}]
},
{ container, getByText } = render(<XButtons model={btnCommandDownloadProps} />),
contextMenuButton = container.querySelector('i[data-icon-name="ChevronDown"]') as HTMLLIElement

expect(wave.args['command1']).toBe(false)
fireEvent.click(contextMenuButton)
fireEvent.click(getByText('Command 1'))
expect(wave.args['command1']).toBe(true)

expect(pushMock).toHaveBeenCalled()
expect(pushMock).toHaveBeenCalledTimes(1)

expect(wave.args['command2']).toBe(false)
fireEvent.click(contextMenuButton)
fireEvent.click(getByText('Command 2'))
expect(wave.args['command2']).toBe(false)

expect(pushMock).toHaveBeenCalledTimes(1)
})

it('Ignores items when command has download link specified', () => {
const
btnCommandDownloadProps: Buttons = {
items: [{
button: {
name,
label: name,
commands: [
{ name: 'command1', label: 'Command 1', items: [{ name: 'commandItem1', label: 'Command item 1' }] },
{ name: 'command2', label: 'Command 2', path, download: true, items: [{ name: 'commandItem2', label: 'Command item 2' }] },
]
}
}]
},
{ container, queryByText } = render(<XButtons model={btnCommandDownloadProps} />),
contextMenuButton = container.querySelector('i[data-icon-name="ChevronDown"]') as HTMLLIElement

fireEvent.click(contextMenuButton)

expect(queryByText('Command item 1')).not.toBeInTheDocument()
const menuItem1 = document.querySelectorAll('button.ms-ContextualMenu-link')[0] as HTMLButtonElement
fireEvent.click(menuItem1)
expect(queryByText('Command item 1')).toBeInTheDocument()

expect(queryByText('Command item 2')).not.toBeInTheDocument()
const menuItem2 = document.querySelectorAll('button.ms-ContextualMenu-link')[1] as HTMLButtonElement
fireEvent.click(menuItem2)
expect(queryByText('Command item 2')).not.toBeInTheDocument()
})

it('Opens link in a new tab when command has path specified', () => {
const windowOpenMock = jest.fn()
window.open = windowOpenMock
const
btnCommandDownloadProps: Buttons = {
items: [{
button: {
name,
label: name,
commands: [
{ name: 'command', label: 'Command', path },
]
}
}]
},
{ container, getByText } = render(<XButtons model={btnCommandDownloadProps} />),
contextMenuButton = container.querySelector('i[data-icon-name="ChevronDown"]') as HTMLLIElement

fireEvent.click(contextMenuButton)
fireEvent.click(getByText('Command'))

expect(windowOpenMock).toHaveBeenCalled()
expect(windowOpenMock).toHaveBeenCalledWith(path, '_blank')
})
})
})
Loading

0 comments on commit cd469fc

Please sign in to comment.