Jump to content
Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
If you make an edit, a temporary account will be created for you.

Module:Feedback: Difference between revisions

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


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


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


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


return v ~= ''
return value == 'true'
and v ~= '0'
or value == 'yes'
and v ~= 'false'
or value == '1'
and v ~= 'no'
or value == 'resolved'
and v ~= 'off'
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 = mw.text.trim(tostring(user or ''))
user = trim(user)


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




local function escapeAttribute(value)
local function currentSubjectPage()
value = tostring(value or '')
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 mw.text.nowiki(value)
return wrapper
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Feedback card
-- RESOLUTION
------------------------------------------------------------
------------------------------------------------------------


local function renderCard(entry, showInstruction)
local function renderResolution(parent, entry)
local resolvedBy = trim(entry.resolvedby)
local resolvedDate = trim(entry.resolveddate)
 
if resolvedBy == '' and resolvedDate == '' then
return
end


local resolved = (entry.status == 'resolved')
local resolution =
parent:tag('div')
:addClass('feedback-resolution')


local user = entry.user or 'Anonymous'
resolution:wikitext('Resolved by ')
local feedback = entry.feedback or 'No feedback provided'
local page = entry.page
local id = entry.id
local resolvedBy = entry.resolvedby
local resolvedDate = entry.resolveddate


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


local root = mw.html.create('div')
if resolvedDate ~= '' then
:addClass('feedback-box')
resolution:wikitext(
:addClass(
' on ' .. mw.text.nowiki(resolvedDate)
resolved
and 'feedback-box--resolved'
or 'feedback-box--unresolved'
)
)
end
end




--------------------------------------------------------
------------------------------------------------------------
-- Metadata
-- RESOLVE CONTROL
--------------------------------------------------------
------------------------------------------------------------


local meta = root:tag('div')
local function renderResolveControl(
:addClass('feedback-meta')
parent,
id,
page,
compact
)
if trim(id) == '' then
return
end


local wrapper =
parent:tag('div')
:addClass('feedback-resolve')


if page and page ~= '' then
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'
)


meta:tag('span')
if compact then
:addClass('feedback-page')
control
:wikitext(
:addClass(
string.format('[[%s]]', page)
'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'


meta:tag('span')
local id = trim(entry.id)
:addClass('feedback-separator')
local page = trim(entry.page)
:wikitext(' • ')
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
end


--------------------------------------------------------
-- Main content column
--------------------------------------------------------


meta:tag('span')
local content =
:addClass('feedback-user')
root:tag('div')
:wikitext(formatUser(user))
:addClass('feedback-content')


--------------------------------------------------------
-- Main line
--------------------------------------------------------


meta:tag('span')
local main =
:addClass('feedback-separator')
content:tag('div')
:wikitext(' • ')
:addClass('feedback-main-line')


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


meta:tag('span')
main:tag('span')
:addClass('feedback-status')
:addClass('feedback-status')
:addClass(
:addClass(
Line 112: Line 329:
or 'Unresolved'
or 'Unresolved'
)
)


--------------------------------------------------------
--------------------------------------------------------
Line 118: Line 334:
--------------------------------------------------------
--------------------------------------------------------


root:tag('div')
main:tag('span')
:addClass('feedback-body')
:addClass('feedback-text')
:wikitext(feedback)
:wikitext(
 
feedback ~= ''
and feedback
or 'No feedback provided'
)


--------------------------------------------------------
--------------------------------------------------------
-- Resolution information
-- Metadata
--------------------------------------------------------
--------------------------------------------------------


if resolved and resolvedBy and resolvedBy ~= '' then
local meta =
main:tag('span')
:addClass('feedback-inline-meta')


local resolutionMeta =
if user ~= '' then
'Resolved by ' .. formatUser(resolvedBy)
meta:wikitext(
 
' · ' .. formatUser(user)
if resolvedDate and resolvedDate ~= '' then
)
resolutionMeta =
end
resolutionMeta
.. ' on '
.. resolvedDate
end
 
root:tag('div')
:addClass('feedback-resolution')
:wikitext(resolutionMeta)


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


--------------------------------------------------------
-- Attachments
--------------------------------------------------------
content:node(
    renderAttachments(entry)
)


--------------------------------------------------------
--------------------------------------------------------
-- Resolution controls
-- Resolution information
--------------------------------------------------------
--------------------------------------------------------


if showInstruction and not resolved then
renderResolution(
 
content,
local instruction = root:tag('div')
entry
:addClass('feedback-instruction')
)


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


instruction:tag('span')
if id == '' then
content:tag('div')
:addClass('feedback-missing-id')
:wikitext(
:wikitext(
'If this feedback has been resolved, '
'This feedback entry is missing an ID.'
.. 'click the button below.'
)
)
end


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


if id and id ~= '' then
if not resolved then
 
renderResolveControl(
instruction:tag('button')
root,
:addClass('feedback-resolve-button')
id,
:attr('type', 'button')
page,
:attr('data-feedback-id', escapeAttribute(id))
false
:wikitext('Resolve')
)
 
end
 
end
end


return tostring(root)
return tostring(root)
Line 181: Line 413:


------------------------------------------------------------
------------------------------------------------------------
-- Save feedback
-- COMPACT CARD
------------------------------------------------------------
------------------------------------------------------------


local function saveFeedback(data)
local function renderCompactCard(entry)
return renderFullCard(entry)
end


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


------------------------------------------------------------
-- SAVE FEEDBACK
------------------------------------------------------------


local bucket = mw.ext.bucket('feedback')
local function saveFeedback(data)
local bucket = getBucket()


if not bucket then
if not bucket then
return false, 'Feedback bucket could not be opened'
return false, 'Bucket extension is not available'
end
end


local success, err = pcall(function()
local success, err = pcall(function()
bucket.put({
bucket.put({
page = data.page,
page = data.page,
Line 206: Line 439:
feedback = data.feedback,
feedback = data.feedback,
id = data.id,
id = data.id,
createddate = data.createddate,
link = data.link,
image = data.image,
resolvedby = data.resolvedby,
resolvedby = data.resolvedby,
resolveddate = data.resolveddate
resolveddate = data.resolveddate
})
})
end)
end)


if not success then
if not success then
return false, tostring(err)
return false, tostring(err)
end
end


return true
return true
Line 223: Line 456:


------------------------------------------------------------
------------------------------------------------------------
-- Main
-- MAIN
--
--
-- Used by Template:Feedback
-- Used by Template:Feedback
--
-- Example:
--
-- {{Feedback
--  |feedback=Something could be improved here.
--  |user=Rei
--  |resolved=false
--  |id=1758012345678
--  |resolvedby=
--  |resolveddate=
-- }}
------------------------------------------------------------
------------------------------------------------------------


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


Line 246: Line 467:
parent and parent.args
parent and parent.args
or frame.args
or frame.args
--------------------------------------------------------
-- Get page title
--------------------------------------------------------


local pageTitle = nil
local pageTitle = nil


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


Line 264: Line 479:
pageTitle = title
pageTitle = title
end
end
end
end


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




function p._main(args, pageTitle)
function p._main(args, pageTitle)
local feedback =
local feedback =
args.feedback
trim(args.feedback)
or 'No feedback provided'


if feedback == '' then
feedback = 'No feedback provided'
end


local user =
local user =
args.user
trim(args.user)
or 'Anonymous'


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


local resolved =
local resolved =
isTruthy(args.resolved)
isTruthy(args.resolved)


local status =
local status =
Line 294: Line 511:
or 'unresolved'
or 'unresolved'


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


local id =
local id =
mw.text.trim(
trim(args.id)
tostring(args.id or '')
)
 
 
local resolvedBy =
mw.text.trim(
tostring(args.resolvedby or '')
)


local createddate =
trim(args.createddate)


local resolvedDate =
local link =
mw.text.trim(
trim(args.link)
tostring(args.resolveddate or '')
)


local image =
trim(args.image)


local currentPage =
local resolvedby =
pageTitle
trim(args.resolvedby)
or mw.title.getCurrentTitle().fullText


local resolveddate =
trim(args.resolveddate)


--------------------------------------------------------
--------------------------------------------------------
Line 322: Line 541:
--------------------------------------------------------
--------------------------------------------------------


local saved, saveError = saveFeedback({
local saved, saveError =
page = currentPage,
saveFeedback({
user = user,
page = currentPage,
status = status,
user = user,
feedback = feedback
status = status,
})
feedback = feedback,
 
id = id,
createddate = createddate,
link = link,
image = image,
resolvedby = resolvedby,
resolveddate = resolveddate
})


if not saved then
if not saved then
 
return
return '<strong style="color:red;">Feedback bucket error: '
'<strong style="color:red;">Feedback bucket error: '
.. mw.text.nowiki(saveError or 'Unknown error')
.. mw.text.nowiki(
saveError or 'Unknown error'
)
.. '</strong>'
.. '</strong>'
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Render feedback card
-- Render
--------------------------------------------------------
--------------------------------------------------------


local card = renderCard({
local card =
 
renderFullCard({
page = nil,
page = currentPage,
 
user = user,
user = user,
status = status,
 
feedback = feedback,
status = status,
id = id,
 
createddate = createddate,
feedback = feedback,
link = link,
 
image = image,
id = id,
resolvedby = resolvedby,
 
resolveddate = resolveddate
resolvedby = resolvedBy,
})
 
resolveddate = resolvedDate
 
}, true)
 


--------------------------------------------------------
--------------------------------------------------------
-- Categorize feedback
-- Categories
--------------------------------------------------------
--------------------------------------------------------


Line 369: Line 589:


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


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


return card .. category
return card .. category
end
end




------------------------------------------------------------
------------------------------------------------------------
-- Render feedback list
-- RENDER FEEDBACK LIST
------------------------------------------------------------
------------------------------------------------------------


local function renderFeedbackList(results)
local function renderFeedbackList(results)
if not results or #results == 0 then
if not results or #results == 0 then
return '<em>No feedback entries found.</em>'
return '<em>No feedback entries found.</em>'
end
end


local list = {}
local list = {}


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


 
return table.concat(
return table.concat(list, '\n')
list,
 
'\n'
)
end
end




------------------------------------------------------------
------------------------------------------------------------
-- View feedback
-- PAGE FEEDBACK
------------------------------------------------------------
------------------------------------------------------------


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


local parent = frame:getParent()
local entries =
queryFeedback(page)


local unresolved = {}
local resolved = {}


local args
for _, entry in ipairs(entries) do
 
if trim(entry.status):lower()
if frame
== 'resolved'
and frame.args
then
and frame.args.status
table.insert(
then
resolved,
 
entry
args = frame.args
)
 
else
elseif parent
table.insert(
and parent.args
unresolved,
and parent.args.status
entry
then
)
 
end
args = parent.args
 
elseif frame
and frame.args
then
 
args = frame.args
 
elseif parent
and parent.args
then
 
args = parent.args
 
else
 
args = {}
 
end
end


local root =
mw.html.create('section')
:attr(
'id',
'feedback-section'
)
:addClass('feedback-page')


return p._view(args, frame)
--------------------------------------------------------
 
-- Heading
end
--------------------------------------------------------
 


function p._view(args, frame)
root:tag('h2')
:addClass('feedback-page-title')
:wikitext('Feedbacks')


--------------------------------------------------------
--------------------------------------------------------
-- Check Bucket
-- Filters
--------------------------------------------------------
--------------------------------------------------------


if not (mw.ext and mw.ext.bucket) then
local filters =
 
root:tag('div')
return
:addClass('feedback-filters')
'<em>Bucket extension is not available.</em>'


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
end


addFilter(
'Unresolved',
'unresolved',
true
)


frame = frame or mw.getCurrentFrame()
addFilter(
'Resolved',
'resolved',
false
)


addFilter(
'All',
'all',
false
)


--------------------------------------------------------
--------------------------------------------------------
-- Determine requested status
-- Unresolved
--------------------------------------------------------
--------------------------------------------------------


local rawStatus =
local unresolvedList =
args.status
root:tag('div')
or args[1]
:addClass(
 
'feedback-page-list'
 
)
local filterStatus =
:addClass(
rawStatus
'feedback-page-list--unresolved'
and mw.text.trim(rawStatus):lower()
)
or nil
: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


--------------------------------------------------------
--------------------------------------------------------
-- Specific status
-- Resolved
--------------------------------------------------------
--------------------------------------------------------


if filterStatus == 'resolved'
local resolvedList =
or filterStatus == 'unresolved'
root:tag('div')
then
:addClass(
 
'feedback-page-list'
local query =
)
mw.ext.bucket('feedback')
:addClass(
 
'feedback-page-list--resolved'
 
)
query.select(
'page',
'user',
'status',
'feedback'
)
 
 
query.where(
'status',
filterStatus
)
 
 
return renderFeedbackList(
query.run()
)


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
end


--------------------------------------------------------
--------------------------------------------------------
-- All feedback
-- All
--------------------------------------------------------
--------------------------------------------------------


if filterStatus == 'all' then
local allList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--all'
)


local query =
if #entries == 0 then
mw.ext.bucket('feedback')
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


query.select(
'page',
'user',
'status',
'feedback'
)


------------------------------------------------------------
-- COUNTS
------------------------------------------------------------


return renderFeedbackList(
function p.counts(frame)
query.run()
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
end


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


--------------------------------------------------------
-- Tabber mode
--------------------------------------------------------


local qAll =
------------------------------------------------------------
mw.ext.bucket('feedback')
-- TABLE / DASHBOARD
------------------------------------------------------------


local function renderTable(entries)
if not entries or #entries == 0 then
return '<em>No feedback entries found.</em>'
end


qAll.select(
local table =
'page',
mw.html.create('table')
'user',
:addClass('feedback-table')
'status',
'feedback'
)


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


local allEntries =
head:tag('th')
qAll.run()
:wikitext('Status')


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


local allList = {}
head:tag('th')
local unresolvedList = {}
:wikitext('Feedback')
local resolvedList = {}


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


--------------------------------------------------------
head:tag('th')
-- Split entries by status
:wikitext('Date')
--------------------------------------------------------


if allEntries and #allEntries > 0 then
local body =
table:tag('tbody')


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


local card =
local resolved =
renderCard(row, false)
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'
)


table.insert(
row:tag('td')
allList,
:addClass('feedback-table-page')
card
:wikitext(
entry.page or ''
)
)


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


if row.status == 'resolved' then
row:tag('td')
:wikitext(
formatUser(entry.user)
)


table.insert(
row:tag('td')
resolvedList,
:wikitext(
card
entry.createddate or ''
)
)
end


else
return tostring(table)
end


table.insert(
unresolvedList,
card
)


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


end
local args


end
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
-- Build tab contents
and frame.args
--------------------------------------------------------
then
args = frame.args


local allContent
elseif parent
and parent.args
then
args = parent.args


if #allList > 0 then
else
args = {}
end


allContent =
return p._view(
table.concat(allList, '\n')
args,
frame
)
end


else


allContent =
function p._view(args, frame)
'<em>No feedback entries found.</em>'
local bucket =
getBucket()


if not bucket then
return
'<em>Bucket extension is not available.</em>'
end
end


local rawStatus =
args.status
or args[1]


local unresolvedContent
local filterStatus =
rawStatus
and trim(rawStatus):lower()
or nil


if #unresolvedList > 0 then
--------------------------------------------------------
-- Query
--------------------------------------------------------


unresolvedContent =
local query =
table.concat(unresolvedList, '\n')
bucket


else
query.select(
 
'page',
unresolvedContent =
'user',
'<em>No unresolved feedback entries found.</em>'
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)


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


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


local resolvedContent
if not success or not entries then
return
'<em>Could not load feedback.</em>'
end


if #resolvedList > 0 then
entries =
sortNewestFirst(entries)


resolvedContent =
--------------------------------------------------------
table.concat(resolvedList, '\n')
-- Specific status
 
--------------------------------------------------------
else
 
resolvedContent =
'<em>No resolved feedback entries found.</em>'


if filterStatus == 'resolved'
or filterStatus == 'unresolved'
then
return renderFeedbackList(
entries
)
end
end


--------------------------------------------------------
--------------------------------------------------------
-- Tabber
-- All
--------------------------------------------------------
--------------------------------------------------------


local tabberBody = string.format(
return renderTable(entries)
'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
end
allContent,
unresolvedContent,
resolvedContent
)
 
 
return frame:callParserFunction(
'#tag',
{
'tabber',
tabberBody
}
)


end


p.table = p.view


return p
return p