Module:Feedback: Difference between revisions
Appearance
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 | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
| Line 12: | Line 12: | ||
value = mw.text.trim(tostring(value)):lower() | value = mw.text.trim(tostring(value)):lower() | ||
return value | return value == 'true' | ||
or value == 'yes' | |||
or value == '1' | |||
or value == 'resolved' | |||
end | end | ||
local function trim(value) | local function trim(value) | ||
return mw.text.trim( | if value == nil then | ||
return '' | |||
end | |||
return mw.text.trim(tostring(value)) | |||
end | end | ||
| Line 30: | Line 31: | ||
user = trim(user) | user = trim(user) | ||
if user == ' | 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() | ||
if title.subjectPageTitle then | |||
return title.subjectPageTitle.fullText | |||
end | |||
return title | 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 | return nil | ||
end | end | ||
return bucket | return mw.ext.bucket('feedback') | ||
end | end | ||
local function sortNewestFirst( | local function sortNewestFirst(entries) | ||
table.sort( | table.sort( | ||
entries, | |||
function(a, b) | function(a, b) | ||
local aDate = trim(a.createddate) | |||
local bDate = trim(b.createddate) | |||
return aDate > bDate | |||
end | end | ||
) | ) | ||
return | return entries | ||
end | end | ||
local function queryFeedback(page | local function queryFeedback(page) | ||
local bucket | local bucket = getBucket() | ||
if not bucket then | if not bucket then | ||
return | return {} | ||
end | end | ||
local query = | 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 {} | |||
if not | |||
return | |||
end | end | ||
return | return sortNewestFirst(results) | ||
end | end | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- ATTACHMENTS | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
local function | local function renderAttachments(entry) | ||
local wrapper = | |||
mw.html.create('div') | |||
:addClass('feedback-attachments') | |||
local | 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 | end | ||
local image = trim(entry.image) | |||
if image ~= '' then | |||
: | image = mw.ustring.gsub( | ||
image, | |||
'^File:', | |||
'' | |||
) | |||
local title = | |||
mw.title.new('File:' .. image) | |||
local | |||
if title then | |||
local imageWrapper = | |||
wrapper:tag('span') | |||
:addClass('feedback-attachment-image') | |||
:wikitext( | |||
' | imageWrapper:tag('span') | ||
.. ' | :addClass('feedback-attachment-icon') | ||
.. | |||
imageWrapper:wikitext( | |||
'[' .. | |||
title:fullUrl() .. | |||
' ' .. | |||
mw.text.nowiki(image) .. | |||
']' | |||
) | ) | ||
end | |||
end | end | ||
return wrapper | |||
end | end | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- RESOLUTION | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
local function renderResolution( | local function renderResolution(parent, entry) | ||
local resolvedBy = trim(entry.resolvedby) | |||
local resolvedBy = | local resolvedDate = trim(entry.resolveddate) | ||
if resolvedBy == '' and resolvedDate == '' then | |||
if resolvedBy == '' | |||
return | return | ||
end | end | ||
local resolution = | local resolution = | ||
parent:tag('div') | |||
:addClass('feedback-resolution') | :addClass('feedback-resolution') | ||
resolution:wikitext(' | 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) | ||
) | ) | ||
end | end | ||
| Line 333: | Line 210: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- RESOLVE CONTROL | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
| Line 339: | Line 216: | ||
parent, | parent, | ||
id, | id, | ||
page, | |||
compact | compact | ||
) | ) | ||
if trim(id) == '' then | |||
if | |||
return | return | ||
end | end | ||
local wrapper = | |||
parent:tag('div') | |||
:addClass('feedback-resolve') | |||
local control = | local control = | ||
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( | ||
'feedback-resolve-button--compact' | |||
:attr('title', 'Resolve feedback') | ) | ||
:attr( | |||
'title', | |||
'Resolve feedback' | |||
) | |||
:wikitext('✓') | :wikitext('✓') | ||
else | else | ||
control:wikitext('Resolve') | |||
control | |||
end | end | ||
end | end | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- 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) | ||
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 = | ||
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 ' | and 'Resolved' | ||
or ' | or 'Unresolved' | ||
) | ) | ||
-------------------------------------------------------- | |||
-- Feedback text | |||
-------------------------------------------------------- | |||
main:tag('span') | main:tag('span') | ||
:addClass('feedback-text') | :addClass('feedback-text') | ||
:wikitext( | :wikitext( | ||
feedback ~= '' | |||
and feedback | |||
or 'No feedback provided' | or 'No feedback provided' | ||
) | ) | ||
| Line 435: | Line 346: | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
local | local meta = | ||
main:tag('span') | main:tag('span') | ||
:addClass('feedback-inline-meta') | :addClass('feedback-inline-meta') | ||
if | if user ~= '' then | ||
meta:wikitext( | |||
' · ' .. formatUser(user) | |||
' · ' | |||
) | ) | ||
end | end | ||
if | if createddate ~= '' then | ||
meta:wikitext( | |||
' · ' .. mw.text.nowiki(createddate) | |||
' ' | |||
) | ) | ||
end | end | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
-- | -- Attachments | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
content:node( | |||
renderAttachments(entry) | |||
) | |||
-------------------------------------------------------- | -------------------------------------------------------- | ||
-- | -- Resolution information | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
renderResolution( | |||
content, | |||
entry | entry | ||
) | ) | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
-- | -- Missing ID warning | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
if | if id == '' then | ||
content:tag('div') | |||
:addClass('feedback-missing-id') | |||
entry | :wikitext( | ||
'This feedback entry is missing an ID.' | |||
) | |||
end | end | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
-- | -- Resolve button | ||
-- | |||
-- IMPORTANT: | |||
-- This is outside .feedback-content so it | |||
-- remains on the right. | |||
-------------------------------------------------------- | -------------------------------------------------------- | ||
if not resolved | if not resolved then | ||
renderResolveControl( | |||
root, | |||
id, | |||
page, | |||
false | |||
) | |||
end | end | ||
| Line 520: | Line 413: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- COMPACT CARD | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
local function renderCompactCard(entry) | local function renderCompactCard(entry) | ||
return renderFullCard(entry) | |||
end | |||
------------------------------------------------------------ | |||
-- SAVE FEEDBACK | |||
------------------------------------------------------------ | |||
local | local function saveFeedback(data) | ||
local bucket = getBucket() | |||
if not bucket then | |||
return false, 'Bucket extension is not available' | |||
end | |||
local | 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 | if not success then | ||
return false, tostring(err) | |||
end | end | ||
local | 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 | |||
local | function p._main(args, pageTitle) | ||
local feedback = | |||
trim(args.feedback) | |||
if | if feedback == '' then | ||
feedback = 'No feedback provided' | |||
end | |||
local user = | |||
trim(args.user) | |||
if user == '' then | |||
user = 'Anonymous' | |||
end | end | ||
local resolved = | |||
and | isTruthy(args.resolved) | ||
local status = | |||
resolved | |||
and 'resolved' | |||
or 'unresolved' | |||
local currentPage = | |||
trim(pageTitle) | |||
if currentPage == '' then | |||
currentPage = | |||
mw.title.getCurrentTitle().fullText | |||
end | 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 | 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 | end | ||
return table.concat( | |||
list, | |||
'\n' | |||
) | ) | ||
end | end | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- PAGE FEEDBACK | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
function p.page(frame) | function p.page(frame) | ||
local page = | local page = | ||
currentSubjectPage() | currentSubjectPage() | ||
local | local entries = | ||
queryFeedback(page) | queryFeedback(page) | ||
local unresolved = {} | local unresolved = {} | ||
local resolved = {} | local resolved = {} | ||
for _, | for _, entry in ipairs(entries) do | ||
if trim(entry.status):lower() | |||
if | == 'resolved' | ||
then | |||
table.insert( | table.insert( | ||
resolved, | resolved, | ||
entry | |||
) | ) | ||
else | else | ||
table.insert( | table.insert( | ||
unresolved, | unresolved, | ||
entry | |||
) | ) | ||
end | end | ||
end | end | ||
| Line 711: | Line 663: | ||
local root = | local root = | ||
mw.html.create('section') | mw.html.create('section') | ||
:attr( | :attr( | ||
' | 'id', | ||
'feedback-section' | |||
) | ) | ||
:addClass('feedback-page') | |||
-------------------------------------------------------- | -------------------------------------------------------- | ||
| Line 727: | Line 674: | ||
root:tag('h2') | root:tag('h2') | ||
:addClass('feedback-page- | :addClass('feedback-page-title') | ||
:wikitext('Feedbacks') | :wikitext('Feedbacks') | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
-- | -- Filters | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
local filters = | local filters = | ||
root:tag('div') | root:tag('div') | ||
:addClass('feedback | :addClass('feedback-filters') | ||
local | local function addFilter( | ||
label, | |||
value, | |||
active | |||
) | |||
return filters:tag('button') | |||
:attr( | :attr( | ||
' | 'type', | ||
'button' | 'button' | ||
) | ) | ||
:attr( | :attr( | ||
' | 'data-feedback-filter', | ||
value | |||
) | ) | ||
: | :addClass('feedback-filter') | ||
:addClass( | |||
' | active | ||
and 'feedback-filter--active' | |||
or nil | |||
) | ) | ||
:wikitext(label) | |||
end | |||
addFilter( | |||
'Unresolved ' | 'Unresolved', | ||
'unresolved', | |||
true | |||
) | ) | ||
addFilter( | |||
'Resolved', | |||
'resolved', | |||
false | |||
) | |||
addFilter( | |||
'All', | |||
'all', | |||
false | |||
) | |||
-------------------------------------------------------- | -------------------------------------------------------- | ||
-- Unresolved | -- Unresolved | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
| Line 808: | Line 737: | ||
:addClass( | :addClass( | ||
'feedback-page-list--unresolved' | 'feedback-page-list--unresolved' | ||
) | |||
:addClass( | |||
'feedback-page-list--visible' | |||
) | ) | ||
if #unresolved | if #unresolved == 0 then | ||
unresolvedList:tag('div') | |||
for _, | :addClass('feedback-empty') | ||
unresolvedList: | :wikitext( | ||
renderCompactCard( | 'No unresolved feedback.' | ||
) | |||
else | |||
for _, entry in ipairs(unresolved) do | |||
unresolvedList:wikitext( | |||
renderCompactCard(entry) | |||
) | ) | ||
end | end | ||
end | |||
-------------------------------------------------------- | |||
-- Resolved | |||
-------------------------------------------------------- | |||
local resolvedList = | |||
root:tag('div') | |||
:addClass( | |||
'feedback-page-list' | |||
) | |||
:addClass( | :addClass( | ||
'feedback-page- | 'feedback-page-list--resolved' | ||
) | ) | ||
if #resolved == 0 then | |||
resolvedList:tag('div') | |||
:addClass('feedback-empty') | |||
:wikitext( | :wikitext( | ||
'No | 'No resolved feedback.' | ||
) | |||
else | |||
for _, entry in ipairs(resolved) do | |||
resolvedList:wikitext( | |||
renderCompactCard(entry) | |||
) | ) | ||
end | |||
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( | |||
for _, | renderCompactCard(entry) | ||
renderCompactCard( | |||
) | ) | ||
end | end | ||
end | end | ||
| Line 858: | Line 815: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- COUNTS | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
function p.counts(frame) | function p.counts(frame) | ||
local entries = | local entries = | ||
queryFeedback( | queryFeedback( | ||
currentSubjectPage() | |||
) | |||
local unresolved = 0 | local unresolved = 0 | ||
local resolved = 0 | local resolved = 0 | ||
for _, | for _, entry in ipairs(entries) do | ||
if trim(entry.status):lower() | |||
if | == 'resolved' | ||
then | |||
resolved = resolved + 1 | resolved = resolved + 1 | ||
else | else | ||
unresolved = unresolved + 1 | unresolved = unresolved + 1 | ||
end | end | ||
end | end | ||
| Line 902: | Line 853: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- TABLE / DASHBOARD | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
function | local function renderTable(entries) | ||
if not entries or #entries == 0 then | |||
return '<em>No feedback entries found.</em>' | |||
return | |||
end | end | ||
local | local table = | ||
mw.html.create('table') | mw.html.create('table') | ||
:addClass('feedback-table') | :addClass('feedback-table') | ||
local | local head = | ||
table:tag('thead') | |||
:tag('tr') | |||
head:tag('th') | |||
:wikitext('Status') | |||
head:tag('th') | |||
:wikitext('Page') | :wikitext('Page') | ||
head:tag('th') | |||
:wikitext('Feedback') | :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 = | local row = | ||
body:tag('tr') | |||
local | local resolved = | ||
trim(entry.status):lower() | |||
== 'resolved' | |||
row:tag('td') | row:tag('td') | ||
:addClass( | :addClass( | ||
'feedback-table- | resolved | ||
and 'feedback-table-status--resolved' | |||
or 'feedback-table-status--unresolved' | |||
) | ) | ||
:wikitext( | :wikitext( | ||
' | resolved | ||
and 'Resolved' | |||
or 'Unresolved' | |||
) | ) | ||
row:tag('td') | row:tag('td') | ||
:addClass('feedback-table-page') | :addClass('feedback-table-page') | ||
:wikitext( | :wikitext( | ||
entry.page or '' | |||
) | ) | ||
row:tag('td') | row:tag('td') | ||
:addClass('feedback-table- | :addClass('feedback-table-text') | ||
:wikitext( | :wikitext( | ||
entry.feedback or '' | |||
) | ) | ||
row:tag('td') | row:tag('td') | ||
:wikitext( | :wikitext( | ||
formatUser(entry.user) | |||
) | ) | ||
row:tag('td') | row:tag('td') | ||
:wikitext( | :wikitext( | ||
entry. | entry.createddate or '' | ||
) | ) | ||
end | end | ||
return tostring( | return tostring(table) | ||
end | end | ||
function p.view(frame) | function p.view(frame) | ||
local parent = | local parent = | ||
frame:getParent() | frame:getParent() | ||
local args | 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 | then | ||
args = parent.args | |||
else | |||
args = {} | |||
end | |||
return p._view( | |||
args, | |||
frame | |||
) | |||
end | |||
function p._view(args, frame) | |||
local bucket = | |||
getBucket() | |||
return | 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 | ||
-------------------------------------------------------- | -------------------------------------------------------- | ||
local | 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 | if not success or not entries then | ||
return | |||
'<em>Could not load feedback.</em>' | |||
end | end | ||
entries = | |||
sortNewestFirst(entries) | |||
-------------------------------------------------------- | |||
-- Specific status | |||
-------------------------------------------------------- | |||
if filterStatus == 'resolved' | |||
or filterStatus == 'unresolved' | |||
then | |||
return renderFeedbackList( | |||
entries | |||
) | ) | ||
end | |||
-------------------------------------------------------- | |||
-- All | |||
-------------------------------------------------------- | |||
return renderTable(entries) | |||
end | end | ||
p.table = p.view | p.table = p.view | ||
return p | return p | ||
Latest revision as of 13:57, 16 September 2026
| Main | Documentation | Usage |
This module implements {{Feedback}}.
local p = {}
------------------------------------------------------------
-- HELPERS
------------------------------------------------------------
local function isTruthy(value)
if value == nil then
return false
end
value = mw.text.trim(tostring(value)):lower()
return value == 'true'
or value == 'yes'
or value == '1'
or value == 'resolved'
end
local function trim(value)
if value == nil then
return ''
end
return mw.text.trim(tostring(value))
end
local function formatUser(user)
user = trim(user)
if user == '' then
return 'Anonymous'
end
return string.format(
'[[User:%s|%s]]',
user,
user
)
end
local function currentSubjectPage()
local title = mw.title.getCurrentTitle()
if title.subjectPageTitle then
return title.subjectPageTitle.fullText
end
return title.fullText
end
local function getBucket()
if not (mw.ext and mw.ext.bucket) then
return nil
end
return mw.ext.bucket('feedback')
end
local function sortNewestFirst(entries)
table.sort(
entries,
function(a, b)
local aDate = trim(a.createddate)
local bDate = trim(b.createddate)
return aDate > bDate
end
)
return entries
end
local function queryFeedback(page)
local bucket = getBucket()
if not bucket then
return {}
end
local query = bucket
query.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)
query.where(
'page',
page
)
local success, results = pcall(function()
return query.run()
end)
if not success or not results then
return {}
end
return sortNewestFirst(results)
end
------------------------------------------------------------
-- ATTACHMENTS
------------------------------------------------------------
local function renderAttachments(entry)
local wrapper =
mw.html.create('div')
:addClass('feedback-attachments')
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