Module:Feedback: Difference between revisions
Appearance
ReisuDesign (talk | contribs) No edit summary Tag: Manual revert |
ReisuDesign (talk | contribs) No edit summary |
||
| Line 121: | Line 121: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
local function renderAttachments( | local function renderAttachments(entry) | ||
local wrapper = mw.html.create('div') | |||
:addClass('feedback-attachments') | |||
if entry.link and trim(entry.link) ~= '' then | |||
local link = mw.html.create('a') | |||
:addClass('feedback-link') | |||
:attr('href', trim(entry.link)) | |||
:attr('target', '_blank') | |||
:attr('rel', 'noopener noreferrer') | |||
:wikitext('View link') | |||
wrapper:node(link) | |||
end | |||
if entry.image and trim(entry.image) ~= '' then | |||
local image = mw.html.create('img') | |||
:addClass('feedback-image') | |||
:attr('src', trim(entry.image)) | |||
:attr('alt', 'Feedback image') | |||
:attr('loading', 'lazy') | |||
wrapper:node(image) | |||
end | |||
return tostring(wrapper) | |||
end | end | ||
Revision as of 13:09, 16 September 2026
| Main | Documentation | Usage |
This module implements {{Feedback}}.
local p = {}
------------------------------------------------------------
-- HELPERS
------------------------------------------------------------
local function isTruthy(value)
if value == nil then
return false
end
value = mw.text.trim(tostring(value)):lower()
return value == 'true'
or value == 'yes'
or value == '1'
or value == 'resolved'
end
local function trim(value)
if value == nil then
return ''
end
return mw.text.trim(tostring(value))
end
local function formatUser(user)
user = trim(user)
if user == '' then
return 'Anonymous'
end
return string.format(
'[[User:%s|%s]]',
user,
user
)
end
local function currentSubjectPage()
local title = mw.title.getCurrentTitle()
if title.subjectPageTitle then
return title.subjectPageTitle.fullText
end
return title.fullText
end
local function getBucket()
if not (mw.ext and mw.ext.bucket) then
return nil
end
return mw.ext.bucket('feedback')
end
local function sortNewestFirst(entries)
table.sort(
entries,
function(a, b)
local aDate = trim(a.createddate)
local bDate = trim(b.createddate)
return aDate > bDate
end
)
return entries
end
local function queryFeedback(page)
local bucket = getBucket()
if not bucket then
return {}
end
local query = bucket
query.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)
query.where(
'page',
page
)
local success, results = pcall(function()
return query.run()
end)
if not success or not results then
return {}
end
return sortNewestFirst(results)
end
------------------------------------------------------------
-- ATTACHMENTS
------------------------------------------------------------
local function renderAttachments(entry)
local wrapper = mw.html.create('div')
:addClass('feedback-attachments')
if entry.link and trim(entry.link) ~= '' then
local link = mw.html.create('a')
:addClass('feedback-link')
:attr('href', trim(entry.link))
:attr('target', '_blank')
:attr('rel', 'noopener noreferrer')
:wikitext('View link')
wrapper:node(link)
end
if entry.image and trim(entry.image) ~= '' then
local image = mw.html.create('img')
:addClass('feedback-image')
:attr('src', trim(entry.image))
:attr('alt', 'Feedback image')
:attr('loading', 'lazy')
wrapper:node(image)
end
return tostring(wrapper)
end
------------------------------------------------------------
-- RESOLUTION
------------------------------------------------------------
local function renderResolution(parent, entry)
local resolvedBy = trim(entry.resolvedby)
local resolvedDate = trim(entry.resolveddate)
if resolvedBy == '' and resolvedDate == '' then
return
end
local resolution =
parent:tag('div')
:addClass('feedback-resolution')
resolution:wikitext('↳ Resolved by ')
if resolvedBy ~= '' then
resolution:wikitext(
formatUser(resolvedBy)
)
else
resolution:wikitext('Unknown')
end
if resolvedDate ~= '' then
resolution:wikitext(
' on ' .. mw.text.nowiki(resolvedDate)
)
end
end
------------------------------------------------------------
-- RESOLVE CONTROL
------------------------------------------------------------
local function renderResolveControl(
parent,
id,
page,
compact
)
if trim(id) == '' then
return
end
local wrapper =
parent:tag('div')
:addClass('feedback-resolve')
local control =
wrapper:tag('span')
:addClass('feedback-resolve-button')
:attr(
'data-feedback-id',
id
)
:attr(
'data-feedback-page',
page or ''
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'aria-label',
'Resolve feedback'
)
if compact then
control
:addClass(
'feedback-resolve-button--compact'
)
:attr(
'title',
'Resolve feedback'
)
:wikitext('✓')
else
control:wikitext('Resolve')
end
end
------------------------------------------------------------
-- FULL FEEDBACK CARD
------------------------------------------------------------
local function renderFullCard(entry)
local resolved =
trim(entry.status):lower() == 'resolved'
local id = trim(entry.id)
local page = trim(entry.page)
local feedback = trim(entry.feedback)
local user = trim(entry.user)
local createddate = trim(entry.createddate)
local root =
mw.html.create('div')
:addClass('feedback-box')
:addClass(
resolved
and 'feedback-box--resolved'
or 'feedback-box--unresolved'
)
if id ~= '' then
root:attr(
'data-feedback-id',
id
)
end
--------------------------------------------------------
-- Main content column
--------------------------------------------------------
local content =
root:tag('div')
:addClass('feedback-content')
--------------------------------------------------------
-- Main line
--------------------------------------------------------
local main =
content:tag('div')
:addClass('feedback-main-line')
--------------------------------------------------------
-- Status
--------------------------------------------------------
main:tag('span')
:addClass('feedback-status')
:addClass(
resolved
and 'feedback-status--resolved'
or 'feedback-status--unresolved'
)
:wikitext(
resolved
and 'Resolved'
or 'Unresolved'
)
--------------------------------------------------------
-- Feedback text
--------------------------------------------------------
main:tag('span')
:addClass('feedback-text')
:wikitext(
feedback ~= ''
and feedback
or 'No feedback provided'
)
--------------------------------------------------------
-- Metadata
--------------------------------------------------------
local meta =
main:tag('span')
:addClass('feedback-inline-meta')
if user ~= '' then
meta:wikitext(
' · ' .. formatUser(user)
)
end
if createddate ~= '' then
meta:wikitext(
' · ' .. mw.text.nowiki(createddate)
)
end
--------------------------------------------------------
-- Attachments
--------------------------------------------------------
renderAttachments(
content,
entry
)
--------------------------------------------------------
-- Resolution information
--------------------------------------------------------
renderResolution(
content,
entry
)
--------------------------------------------------------
-- Missing ID warning
--------------------------------------------------------
if id == '' then
content:tag('div')
:addClass('feedback-missing-id')
:wikitext(
'This feedback entry is missing an ID.'
)
end
--------------------------------------------------------
-- Resolve button
--
-- IMPORTANT:
-- This is outside .feedback-content so it
-- remains on the right.
--------------------------------------------------------
if not resolved then
renderResolveControl(
root,
id,
page,
false
)
end
return tostring(root)
end
------------------------------------------------------------
-- COMPACT CARD
------------------------------------------------------------
local function renderCompactCard(entry)
return renderFullCard(entry)
end
------------------------------------------------------------
-- SAVE FEEDBACK
------------------------------------------------------------
local function saveFeedback(data)
local bucket = getBucket()
if not bucket then
return false, 'Bucket extension is not available'
end
local success, err = pcall(function()
bucket.put({
page = data.page,
user = data.user,
status = data.status,
feedback = data.feedback,
id = data.id,
createddate = data.createddate,
link = data.link,
image = data.image,
resolvedby = data.resolvedby,
resolveddate = data.resolveddate
})
end)
if not success then
return false, tostring(err)
end
return true
end
------------------------------------------------------------
-- MAIN
--
-- Used by Template:Feedback
------------------------------------------------------------
function p.main(frame)
local parent = frame:getParent()
local args =
parent and parent.args
or frame.args
local pageTitle = nil
if parent and parent.getTitle then
local title = parent:getTitle()
if title
and title ~= ''
and not title:find('^Template:')
then
pageTitle = title
end
end
return p._main(
args,
pageTitle
)
end
function p._main(args, pageTitle)
local feedback =
trim(args.feedback)
if feedback == '' then
feedback = 'No feedback provided'
end
local user =
trim(args.user)
if user == '' then
user = 'Anonymous'
end
local resolved =
isTruthy(args.resolved)
local status =
resolved
and 'resolved'
or 'unresolved'
local currentPage =
trim(pageTitle)
if currentPage == '' then
currentPage =
mw.title.getCurrentTitle().fullText
end
local id =
trim(args.id)
local createddate =
trim(args.createddate)
local link =
trim(args.link)
local image =
trim(args.image)
local resolvedby =
trim(args.resolvedby)
local resolveddate =
trim(args.resolveddate)
--------------------------------------------------------
-- Save to Bucket
--------------------------------------------------------
local saved, saveError =
saveFeedback({
page = currentPage,
user = user,
status = status,
feedback = feedback,
id = id,
createddate = createddate,
link = link,
image = image,
resolvedby = resolvedby,
resolveddate = resolveddate
})
if not saved then
return
'<strong style="color:red;">Feedback bucket error: '
.. mw.text.nowiki(
saveError or 'Unknown error'
)
.. '</strong>'
end
--------------------------------------------------------
-- Render
--------------------------------------------------------
local card =
renderFullCard({
page = currentPage,
user = user,
status = status,
feedback = feedback,
id = id,
createddate = createddate,
link = link,
image = image,
resolvedby = resolvedby,
resolveddate = resolveddate
})
--------------------------------------------------------
-- Categories
--------------------------------------------------------
local category
if resolved then
category =
'[[Category:Resolved Feedback]]'
else
category =
'[[Category:Unresolved Feedback]]'
end
if id == '' then
category =
category ..
'[[Category:Feedbacks Missing ID]]'
end
return card .. category
end
------------------------------------------------------------
-- RENDER FEEDBACK LIST
------------------------------------------------------------
local function renderFeedbackList(results)
if not results or #results == 0 then
return '<em>No feedback entries found.</em>'
end
local list = {}
for _, row in ipairs(results) do
table.insert(
list,
renderFullCard(row)
)
end
return table.concat(
list,
'\n'
)
end
------------------------------------------------------------
-- PAGE FEEDBACK
------------------------------------------------------------
function p.page(frame)
local page =
currentSubjectPage()
local entries =
queryFeedback(page)
local unresolved = {}
local resolved = {}
for _, entry in ipairs(entries) do
if trim(entry.status):lower()
== 'resolved'
then
table.insert(
resolved,
entry
)
else
table.insert(
unresolved,
entry
)
end
end
local root =
mw.html.create('section')
:attr(
'id',
'feedback-section'
)
:addClass('feedback-page')
--------------------------------------------------------
-- Heading
--------------------------------------------------------
root:tag('h2')
:addClass('feedback-page-title')
:wikitext('Feedbacks')
--------------------------------------------------------
-- Filters
--------------------------------------------------------
local filters =
root:tag('div')
:addClass('feedback-filters')
local function addFilter(
label,
value,
active
)
return filters:tag('button')
:attr(
'type',
'button'
)
:attr(
'data-feedback-filter',
value
)
:addClass('feedback-filter')
:addClass(
active
and 'feedback-filter--active'
or nil
)
:wikitext(label)
end
addFilter(
'Unresolved',
'unresolved',
true
)
addFilter(
'Resolved',
'resolved',
false
)
addFilter(
'All',
'all',
false
)
--------------------------------------------------------
-- Unresolved
--------------------------------------------------------
local unresolvedList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--unresolved'
)
:addClass(
'feedback-page-list--visible'
)
if #unresolved == 0 then
unresolvedList:tag('div')
:addClass('feedback-empty')
:wikitext(
'No unresolved feedback.'
)
else
for _, entry in ipairs(unresolved) do
unresolvedList:wikitext(
renderCompactCard(entry)
)
end
end
--------------------------------------------------------
-- Resolved
--------------------------------------------------------
local resolvedList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--resolved'
)
if #resolved == 0 then
resolvedList:tag('div')
:addClass('feedback-empty')
:wikitext(
'No resolved feedback.'
)
else
for _, entry in ipairs(resolved) do
resolvedList:wikitext(
renderCompactCard(entry)
)
end
end
--------------------------------------------------------
-- All
--------------------------------------------------------
local allList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--all'
)
if #entries == 0 then
allList:tag('div')
:addClass('feedback-empty')
:wikitext(
'No feedback.'
)
else
for _, entry in ipairs(entries) do
allList:wikitext(
renderCompactCard(entry)
)
end
end
return tostring(root)
end
------------------------------------------------------------
-- COUNTS
------------------------------------------------------------
function p.counts(frame)
local entries =
queryFeedback(
currentSubjectPage()
)
local unresolved = 0
local resolved = 0
for _, entry in ipairs(entries) do
if trim(entry.status):lower()
== 'resolved'
then
resolved = resolved + 1
else
unresolved = unresolved + 1
end
end
return tostring(
mw.html.create('span')
:addClass('feedback-count-data')
:attr(
'data-unresolved',
unresolved
)
:attr(
'data-resolved',
resolved
)
)
end
------------------------------------------------------------
-- TABLE / DASHBOARD
------------------------------------------------------------
local function renderTable(entries)
if not entries or #entries == 0 then
return '<em>No feedback entries found.</em>'
end
local table =
mw.html.create('table')
:addClass('feedback-table')
local head =
table:tag('thead')
:tag('tr')
head:tag('th')
:wikitext('Status')
head:tag('th')
:wikitext('Page')
head:tag('th')
:wikitext('Feedback')
head:tag('th')
:wikitext('User')
head:tag('th')
:wikitext('Date')
local body =
table:tag('tbody')
for _, entry in ipairs(entries) do
local row =
body:tag('tr')
local resolved =
trim(entry.status):lower()
== 'resolved'
row:tag('td')
:addClass(
resolved
and 'feedback-table-status--resolved'
or 'feedback-table-status--unresolved'
)
:wikitext(
resolved
and 'Resolved'
or 'Unresolved'
)
row:tag('td')
:addClass('feedback-table-page')
:wikitext(
entry.page or ''
)
row:tag('td')
:addClass('feedback-table-text')
:wikitext(
entry.feedback or ''
)
row:tag('td')
:wikitext(
formatUser(entry.user)
)
row:tag('td')
:wikitext(
entry.createddate or ''
)
end
return tostring(table)
end
function p.view(frame)
local parent =
frame:getParent()
local args
if frame
and frame.args
and frame.args.status
then
args = frame.args
elseif parent
and parent.args
and parent.args.status
then
args = parent.args
elseif frame
and frame.args
then
args = frame.args
elseif parent
and parent.args
then
args = parent.args
else
args = {}
end
return p._view(
args,
frame
)
end
function p._view(args, frame)
local bucket =
getBucket()
if not bucket then
return
'<em>Bucket extension is not available.</em>'
end
local rawStatus =
args.status
or args[1]
local filterStatus =
rawStatus
and trim(rawStatus):lower()
or nil
--------------------------------------------------------
-- Query
--------------------------------------------------------
local query =
bucket
query.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)
if filterStatus == 'resolved'
or filterStatus == 'unresolved'
then
query.where(
'status',
filterStatus
)
end
local success, entries =
pcall(function()
return query.run()
end)
if not success or not entries then
return
'<em>Could not load feedback.</em>'
end
entries =
sortNewestFirst(entries)
--------------------------------------------------------
-- Specific status
--------------------------------------------------------
if filterStatus == 'resolved'
or filterStatus == 'unresolved'
then
return renderFeedbackList(
entries
)
end
--------------------------------------------------------
-- All
--------------------------------------------------------
return renderTable(entries)
end
p.table = p.view
return p