Jump to content

Module:Feedback: Difference between revisions

From The Petit Planet Wiki
ReisuDesign (talk | contribs)
No edit summary
ReisuDesign (talk | contribs)
Undo revision 24119 by ReisuDesign (talk)
Tag: Undo
Line 1: Line 1:
local p = {}
local p = {}


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


local function isTruthy(value)
local function isTruthy(value)
if value == nil then
if value == nil then
return false
return false
end
end


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


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




local function trim(value)
local function trim(value)
if value == nil then
return ''
end


return mw.text.trim(tostring(value))
return mw.text.trim(
tostring(value or '')
)
 
end
end




local function formatUser(user)
local function formatUser(user)
user = trim(user)


if user == '' then
user =
trim(user)
 
if user == ''
or user:lower() == 'anonymous'
then
return 'Anonymous'
return 'Anonymous'
end
end
Line 40: Line 50:
user
user
)
)
end
end




local function currentSubjectPage()
local function currentSubjectPage()
local title = mw.title.getCurrentTitle()


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


return title.fullText
end
end




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
return nil, 'Bucket extension is not available'
end
end


return mw.ext.bucket('feedback')
 
local bucket =
mw.ext.bucket('feedback')
 
 
if not bucket then
return nil, 'Feedback bucket could not be opened'
end
 
 
return bucket
 
end
end




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


return aDate > bDate
local aDate =
trim(a.createddate)
 
local bDate =
trim(b.createddate)
 
 
if aDate == ''
and bDate ~= ''
then
return false
end
 
 
if aDate ~= ''
and bDate == ''
then
return true
end
 
 
if aDate ~= bDate then
return aDate > bDate
end
 
 
return trim(a.id) > trim(b.id)
 
end
end
)
)


return entries
 
return results
 
end
end




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


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


local query = bucket


query.select(
return sortNewestFirst(
'page',
results or {}
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)
)


query.where(
end
'page',
 
page
 
)
------------------------------------------------------------
-- Save feedback
------------------------------------------------------------
 
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
 
 
return true
 
end
 
 
------------------------------------------------------------
-- Shared metadata
------------------------------------------------------------
 
local function renderMetadata(
root,
entry
)
 
local meta =
root:tag('div')
:addClass(
'feedback-meta'
)
 
 
if entry.user
and entry.user ~= ''
then
 
meta:tag('span')
:addClass(
'feedback-user'
)
:wikitext(
formatUser(
entry.user
)
)
 
end
 
 
if entry.createddate
and entry.createddate ~= ''
then


local success, results = pcall(function()
meta:tag('span')
return query.run()
:addClass(
end)
'feedback-meta-date'
)
:wikitext(
' · ' ..
mw.text.nowiki(
entry.createddate
)
)


if not success or not results then
return {}
end
end


return sortNewestFirst(results)
end
end




------------------------------------------------------------
------------------------------------------------------------
-- ATTACHMENTS
-- Attachments
------------------------------------------------------------
------------------------------------------------------------


local function renderAttachments(parent, entry)
local function renderAttachments(
local link = trim(entry.link)
root,
local image = trim(entry.image)
entry
)


if link == '' and image == '' then
local link =
trim(entry.link)
 
 
local image =
trim(entry.image)
 
 
if link == ''
and image == ''
then
return
return
end
end


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


if link ~= '' then
if link ~= '' then
attachments:tag('a')
 
:addClass('feedback-link')
attachments:tag('span')
:attr('href', link)
:addClass(
:attr('target', '_blank')
'feedback-attachment'
:attr('rel', 'noopener noreferrer')
)
:wikitext('View link')
:wikitext(
'🔗 ' ..
'[' ..
link ..
' Link]'
)
 
end
end


if image ~= '' then
if image ~= '' then
if link ~= '' then
attachments:wikitext(' ')
end


attachments:tag('a')
local imageAttachment =
:addClass('feedback-image-link')
attachments:tag('span')
:attr(
:addClass(
'href',
'feedback-attachment'
'./File:' .. image
)
 
 
imageAttachment:tag('span')
:addClass(
'feedback-image-icon'
)
:wikitext('▣')
 
 
imageAttachment:tag('span')
:addClass(
'feedback-image-link'
)
:wikitext(
'[[Media:' ..
image ..
'|Picture]]'
)
)
:wikitext('View image')
 
end
end
end
end




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


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


if resolvedBy == '' and resolvedDate == '' then
if resolvedBy == ''
and resolvedDate == ''
then
return
return
end
end


local resolution =
local resolution =
parent:tag('div')
root:tag('div')
:addClass('feedback-resolution')
:addClass(
'feedback-resolution'
)
 
 
resolution:wikitext(
'↳ Resolved by '
)


resolution:wikitext('↳ Resolved by ')


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


if resolvedDate ~= '' then
if resolvedDate ~= '' then
resolution:wikitext(
resolution:wikitext(
' on ' .. mw.text.nowiki(resolvedDate)
' on ' ..
mw.text.nowiki(
resolvedDate
)
)
)
end
end
end
end




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


Line 202: Line 480:
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 =
wrapper:tag('span')
parent:tag('span')
:addClass('feedback-resolve-button')
:addClass(
:attr(
'feedback-resolve-button'
'data-feedback-id',
id
)
)
:attr(
 
'data-feedback-page',
 
page or ''
 
control:attr(
'data-feedback-id',
id
)
 
 
if page
and page ~= ''
then
 
control:attr(
'data-feedback-page',
page
)
 
end
 
 
if compact then
 
control
:addClass(
'feedback-resolve-button--compact'
)
)
:attr(
:attr(
Line 233: Line 532:
'Resolve feedback'
'Resolve feedback'
)
)
:attr(
'title',
'Resolve feedback'
)
:wikitext(
'✓'
)
else


if compact then
control
control
:addClass(
:attr(
'feedback-resolve-button--compact'
'role',
'button'
)
:attr(
'tabindex',
'0'
)
)
:attr(
:attr(
'title',
'aria-label',
'Resolve feedback'
'Resolve feedback'
)
)
:wikitext('✓')
:wikitext(
else
'Resolve'
control:wikitext('Resolve')
)
 
end
end
end
end




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


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


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 =
local root =
mw.html.create('div')
mw.html.create('div')
:addClass('feedback-box')
:addClass(
'feedback-box'
)
:addClass(
:addClass(
resolved
resolved
Line 272: Line 594:
or 'feedback-box--unresolved'
or 'feedback-box--unresolved'
)
)


if id ~= '' then
if id ~= '' then
root:attr(
root:attr(
'data-feedback-id',
'data-feedback-id',
id
id
)
)
end
end


--------------------------------------------------------
-- Main content column
--------------------------------------------------------
local content =
root:tag('div')
:addClass('feedback-content')


--------------------------------------------------------
--------------------------------------------------------
Line 293: Line 611:


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


--------------------------------------------------------
-- Status
--------------------------------------------------------


main:tag('span')
main:tag('span')
:addClass('feedback-status')
:addClass(
'feedback-status'
)
:addClass(
:addClass(
resolved
resolved
Line 309: Line 628:
: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(
feedback ~= ''
entry.feedback
and feedback
or 'No feedback provided'
or 'No feedback provided'
)
)


--------------------------------------------------------
--------------------------------------------------------
-- Metadata
-- Inline metadata
--------------------------------------------------------
--------------------------------------------------------


local meta =
local metadata =
main:tag('span')
main:tag('span')
:addClass('feedback-inline-meta')
:addClass(
'feedback-inline-meta'
)
 
 
if entry.user
and entry.user ~= ''
then
 
metadata:wikitext(
' · ' ..
formatUser(
entry.user
)
)
 
end
 


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


if createddate ~= '' then
 
meta:wikitext(
--------------------------------------------------------
' · ' .. mw.text.nowiki(createddate)
-- Resolve button
--------------------------------------------------------
 
if not resolved
and id ~= ''
then
 
renderResolveControl(
main,
id,
page,
false
)
)
end
end


--------------------------------------------------------
--------------------------------------------------------
Line 350: Line 705:


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


--------------------------------------------------------
--------------------------------------------------------
-- Resolution information
-- Resolution
--------------------------------------------------------
--------------------------------------------------------


renderResolution(
if resolved then
content,
 
entry
renderResolution(
)
root,
entry
)
 
end
 


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


if id == '' then
if not resolved
content:tag('div')
and id == ''
:addClass('feedback-missing-id')
then
 
root:tag('div')
:addClass(
'feedback-missing-id'
)
:wikitext(
:wikitext(
'This feedback entry is missing an ID.'
'Add an ID to enable the Resolve button.'
)
)
end
end


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


if not resolved then
return tostring(root)
renderResolveControl(
root,
id,
page,
false
)
end


return tostring(root)
end
end




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


local function renderCompactCard(entry)
local function renderCompactCard(
return renderFullCard(entry)
entry
end
)
 
local resolved =
entry.status == 'resolved'
 
 
local id =
trim(entry.id)
 
 
local page =
trim(entry.page)




------------------------------------------------------------
local root =
-- SAVE FEEDBACK
mw.html.create('div')
------------------------------------------------------------
:addClass(
'feedback-page-card'
)
:addClass(
resolved
and 'feedback-page-card--resolved'
or 'feedback-page-card--unresolved'
)


local function saveFeedback(data)
local bucket = getBucket()


if not bucket then
if id ~= '' then
return false, 'Bucket extension is not available'
end


local success, err = pcall(function()
root:attr(
bucket.put({
'data-feedback-id',
page = data.page,
id
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
end


return true
end


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


------------------------------------------------------------
-- MAIN
--
-- Used by Template:Feedback
------------------------------------------------------------


function p.main(frame)
local firstLine =
local parent = frame:getParent()
content:tag('div')
:addClass(
'feedback-page-card-line'
)


local args =
parent and parent.args
or frame.args


local pageTitle = nil
firstLine:tag('span')
:addClass(
'feedback-status'
)
:addClass(
resolved
and 'feedback-status--resolved'
or 'feedback-status--unresolved'
)
:wikitext(
resolved
and 'RESOLVED'
or 'UNRESOLVED'
)


if parent and parent.getTitle then
local title = parent:getTitle()


if title
firstLine:tag('span')
and title ~= ''
:addClass(
and not title:find('^Template:')
'feedback-page-card-text'
then
)
pageTitle = title
:wikitext(
end
entry.feedback
end
or 'No feedback provided'
)
 


return p._main(
--------------------------------------------------------
args,
-- Metadata
pageTitle
--------------------------------------------------------
)
end


local meta =
content:tag('div')
:addClass(
'feedback-page-card-meta'
)


function p._main(args, pageTitle)
local feedback =
trim(args.feedback)


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


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


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


local resolved =
isTruthy(args.resolved)


local status =
if entry.createddate
resolved
and entry.createddate ~= ''
and 'resolved'
then
or 'unresolved'


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


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


local id =
trim(args.id)


local createddate =
--------------------------------------------------------
trim(args.createddate)
-- Resolution
--------------------------------------------------------
 
if resolved then
 
local resolvedBy =
trim(entry.resolvedby)


local link =
trim(args.link)


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


local resolvedby =
trim(args.resolvedby)


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


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


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
resolution:wikitext(
return
'↳ Resolved by '
'<strong style="color:red;">Feedback bucket error: '
.. mw.text.nowiki(
saveError or 'Unknown error'
)
)
.. '</strong>'
end


--------------------------------------------------------
-- Render
--------------------------------------------------------


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


--------------------------------------------------------
end
-- Categories
--------------------------------------------------------


local category
end


if resolved then
category =
'[[Category:Resolved Feedback]]'
else
else
category =
'[[Category:Unresolved Feedback]]'
end


if id == '' then
if id ~= '' then
category =
category ..
'[[Category:Feedbacks Missing ID]]'
end


return card .. category
renderResolveControl(
end
root,
id,
page,
true
)


end


------------------------------------------------------------
-- RENDER FEEDBACK LIST
------------------------------------------------------------
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(
-- Attachments
list,
--------------------------------------------------------
renderFullCard(row)
)
end


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




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


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


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


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


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


local root =
local root =
mw.html.create('section')
mw.html.create('section')
:addClass(
'feedback-page-section'
)
:attr(
:attr(
'id',
'id',
'feedback-section'
'feedback-section'
)
)
:addClass('feedback-page')
:attr(
'data-unresolved-count',
#unresolved
)
:attr(
'data-resolved-count',
#resolved
)
 


--------------------------------------------------------
--------------------------------------------------------
Line 658: Line 1,034:


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


--------------------------------------------------------
--------------------------------------------------------
Line 667: Line 1,048:
local filters =
local filters =
root:tag('div')
root:tag('div')
:addClass('feedback-filters')
:addClass(
'feedback-page-filters'
)


local function addFilter(
 
label,
local unresolvedFilter =
value,
filters:tag('span')
active
:addClass(
)
'feedback-page-filter'
return filters:tag('button')
)
:addClass(
'feedback-page-filter--active'
)
:attr(
:attr(
'type',
'role',
'button'
'button'
)
:attr(
'tabindex',
'0'
)
)
:attr(
:attr(
'data-feedback-filter',
'data-feedback-filter',
value
'unresolved'
)
)
:addClass('feedback-filter')
 
 
unresolvedFilter:wikitext(
'Unresolved '
)
 
 
unresolvedFilter:tag('span')
:addClass(
'feedback-page-filter-count'
)
:wikitext(
tostring(
#unresolved
)
)
 
 
if #resolved > 0 then
 
local resolvedFilter =
filters:tag('span')
:addClass(
'feedback-page-filter'
)
:attr(
'role',
'button'
)
:attr(
'tabindex',
'0'
)
:attr(
'data-feedback-filter',
'resolved'
)
 
 
resolvedFilter:wikitext(
'Resolved '
)
 
 
resolvedFilter:tag('span')
:addClass(
:addClass(
active
'feedback-page-filter-count'
and 'feedback-filter--active'
)
or nil
:wikitext(
tostring(
#resolved
)
)
)
:wikitext(label)
 
end
end


addFilter(
'Unresolved',
'unresolved',
true
)
addFilter(
'Resolved',
'resolved',
false
)
addFilter(
'All',
'all',
false
)


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


Line 722: Line 1,142:
'feedback-page-list--unresolved'
'feedback-page-list--unresolved'
)
)
:addClass(
 
'feedback-page-list--visible'
 
if #unresolved > 0 then
 
for _, row in ipairs(unresolved) do
 
unresolvedList:node(
renderCompactCard(
row
)
)
)


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


--------------------------------------------------------
--------------------------------------------------------
-- Resolved
-- Resolved list
--------------------------------------------------------
--------------------------------------------------------


local resolvedList =
if #resolved > 0 then
root:tag('div')
 
:addClass(
local resolvedList =
'feedback-page-list'
root:tag('div')
)
:addClass(
:addClass(
'feedback-page-list'
'feedback-page-list--resolved'
)
)
: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


--------------------------------------------------------
for _, row in ipairs(resolved) do
-- All
--------------------------------------------------------


local allList =
resolvedList:node(
root:tag('div')
renderCompactCard(
:addClass(
row
'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
end
end


return tostring(root)
return tostring(root)
end
end




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


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


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


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


return tostring(
return tostring(
mw.html.create('span')
mw.html.create('span')
:addClass('feedback-count-data')
:addClass(
'feedback-count-data'
)
:attr(
:attr(
'data-unresolved',
'data-unresolved',
Line 832: Line 1,253:
resolved
resolved
)
)
)
)
end
end




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


local function renderTable(entries)
function p.main(frame)
if not entries or #entries == 0 then
 
return '<em>No feedback entries found.</em>'
local parent =
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 table =
 
local card =
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 =
tableRoot:tag('tr')
 
 
if includeResolved then
 
header:tag('th')
:wikitext(
'Status'
)
 
end
 
 
header:tag('th')
:wikitext(
'Page'
)
 
 
header:tag('th')
:wikitext(
'Username'
)
 
 
header:tag('th')
:wikitext(
'Date'
)
 


local head =
header:tag('th')
table:tag('thead')
:wikitext(
:tag('tr')
'Feedback'
)


head:tag('th')
:wikitext('Status')


head:tag('th')
if includeResolved then
:wikitext('Page')
 
header:tag('th')
:wikitext(
'Resolved by'
)
 


head:tag('th')
header:tag('th')
:wikitext('Feedback')
:wikitext(
'Resolved date'
)


head:tag('th')
end
:wikitext('User')


head:tag('th')
:wikitext('Date')


local body =
if #entries == 0 then
table:tag('tbody')


for _, entry in ipairs(entries) do
local row =
local row =
body:tag('tr')
tableRoot:tag('tr')
 
 
local colspan =
includeResolved
and '7'
or '4'


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


row:tag('td')
row:tag('td')
:attr(
'colspan',
colspan
)
:addClass(
:addClass(
resolved
'feedback-table-empty'
and 'feedback-table-status--resolved'
or 'feedback-table-status--unresolved'
)
)
:wikitext(
:wikitext(
resolved
'No feedback entries found.'
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-text')
:addClass(
'feedback-table-user'
)
:wikitext(
:wikitext(
entry.feedback or ''
formatUser(
entry.user
)
)
)


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


row:tag('td')
row:tag('td')
:addClass(
'feedback-table-feedback'
)
:wikitext(
:wikitext(
entry.createddate or ''
entry.feedback
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(table)
 
return tostring(
tableRoot
)
 
end
end


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


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


local args


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


elseif parent
--------------------------------------------------------
and parent.args
-- Specific table
and parent.args.status
--------------------------------------------------------
then
args = parent.args


elseif frame
if status == 'resolved'
and frame.args
or status == 'unresolved'
then
then
args = frame.args


elseif parent
local entries =
and parent.args
queryFeedback(
then
nil,
args = parent.args
status
)


else
args = {}
end


return p._view(
if not entries then
args,
return ''
frame
end
)
end




function p._view(args, frame)
return renderTable(
local bucket =
entries,
getBucket()
false
)


if not bucket then
return
'<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


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


local query =
local unresolved =
bucket
queryFeedback(
nil,
'unresolved'
)


query.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)


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


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


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


entries =
sortNewestFirst(entries)


--------------------------------------------------------
local root =
-- Specific status
mw.html.create('div')
--------------------------------------------------------
:addClass(
'feedback-dashboard'
)
 
 
root:tag('h2')
:addClass(
'feedback-dashboard-heading'
)
:wikitext(
'Unresolved Feedback'
)
 
 
root:node(
renderTable(
unresolved,
false
)
)
 
 
root:tag('h2')
:addClass(
'feedback-dashboard-heading'
)
:wikitext(
'All Feedback'
)
 


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


--------------------------------------------------------
return tostring(
-- All
root
--------------------------------------------------------
)


return renderTable(entries)
end
end


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


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


return p
return p

Revision as of 12:54, 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 ~= ''
		and value ~= '0'
		and value ~= 'false'
		and value ~= 'no'
		and value ~= 'off'
end


local function trim(value)

	return mw.text.trim(
		tostring(value or '')
	)

end


local function formatUser(user)

	user =
		trim(user)

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

	return string.format(
		'[[User:%s|%s]]',
		user,
		user
	)

end


local function currentSubjectPage()

	local title =
		mw.title.getCurrentTitle()

	return title.subjectPageTitle.fullText

end


local function getBucket()

	if not (mw.ext and mw.ext.bucket) then
		return nil, 'Bucket extension is not available'
	end


	local bucket =
		mw.ext.bucket('feedback')


	if not bucket then
		return nil, 'Feedback bucket could not be opened'
	end


	return bucket

end


local function sortNewestFirst(results)

	if not results then
		return {}
	end


	table.sort(
		results,
		function(a, b)

			local aDate =
				trim(a.createddate)

			local bDate =
				trim(b.createddate)


			if aDate == ''
				and bDate ~= ''
			then
				return false
			end


			if aDate ~= ''
				and bDate == ''
			then
				return true
			end


			if aDate ~= bDate then
				return aDate > bDate
			end


			return trim(a.id) > trim(b.id)

		end
	)


	return results

end


local function queryFeedback(
	page,
	status
)

	local bucket, err =
		getBucket()


	if not bucket then
		return nil, err
	end


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


	if page and page ~= '' then

		query.where(
			'page',
			page
		)

	end


	if status
		and status ~= ''
		and status ~= 'all'
	then

		query.where(
			'status',
			status
		)

	end


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


	if not success then
		return nil, tostring(results)
	end


	return sortNewestFirst(
		results or {}
	)

end


------------------------------------------------------------
-- Save feedback
------------------------------------------------------------

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


	return true

end


------------------------------------------------------------
-- Shared metadata
------------------------------------------------------------

local function renderMetadata(
	root,
	entry
)

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


	if entry.user
		and entry.user ~= ''
	then

		meta:tag('span')
			:addClass(
				'feedback-user'
			)
			:wikitext(
				formatUser(
					entry.user
				)
			)

	end


	if entry.createddate
		and entry.createddate ~= ''
	then

		meta:tag('span')
			:addClass(
				'feedback-meta-date'
			)
			:wikitext(
				' · ' ..
				mw.text.nowiki(
					entry.createddate
				)
			)

	end

end


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

		attachments:tag('span')
			:addClass(
				'feedback-attachment'
			)
			:wikitext(
				'🔗 ' ..
				'[' ..
				link ..
				' Link]'
			)

	end


	if image ~= '' then

		local imageAttachment =
			attachments:tag('span')
				:addClass(
					'feedback-attachment'
				)


		imageAttachment:tag('span')
			:addClass(
				'feedback-image-icon'
			)
			:wikitext('▣')


		imageAttachment:tag('span')
			:addClass(
				'feedback-image-link'
			)
			:wikitext(
				'[[Media:' ..
				image ..
				'|Picture]]'
			)

	end

end


------------------------------------------------------------
-- Resolution information
------------------------------------------------------------

local function renderResolution(
	root,
	entry
)

	local resolvedBy =
		trim(entry.resolvedby)


	local resolvedDate =
		trim(entry.resolveddate)


	if resolvedBy == ''
		and resolvedDate == ''
	then
		return
	end


	local resolution =
		root: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 not id
		or id == ''
	then
		return
	end


	local control =
		parent:tag('span')
			:addClass(
				'feedback-resolve-button'
			)



	control:attr(
		'data-feedback-id',
		id
	)


	if page
		and page ~= ''
	then

		control:attr(
			'data-feedback-page',
			page
		)

	end


	if compact then

		control
			:addClass(
				'feedback-resolve-button--compact'
			)
			:attr(
				'role',
				'button'
			)
			:attr(
				'tabindex',
				'0'
			)
			:attr(
				'aria-label',
				'Resolve feedback'
			)
			:attr(
				'title',
				'Resolve feedback'
			)
			:wikitext(
				'✓'
			)

	else

		control
			:attr(
				'role',
				'button'
			)
			:attr(
				'tabindex',
				'0'
			)
			:attr(
				'aria-label',
				'Resolve feedback'
			)
			:wikitext(
				'Resolve'
			)

	end

end


------------------------------------------------------------
-- Full feedback card
------------------------------------------------------------

local function renderFullCard(
	entry
)

	local resolved =
		entry.status == 'resolved'


	local id =
		trim(entry.id)


	local page =
		trim(entry.page)


	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 line
	--------------------------------------------------------

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


	main:tag('span')
		:addClass(
			'feedback-status'
		)
		:addClass(
			resolved
				and 'feedback-status--resolved'
				or 'feedback-status--unresolved'
		)
		:wikitext(
			resolved
				and 'RESOLVED'
				or 'UNRESOLVED'
		)


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


	--------------------------------------------------------
	-- Inline metadata
	--------------------------------------------------------

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


	if entry.user
		and entry.user ~= ''
	then

		metadata:wikitext(
			' · ' ..
			formatUser(
				entry.user
			)
		)

	end


	if entry.createddate
		and entry.createddate ~= ''
	then

		metadata:wikitext(
			' ' ..
			mw.text.nowiki(
				entry.createddate
			)
		)

	end


	--------------------------------------------------------
	-- Resolve button
	--------------------------------------------------------

	if not resolved
		and id ~= ''
	then

		renderResolveControl(
			main,
			id,
			page,
			false
		)

	end


	--------------------------------------------------------
	-- Attachments
	--------------------------------------------------------

	renderAttachments(
		root,
		entry
	)


	--------------------------------------------------------
	-- Resolution
	--------------------------------------------------------

	if resolved then

		renderResolution(
			root,
			entry
		)

	end


	--------------------------------------------------------
	-- Missing ID
	--------------------------------------------------------

	if not resolved
		and id == ''
	then

		root:tag('div')
			:addClass(
				'feedback-missing-id'
			)
			:wikitext(
				'Add an ID to enable the Resolve button.'
			)

	end


	return tostring(root)

end


------------------------------------------------------------
-- Compact article card
------------------------------------------------------------

local function renderCompactCard(
	entry
)

	local resolved =
		entry.status == 'resolved'


	local id =
		trim(entry.id)


	local page =
		trim(entry.page)


	local root =
		mw.html.create('div')
			:addClass(
				'feedback-page-card'
			)
			:addClass(
				resolved
					and 'feedback-page-card--resolved'
					or 'feedback-page-card--unresolved'
			)


	if id ~= '' then

		root:attr(
			'data-feedback-id',
			id
		)

	end


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


	local firstLine =
		content:tag('div')
			:addClass(
				'feedback-page-card-line'
			)


	firstLine:tag('span')
		:addClass(
			'feedback-status'
		)
		:addClass(
			resolved
				and 'feedback-status--resolved'
				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'
		)


	--------------------------------------------------------
	-- Metadata
	--------------------------------------------------------

	local meta =
		content:tag('div')
			:addClass(
				'feedback-page-card-meta'
			)


	if entry.user
		and entry.user ~= ''
	then

		meta:wikitext(
			formatUser(
				entry.user
			)
		)

	end


	if entry.createddate
		and entry.createddate ~= ''
	then

		meta:wikitext(
			' · ' ..
			mw.text.nowiki(
				entry.createddate
			)
		)

	end


	--------------------------------------------------------
	-- Resolution
	--------------------------------------------------------

	if resolved then

		local resolvedBy =
			trim(entry.resolvedby)


		local resolvedDate =
			trim(entry.resolveddate)


		if resolvedBy ~= ''
			or resolvedDate ~= ''
		then

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


			resolution:wikitext(
				'↳ Resolved by '
			)


			if resolvedBy ~= '' then

				resolution:wikitext(
					formatUser(
						resolvedBy
					)
				)

			else

				resolution:wikitext(
					'Unknown'
				)

			end


			if resolvedDate ~= '' then

				resolution:wikitext(
					' · ' ..
					mw.text.nowiki(
						resolvedDate
					)
				)

			end

		end

	else

		if id ~= '' then

			renderResolveControl(
				root,
				id,
				page,
				true
			)

		end

	end


	--------------------------------------------------------
	-- Attachments
	--------------------------------------------------------

	renderAttachments(
		content,
		entry
	)


	return tostring(root)

end


------------------------------------------------------------
-- Page feedback section
------------------------------------------------------------

function p.page(frame)

	local page =
		currentSubjectPage()


	local allEntries =
		queryFeedback(
			page
		)


	if not allEntries then
		return ''
	end


	if #allEntries == 0 then
		return ''
	end


	local unresolved = {}
	local resolved = {}


	for _, row in ipairs(allEntries) do

		if row.status == 'resolved' then

			table.insert(
				resolved,
				row
			)

		else

			table.insert(
				unresolved,
				row
			)

		end

	end


	local root =
		mw.html.create('section')
			:addClass(
				'feedback-page-section'
			)
			:attr(
				'id',
				'feedback-section'
			)
			:attr(
				'data-unresolved-count',
				#unresolved
			)
			:attr(
				'data-resolved-count',
				#resolved
			)


	--------------------------------------------------------
	-- Heading
	--------------------------------------------------------

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


	--------------------------------------------------------
	-- Filters
	--------------------------------------------------------

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


	local unresolvedFilter =
		filters:tag('span')
			:addClass(
				'feedback-page-filter'
			)
			:addClass(
				'feedback-page-filter--active'
			)
			:attr(
				'role',
				'button'
			)
			:attr(
				'tabindex',
				'0'
			)
			:attr(
				'data-feedback-filter',
				'unresolved'
			)


	unresolvedFilter:wikitext(
		'Unresolved '
	)


	unresolvedFilter:tag('span')
		:addClass(
			'feedback-page-filter-count'
		)
		:wikitext(
			tostring(
				#unresolved
			)
		)


	if #resolved > 0 then

		local resolvedFilter =
			filters:tag('span')
				: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
	--------------------------------------------------------

	local unresolvedList =
		root:tag('div')
			:addClass(
				'feedback-page-list'
			)
			:addClass(
				'feedback-page-list--unresolved'
			)


	if #unresolved > 0 then

		for _, row in ipairs(unresolved) do

			unresolvedList:node(
				renderCompactCard(
					row
				)
			)

		end

	else

		unresolvedList:tag('div')
			:addClass(
				'feedback-page-empty'
			)
			:wikitext(
				'No unresolved feedback.'
			)

	end


	--------------------------------------------------------
	-- Resolved list
	--------------------------------------------------------

	if #resolved > 0 then

		local resolvedList =
			root:tag('div')
				:addClass(
					'feedback-page-list'
				)
				:addClass(
					'feedback-page-list--resolved'
				)


		for _, row in ipairs(resolved) do

			resolvedList:node(
				renderCompactCard(
					row
				)
			)

		end

	end


	return tostring(root)

end


------------------------------------------------------------
-- Feedback counts
------------------------------------------------------------

function p.counts(frame)

	local page =
		currentSubjectPage()


	local entries =
		queryFeedback(
			page
		)


	if not entries then
		return ''
	end


	local unresolved = 0
	local resolved = 0


	for _, row in ipairs(entries) do

		if row.status == '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


------------------------------------------------------------
-- Main / individual feedback
------------------------------------------------------------

function p.main(frame)

	local parent =
		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


	local card =
		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')
			:addClass(
				'feedback-table'
			)


	local header =
		tableRoot:tag('tr')


	if includeResolved then

		header:tag('th')
			:wikitext(
				'Status'
			)

	end


	header:tag('th')
		:wikitext(
			'Page'
		)


	header:tag('th')
		:wikitext(
			'Username'
		)


	header:tag('th')
		:wikitext(
			'Date'
		)


	header:tag('th')
		:wikitext(
			'Feedback'
		)


	if includeResolved then

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


		header:tag('th')
			:wikitext(
				'Resolved date'
			)

	end


	if #entries == 0 then

		local row =
			tableRoot:tag('tr')


		local colspan =
			includeResolved
				and '7'
				or '4'


		row:tag('td')
			:attr(
				'colspan',
				colspan
			)
			:addClass(
				'feedback-table-empty'
			)
			:wikitext(
				'No feedback entries found.'
			)


		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')
			:addClass(
				'feedback-table-page'
			)
			:wikitext(
				'[[' ..
				entry.page ..
				']]'
			)


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


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


		row:tag('td')
			:addClass(
				'feedback-table-feedback'
			)
			:wikitext(
				entry.feedback
					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


	return tostring(
		tableRoot
	)

end


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

function p.view(frame)

	local parent =
		frame:getParent()


	local args =
		parent
			and parent.args
			or frame.args


	local status =
		trim(
			args.status or ''
		):lower()


	--------------------------------------------------------
	-- Specific table
	--------------------------------------------------------

	if status == 'resolved'
		or status == 'unresolved'
	then

		local entries =
			queryFeedback(
				nil,
				status
			)


		if not entries then
			return ''
		end


		return renderTable(
			entries,
			false
		)

	end


	--------------------------------------------------------
	-- Dashboard
	--------------------------------------------------------

	local unresolved =
		queryFeedback(
			nil,
			'unresolved'
		)


	local all =
		queryFeedback(
			nil,
			'all'
		)


	if not unresolved then
		unresolved = {}
	end


	if not all then
		all = {}
	end


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


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


	root:node(
		renderTable(
			unresolved,
			false
		)
	)


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


	root:node(
		renderTable(
			all,
			true
		)
	)


	return tostring(
		root
	)

end


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

p.table = p.view


return p