Jump to content

Module:Feedback: Difference between revisions

From The Petit Planet Wiki
ReisuDesign (talk | contribs)
No edit summary
ReisuDesign (talk | contribs)
No edit summary
 
(12 intermediate revisions by the same user not shown)
Line 2: Line 2:


------------------------------------------------------------
------------------------------------------------------------
-- Helpers
-- HELPERS
------------------------------------------------------------
------------------------------------------------------------


Line 12: Line 12:
value = mw.text.trim(tostring(value)):lower()
value = mw.text.trim(tostring(value)):lower()


return value ~= ''
return value == 'true'
and value ~= '0'
or value == 'yes'
and value ~= 'false'
or value == '1'
and value ~= 'no'
or value == 'resolved'
and value ~= 'off'
end
end




local function trim(value)
local function trim(value)
return mw.text.trim(
if value == nil then
tostring(value or '')
return ''
)
end
 
return mw.text.trim(tostring(value))
end
end


Line 30: Line 31:
user = trim(user)
user = trim(user)


if user == '' or user:lower() == 'anonymous' then
if user == '' then
return 'Anonymous'
return 'Anonymous'
end
end
Line 43: Line 44:


local function currentSubjectPage()
local function currentSubjectPage()
local title =
local title = mw.title.getCurrentTitle()
mw.title.getCurrentTitle()
 
if title.subjectPageTitle then
return title.subjectPageTitle.fullText
end


return title.subjectPageTitle.fullText
return title.fullText
end
end


Line 52: Line 56:
local function getBucket()
local function getBucket()
if not (mw.ext and mw.ext.bucket) then
if not (mw.ext and mw.ext.bucket) then
return nil, 'Bucket extension is not available'
return nil
end
 
local bucket =
mw.ext.bucket('feedback')
 
if not bucket then
return nil, 'Feedback bucket could not be opened'
end
end


return bucket
return mw.ext.bucket('feedback')
end
end




local function sortNewestFirst(results)
local function sortNewestFirst(entries)
if not results then
return {}
end
 
table.sort(
table.sort(
results,
entries,
function(a, b)
function(a, b)
local aDate = trim(a.createddate)
local bDate = trim(b.createddate)


local aDate =
return aDate > bDate
trim(a.createddate)
 
local bDate =
trim(b.createddate)
 
-- Entries without a date go last.
if aDate == '' and bDate ~= '' then
return false
end
 
if aDate ~= '' and bDate == '' then
return true
end
 
if aDate ~= bDate then
return aDate > bDate
end
 
-- Stable-ish fallback.
return trim(a.id) > trim(b.id)
end
end
)
)


return results
return entries
end
end




local function queryFeedback(page, status)
local function queryFeedback(page)
local bucket, err =
local bucket = getBucket()
getBucket()


if not bucket then
if not bucket then
return nil, err
return {}
end
end


local query =
local query = bucket
bucket.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)
 
if page and page ~= '' then
query.where(
'page',
page
)
end


if status
query.select(
and status ~= ''
'page',
and status ~= 'all'
'user',
then
'status',
 
'feedback',
query.where(
'id',
'status',
'createddate',
status
'link',
)
'image',
 
'resolvedby',
end
'resolveddate'
 
)
local success, results =
pcall(function()
return query.run()
end)


if not success then
query.where(
return nil, tostring(results)
'page',
end
page
 
return sortNewestFirst(
results or {}
)
)
end


local success, results = pcall(function()
return query.run()
end)


------------------------------------------------------------
if not success or not results then
-- Save feedback
return {}
------------------------------------------------------------
 
local function saveFeedback(data)
 
local bucket, err =
getBucket()
 
if not bucket then
return false, err
end
 
local success, saveError =
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(saveError)
end
end


return true
return sortNewestFirst(results)
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Shared metadata
-- ATTACHMENTS
------------------------------------------------------------
------------------------------------------------------------


local function renderMetadata(root, entry)
local function renderAttachments(entry)
local wrapper =
mw.html.create('div')
:addClass('feedback-attachments')


local meta =
local link = trim(entry.link)
root:tag('div')
:addClass('feedback-meta')
 
if entry.user and entry.user ~= '' then
 
meta:tag('span')
:addClass('feedback-user')
:wikitext(
formatUser(entry.user)
)


if link ~= '' then
local linkWrapper =
wrapper:tag('span')
:addClass('feedback-attachment-link')
linkWrapper:tag('span')
:addClass('feedback-attachment-icon')
linkWrapper:wikitext(
'[' .. link .. ' ' .. mw.text.nowiki(link) .. ']'
)
end
end


if entry.createddate
local image = trim(entry.image)
and entry.createddate ~= ''
then


meta:tag('span')
if image ~= '' then
:addClass('feedback-meta-date')
image = mw.ustring.gsub(
:wikitext(
image,
' · '
'^File:',
.. mw.text.nowiki(
''
entry.createddate
)
)
)


end
local title =
end
mw.title.new('File:' .. image)
 
 
------------------------------------------------------------
-- Attachments
------------------------------------------------------------
 
local function renderAttachments(root, entry)
 
local link =
trim(entry.link)
 
local image =
trim(entry.image)
 
if link == ''
and image == ''
then
return
end
 
local attachments =
root:tag('div')
:addClass('feedback-attachments')


if link ~= '' then
if title then
 
local imageWrapper =
attachments:tag('span')
wrapper:tag('span')
:addClass('feedback-attachment')
:addClass('feedback-attachment-image')
:wikitext(
'🔗 '
imageWrapper:tag('span')
.. '['
:addClass('feedback-attachment-icon')
.. link
.. ' Link]'
imageWrapper:wikitext(
'[' ..
title:fullUrl() ..
' ' ..
mw.text.nowiki(image) ..
']'
)
)
 
end
end
end


if image ~= '' then
return wrapper
 
attachments:tag('span')
:addClass('feedback-attachment')
 
attachments:tag('span')
:addClass('feedback-image-icon')
:wikitext('▣')
 
attachments:tag('span')
:addClass('feedback-image-link')
:wikitext(
'[[Media:'
.. image
.. '|Picture]]'
)
 
end
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Resolution information
-- RESOLUTION
------------------------------------------------------------
------------------------------------------------------------


local function renderResolution(root, entry)
local function renderResolution(parent, entry)
 
local resolvedBy = trim(entry.resolvedby)
local resolvedBy =
local resolvedDate = trim(entry.resolveddate)
trim(entry.resolvedby)


local resolvedDate =
if resolvedBy == '' and resolvedDate == '' then
trim(entry.resolveddate)
 
if resolvedBy == ''
and resolvedDate == ''
then
return
return
end
end


local resolution =
local resolution =
root:tag('div')
parent:tag('div')
:addClass('feedback-resolution')
:addClass('feedback-resolution')


resolution:wikitext('↳ Resolved by ')
resolution:wikitext('Resolved by ')


if resolvedBy ~= '' then
if resolvedBy ~= '' then
Line 325: Line 203:
if resolvedDate ~= '' then
if resolvedDate ~= '' then
resolution:wikitext(
resolution:wikitext(
' on '
' on ' .. mw.text.nowiki(resolvedDate)
.. mw.text.nowiki(resolvedDate)
)
)
end
end
Line 333: Line 210:


------------------------------------------------------------
------------------------------------------------------------
-- Resolve control
-- RESOLVE CONTROL
------------------------------------------------------------
------------------------------------------------------------


Line 339: Line 216:
parent,
parent,
id,
id,
page,
compact
compact
)
)
 
if trim(id) == '' then
if not id or id == '' then
return
return
end
end
local wrapper =
parent:tag('div')
:addClass('feedback-resolve')


local control =
local control =
parent:tag('span')
wrapper:tag('span')
:addClass('feedback-resolve-button')
: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
if compact then
control
control
:addClass('feedback-resolve-button--compact')
:addClass(
:attr('aria-label', 'Resolve feedback')
'feedback-resolve-button--compact'
:attr('title', 'Resolve feedback')
)
:attr(
'title',
'Resolve feedback'
)
:wikitext('✓')
:wikitext('✓')
else
else
 
control:wikitext('Resolve')
control
:attr('role', 'button')
:attr('tabindex', '0')
:attr('aria-label', 'Resolve feedback')
:wikitext('Resolve')
 
end
end
control:attr(
'data-feedback-id',
id
)
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Full feedback card
-- FULL FEEDBACK CARD
------------------------------------------------------------
------------------------------------------------------------


local function renderFullCard(entry)
local function renderFullCard(entry)
local resolved =
local resolved =
entry.status == 'resolved'
trim(entry.status):lower() == 'resolved'


local id =
local id = trim(entry.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 =
local root =
Line 402: Line 296:
)
)
end
end
--------------------------------------------------------
-- Main content column
--------------------------------------------------------
local content =
root:tag('div')
:addClass('feedback-content')


--------------------------------------------------------
--------------------------------------------------------
Line 408: Line 310:


local main =
local main =
root:tag('div')
content:tag('div')
:addClass('feedback-main-line')
:addClass('feedback-main-line')
--------------------------------------------------------
-- Status
--------------------------------------------------------


main:tag('span')
main:tag('span')
Line 420: Line 326:
:wikitext(
:wikitext(
resolved
resolved
and 'RESOLVED'
and 'Resolved'
or 'UNRESOLVED'
or 'Unresolved'
)
)
--------------------------------------------------------
-- Feedback text
--------------------------------------------------------


main:tag('span')
main:tag('span')
:addClass('feedback-text')
:addClass('feedback-text')
:wikitext(
:wikitext(
entry.feedback
feedback ~= ''
and feedback
or 'No feedback provided'
or 'No feedback provided'
)
)
Line 435: Line 346:
--------------------------------------------------------
--------------------------------------------------------


local metadata =
local meta =
main:tag('span')
main:tag('span')
:addClass('feedback-inline-meta')
:addClass('feedback-inline-meta')


if entry.user
if user ~= '' then
and entry.user ~= ''
meta:wikitext(
then
' · ' .. formatUser(user)
 
metadata:wikitext(
' · '
.. formatUser(entry.user)
)
)
end
end


if entry.createddate
if createddate ~= '' then
and entry.createddate ~= ''
meta:wikitext(
then
' · ' .. mw.text.nowiki(createddate)
 
metadata:wikitext(
' '
.. mw.text.nowiki(
entry.createddate
)
)
)
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Resolve button
-- Attachments
--------------------------------------------------------
--------------------------------------------------------
 
if not resolved
content:node(
and id ~= ''
    renderAttachments(entry)
then
)
 
renderResolveControl(
main,
id,
false
)
 
end


--------------------------------------------------------
--------------------------------------------------------
-- Attachments
-- Resolution information
--------------------------------------------------------
--------------------------------------------------------


renderAttachments(
renderResolution(
root,
content,
entry
entry
)
)


--------------------------------------------------------
--------------------------------------------------------
-- Resolution
-- Missing ID warning
--------------------------------------------------------
--------------------------------------------------------


if resolved then
if id == '' then
renderResolution(
content:tag('div')
root,
:addClass('feedback-missing-id')
entry
:wikitext(
)
'This feedback entry is missing an ID.'
)
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Missing ID warning
-- Resolve button
--
-- IMPORTANT:
-- This is outside .feedback-content so it
-- remains on the right.
--------------------------------------------------------
--------------------------------------------------------


if not resolved
if not resolved then
and id == ''
renderResolveControl(
then
root,
 
id,
root:tag('div')
page,
:addClass('feedback-missing-id')
false
:wikitext(
)
'Add an ID to enable the Resolve button.'
)
 
end
end


Line 520: Line 413:


------------------------------------------------------------
------------------------------------------------------------
-- Compact article card
-- COMPACT CARD
------------------------------------------------------------
------------------------------------------------------------


local function renderCompactCard(entry)
local function renderCompactCard(entry)
return renderFullCard(entry)
end
------------------------------------------------------------
-- SAVE FEEDBACK
------------------------------------------------------------


local resolved =
local function saveFeedback(data)
entry.status == 'resolved'
local bucket = getBucket()


local id =
if not bucket then
trim(entry.id)
return false, 'Bucket extension is not available'
end


local root =
local success, err = pcall(function()
mw.html.create('div')
bucket.put({
:addClass('feedback-page-card')
page = data.page,
:addClass(
user = data.user,
resolved
status = data.status,
and 'feedback-page-card--resolved'
feedback = data.feedback,
or 'feedback-page-card--unresolved'
id = data.id,
)
createddate = data.createddate,
link = data.link,
image = data.image,
resolvedby = data.resolvedby,
resolveddate = data.resolveddate
})
end)


if id ~= '' then
if not success then
root:attr(
return false, tostring(err)
'data-feedback-id',
id
)
end
end


local content =
return true
root:tag('div')
end
:addClass('feedback-page-card-content')
 
 
------------------------------------------------------------
-- 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()


local firstLine =
if title
content:tag('div')
and title ~= ''
:addClass('feedback-page-card-line')
and not title:find('^Template:')
then
pageTitle = title
end
end


firstLine:tag('span')
return p._main(
:addClass('feedback-status')
args,
:addClass(
pageTitle
resolved
)
and 'feedback-status--resolved'
end
or 'feedback-status--unresolved'
)
:wikitext(
resolved
and 'RESOLVED'
or 'UNRESOLVED'
)


firstLine:tag('span')
:addClass('feedback-page-card-text')
:wikitext(
entry.feedback
or 'No feedback provided'
)


local meta =
function p._main(args, pageTitle)
content:tag('div')
local feedback =
:addClass('feedback-page-card-meta')
trim(args.feedback)


if entry.user
if feedback == '' then
and entry.user ~= ''
feedback = 'No feedback provided'
then
end


meta:wikitext(
local user =
formatUser(entry.user)
trim(args.user)
)


if user == '' then
user = 'Anonymous'
end
end


if entry.createddate
local resolved =
and entry.createddate ~= ''
isTruthy(args.resolved)
then
 
local status =
resolved
and 'resolved'
or 'unresolved'


meta:wikitext(
local currentPage =
' · '
trim(pageTitle)
.. mw.text.nowiki(
entry.createddate
)
)


if currentPage == '' then
currentPage =
mw.title.getCurrentTitle().fullText
end
end


if resolved then
local id =
trim(args.id)
 
local createddate =
trim(args.createddate)
 
local link =
trim(args.link)


local resolvedBy =
local image =
trim(entry.resolvedby)
trim(args.image)


local resolvedDate =
local resolvedby =
trim(entry.resolveddate)
trim(args.resolvedby)


if resolvedBy ~= ''
local resolveddate =
or resolvedDate ~= ''
trim(args.resolveddate)
then


local resolution =
--------------------------------------------------------
content:tag('div')
-- Save to Bucket
:addClass(
--------------------------------------------------------
'feedback-page-card-resolution'
)


resolution:wikitext(
local saved, saveError =
'↳ Resolved by '
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


if resolvedBy ~= '' then
--------------------------------------------------------
resolution:wikitext(
-- Render
formatUser(resolvedBy)
--------------------------------------------------------
)
else
resolution:wikitext(
'Unknown'
)
end


if resolvedDate ~= '' then
local card =
resolution:wikitext(
renderFullCard({
' · '
page = currentPage,
.. mw.text.nowiki(
user = user,
resolvedDate
status = status,
)
feedback = feedback,
)
id = id,
end
createddate = createddate,
end
link = link,
image = image,
resolvedby = resolvedby,
resolveddate = resolveddate
})
 
--------------------------------------------------------
-- Categories
--------------------------------------------------------
 
local category


if resolved then
category =
'[[Category:Resolved Feedback]]'
else
else
category =
'[[Category:Unresolved Feedback]]'
end
if id == '' then
category =
category ..
'[[Category:Feedbacks Missing ID]]'
end
return card .. category
end


if id ~= '' then


renderResolveControl(
------------------------------------------------------------
root,
-- RENDER FEEDBACK LIST
id,
------------------------------------------------------------
true
 
)
local function renderFeedbackList(results)
if not results or #results == 0 then
return '<em>No feedback entries found.</em>'
end


end
local list = {}


for _, row in ipairs(results) do
table.insert(
list,
renderFullCard(row)
)
end
end


renderAttachments(
return table.concat(
content,
list,
entry
'\n'
)
)
return tostring(root)
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Page feedback section
-- PAGE FEEDBACK
------------------------------------------------------------
------------------------------------------------------------


function p.page(frame)
function p.page(frame)
local page =
local page =
currentSubjectPage()
currentSubjectPage()


local allEntries, err =
local entries =
queryFeedback(page)
queryFeedback(page)
if not allEntries then
return ''
end
if #allEntries == 0 then
return ''
end


local unresolved = {}
local unresolved = {}
local resolved = {}
local resolved = {}


for _, row in ipairs(allEntries) do
for _, entry in ipairs(entries) do
 
if trim(entry.status):lower()
if row.status == 'resolved' then
== 'resolved'
 
then
table.insert(
table.insert(
resolved,
resolved,
row
entry
)
)
else
else
table.insert(
table.insert(
unresolved,
unresolved,
row
entry
)
)
end
end
end
end
Line 711: Line 663:
local root =
local root =
mw.html.create('section')
mw.html.create('section')
:addClass('feedback-page-section')
:attr('id', 'feedback-section')
:attr(
'data-unresolved-count',
#unresolved
)
:attr(
:attr(
'data-resolved-count',
'id',
#resolved
'feedback-section'
)
)
:addClass('feedback-page')


--------------------------------------------------------
--------------------------------------------------------
Line 727: Line 674:


root:tag('h2')
root:tag('h2')
:addClass('feedback-page-heading')
:addClass('feedback-page-title')
:wikitext('Feedbacks')
:wikitext('Feedbacks')


--------------------------------------------------------
--------------------------------------------------------
-- Filter controls
-- Filters
--------------------------------------------------------
--------------------------------------------------------


local filters =
local filters =
root:tag('div')
root:tag('div')
:addClass('feedback-page-filters')
:addClass('feedback-filters')


local unresolvedFilter =
local function addFilter(
filters:tag('span')
label,
:addClass('feedback-page-filter')
value,
:addClass(
active
'feedback-page-filter--active'
)
)
return filters:tag('button')
:attr(
:attr(
'role',
'type',
'button'
'button'
)
)
:attr(
:attr(
'tabindex',
'data-feedback-filter',
'0'
value
)
)
:attr(
:addClass('feedback-filter')
'data-feedback-filter',
:addClass(
'unresolved'
active
and 'feedback-filter--active'
or nil
)
)
:wikitext(label)
end


unresolvedFilter:wikitext(
addFilter(
'Unresolved '
'Unresolved',
'unresolved',
true
)
)


unresolvedFilter:tag('span')
addFilter(
:addClass('feedback-page-filter-count')
'Resolved',
:wikitext(
'resolved',
tostring(#unresolved)
false
)
)


if #resolved > 0 then
addFilter(
 
'All',
local resolvedFilter =
'all',
filters:tag('span')
false
:addClass('feedback-page-filter')
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'data-feedback-filter',
'resolved'
)
 
resolvedFilter:wikitext(
'Resolved '
)
 
resolvedFilter:tag('span')
:addClass('feedback-page-filter-count')
:wikitext(
tostring(#resolved)
)
 
end


--------------------------------------------------------
--------------------------------------------------------
-- Unresolved list
-- Unresolved
--------------------------------------------------------
--------------------------------------------------------


Line 808: Line 737:
:addClass(
:addClass(
'feedback-page-list--unresolved'
'feedback-page-list--unresolved'
)
:addClass(
'feedback-page-list--visible'
)
)


if #unresolved > 0 then
if #unresolved == 0 then
 
unresolvedList:tag('div')
for _, row in ipairs(unresolved) do
:addClass('feedback-empty')
unresolvedList:node(
:wikitext(
renderCompactCard(row)
'No unresolved feedback.'
)
else
for _, entry in ipairs(unresolved) do
unresolvedList:wikitext(
renderCompactCard(entry)
)
)
end
end
end


else
--------------------------------------------------------
-- Resolved
--------------------------------------------------------


unresolvedList:tag('div')
local resolvedList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
:addClass(
'feedback-page-empty'
'feedback-page-list--resolved'
)
)
if #resolved == 0 then
resolvedList:tag('div')
:addClass('feedback-empty')
:wikitext(
:wikitext(
'No unresolved feedback.'
'No resolved feedback.'
)
else
for _, entry in ipairs(resolved) do
resolvedList:wikitext(
renderCompactCard(entry)
)
)
 
end
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Resolved list
-- All
--------------------------------------------------------
--------------------------------------------------------


if #resolved > 0 then
local allList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--all'
)


local resolvedList =
if #entries == 0 then
root:tag('div')
allList:tag('div')
:addClass(
:addClass('feedback-empty')
'feedback-page-list'
:wikitext(
)
'No feedback.'
:addClass(
)
'feedback-page-list--resolved'
else
)
for _, entry in ipairs(entries) do
 
allList:wikitext(
for _, row in ipairs(resolved) do
renderCompactCard(entry)
resolvedList:node(
renderCompactCard(row)
)
)
end
end
end
end


Line 858: Line 815:


------------------------------------------------------------
------------------------------------------------------------
-- Feedback counts
-- COUNTS
------------------------------------------------------------
------------------------------------------------------------


function p.counts(frame)
function p.counts(frame)
local page =
currentSubjectPage()
local entries =
local entries =
queryFeedback(page)
queryFeedback(
 
currentSubjectPage()
if not entries then
)
return ''
end


local unresolved = 0
local unresolved = 0
local resolved = 0
local resolved = 0


for _, row in ipairs(entries) do
for _, entry in ipairs(entries) do
 
if trim(entry.status):lower()
if row.status == 'resolved' then
== 'resolved'
then
resolved = resolved + 1
resolved = resolved + 1
else
else
unresolved = unresolved + 1
unresolved = unresolved + 1
end
end
end
end


Line 902: Line 853:


------------------------------------------------------------
------------------------------------------------------------
-- Main / individual feedback
-- TABLE / DASHBOARD
------------------------------------------------------------
------------------------------------------------------------


function p.main(frame)
local function renderTable(entries)
 
if not entries or #entries == 0 then
local parent =
return '<em>No feedback entries found.</em>'
frame:getParent()
 
local args =
parent and parent.args
or frame.args
 
local feedback =
args.feedback
or 'No feedback provided'
 
local user =
args.user
or 'Anonymous'
 
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)
 
local resolved =
isTruthy(args.resolved)
 
local status =
resolved
and 'resolved'
or 'unresolved'
 
local page =
currentSubjectPage()
 
local saved, saveError =
saveFeedback({
page = page,
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
end


local card =
local table =
renderFullCard({
page = page,
user = user,
status = status,
feedback = feedback,
id = id,
createddate = createdDate,
link = link,
image = image,
resolvedby = resolvedBy,
resolveddate = resolvedDate
})
 
local categories = {}
 
if resolved then
 
table.insert(
categories,
'[[Category:Resolved Feedback]]'
)
 
else
 
table.insert(
categories,
'[[Category:Unresolved Feedback]]'
)
 
end
 
if id == '' then
 
table.insert(
categories,
'[[Category:Feedbacks Missing ID]]'
)
 
end
 
return card
.. table.concat(categories)
end
 
 
------------------------------------------------------------
-- Feedback table
------------------------------------------------------------
 
local function renderTable(entries, includeResolved)
 
local tableRoot =
mw.html.create('table')
mw.html.create('table')
:addClass('feedback-table')
:addClass('feedback-table')


local header =
local head =
tableRoot:tag('tr')
table:tag('thead')
:tag('tr')


if includeResolved then
head:tag('th')
:wikitext('Status')


header:tag('th')
head:tag('th')
:wikitext('Status')
 
end
 
header:tag('th')
:wikitext('Page')
:wikitext('Page')


header:tag('th')
head:tag('th')
:wikitext('Username')
 
header:tag('th')
:wikitext('Date')
 
header:tag('th')
:wikitext('Feedback')
:wikitext('Feedback')


if includeResolved then
head:tag('th')
:wikitext('User')


header:tag('th')
head:tag('th')
:wikitext('Resolved by')
:wikitext('Date')


header:tag('th')
local body =
:wikitext('Resolved date')
table:tag('tbody')
 
end
 
if #entries == 0 then


for _, entry in ipairs(entries) do
local row =
local row =
tableRoot:tag('tr')
body:tag('tr')


local colspan =
local resolved =
includeResolved
trim(entry.status):lower()
and '7'
== 'resolved'
or '4'


row:tag('td')
row:tag('td')
:attr(
'colspan',
colspan
)
:addClass(
:addClass(
'feedback-table-empty'
resolved
and 'feedback-table-status--resolved'
or 'feedback-table-status--unresolved'
)
)
:wikitext(
:wikitext(
'No feedback entries found.'
resolved
and 'Resolved'
or 'Unresolved'
)
)
return tostring(tableRoot)
end
for _, entry in ipairs(entries) do
local row =
tableRoot:tag('tr')
:addClass(
entry.status == 'resolved'
and 'feedback-table-row--resolved'
or 'feedback-table-row--unresolved'
)
if includeResolved then
row:tag('td')
:addClass('feedback-table-status')
:wikitext(
entry.status == 'resolved'
and 'Resolved'
or 'Unresolved'
)
end


row:tag('td')
row:tag('td')
:addClass('feedback-table-page')
:addClass('feedback-table-page')
:wikitext(
:wikitext(
'[['
entry.page or ''
.. entry.page
.. ']]'
)
)


row:tag('td')
row:tag('td')
:addClass('feedback-table-user')
:addClass('feedback-table-text')
:wikitext(
:wikitext(
formatUser(entry.user)
entry.feedback or ''
)
)


row:tag('td')
row:tag('td')
:addClass('feedback-table-date')
:wikitext(
:wikitext(
mw.text.nowiki(
formatUser(entry.user)
entry.createddate
or '—'
)
)
)


row:tag('td')
row:tag('td')
:addClass('feedback-table-feedback')
:wikitext(
:wikitext(
entry.feedback
entry.createddate or ''
or ''
)
)
if includeResolved then
row:tag('td')
:addClass(
'feedback-table-resolved-by'
)
:wikitext(
entry.resolvedby
and formatUser(
entry.resolvedby
)
or '—'
)
row:tag('td')
:addClass(
'feedback-table-resolved-date'
)
:wikitext(
entry.resolveddate
and mw.text.nowiki(
entry.resolveddate
)
or '—'
)
end
end
end


return tostring(tableRoot)
return tostring(table)
end
end


------------------------------------------------------------
-- Global feedback page
------------------------------------------------------------


function p.view(frame)
function p.view(frame)
local parent =
local parent =
frame:getParent()
frame:getParent()


local args =
local args
parent and parent.args
 
or frame.args
if frame
and frame.args
and frame.args.status
then
args = frame.args


local status =
elseif parent
trim(
and parent.args
args.status
and parent.args.status
or ''
then
):lower()
args = parent.args


--------------------------------------------------------
elseif frame
-- Specific table
and frame.args
--------------------------------------------------------
then
args = frame.args


if status == 'resolved'
elseif parent
or status == 'unresolved'
and parent.args
then
then
args = parent.args
else
args = {}
end
return p._view(
args,
frame
)
end


local entries =
queryFeedback(
nil,
status
)


if not entries then
function p._view(args, frame)
return ''
local bucket =
end
getBucket()


return renderTable(
if not bucket then
entries,
return
false
'<em>Bucket extension is not available.</em>'
)
end
end
local rawStatus =
args.status
or args[1]
local filterStatus =
rawStatus
and trim(rawStatus):lower()
or nil


--------------------------------------------------------
--------------------------------------------------------
-- Dashboard
-- Query
--------------------------------------------------------
--------------------------------------------------------


local unresolved =
local query =
queryFeedback(
bucket
nil,
 
'unresolved'
query.select(
)
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)


local all =
if filterStatus == 'resolved'
queryFeedback(
or filterStatus == 'unresolved'
nil,
then
'all'
query.where(
'status',
filterStatus
)
)
end


if not unresolved then
local success, entries =
unresolved = {}
pcall(function()
end
return query.run()
end)


if not all then
if not success or not entries then
all = {}
return
'<em>Could not load feedback.</em>'
end
end


local root =
entries =
mw.html.create('div')
sortNewestFirst(entries)
:addClass('feedback-dashboard')


root:tag('h2')
--------------------------------------------------------
:addClass('feedback-dashboard-heading')
-- Specific status
:wikitext('Unresolved Feedback')
--------------------------------------------------------


root:node(
if filterStatus == 'resolved'
renderTable(
or filterStatus == 'unresolved'
unresolved,
then
false
return renderFeedbackList(
entries
)
)
)
end


root:tag('h2')
--------------------------------------------------------
:addClass('feedback-dashboard-heading')
-- All
:wikitext('All Feedback')
--------------------------------------------------------


root:node(
return renderTable(entries)
renderTable(
all,
true
)
)
 
return tostring(root)
end
end


------------------------------------------------------------
-- Aliases
------------------------------------------------------------


p.table = p.view
p.table = p.view


return p
return p

Latest revision as of 13:57, 16 September 2026

Module documentation

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')

	local link = trim(entry.link)

	if link ~= '' then
		local linkWrapper =
			wrapper:tag('span')
				:addClass('feedback-attachment-link')
	
		linkWrapper:tag('span')
			:addClass('feedback-attachment-icon')
	
		linkWrapper:wikitext(
			'[' .. link .. ' ' .. mw.text.nowiki(link) .. ']'
		)
	end

	local image = trim(entry.image)

	if image ~= '' then
		image = mw.ustring.gsub(
			image,
			'^File:',
			''
		)

		local title =
			mw.title.new('File:' .. image)

		if title then
			local imageWrapper =
				wrapper:tag('span')
					:addClass('feedback-attachment-image')
		
			imageWrapper:tag('span')
				:addClass('feedback-attachment-icon')
		
			imageWrapper:wikitext(
				'[' ..
				title:fullUrl() ..
				' ' ..
				mw.text.nowiki(image) ..
				']'
			)
		end
	end

	return 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
	--------------------------------------------------------
	
	content:node(
	    renderAttachments(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