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
 
(16 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 resolved = (entry.status == 'resolved')
local image = trim(entry.image)


local user =
if image ~= '' then
entry.user
image = mw.ustring.gsub(
or 'Anonymous'
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 feedback =
local resolution =
entry.feedback
parent:tag('div')
or 'No feedback provided'
:addClass('feedback-resolution')


local page =
resolution:wikitext('Resolved by ')
entry.page


local id =
if resolvedBy ~= '' then
mw.text.trim(
resolution:wikitext(
tostring(entry.id or '')
formatUser(resolvedBy)
)
)
else
resolution:wikitext('Unknown')
end


local resolvedBy =
if resolvedDate ~= '' then
mw.text.trim(
resolution:wikitext(
tostring(entry.resolvedby or '')
' on ' .. mw.text.nowiki(resolvedDate)
)
)
end
end


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


local root = mw.html.create('div')
------------------------------------------------------------
:addClass('feedback-box')
-- RESOLVE CONTROL
:addClass(
------------------------------------------------------------
resolved
and 'feedback-box--resolved'
or 'feedback-box--unresolved'
)


--------------------------------------------------------
local function renderResolveControl(
-- Meta
parent,
--------------------------------------------------------
id,
page,
compact
)
if trim(id) == '' then
return
end


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


if page and page ~= '' then
local control =
meta:tag('span')
wrapper:tag('span')
:addClass('feedback-page')
:addClass('feedback-resolve-button')
:wikitext(
:attr(
string.format(
'data-feedback-id',
'[[%s]]',
id
page
)
)
: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-separator')
control
:wikitext(' • ')
:addClass(
'feedback-resolve-button--compact'
)
:attr(
'title',
'Resolve feedback'
)
:wikitext('✓')
else
control:wikitext('Resolve')
end
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'
)


meta:tag('span')
if id ~= '' then
:addClass('feedback-user')
root:attr(
:wikitext(
'data-feedback-id',
formatUser(user)
id
)
)
end
--------------------------------------------------------
-- Main content column
--------------------------------------------------------
local content =
root:tag('div')
:addClass('feedback-content')
--------------------------------------------------------
-- Main line
--------------------------------------------------------


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


meta:tag('span')
--------------------------------------------------------
-- Status
--------------------------------------------------------
 
main:tag('span')
:addClass('feedback-status')
:addClass('feedback-status')
:addClass(
:addClass(
Line 123: Line 331:


--------------------------------------------------------
--------------------------------------------------------
-- Feedback
-- Feedback text
--------------------------------------------------------
--------------------------------------------------------


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 ~= '' then
local meta =
main:tag('span')
:addClass('feedback-inline-meta')


local resolution = root:tag('div')
if user ~= '' then
:addClass('feedback-resolution')
meta:wikitext(
' · ' .. formatUser(user)
)
end


resolution:wikitext(
if createddate ~= '' then
'Resolved by '
meta:wikitext(
.. formatUser(resolvedBy)
' · ' .. mw.text.nowiki(createddate)
)
)
if resolvedDate ~= '' then
resolution:wikitext(
' on '
.. mw.text.nowiki(resolvedDate)
)
end
end
end


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


if showInstruction and not resolved then
--------------------------------------------------------
-- Resolution information
--------------------------------------------------------


local instruction = root:tag('div')
renderResolution(
:addClass('feedback-instruction')
content,
entry
)


if id ~= '' then
--------------------------------------------------------
-- Missing ID warning
--------------------------------------------------------


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


instruction:tag('button')
--------------------------------------------------------
:addClass('feedback-resolve-button')
-- Resolve button
:attr('type', 'button')
--
:attr('data-feedback-id', id)
-- IMPORTANT:
:wikitext('Resolve')
-- This is outside .feedback-content so it
-- remains on the right.
--------------------------------------------------------


else
if not resolved then
 
renderResolveControl(
instruction:wikitext(
root,
'Add an ID to this feedback to enable the Resolve button.'
id,
)
page,
 
false
end
)
end
end


Line 187: 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'
-- SAVE FEEDBACK
end
------------------------------------------------------------


local bucket =
local function saveFeedback(data)
mw.ext.bucket('feedback')
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 211: 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)


Line 226: Line 456:


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


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


local args =
local args =
Line 241: Line 471:


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


if title
if title
Line 261: Line 489:


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


local feedback =
if feedback == '' then
args.feedback
feedback = 'No feedback provided'
or 'No feedback provided'
end


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


local id =
if user == '' then
args.id
user = 'Anonymous'
or ''
end
 
local resolvedBy =
args.resolvedby
or ''
 
local resolvedDate =
args.resolveddate
or ''


local resolved =
local resolved =
Line 291: Line 512:


local currentPage =
local currentPage =
pageTitle
trim(pageTitle)
or mw.title.getCurrentTitle().fullText
 
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)


--------------------------------------------------------
--------------------------------------------------------
Line 305: Line 548:
feedback = feedback,
feedback = feedback,
id = id,
id = id,
resolvedby = resolvedBy,
createddate = createddate,
resolveddate = resolvedDate
link = link,
image = image,
resolvedby = resolvedby,
resolveddate = resolveddate
})
})


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


--------------------------------------------------------
--------------------------------------------------------
-- Render card
-- Render
--------------------------------------------------------
--------------------------------------------------------


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


--------------------------------------------------------
--------------------------------------------------------
Line 336: Line 586:
--------------------------------------------------------
--------------------------------------------------------


local categories = {}
local category


if resolved then
if resolved then
table.insert(
category =
categories,
'[[Category:Resolved Feedback]]'
'[[Category:Resolved Feedback]]'
)
else
else
table.insert(
category =
categories,
'[[Category:Unresolved Feedback]]'
'[[Category:Unresolved Feedback]]'
)
end
end


if mw.text.trim(
if id == '' then
tostring(id)
category =
) == '' then
category ..
 
table.insert(
categories,
'[[Category:Feedbacks Missing ID]]'
'[[Category:Feedbacks Missing ID]]'
)
end
end


return card
return card .. category
.. table.concat(categories)
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>'
Line 378: Line 618:


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


Line 397: Line 632:


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


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


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


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


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


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


elseif parent
--------------------------------------------------------
and parent.args
-- Heading
and parent.args.status
--------------------------------------------------------
then


args =
root:tag('h2')
parent.args
:addClass('feedback-page-title')
:wikitext('Feedbacks')


elseif frame
--------------------------------------------------------
and frame.args
-- Filters
then
--------------------------------------------------------


args =
local filters =
frame.args
root:tag('div')
:addClass('feedback-filters')


elseif parent
local function addFilter(
and parent.args
label,
then
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


args =
addFilter(
parent.args
'Unresolved',
'unresolved',
true
)


else
addFilter(
'Resolved',
'resolved',
false
)


args = {}
addFilter(
 
'All',
end
'all',
 
false
return p._view(
args,
frame
)
)
end


--------------------------------------------------------
-- Unresolved
--------------------------------------------------------


function p._view(args, frame)
local unresolvedList =
root:tag('div')
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--unresolved'
)
:addClass(
'feedback-page-list--visible'
)


if not (mw.ext and mw.ext.bucket) then
if #unresolved == 0 then
return '<em>Bucket extension is not available.</em>'
unresolvedList:tag('div')
:addClass('feedback-empty')
:wikitext(
'No unresolved feedback.'
)
else
for _, entry in ipairs(unresolved) do
unresolvedList:wikitext(
renderCompactCard(entry)
)
end
end
end
frame =
frame
or mw.getCurrentFrame()
local rawStatus =
args.status
or args[1]
local filterStatus =
rawStatus
and mw.text.trim(rawStatus):lower()
or nil


--------------------------------------------------------
--------------------------------------------------------
-- Filtered views
-- Resolved
--------------------------------------------------------
--------------------------------------------------------


if filterStatus == 'resolved'
local resolvedList =
or filterStatus == 'unresolved'
root:tag('div')
then
:addClass(
'feedback-page-list'
)
:addClass(
'feedback-page-list--resolved'
)


local query =
if #resolved == 0 then
mw.ext.bucket('feedback')
resolvedList:tag('div')
 
:addClass('feedback-empty')
query.select(
:wikitext(
'page',
'No resolved feedback.'
'user',
)
'status',
else
'feedback',
for _, entry in ipairs(resolved) do
'id',
resolvedList:wikitext(
'resolvedby',
renderCompactCard(entry)
'resolveddate'
)
)
end
 
query.where(
'status',
filterStatus
)
 
return renderFeedbackList(
query.run()
)
end
end


Line 504: Line 787:
--------------------------------------------------------
--------------------------------------------------------


if filterStatus == 'all' then
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


local query =
return tostring(root)
mw.ext.bucket('feedback')
end


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


return renderFeedbackList(
------------------------------------------------------------
query.run()
-- COUNTS
------------------------------------------------------------
 
function p.counts(frame)
local entries =
queryFeedback(
currentSubjectPage()
)
)
end


--------------------------------------------------------
local unresolved = 0
-- Default tabbed view
local resolved = 0
--------------------------------------------------------


local qAll =
for _, entry in ipairs(entries) do
mw.ext.bucket('feedback')
if trim(entry.status):lower()
== 'resolved'
then
resolved = resolved + 1
else
unresolved = unresolved + 1
end
end


qAll.select(
return tostring(
'page',
mw.html.create('span')
'user',
:addClass('feedback-count-data')
'status',
:attr(
'feedback',
'data-unresolved',
'id',
unresolved
'resolvedby',
)
'resolveddate'
:attr(
'data-resolved',
resolved
)
)
)
end


local allEntries =
qAll.run()


local allList = {}
------------------------------------------------------------
local unresolvedList = {}
-- TABLE / DASHBOARD
local resolvedList = {}
------------------------------------------------------------


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


for _, row in ipairs(allEntries) do
local table =
mw.html.create('table')
:addClass('feedback-table')


local card =
local head =
renderCard(
table:tag('thead')
row,
:tag('tr')
false
)


table.insert(
head:tag('th')
allList,
:wikitext('Status')
card
)


if row.status == 'resolved' then
head:tag('th')
:wikitext('Page')


table.insert(
head:tag('th')
resolvedList,
:wikitext('Feedback')
card
)


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


table.insert(
head:tag('th')
unresolvedList,
:wikitext('Date')
card
)


end
local body =
end
table:tag('tbody')
end


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


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


if #allList > 0 then
row:tag('td')
:addClass(
resolved
and 'feedback-table-status--resolved'
or 'feedback-table-status--unresolved'
)
:wikitext(
resolved
and 'Resolved'
or 'Unresolved'
)


allContent =
row:tag('td')
table.concat(
:addClass('feedback-table-page')
allList,
:wikitext(
'\n'
entry.page or ''
)
)


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


allContent =
row:tag('td')
'<em>No feedback entries found.</em>'
:wikitext(
formatUser(entry.user)
)


row:tag('td')
:wikitext(
entry.createddate or ''
)
end
end


--------------------------------------------------------
return tostring(table)
-- Unresolved content
end
--------------------------------------------------------
 
 
function p.view(frame)
local parent =
frame:getParent()
 
local args
 
if frame
and frame.args
and frame.args.status
then
args = frame.args


local unresolvedContent
elseif parent
and parent.args
and parent.args.status
then
args = parent.args


if #unresolvedList > 0 then
elseif frame
and frame.args
then
args = frame.args


unresolvedContent =
elseif parent
table.concat(
and parent.args
unresolvedList,
then
'\n'
args = parent.args
)


else
else
args = {}
end


unresolvedContent =
return p._view(
'<em>No unresolved feedback entries found.</em>'
args,
frame
)
end


function p._view(args, frame)
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 filterStatus =
rawStatus
and trim(rawStatus):lower()
or nil


--------------------------------------------------------
--------------------------------------------------------
-- Resolved content
-- Query
--------------------------------------------------------
--------------------------------------------------------


local resolvedContent
local query =
bucket


if #resolvedList > 0 then
query.select(
'page',
'user',
'status',
'feedback',
'id',
'createddate',
'link',
'image',
'resolvedby',
'resolveddate'
)


resolvedContent =
if filterStatus == 'resolved'
table.concat(
or filterStatus == 'unresolved'
resolvedList,
then
'\n'
query.where(
)
'status',
filterStatus
)
end


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


resolvedContent =
if not success or not entries then
'<em>No resolved feedback entries found.</em>'
return
'<em>Could not load feedback.</em>'
end


end
entries =
sortNewestFirst(entries)


--------------------------------------------------------
--------------------------------------------------------
-- Tabber
-- Specific status
--------------------------------------------------------
--------------------------------------------------------


local tabberBody =
if filterStatus == 'resolved'
string.format(
or filterStatus == 'unresolved'
'All=\n%s\n|-|Unresolved=\n%s\n|-|Resolved=\n%s',
then
allContent,
return renderFeedbackList(
unresolvedContent,
entries
resolvedContent
)
)
end


return frame:callParserFunction(
--------------------------------------------------------
'#tag',
-- All
{
--------------------------------------------------------
'tabber',
 
tabberBody
return renderTable(entries)
}
)
end
end
p.table = p.view


return p
return p