Module:Feedback: Difference between revisions
Appearance
ReisuDesign (talk | contribs) Created page with "local p = {} ------------------------------------------------------------ -- Helpers ------------------------------------------------------------ local function isTruthy(v) if v == nil then return false end v = mw.text.trim(tostring(v)):lower() return v ~= '' and v ~= '0' and v ~= 'false' and v ~= 'no' and v ~= 'off' end local function formatUser(user) user = mw.text.trim(tostring(user or '')) if user..." |
ReisuDesign (talk | contribs) No edit summary |
||
| (20 intermediate revisions by the same user not shown) | |||
| Line 2: | Line 2: | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- HELPERS | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
local function isTruthy( | 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 | end | ||
local function formatUser(user) | local function formatUser(user) | ||
user = trim(user) | |||
if user == '' then | |||
return 'Anonymous' | |||
end | |||
return string.format( | |||
'[[User:%s|%s]]', | |||
user, | |||
user | |||
) | |||
end | 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 | end | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- RESOLUTION | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
local function | 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 | end | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- RESOLVE CONTROL | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
function | 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 | end | ||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
-- | -- MAIN | ||
-- | |||
-- Used by Template:Feedback | |||
------------------------------------------------------------ | ------------------------------------------------------------ | ||
local | 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) | |||
end | 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 | 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 | 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