Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rude-lamps-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fedimod/fires-server": patch
---

Implement ability to assign labels and internal comment during import
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Dataset from '#models/dataset'
import Label from '#models/label'
import { ImportFileService } from '#services/import_file_service'
import {
importFileValidator,
Expand Down Expand Up @@ -35,6 +36,7 @@ export default class ImportsController {

const data = await request.validateUsing(importFileValidator)
const dataset = await Dataset.findOrFail(data.dataset)
const labels = await Label.query().orderBy('deprecatedAt', 'desc').orderBy('id', 'desc')

if (!data.file.isValid || !data.file.tmpPath) {
session.flash('notification', {
Expand Down Expand Up @@ -63,6 +65,10 @@ export default class ImportsController {
unchanged: results.unchanged,
missing: results.missing,
defaultType: data.defaultType,
labels: labels.filter((l) => l.deprecatedAt === null).map((label) => label.serialize()),
deprecatedLabels: labels
.filter((l) => l.deprecatedAt !== null)
.map((label) => label.serialize()),
})
}

Expand Down
34 changes: 28 additions & 6 deletions components/fires-server/app/validators/admin/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ import Label from '#models/label'
import vine from '@vinejs/vine'
import { entityKeyActor, entityKeyDomain } from '#validators/admin/dataset_change'

const labelsValidator = vine.createRule(
async (value, _options, field) => {
if (!field.isValid) {
return
}

if (!Array.isArray(value)) {
field.report('Invalid labels value', 'invalid_type', field)
return
}

const labels = await Label.findMany(value)
if (value.length !== labels.length) {
field.report('Invalid labels', 'invalid', field)
return
}
},
{ isAsync: true }
)

export const importValidator = vine.compile(
vine.object({
dataset: vine
Expand Down Expand Up @@ -59,13 +79,15 @@ export const performImportValidator = vine.compile(
type: vine.enum(['recommendation', 'advisory', 'retraction']),
recommended_policy: vine.enum(DatasetChange.policies),
labels: vine
.array(
vine.string().exists({
table: Label.table,
column: 'id',
})
)
.array(vine.string())
.parse((value) => {
if (typeof value === 'string') {
return value.split(/,\s*/)
}
return value
})
.distinct()
.use(labelsValidator())
.optional(),
comment: vine.string().optional(),
entity_kind: vine.enum(DatasetChange.entities),
Expand Down
1 change: 1 addition & 0 deletions components/fires-server/resources/scripts/admin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-nocheck
import './lib/copyable'
import './admin/import'

function addTranslation() {
const template = document.getElementById('new-translation')
Expand Down
140 changes: 140 additions & 0 deletions components/fires-server/resources/scripts/admin/import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
;(() => {
const reviewForm = document.getElementById('import-review')
const manageLabelsDialog = document.getElementById('import-manage-labels')
if (!(reviewForm instanceof HTMLFormElement && manageLabelsDialog instanceof HTMLDialogElement)) {
return
}

const html = document.documentElement

const isOpenClass = 'modal-is-open'

/**
* @type {HTMLDialogElement|null}
*/
let visibleModal = null

/**
*
* @param {HTMLDialogElement} modal
*/
const openModal = (modal) => {
html.classList.add(isOpenClass)
modal.showModal()
visibleModal = modal
}

/**
*
* @param {HTMLDialogElement} modal
*/
const closeModal = (modal) => {
handleClose(modal)
modal.close('cancel')
}
/**
* @param {HTMLDialogElement} modal
*/
const handleClose = (modal) => {
html.classList.remove(isOpenClass)
visibleModal = null
}

document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && visibleModal) {
closeModal(visibleModal)
}
})

document.addEventListener('click', (event) => {
if (!event.target || !(event.target instanceof HTMLButtonElement)) return

// open modal button
if (!visibleModal && event.target.dataset.row) {
event.preventDefault()

const row = event.target.dataset.row
const rowComment = event.target.dataset.rowComment
const labelInput = document.getElementById(`${row}-labels`)
const commentInput = document.getElementById(`${row}-comment`)

if (!(labelInput instanceof HTMLInputElement)) {
return
}

const labels = labelInput.value.split(',').filter((v) => v !== '')

manageLabelsDialog.querySelectorAll('input[type="checkbox"]').forEach((input) => {
if (!(input instanceof HTMLInputElement)) return
if (labels.includes(input.value)) {
input.setAttribute('checked', 'checked')
input.checked = true
} else {
input.removeAttribute('checked')
input.checked = false
}
})

const commentTextarea = manageLabelsDialog.querySelector('input[name="comment"]')
if (commentTextarea instanceof HTMLInputElement && commentInput instanceof HTMLInputElement) {
commentTextarea.value = commentInput.value ?? ''
}

const rowCommentSection = manageLabelsDialog.querySelector('#manage-row-comment')
const rowCommentField = manageLabelsDialog.querySelector('#manage-row-comment-text')
if (rowCommentField instanceof HTMLParagraphElement) {
if (!rowComment) {
rowCommentSection?.classList.add('d-hidden')
} else {
rowCommentSection?.classList.remove('d-hidden')
rowCommentField.innerText = rowComment
}
}

manageLabelsDialog.dataset.row = row
openModal(manageLabelsDialog)
}

// close modal button
if (visibleModal && event.target.getAttribute('rel') === 'prev') {
closeModal(visibleModal)
}
})

manageLabelsDialog.addEventListener('close', (event) => {
handleClose(manageLabelsDialog)
if (!event.currentTarget || !(event.currentTarget instanceof HTMLDialogElement)) return
if (event.currentTarget.returnValue == 'cancel') {
return
}

const form = manageLabelsDialog.getElementsByTagName('form')[0]
const formData = new FormData(form)

const comment = formData.get('comment')?.toString()
const labels = Array.from(formData.getAll('labels[]'))
const row = manageLabelsDialog.dataset.row
const labelInput = document.getElementById(`${row}-labels`)
const commentInput = document.getElementById(`${row}-comment`)

if (!(labelInput instanceof HTMLInputElement) || !(commentInput instanceof HTMLInputElement)) {
return
}

labelInput.value = labels.join(',')
if (comment) {
commentInput.value = comment
} else {
commentInput.value = ''
}

console.log(`Set ${row} to ${labelInput.value}`)
})

manageLabelsDialog.querySelector('#cancel')?.addEventListener('click', () => {
if (visibleModal === manageLabelsDialog) {
manageLabelsDialog.dataset.row = undefined
closeModal(visibleModal)
}
})
})()
40 changes: 39 additions & 1 deletion components/fires-server/resources/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ p.lead {
margin-bottom: calc(var(--pico-typography-spacing-vertical) * 2);
}

blockquote > p:last-child {
margin-bottom: 0;
}

.d-hidden {
display: none;
}
Expand Down Expand Up @@ -200,6 +204,10 @@ button.inline {
margin-left: calc(var(--pico-spacing) * 2);
}

.mb-0 {
margin-bottom: 0 !important;
}

dl.labels dt {
font-size: 1.4rem;
font-weight: bold;
Expand Down Expand Up @@ -313,7 +321,8 @@ input:is([type='file']) {
}
}

[role='group']:has(+ .form-hint) {
[role='group']:has(+ .form-hint),
:is(h1, h2, h3, h4, h5, h6):has(+ .form-hint) {
margin-bottom: 0;

& + .form-hint {
Expand Down Expand Up @@ -543,3 +552,32 @@ select[disabled] {
pointer-events: none;
cursor: pointer;
}

dialog article {
padding: 0;

header,
footer {
margin: 0;
padding: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);
background: var(--pico-color-slate-850);
border: none;
}

header {
position: sticky;
top: 0px;
}

form {
padding: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);
}

footer {
position: sticky;
bottom: 0px;
// Negative to account for form > footer
margin: var(--pico-block-spacing-vertical) calc(var(--pico-block-spacing-horizontal) * -1);
margin-bottom: calc(var(--pico-block-spacing-vertical) * -1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<strong>Change Type</strong>
<strong>Kind</strong>
<strong>Key</strong>
<strong>Recommended Policy</strong>
<strong>Policy</strong>
<span></span> {{-- Edit button --}}
@if(retraction)
<span></span>
@endif
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="grid row">
<div class="grid row" id="changes-{{ index }}">
<input
type="hidden"
name="changes[{{ index }}][entity_kind]"
Expand All @@ -20,11 +20,20 @@

<input
type="hidden"
id="changes-{{ index }}-comment"
name="changes[{{ index }}][comment]"
value="{{ record.comment }}"
{{ html.attrs({ disabled: noUpdate }) }}
/>

<input
type="hidden"
id="changes-{{ index }}-labels"
name="changes[{{ index }}][labels]"
value=""
{{ html.attrs({ disabled: noUpdate }) }}
/>

<select
name="changes[{{ index }}][type]"
class="form-control"
Expand Down Expand Up @@ -73,6 +82,9 @@
{{ record.recommendedPolicy }}
@endif
</span>
@unless(noUpdate)
<button class="manage-labels" data-row="changes-{{ index }}" data-row-comment="{{ record.comment }}">Edit</button>
@end
@if(retraction)
<button class="remove-row" type="button">Ignore</button>
@endif
Expand Down
Loading