cells: add wcs action button cell in frontend card cell (#75908)
gitea/combo/pipeline/head This commit looks good
Details
gitea/combo/pipeline/head This commit looks good
Details
This commit is contained in:
parent
51ad64b477
commit
03a6bfc328
|
@ -1644,6 +1644,23 @@ class WcsCardCell(CardMixin, CellBase):
|
|||
card_data['urls'] = {}
|
||||
if self.custom_schema:
|
||||
for item in self.get_custom_schema().get('cells') or []:
|
||||
if item.get('varname') == '@action@':
|
||||
if item.get('action_ask_confirmation', False):
|
||||
render_template(
|
||||
item=item,
|
||||
template_key='action_confirmation_template',
|
||||
template_context=custom_context,
|
||||
target_key='custom_fields',
|
||||
target_context=card_data,
|
||||
)
|
||||
render_template(
|
||||
item=item,
|
||||
template_key='action_label',
|
||||
template_context=custom_context,
|
||||
target_key='custom_fields',
|
||||
target_context=card_data,
|
||||
)
|
||||
|
||||
if item.get('varname') not in ['@custom@', '@link@']:
|
||||
continue
|
||||
if not item.get('template'):
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
class WcsTriggerButton extends HTMLElement {
|
||||
#triggerUrl
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', this.onClick.bind(this))
|
||||
const label = this.getAttribute('label')
|
||||
this.#triggerUrl = this.getAttribute('trigger-url')
|
||||
this.innerHTML = `<button>${label}</button>`
|
||||
if(this.#triggerUrl === null) {
|
||||
const unavailableMode = this.getAttribute('unavailable-mode')
|
||||
if(unavailableMode === 'hide') {
|
||||
this.hidden = true
|
||||
}
|
||||
else {
|
||||
const button = this.querySelector('button')
|
||||
button.disabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
const confirmationMessage = this.getAttribute('confirmation-message')
|
||||
|
||||
if(confirmationMessage && !confirm(confirmationMessage)) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
this.getAttribute('proxy-url'),
|
||||
{
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'X-CSRFToken': this.getAttribute('csrf-token'),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'trigger-url': this.#triggerUrl
|
||||
}),
|
||||
}
|
||||
)
|
||||
let json = {}
|
||||
try {
|
||||
json = await response.json()
|
||||
} catch {}
|
||||
|
||||
const button = this.querySelector('button')
|
||||
if(!response.ok || json.err === 1) {
|
||||
alert('An error occured, please retry later.')
|
||||
button.disabled = false
|
||||
}
|
||||
else {
|
||||
button.disabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('wcs-trigger-button', WcsTriggerButton)
|
||||
|
|
@ -57,6 +57,28 @@
|
|||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif item.varname == "@action@" %}
|
||||
<div class="{{ item.cell_size|default:"" }}">
|
||||
<wcs-trigger-button
|
||||
{% if item.action_ask_confirmation %}
|
||||
{% with card.custom_fields|get:item.action_confirmation_template|force_escape as confirmation_message %}
|
||||
confirmation-message="{{ confirmation_message }}"
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% with card.custom_fields|get:item.action_label|force_escape as label %}
|
||||
label="{{ label }}"
|
||||
{% endwith %}
|
||||
proxy-url='{% url 'wcs-trigger-proxy' %}'
|
||||
unavailable-mode="{{ item.unavailable_action_mode }}"
|
||||
{% with card.actions|get:item.trigger_id as trigger_url %}
|
||||
{% if trigger_url %}
|
||||
trigger-url='{{ trigger_url }}'
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
csrf-token='{{ csrf_token }}'
|
||||
>
|
||||
</wcs-action-button>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if item.varname %}
|
||||
{% with fields_by_varnames|get:item.varname as field %}
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
{% if item.template and link %}
|
||||
<th>{{ item.header|default:"" }}</th>
|
||||
{% endif %}
|
||||
{% elif item.varname == "@action@" %}
|
||||
<th>{{ item.header|default:"" }}</th>
|
||||
{% else %}
|
||||
{% if item.varname %}
|
||||
{% with fields_by_varnames|get:item.varname as field %}
|
||||
|
|
|
@ -30,6 +30,28 @@
|
|||
{% endwith %}
|
||||
{% if not ul_display %}</td>{% endif %}
|
||||
{% endif %}
|
||||
{% elif item.varname == "@action@" %}
|
||||
{% if not ul_display %}<td>{% endif %}
|
||||
<wcs-trigger-button
|
||||
{% if item.action_ask_confirmation %}
|
||||
{% with card.custom_fields|get:item.action_confirmation_template|force_escape as confirmation_message %}
|
||||
confirmation-message="{{ confirmation_message }}"
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% with card.custom_fields|get:item.action_label|force_escape as label %}
|
||||
label="{{ label }}"
|
||||
{% endwith %}
|
||||
proxy-url='{% url 'wcs-trigger-proxy' %}'
|
||||
unavailable-mode="{{ item.unavailable_action_mode }}"
|
||||
{% with card.actions|get:item.trigger_id as trigger_url %}
|
||||
{% if trigger_url %}
|
||||
trigger-url='{{ trigger_url }}'
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
csrf-token='{{ csrf_token }}'
|
||||
>
|
||||
</wcs-action-button>
|
||||
{% if not ul_display %}</td>{% endif %}
|
||||
{% else %}
|
||||
{% if item.varname %}
|
||||
{% with fields_by_varnames|get:item.varname as field and card.fields|get:item.varname as value %}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { test } from 'vitest'
|
||||
|
||||
export const domTest = test.extend({
|
||||
// Empty {} is required by vitest
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
appendToDom: async ({}, use) => {
|
||||
const wrappers = []
|
||||
const appendToDom = (htmlContent) => {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.innerHTML = htmlContent
|
||||
document.appendChild(wrapper)
|
||||
wrappers.push(wrapper)
|
||||
return wrapper
|
||||
}
|
||||
await use(appendToDom)
|
||||
for (const wrapper of wrappers) {
|
||||
wrapper.remove()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export async function flushPromises() {
|
||||
await new Promise((resolve) => setTimeout(resolve))
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
import { expect, test, vi} from 'vitest'
|
||||
import { domTest, flushPromises } from '../test-utils.js'
|
||||
import '../../../combo/apps/wcs/static/js/combo.wcs-trigger-button.js'
|
||||
|
||||
domTest('render button label', async ({appendToDom}) => {
|
||||
let dom = appendToDom(`
|
||||
<wcs-trigger-button label="Test label"></wcs/trigger-button>
|
||||
`)
|
||||
const innerButton = dom.querySelector('button')
|
||||
expect(innerButton.innerText).toBe("Test label")
|
||||
})
|
||||
|
||||
domTest('show button if trigger-url attribute is set', async ({appendToDom}) => {
|
||||
const dom = appendToDom(`
|
||||
<wcs-trigger-button label="Test label" trigger-url="https://dummy.org"></wcs/trigger-button>
|
||||
`)
|
||||
const triggerButton = dom.querySelector('wcs-trigger-button')
|
||||
const innerButton = dom.querySelector('button')
|
||||
expect(triggerButton.hidden).toBe(false)
|
||||
expect(innerButton.disabled).toBe(false)
|
||||
})
|
||||
|
||||
domTest('hide button if trigger-url is not set and unavailable-mode is hide', async ({appendToDom}) => {
|
||||
const dom = appendToDom(`
|
||||
<wcs-trigger-button unavailable-mode="hide"></wcs/trigger-button>
|
||||
`)
|
||||
const triggerButton = dom.querySelector('wcs-trigger-button')
|
||||
const innerButton = dom.querySelector('button')
|
||||
expect(triggerButton.hidden).toBe(true)
|
||||
expect(innerButton.disabled).toBe(false)
|
||||
})
|
||||
|
||||
domTest('disable button if trigger-url is not set and unavailable-mode is disable', async ({appendToDom}) => {
|
||||
const dom = appendToDom(`
|
||||
<wcs-trigger-button unavailable-mode="disable"></wcs/trigger-button>
|
||||
`)
|
||||
const triggerButton = dom.querySelector('wcs-trigger-button')
|
||||
const innerButton = dom.querySelector('button')
|
||||
expect(triggerButton.hidden).toBe(false)
|
||||
expect(innerButton.disabled).toBe(true)
|
||||
})
|
||||
|
||||
export const triggerTest = domTest.extend({
|
||||
clickTrigger: async ({appendToDom}, use) => {
|
||||
const load = async (domContent, mockFetch) => {
|
||||
const dom = appendToDom(domContent)
|
||||
|
||||
fetch.mockImplementationOnce(mockFetch)
|
||||
|
||||
const innerButton = dom.querySelector('button')
|
||||
innerButton.dispatchEvent(new Event('click', {bubbles: true}))
|
||||
await flushPromises()
|
||||
|
||||
return dom
|
||||
}
|
||||
|
||||
const alertBackup = global.alert
|
||||
const confirmBackup = global.confirm
|
||||
const fetchBackup = global.fetch
|
||||
|
||||
global.fetch = vi.fn()
|
||||
global.alert = vi.fn()
|
||||
global.confirm = vi.fn()
|
||||
|
||||
await use(load)
|
||||
|
||||
global.fetch = fetchBackup
|
||||
global.confirm = confirmBackup
|
||||
global.alert = alertBackup
|
||||
},
|
||||
})
|
||||
|
||||
triggerTest('trigger is called on button click', async ({clickTrigger}) => {
|
||||
const dom = await clickTrigger(
|
||||
`<wcs-trigger-button
|
||||
trigger-url="https://trigger.test"
|
||||
csrf-token="test-csrf-token"
|
||||
proxy-url="https://proxy.test">
|
||||
</wcs/trigger-button>`,
|
||||
async (url, options) => {
|
||||
expect(url).toBe("https://proxy.test")
|
||||
expect(options.headers['X-CSRFToken']).toBe('test-csrf-token')
|
||||
expect(options.method).toBe('POST')
|
||||
expect(JSON.parse(options.body)).toStrictEqual({'trigger-url': 'https://trigger.test'})
|
||||
return {ok: true}
|
||||
}
|
||||
)
|
||||
|
||||
const innerButton = dom.querySelector('wcs-trigger-button button')
|
||||
expect(fetch).toHaveBeenCalledOnce()
|
||||
// Button should be disabled after successfull call
|
||||
expect(innerButton.disabled).toBe(true)
|
||||
})
|
||||
|
||||
triggerTest('error message is shown on unsuccessfull trigger call', async ({clickTrigger}) => {
|
||||
const dom = await clickTrigger(
|
||||
`<wcs-trigger-button></wcs/trigger-button>`,
|
||||
async () => ({ok: false})
|
||||
)
|
||||
|
||||
expect(fetch).toHaveBeenCalledOnce()
|
||||
expect(alert).toHaveBeenCalledWith('An error occured, please retry later.')
|
||||
const triggerButton = dom.querySelector('wcs-trigger-button')
|
||||
const innerButton = dom.querySelector('wcs-trigger-button button')
|
||||
expect(innerButton.disabled).toBe(false)
|
||||
})
|
||||
|
||||
triggerTest('confirmation message is shown if set', async ({clickTrigger}) => {
|
||||
confirm.mockImplementationOnce(() => true)
|
||||
let dom = await clickTrigger(
|
||||
`<wcs-trigger-button confirmation-message="Confirmation">
|
||||
</wcs/trigger-button>`,
|
||||
async () => ({ok: true})
|
||||
)
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith('Confirmation')
|
||||
expect(fetch).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
triggerTest('trigger is not called if confirmation is dismissed', async ({clickTrigger}) => {
|
||||
confirm.mockImplementationOnce(() => false)
|
||||
let dom = await clickTrigger(
|
||||
`<wcs-trigger-button confirmation-message="Confirmation">
|
||||
</wcs/trigger-button>`
|
||||
)
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith('Confirmation')
|
||||
expect(fetch).not.toHaveBeenCalledOnce()
|
||||
})
|
|
@ -932,7 +932,7 @@ def test_card_cell_table_mode_render(mock_send, context, app):
|
|||
assert len(requests_get.call_args_list) == 1
|
||||
assert (
|
||||
requests_get.call_args_list[0][0][0]
|
||||
== '/api/cards/card_model_1/list/foo?include-fields=on&include-submission=on&include-workflow=on&filter-internal-id=11'
|
||||
== '/api/cards/card_model_1/list/foo?include-fields=on&include-submission=on&include-workflow=on&include-actions=on&filter-internal-id=11'
|
||||
)
|
||||
assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://127.0.0.1:8999/'
|
||||
|
||||
|
@ -944,7 +944,7 @@ def test_card_cell_table_mode_render(mock_send, context, app):
|
|||
assert len(requests_get.call_args_list) == 1
|
||||
assert (
|
||||
requests_get.call_args_list[0][0][0]
|
||||
== '/api/cards/card_model_1/list?include-fields=on&include-submission=on&include-workflow=on&filter-internal-id=11'
|
||||
== '/api/cards/card_model_1/list?include-fields=on&include-submission=on&include-workflow=on&include-actions=on&filter-internal-id=11'
|
||||
)
|
||||
assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://127.0.0.1:8999/'
|
||||
|
||||
|
@ -1299,6 +1299,55 @@ def test_card_cell_table_mode_render_custom_schema_link_entry(mock_send, context
|
|||
assert PyQuery(result).find('table tr td a') == []
|
||||
|
||||
|
||||
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
|
||||
def test_card_cell_table_mode_render_custom_schema_action_entry(mock_send, context):
|
||||
page = Page.objects.create(title='xxx', template_name='standard')
|
||||
cell = WcsCardCell.objects.create(
|
||||
page=page,
|
||||
placeholder='content',
|
||||
order=0,
|
||||
carddef_reference='default:card_model_1',
|
||||
custom_schema={
|
||||
'cells': [
|
||||
{
|
||||
'varname': '@action@',
|
||||
'trigger_id': 'jump:trigger-1',
|
||||
'action_label': 'Label {{ card.fields.fielda }}',
|
||||
'action_ask_confirmation': True,
|
||||
'action_confirmation_template': 'Confirmation {{ card.fields.fielda }}',
|
||||
'unavailable_action_mode': 'hide',
|
||||
},
|
||||
]
|
||||
},
|
||||
display_mode='table',
|
||||
related_card_path='__all__',
|
||||
)
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
cell.modify_global_context(context, request)
|
||||
context['synchronous'] = True # to get fresh content
|
||||
|
||||
result = cell.render(context)
|
||||
button = PyQuery(result).find('table tr:first-child td:first-child wcs-trigger-button')
|
||||
assert button.attr['label'] == 'Label <i>a</i>'
|
||||
assert button.attr['proxy-url'] == reverse('wcs-trigger-proxy')
|
||||
assert button.attr['trigger-url'] == 'https://jump.test/trigger-1'
|
||||
assert button.attr['confirmation-message'] == 'Confirmation <i>a</i>'
|
||||
assert button.attr['unavailable-mode'] == 'hide'
|
||||
|
||||
cell.custom_schema['cells'][0]['trigger_id'] = 'unavailable-trigger'
|
||||
cell.save()
|
||||
result = cell.render(context)
|
||||
button = PyQuery(result).find('table tr:first-child td:first-child wcs-trigger-button')
|
||||
assert button.attr['trigger-url'] is None
|
||||
|
||||
cell.custom_schema['cells'][0]['action_ask_confirmation'] = False
|
||||
cell.save()
|
||||
result = cell.render(context)
|
||||
button = PyQuery(result).find('table tr:first-child td:first-child wcs-trigger-button')
|
||||
assert button.attr['action_confirmation_template'] is None
|
||||
|
||||
|
||||
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
|
||||
@pytest.mark.parametrize('with_headers', [True, False])
|
||||
def test_card_cell_table_mode_render_with_headers(mock_send, context, with_headers):
|
||||
|
@ -1339,6 +1388,7 @@ def test_card_cell_table_mode_render_with_headers(mock_send, context, with_heade
|
|||
{'varname': 'info:last_update_time'},
|
||||
{'varname': 'info:status'},
|
||||
{'varname': 'info:text'},
|
||||
{'varname': '@action@', 'header': 'Action', 'action_label': '', 'trigger_id': ''},
|
||||
],
|
||||
},
|
||||
display_mode='table',
|
||||
|
@ -1351,7 +1401,7 @@ def test_card_cell_table_mode_render_with_headers(mock_send, context, with_heade
|
|||
|
||||
result = cell.render(context)
|
||||
if with_headers:
|
||||
assert len(PyQuery(result).find('table thead th')) == 14
|
||||
assert len(PyQuery(result).find('table thead th')) == 15
|
||||
assert PyQuery(result).find('table thead th:nth-child(1)').text() == ''
|
||||
assert PyQuery(result).find('table thead th:nth-child(2)').text() == 'My Custom Header'
|
||||
assert PyQuery(result).find('table thead th:nth-child(3)').text() == 'Field B'
|
||||
|
@ -1366,6 +1416,7 @@ def test_card_cell_table_mode_render_with_headers(mock_send, context, with_heade
|
|||
assert PyQuery(result).find('table thead th:nth-child(12)').text() == 'Last modified'
|
||||
assert PyQuery(result).find('table thead th:nth-child(13)').text() == 'Status'
|
||||
assert PyQuery(result).find('table thead th:nth-child(14)').text() == 'Text'
|
||||
assert PyQuery(result).find('table thead th:nth-child(15)').text() == 'Action'
|
||||
else:
|
||||
assert PyQuery(result).find('table thead') == []
|
||||
|
||||
|
@ -1879,7 +1930,7 @@ def test_card_cell_list_mode_render(mock_send, context, app):
|
|||
assert len(requests_get.call_args_list) == 1
|
||||
assert (
|
||||
requests_get.call_args_list[0][0][0]
|
||||
== '/api/cards/card_model_1/list/foo?include-fields=on&include-submission=on&include-workflow=on&filter-internal-id=11'
|
||||
== '/api/cards/card_model_1/list/foo?include-fields=on&include-submission=on&include-workflow=on&include-actions=on&filter-internal-id=11'
|
||||
)
|
||||
assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://127.0.0.1:8999/'
|
||||
|
||||
|
@ -1891,7 +1942,7 @@ def test_card_cell_list_mode_render(mock_send, context, app):
|
|||
assert len(requests_get.call_args_list) == 1
|
||||
assert (
|
||||
requests_get.call_args_list[0][0][0]
|
||||
== '/api/cards/card_model_1/list?include-fields=on&include-submission=on&include-workflow=on&filter-internal-id=11'
|
||||
== '/api/cards/card_model_1/list?include-fields=on&include-submission=on&include-workflow=on&include-actions=on&filter-internal-id=11'
|
||||
)
|
||||
assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://127.0.0.1:8999/'
|
||||
|
||||
|
@ -3220,6 +3271,56 @@ def test_card_cell_card_mode_render_custom_schema_link_entry(mock_send, context,
|
|||
assert PyQuery(result).find('.value a') == []
|
||||
|
||||
|
||||
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
|
||||
def test_card_cell_card_mode_render_custom_schema_action_entry(mock_send, context, app):
|
||||
page = Page.objects.create(title='xxx', template_name='standard')
|
||||
cell = WcsCardCell.objects.create(
|
||||
page=page,
|
||||
placeholder='content',
|
||||
order=0,
|
||||
carddef_reference='default:card_model_1',
|
||||
custom_schema={
|
||||
'cells': [
|
||||
{
|
||||
'varname': '@action@',
|
||||
'trigger_id': 'jump:trigger-1',
|
||||
'action_label': 'Label {{ card.fields.fielda }}',
|
||||
'action_ask_confirmation': True,
|
||||
'action_confirmation_template': 'Confirmation {{ card.fields.fielda }}',
|
||||
'unavailable_action_mode': 'hide',
|
||||
},
|
||||
]
|
||||
},
|
||||
related_card_path='',
|
||||
)
|
||||
|
||||
context['card_model_1_id'] = 11
|
||||
request = RequestFactory().get('/')
|
||||
cell.modify_global_context(context, request)
|
||||
cell.repeat_index = 0
|
||||
context['synchronous'] = True # to get fresh content
|
||||
|
||||
result = cell.render(context)
|
||||
button = PyQuery(result).find('wcs-trigger-button')
|
||||
assert button.attr['label'] == 'Label <i>a</i>'
|
||||
assert button.attr['proxy-url'] == reverse('wcs-trigger-proxy')
|
||||
assert button.attr['trigger-url'] == 'https://jump.test/trigger-1'
|
||||
assert button.attr['confirmation-message'] == 'Confirmation <i>a</i>'
|
||||
assert button.attr['unavailable-mode'] == 'hide'
|
||||
|
||||
cell.custom_schema['cells'][0]['trigger_id'] = 'unavailable-trigger'
|
||||
cell.save()
|
||||
result = cell.render(context)
|
||||
button = PyQuery(result).find('wcs-trigger-button')
|
||||
assert button.attr['trigger-url'] is None
|
||||
|
||||
cell.custom_schema['cells'][0]['action_ask_confirmation'] = False
|
||||
cell.save()
|
||||
result = cell.render(context)
|
||||
button = PyQuery(result).find('wcs-trigger-button')
|
||||
assert button.attr['action_confirmation_template'] is None
|
||||
|
||||
|
||||
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
|
||||
def test_card_cell_card_mode_render_all_cards(mock_send, nocache, app):
|
||||
page = Page.objects.create(title='xxx', slug='foo', template_name='standard')
|
||||
|
|
|
@ -166,6 +166,7 @@ WCS_CARDS_DATA = {
|
|||
'item_raw': 'foo',
|
||||
},
|
||||
},
|
||||
'actions': {'jump:trigger-1': 'https://jump.test/trigger-1'},
|
||||
},
|
||||
{
|
||||
'id': 12,
|
||||
|
|
Loading…
Reference in New Issue