feat(2.0): Salvar único — templates entram no form principal da config
- cards de template movidos para dentro do form principal (inputs em array templates_json[op]/templates_active[op]); sem forms aninhados - "Gerar template" virou ação LOCAL no browser (data-seed preenche o editor; nada é salvo até o Salvar único) — sem reload - removidos os handlers save_template/gen_template e os 4 botões Salvar por card - o handler de update salva config + mapa + todos os templates numa tacada, validando o JSON de cada um (inválido = não salvo, com aviso) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5ee12c75d2
commit
aa5bf5eb75
3 changed files with 154 additions and 153 deletions
|
|
@ -10,6 +10,30 @@ if (isset($_POST["update"]) || isset($_POST["sync"])) {
|
||||||
// Save whatever was typed (URL/key/defaults) before anything else.
|
// Save whatever was typed (URL/key/defaults) before anything else.
|
||||||
$config->update($_POST);
|
$config->update($_POST);
|
||||||
|
|
||||||
|
// Templates de payload: salvos junto pelo botão Salvar único.
|
||||||
|
if (!empty($_POST['templates_json']) && is_array($_POST['templates_json'])) {
|
||||||
|
foreach ($_POST['templates_json'] as $op => $json) {
|
||||||
|
if (!in_array($op, PluginRedmineTemplateengine::OPERATIONS, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$json = (string) $json;
|
||||||
|
// Defensivo contra escaping de aspas na camada de entrada.
|
||||||
|
if ($json !== '' && json_decode($json, true) === null && json_decode(stripslashes($json), true) !== null) {
|
||||||
|
$json = stripslashes($json);
|
||||||
|
}
|
||||||
|
if ($json !== '' && json_decode($json, true) === null) {
|
||||||
|
Session::addMessageAfterRedirect(
|
||||||
|
sprintf(__('Template "%s" não é JSON válido — não foi salvo.', 'redmine'), $op),
|
||||||
|
false,
|
||||||
|
ERROR
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$active = !empty($_POST['templates_active'][$op]) && $json !== '';
|
||||||
|
PluginRedmineTemplateengine::save($op, $json, $active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($_POST["sync"])) {
|
if (isset($_POST["sync"])) {
|
||||||
$metadata = PluginRedmineRedmineapi::syncMetadata();
|
$metadata = PluginRedmineRedmineapi::syncMetadata();
|
||||||
if ($metadata !== false) {
|
if ($metadata !== false) {
|
||||||
|
|
@ -26,43 +50,6 @@ if (isset($_POST["update"]) || isset($_POST["sync"])) {
|
||||||
Html::back();
|
Html::back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save a payload template (motor 2.0).
|
|
||||||
if (isset($_POST['save_template'])) {
|
|
||||||
$op = (string) ($_POST['template_op'] ?? '');
|
|
||||||
if (in_array($op, PluginRedmineTemplateengine::OPERATIONS, true)) {
|
|
||||||
$json = (string) ($_POST['template_json'] ?? '');
|
|
||||||
// Defensivo contra escaping de aspas na camada de entrada.
|
|
||||||
if ($json !== '' && json_decode($json, true) === null && json_decode(stripslashes($json), true) !== null) {
|
|
||||||
$json = stripslashes($json);
|
|
||||||
}
|
|
||||||
if ($json !== '' && json_decode($json, true) === null) {
|
|
||||||
Session::addMessageAfterRedirect(__('Template não é JSON válido — nada foi salvo.', 'redmine'), false, ERROR);
|
|
||||||
} else {
|
|
||||||
PluginRedmineTemplateengine::save($op, $json, !empty($_POST['template_active']) && $json !== '');
|
|
||||||
Session::addMessageAfterRedirect(__('Template salvo.', 'redmine'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Html::back();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the seed template for an operation (comportamento 1.x + custom fields descobertos).
|
|
||||||
if (isset($_POST['gen_template'])) {
|
|
||||||
$op = (string) ($_POST['template_op'] ?? '');
|
|
||||||
if (in_array($op, PluginRedmineTemplateengine::OPERATIONS, true)) {
|
|
||||||
$seed = PluginRedmineTemplateengine::mergeDiscoveredCustomFields(
|
|
||||||
$op,
|
|
||||||
PluginRedmineTemplateengine::defaultTemplate($op)
|
|
||||||
);
|
|
||||||
PluginRedmineTemplateengine::save(
|
|
||||||
$op,
|
|
||||||
json_encode($seed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
Session::addMessageAfterRedirect(__('Template gerado (inativo). Revise os campos e ative quando estiver pronto.', 'redmine'));
|
|
||||||
}
|
|
||||||
Html::back();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a manual user mapping override (GLPI user -> Redmine user).
|
// Add a manual user mapping override (GLPI user -> Redmine user).
|
||||||
if (isset($_POST["add_usermap"])) {
|
if (isset($_POST["add_usermap"])) {
|
||||||
$uid = (int) ($_POST['override_users_id'] ?? 0);
|
$uid = (int) ($_POST['override_users_id'] ?? 0);
|
||||||
|
|
|
||||||
|
|
@ -206,10 +206,16 @@ class PluginRedmineConfig extends CommonDBTM {
|
||||||
$templates = [];
|
$templates = [];
|
||||||
foreach (PluginRedmineTemplateengine::OPERATIONS as $op) {
|
foreach (PluginRedmineTemplateengine::OPERATIONS as $op) {
|
||||||
$row = PluginRedmineTemplateengine::getRow($op);
|
$row = PluginRedmineTemplateengine::getRow($op);
|
||||||
|
$seed = PluginRedmineTemplateengine::mergeDiscoveredCustomFields(
|
||||||
|
$op,
|
||||||
|
PluginRedmineTemplateengine::defaultTemplate($op)
|
||||||
|
);
|
||||||
$templates[$op] = [
|
$templates[$op] = [
|
||||||
'label' => $tpl_labels[$op] ?? $op,
|
'label' => $tpl_labels[$op] ?? $op,
|
||||||
'template' => (string) ($row['template'] ?? ''),
|
'template' => (string) ($row['template'] ?? ''),
|
||||||
'is_active' => (int) ($row['is_active'] ?? 0),
|
'is_active' => (int) ($row['is_active'] ?? 0),
|
||||||
|
// seed pro botão "Gerar template" (ação local no browser)
|
||||||
|
'seed' => json_encode($seed, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,115 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# ====================================================================
|
||||||
|
Templates de Payload (motor 2.0) — editor linha-a-linha + paleta
|
||||||
|
==================================================================== #}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title mb-0"><i class="ti ti-code me-1"></i>{{ __('Templates de Payload (avançado)', 'redmine') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{{ __('Personalize o JSON enviado ao Redmine em cada operação. Sem template ativo, o plugin usa o comportamento padrão. "Gerar template" cria o ponto de partida equivalente ao padrão + campos obrigatórios descobertos no seu Redmine.', 'redmine') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Variáveis do GLPI como AUTOCOMPLETE nos campos de valor #}
|
||||||
|
<datalist id="rm-glpi-vars">
|
||||||
|
{% for v in palette_vars %}
|
||||||
|
<option value="{{ v.tag }}">{{ v.group }} — {{ v.desc }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
{# Referência opcional (colapsada) — descoberta das tags para remapeamentos #}
|
||||||
|
<div class="mb-3">
|
||||||
|
<a class="small" data-bs-toggle="collapse" href="#rm-glpi-vars-panel" role="button">
|
||||||
|
<i class="ti ti-variable me-1"></i>{{ __('Variáveis do GLPI disponíveis — o "DE" (referência; nos campos de valor há autocomplete)', 'redmine') }}
|
||||||
|
</a>
|
||||||
|
<div class="collapse" id="rm-glpi-vars-panel">
|
||||||
|
<div class="card card-body p-2 mt-1" style="max-height:300px;overflow-y:auto;columns:2">
|
||||||
|
{% set lastgroup = '' %}
|
||||||
|
{% for v in palette_vars %}
|
||||||
|
{% if v.group != lastgroup %}
|
||||||
|
{% set lastgroup = v.group %}
|
||||||
|
<div class="mt-1 mb-1 text-uppercase text-muted" style="font-size:.7rem;break-inside:avoid">{{ v.group }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="rm-var px-1 rounded" data-tag="{{ v.tag }}" title="{{ v.desc }}" style="break-inside:avoid">
|
||||||
|
<code style="font-size:.75rem">{{ v.tag }}</code>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ---- Editores por operação (largura total) ---- #}
|
||||||
|
<div>
|
||||||
|
{% for op, t in templates %}
|
||||||
|
<div class="card mb-3 rm-tpl-card">
|
||||||
|
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
<strong>{{ t.label }}</strong>
|
||||||
|
<code class="ms-1" style="font-size:.7rem">{{ op }}</code>
|
||||||
|
{% if t.is_active %}
|
||||||
|
<span class="badge bg-green-lt text-green ms-1">{{ __('ativo', 'redmine') }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary-lt ms-1">{{ __('inativo (usa o padrão)', 'redmine') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary rm-tpl-toggle">{{ __('Ver JSON', 'redmine') }}</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary rm-tpl-gen" data-seed="{{ t.seed }}"
|
||||||
|
data-confirm="{{ __('Preencher o editor com o template padrão (+ campos obrigatórios do seu Redmine)? O conteúdo atual do editor será substituído — nada é salvo até você clicar em Salvar.', 'redmine') }}">
|
||||||
|
<i class="ti ti-wand me-1"></i>{{ __('Gerar template', 'redmine') }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% if field_dicts[op] is defined and field_dicts[op] is not empty %}
|
||||||
|
<div class="px-3 py-1 border-bottom">
|
||||||
|
<a class="small" data-bs-toggle="collapse" href="#rmdict-{{ op }}" role="button">
|
||||||
|
<i class="ti ti-book me-1"></i>{{ __('Campos que o Redmine espera — o "PARA" (clique num campo para adicioná-lo)', 'redmine') }}
|
||||||
|
</a>
|
||||||
|
<div class="collapse" id="rmdict-{{ op }}">
|
||||||
|
<table class="table table-sm table-hover mb-1 mt-1" style="font-size:.75rem">
|
||||||
|
<tbody>
|
||||||
|
{% for f in field_dicts[op] %}
|
||||||
|
<tr class="rm-dict-row" data-path="{{ f.path }}" title="{{ __('Clique para adicionar este campo ao template', 'redmine') }}">
|
||||||
|
<td style="white-space:nowrap">
|
||||||
|
<code>{{ f.path }}</code>
|
||||||
|
{% if f.required %}<span class="badge bg-red-lt text-red ms-1">{{ __('obrigatório', 'redmine') }}</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ f.desc }}
|
||||||
|
{% if f.values is not empty %}
|
||||||
|
<div class="text-muted" style="font-size:.7rem">{{ f.values|join(' · ') }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<div class="rm-tpl-lines"></div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary rm-tpl-add mt-1">
|
||||||
|
<i class="ti ti-plus me-1"></i>{{ __('Adicionar campo', 'redmine') }}
|
||||||
|
</button>
|
||||||
|
<textarea name="templates_json[{{ op }}]" class="form-control rm-tpl-json d-none mt-1" rows="10"
|
||||||
|
spellcheck="false" style="font-family: monospace; font-size:.8rem">{{ t.template }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer py-2">
|
||||||
|
<label class="form-check m-0">
|
||||||
|
<input type="checkbox" class="form-check-input" name="templates_active[{{ op }}]" value="1" {{ t.is_active ? 'checked' : '' }}>
|
||||||
|
<span class="form-check-label">{{ __('Ativo (salvo pelo botão Salvar no fim da página)', 'redmine') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-end mb-3">
|
<div class="d-flex justify-content-end mb-3">
|
||||||
<button type="submit" name="update" value="1" class="btn btn-primary">
|
<button type="submit" name="update" value="1" class="btn btn-primary">
|
||||||
<i class="ti ti-device-floppy me-1"></i>{{ _x('button', 'Save') }}
|
<i class="ti ti-device-floppy me-1"></i>{{ _x('button', 'Save') }}
|
||||||
|
|
@ -141,122 +250,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ====================================================================
|
|
||||||
Templates de Payload (motor 2.0) — editor linha-a-linha + paleta
|
|
||||||
==================================================================== #}
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title mb-0"><i class="ti ti-code me-1"></i>{{ __('Templates de Payload (avançado)', 'redmine') }}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
{{ __('Personalize o JSON enviado ao Redmine em cada operação. Sem template ativo, o plugin usa o comportamento padrão. "Gerar template" cria o ponto de partida equivalente ao padrão + campos obrigatórios descobertos no seu Redmine.', 'redmine') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Variáveis do GLPI como AUTOCOMPLETE nos campos de valor #}
|
|
||||||
<datalist id="rm-glpi-vars">
|
|
||||||
{% for v in palette_vars %}
|
|
||||||
<option value="{{ v.tag }}">{{ v.group }} — {{ v.desc }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
{# Referência opcional (colapsada) — descoberta das tags para remapeamentos #}
|
|
||||||
<div class="mb-3">
|
|
||||||
<a class="small" data-bs-toggle="collapse" href="#rm-glpi-vars-panel" role="button">
|
|
||||||
<i class="ti ti-variable me-1"></i>{{ __('Variáveis do GLPI disponíveis — o "DE" (referência; nos campos de valor há autocomplete)', 'redmine') }}
|
|
||||||
</a>
|
|
||||||
<div class="collapse" id="rm-glpi-vars-panel">
|
|
||||||
<div class="card card-body p-2 mt-1" style="max-height:300px;overflow-y:auto;columns:2">
|
|
||||||
{% set lastgroup = '' %}
|
|
||||||
{% for v in palette_vars %}
|
|
||||||
{% if v.group != lastgroup %}
|
|
||||||
{% set lastgroup = v.group %}
|
|
||||||
<div class="mt-1 mb-1 text-uppercase text-muted" style="font-size:.7rem;break-inside:avoid">{{ v.group }}</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="rm-var px-1 rounded" data-tag="{{ v.tag }}" title="{{ v.desc }}" style="break-inside:avoid">
|
|
||||||
<code style="font-size:.75rem">{{ v.tag }}</code>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# ---- Editores por operação (largura total) ---- #}
|
|
||||||
<div>
|
|
||||||
{% for op, t in templates %}
|
|
||||||
<form method="post" action="{{ action_url }}">
|
|
||||||
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token_value }}">
|
|
||||||
<input type="hidden" name="template_op" value="{{ op }}">
|
|
||||||
<div class="card mb-3 rm-tpl-card">
|
|
||||||
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
|
||||||
<span>
|
|
||||||
<strong>{{ t.label }}</strong>
|
|
||||||
<code class="ms-1" style="font-size:.7rem">{{ op }}</code>
|
|
||||||
{% if t.is_active %}
|
|
||||||
<span class="badge bg-green-lt text-green ms-1">{{ __('ativo', 'redmine') }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary-lt ms-1">{{ __('inativo (usa o padrão)', 'redmine') }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary rm-tpl-toggle">{{ __('Ver JSON', 'redmine') }}</button>
|
|
||||||
<button type="submit" name="gen_template" value="1" class="btn btn-sm btn-outline-primary"
|
|
||||||
onclick="return confirm('{{ __('Gerar/regravar o template desta operação a partir do padrão? O conteúdo atual será substituído (fica inativo até você ativar).', 'redmine') }}');">
|
|
||||||
<i class="ti ti-wand me-1"></i>{{ __('Gerar template', 'redmine') }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% if field_dicts[op] is defined and field_dicts[op] is not empty %}
|
|
||||||
<div class="px-3 py-1 border-bottom">
|
|
||||||
<a class="small" data-bs-toggle="collapse" href="#rmdict-{{ op }}" role="button">
|
|
||||||
<i class="ti ti-book me-1"></i>{{ __('Campos que o Redmine espera — o "PARA" (clique num campo para adicioná-lo)', 'redmine') }}
|
|
||||||
</a>
|
|
||||||
<div class="collapse" id="rmdict-{{ op }}">
|
|
||||||
<table class="table table-sm table-hover mb-1 mt-1" style="font-size:.75rem">
|
|
||||||
<tbody>
|
|
||||||
{% for f in field_dicts[op] %}
|
|
||||||
<tr class="rm-dict-row" data-path="{{ f.path }}" title="{{ __('Clique para adicionar este campo ao template', 'redmine') }}">
|
|
||||||
<td style="white-space:nowrap">
|
|
||||||
<code>{{ f.path }}</code>
|
|
||||||
{% if f.required %}<span class="badge bg-red-lt text-red ms-1">{{ __('obrigatório', 'redmine') }}</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ f.desc }}
|
|
||||||
{% if f.values is not empty %}
|
|
||||||
<div class="text-muted" style="font-size:.7rem">{{ f.values|join(' · ') }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="card-body py-2">
|
|
||||||
<div class="rm-tpl-lines"></div>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary rm-tpl-add mt-1">
|
|
||||||
<i class="ti ti-plus me-1"></i>{{ __('Adicionar campo', 'redmine') }}
|
|
||||||
</button>
|
|
||||||
<textarea name="template_json" class="form-control rm-tpl-json d-none mt-1" rows="10"
|
|
||||||
spellcheck="false" style="font-family: monospace; font-size:.8rem">{{ t.template }}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer py-2 d-flex justify-content-between align-items-center">
|
|
||||||
<label class="form-check m-0">
|
|
||||||
<input type="checkbox" class="form-check-input" name="template_active" value="1" {{ t.is_active ? 'checked' : '' }}>
|
|
||||||
<span class="form-check-label">{{ __('Ativo', 'redmine') }}</span>
|
|
||||||
</label>
|
|
||||||
<button type="submit" name="save_template" value="1" class="btn btn-sm btn-primary">
|
|
||||||
<i class="ti ti-device-floppy me-1"></i>{{ _x('button', 'Save') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% verbatim %}
|
{% verbatim %}
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|
@ -376,6 +369,21 @@
|
||||||
setMode();
|
setMode();
|
||||||
});
|
});
|
||||||
addBtn.addEventListener('click', function () { addRow('', ''); });
|
addBtn.addEventListener('click', function () { addRow('', ''); });
|
||||||
|
// Gerar template: preenche o editor localmente (nada é salvo até o Salvar único)
|
||||||
|
var genBtn = card.querySelector('.rm-tpl-gen');
|
||||||
|
if (genBtn) {
|
||||||
|
genBtn.addEventListener('click', function () {
|
||||||
|
if (!confirm(genBtn.dataset.confirm || 'Substituir o conteúdo do editor pelo template padrão?')) { return; }
|
||||||
|
try {
|
||||||
|
ta.value = JSON.stringify(JSON.parse(genBtn.dataset.seed || '{}'), null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
ta.value = genBtn.dataset.seed || '';
|
||||||
|
}
|
||||||
|
rawMode = false;
|
||||||
|
renderLines();
|
||||||
|
setMode();
|
||||||
|
});
|
||||||
|
}
|
||||||
// Dicionário "PARA": clique num campo adiciona a linha com o caminho pronto
|
// Dicionário "PARA": clique num campo adiciona a linha com o caminho pronto
|
||||||
card.querySelectorAll('.rm-dict-row').forEach(function (r) {
|
card.querySelectorAll('.rm-dict-row').forEach(function (r) {
|
||||||
r.style.cursor = 'pointer';
|
r.style.cursor = 'pointer';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue