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
 
(19 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 32: Line 40:
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
end




------------------------------------------------------------
------------------------------------------------------------
-- Feedback card
-- ATTACHMENTS
------------------------------------------------------------
------------------------------------------------------------


local function renderCard(entry, showInstruction)
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 resolved = (entry.status == 'resolved')
local title =
mw.title.new('File:' .. image)


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


local root = mw.html.create('div')
return wrapper
:addClass('feedback-box')
end
:addClass(
 
resolved
 
and 'feedback-box--resolved'
------------------------------------------------------------
or 'feedback-box--unresolved'
-- 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




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


meta:tag('span')
local root =
:addClass('feedback-separator')
mw.html.create('div')
:wikitext(' • ')
: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 102: Line 330:
)
)


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


--------------------------------------------------------
--------------------------------------------------------
-- Feedback text
-- Metadata
--------------------------------------------------------
--------------------------------------------------------


root:tag('div')
local meta =
:addClass('feedback-body')
main:tag('span')
:wikitext(feedback)
: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 instruction
-- Resolution information
--------------------------------------------------------
--------------------------------------------------------


if showInstruction and not resolved then
renderResolution(
content,
entry
)
 
--------------------------------------------------------
-- Missing ID warning
--------------------------------------------------------


root:tag('div')
if id == '' then
:addClass('feedback-instruction')
content:tag('div')
:addClass('feedback-missing-id')
:wikitext(
:wikitext(
'If this feedback has been resolved, edit this page '
'This feedback entry is missing an ID.'
.. 'and set <code>|resolved=true</code>.'
)
)
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
end


return tostring(root)
return tostring(root)
Line 133: 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,
user = data.user,
user = data.user,
status = data.status,
status = data.status,
feedback = data.feedback
feedback = data.feedback,
id = data.id,
createddate = data.createddate,
link = data.link,
image = data.image,
resolvedby = data.resolvedby,
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 172: Line 456:


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


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


Line 192: 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 210: 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 240: Line 511:
or 'unresolved'
or 'unresolved'


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


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


local resolveddate =
trim(args.resolveddate)


--------------------------------------------------------
--------------------------------------------------------
Line 250: 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,
}, true)
resolvedby = resolvedby,
 
resolveddate = resolveddate
})


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


Line 291: 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
--
-- Examples:
--
-- {{#invoke:Feedback|view}}
--
-- {{#invoke:Feedback|view|status=all}}
--
-- {{#invoke:Feedback|view|status=resolved}}
--
-- {{#invoke:Feedback|view|status=unresolved}}
------------------------------------------------------------
------------------------------------------------------------


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


 
root:tag('h2')
function p._view(args, frame)
: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
--
-- Default:
--
-- All
-- Unresolved
-- Resolved
--------------------------------------------------------


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(
p.table = p.view
'#tag',
{
'tabber',
tabberBody
}
)
 
end
 


return p
return p