ep_comments_page
Advanced tools
Comparing version 0.0.3 to 0.0.5
@@ -1,2 +0,1 @@ | ||
var db = require('ep_etherpad-lite/node/db/DB').db; | ||
@@ -12,13 +11,4 @@ var ERR = require("ep_etherpad-lite/node_modules/async-stacktrace"); | ||
if(ERR(err, callback)) return; | ||
//comment does not exists | ||
if(comments == null) comments = {}; | ||
/*var comments = []; | ||
var jsonComments = globalComments.comments; | ||
for (var commentId in jsonComments) | ||
{ | ||
comments[commentId] = jsonComments; | ||
}*/ | ||
callback(null, { comments: comments }); | ||
@@ -56,2 +46,48 @@ }); | ||
}); | ||
}; | ||
}; | ||
exports.getCommentReplies = function (padId, callback) | ||
{ | ||
//get the globalComments replies | ||
db.get("comment-replies:" + padId, function(err, replies) | ||
{ | ||
if(ERR(err, callback)) return; | ||
//comment does not exists | ||
if(replies == null) replies = {}; | ||
callback(null, { replies: replies }); | ||
}); | ||
}; | ||
exports.addCommentReply = function(padId, data, callback) | ||
{ | ||
//create the new reply replyid | ||
var replyId = "c-reply-" + randomString(16); | ||
//get the entry | ||
db.get("comment-replies:" + padId, function(err, replies){ | ||
if(ERR(err, callback)) return; | ||
// the entry doesn't exist so far, let's create it | ||
if(replies == null) replies = {}; | ||
metadata = data.comment; | ||
var reply = { | ||
"commentId": data.commentId, | ||
"text": data.reply, | ||
"author": metadata.author, | ||
"name": metadata.name, | ||
"timestamp": new Date().getTime() | ||
}; | ||
//add the entry for this pad | ||
replies[replyId] = reply; | ||
//save the new element back | ||
db.set("comment-replies:" + padId, replies); | ||
callback(null, replyId, reply); | ||
}); | ||
}; | ||
23
index.js
@@ -14,7 +14,6 @@ var eejs = require('ep_etherpad-lite/node/eejs/'); | ||
// Join the rooms | ||
socket.on('getComments', function (data, callback) { | ||
var padId = data.padId; | ||
socket.join(padId); | ||
commentManager.getComments(padId, function (err, comments){ | ||
@@ -24,7 +23,14 @@ callback(comments); | ||
}); | ||
socket.on('getCommentReplies', function (data, callback) { | ||
var padId = data.padId; | ||
commentManager.getCommentReplies(padId, function (err, replies){ | ||
callback(replies); | ||
}); | ||
}); | ||
// On add events | ||
socket.on('addComment', function (data, callback) { | ||
var padId = data.padId; | ||
var content = data.comment; | ||
commentManager.addComment(padId, content, function (err, commentId, comment){ | ||
@@ -36,2 +42,13 @@ socket.broadcast.to(padId).emit('pushAddComment', commentId, comment); | ||
socket.on('addCommentReply', function (data, callback) { | ||
var padId = data.padId; | ||
var content = data.reply; | ||
var commentId = data.commentId; | ||
commentManager.addCommentReply(padId, data, function (err, replyId, reply){ | ||
reply.replyId = replyId; | ||
socket.broadcast.to(padId).emit('pushAddCommentReply', replyId, reply); | ||
callback(replyId, reply); | ||
}); | ||
}); | ||
}); | ||
@@ -38,0 +55,0 @@ }; |
{ | ||
"description": "Adds comments on sidebar and link it to the text. Support for Page View, requires ep_page_view", | ||
"name": "ep_comments_page", | ||
"version": "0.0.3", | ||
"version": "0.0.5", | ||
"author": { | ||
@@ -6,0 +6,0 @@ "name": "Nicolas Lescop", |
@@ -10,43 +10,2 @@ var _, $, jQuery; | ||
/************************************************************************/ | ||
function prettyDate(time){ | ||
var time_formats = [ | ||
[60, 'seconds', 1], // 60 | ||
[120, '1 minute ago', '1 minute from now'], // 60*2 | ||
[3600, 'minutes', 60], // 60*60, 60 | ||
[7200, '1 hour ago', '1 hour from now'], // 60*60*2 | ||
[86400, 'hours', 3600], // 60*60*24, 60*60 | ||
[172800, 'yesterday', 'tomorrow'], // 60*60*24*2 | ||
[604800, 'days', 86400], // 60*60*24*7, 60*60*24 | ||
[1209600, 'last week', 'next week'], // 60*60*24*7*4*2 | ||
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7 | ||
[4838400, 'last month', 'next month'], // 60*60*24*7*4*2 | ||
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4 | ||
[58060800, 'last year', 'next year'], // 60*60*24*7*4*12*2 | ||
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12 | ||
[5806080000, 'last century', 'next century'], // 60*60*24*7*4*12*100*2 | ||
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100 | ||
]; | ||
/* | ||
var time = ('' + date_str).replace(/-/g,"/").replace(/[TZ]/g," ").replace(/^\s\s*/ /*rappel , '').replace(/\s\s*$/, ''); | ||
if(time.substr(time.length-4,1)==".") time =time.substr(0,time.length-4); | ||
*/ | ||
var seconds = (new Date - new Date(time)) / 1000; | ||
var token = 'ago', list_choice = 1; | ||
if (seconds < 0) { | ||
seconds = Math.abs(seconds); | ||
token = 'from now'; | ||
list_choice = 2; | ||
} | ||
var i = 0, format; | ||
while (format = time_formats[i++]) | ||
if (seconds < format[0]) { | ||
if (typeof format[2] == 'string') | ||
return format[list_choice]; | ||
else | ||
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token; | ||
} | ||
return time; | ||
}; | ||
// Container | ||
@@ -56,3 +15,2 @@ function ep_comments(context){ | ||
this.padOuter = null; | ||
this.sideDiv = null; | ||
this.padInner = null; | ||
@@ -63,3 +21,3 @@ this.ace = context.ace; | ||
this.comments = []; | ||
this.commentReplies = {}; | ||
this.init(); | ||
@@ -76,3 +34,2 @@ } | ||
this.insertContainer(); | ||
this.hideLineNumbers(); | ||
@@ -87,6 +44,15 @@ // Get all comments | ||
this.getCommentReplies(function (replies){ | ||
if (!$.isEmptyObject(replies)){ | ||
// console.log("collecting comment replies"); | ||
self.commentReplies = replies; | ||
self.collectCommentReplies(); | ||
self.commentRepliesListen(); | ||
} | ||
}); | ||
// Init add push event | ||
this.pushComment('add', function (commentId, comment){ | ||
self.setComment(commentId, comment); | ||
console.log('pushComment', comment); | ||
// console.log('pushComment', comment); | ||
window.setTimeout(function() { | ||
@@ -98,28 +64,57 @@ self.collectComments(); | ||
// On click comment icon toolbar | ||
$('.addComment').on('click', function(){ | ||
$('.addComment').on('click', function(e){ | ||
e.preventDefault(); // stops focus from being lost | ||
// If a new comment box doesn't already exist | ||
// Add a new comment and link it to the selection | ||
$('iframe[name="ace_outer"]').contents().find('#sidediv').removeClass('sidedivhidden'); | ||
// $('iframe[name="ace_outer"]').contents().find('#sidediv').removeClass('sidedivhidden'); | ||
if (self.container.find('#newComment').length == 0) self.addComment(); | ||
// console.log("setting focus to .comment-content"); | ||
$('iframe[name="ace_outer"]').contents().find('#comments').find('#newComment').show(); | ||
$('iframe[name="ace_outer"]').contents().find('#comments').find('#newComment').addClass("visible").removeClass("hidden"); | ||
$('iframe[name="ace_outer"]').contents().find('.comment-content').focus(); | ||
}); | ||
// Show all comments | ||
$('iframe[name="ace_outer"]').contents().find('#sidediv').removeClass('sidedivhidden'); | ||
// $('iframe[name="ace_outer"]').contents().find('#sidediv').removeClass('sidedivhidden'); | ||
// Create hover modal | ||
$('iframe[name="ace_outer"]').contents().find("body") | ||
.append("<div class='comment-modal'><p class='comment-modal-name'></p><p class='comment-modal-comment'></p></div>"); | ||
// Listen to reply clicks | ||
this.container.on("click", ".comment-reply-button", function(e){ | ||
var commentId = $(this).parent()[0].id; | ||
$('iframe[name="ace_outer"]').contents().find("#"+commentId).append("<form class='comment-reply'><input class='comment-reply-input'><!--<input type=submit>--></form>"); | ||
}); | ||
// var socket = this.socket; | ||
this.container.on("submit", ".comment-reply", function(e){ | ||
e.preventDefault(); | ||
var data = self.getCommentData(); | ||
data.commentId = $(this).parent()[0].id; | ||
data.reply = $(this).find(".comment-reply-input").val(); | ||
self.socket.emit('addCommentReply', data, function (){ | ||
// Append the reply to the comment | ||
console.warn("addCommentReplyEmit WE EXPECT REPLY ID", data); | ||
$('iframe[name="ace_outer"]').contents().find('#'+data.commentId + ' > .comment-reply > .comment-reply-input').val(""); | ||
self.getCommentReplies(function(replies){ | ||
self.commentReplies = replies; | ||
self.collectCommentReplies(); | ||
}); | ||
}); | ||
}); | ||
}; | ||
// Insert comments container on sideDiv element use for linenumbers | ||
// Insert comments container on element use for linenumbers | ||
ep_comments.prototype.findContainers = function(){ | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
this.padOuter = padOuter; | ||
this.sideDiv = padOuter.find('#sidediv'); | ||
this.padInner = padOuter.find('iframe[name="ace_inner"]'); | ||
this.outerBody = padOuter.find('#outerdocbody') | ||
}; | ||
// Hide line numbers | ||
ep_comments.prototype.hideLineNumbers = function(){ | ||
this.sideDiv.find('table').hide(); | ||
}; | ||
// Collect Comments and link text content to the sidediv | ||
// Collect Comments and link text content to the comments div | ||
ep_comments.prototype.collectComments = function(callback){ | ||
@@ -151,9 +146,2 @@ var self = this; | ||
commentElm = container.find('#'+ commentId); | ||
$this.mouseenter(function(){ | ||
commentElm.addClass('mouseover'); | ||
}).mouseleave(function(){ | ||
commentElm.removeClass('mouseover'); | ||
}); | ||
$(this).on('click', function(){ | ||
@@ -163,6 +151,6 @@ markerTop = $(this).position().top; | ||
containerTop = container.css('top'); | ||
console.log(container); | ||
container.css('top', containerTop - (commentTop - markerTop)); | ||
}); | ||
} | ||
} | ||
@@ -184,7 +172,81 @@ | ||
}); | ||
self.setYofComments() | ||
// now if we apply a class such as mouseover to the editor it will go shitty | ||
// so what we need to do is add CSS for the specific ID to the document... | ||
// It's fucked up but that's how we do it.. | ||
var padInner = this.padInner; | ||
this.container.on("mouseover", ".sidebar-comment", function(e){ | ||
var commentId = e.currentTarget.id; | ||
var inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); | ||
inner.contents().find("head").append("<style>."+commentId+"{ color:orange }</style>"); | ||
// on hover we should show the reply option | ||
}).on("mouseout", ".sidebar-comment", function(e){ | ||
var commentId = e.currentTarget.id; | ||
var inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); | ||
inner.contents().find("head").append("<style>."+commentId+"{ color:black }</style>"); | ||
// TODO this could potentially break ep_font_color | ||
}); | ||
// hover event | ||
this.padInner.contents().on("mouseover", ".comment" ,function(e){ | ||
self.highlightComment(e); | ||
}); | ||
// click event | ||
this.padInner.contents().on("click", ".comment" ,function(e){ | ||
self.highlightComment(e); | ||
}); | ||
this.padInner.contents().on("mouseleave", ".comment" ,function(e){ | ||
var cls = e.currentTarget.classList; | ||
var classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); | ||
var commentId = (classCommentId) ? classCommentId[1] : null; | ||
var commentElm = $('iframe[name="ace_outer"]').contents().find("#comments").find('#'+ commentId); | ||
commentElm.removeClass('mouseover'); | ||
$('iframe[name="ace_outer"]').contents().find('.comment-modal').hide(); | ||
}); | ||
self.setYofComments(); | ||
}; | ||
// Collect Comments and link text content to the comments div | ||
ep_comments.prototype.collectCommentReplies = function(callback){ | ||
console.warn("collectCommentReplies", this.commentReplies); | ||
var self = this; | ||
var container = this.container; | ||
var commentReplies = this.commentReplies; | ||
var padComment = this.padInner.contents().find('.comment'); | ||
$.each(this.commentReplies, function(replyId, replies){ | ||
var commentId = replies.commentId; | ||
var existsAlready = $('iframe[name="ace_outer"]').contents().find('#'+replyId).length; | ||
if(existsAlready) return; | ||
replies.replyId = replyId; | ||
var content = $("#replyTemplate").tmpl(replies); | ||
$('iframe[name="ace_outer"]').contents().find('#'+commentId + ' > .comment-reply-button').before(content); | ||
}); | ||
}; | ||
ep_comments.prototype.highlightComment = function(e){ | ||
var cls = e.currentTarget.classList; | ||
var classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); | ||
var commentId = (classCommentId) ? classCommentId[1] : null; | ||
var commentElm = $('iframe[name="ace_outer"]').contents().find("#comments").find('#'+ commentId); | ||
var commentsVisible = this.container.is(":visible"); | ||
if(commentsVisible){ | ||
// sidebar view highlight | ||
commentElm.addClass('mouseover'); | ||
}else{ | ||
var commentElm = $('iframe[name="ace_outer"]').contents().find("#comments").find('#'+ commentId); | ||
$('iframe[name="ace_outer"]').contents().find('.comment-modal').show().css({ | ||
left: e.clientX +"px", | ||
top: e.clientY + 25 +"px" | ||
}); | ||
// hovering comment view | ||
$('iframe[name="ace_outer"]').contents().find('.comment-modal-comment').html(commentElm.html()); | ||
} | ||
} | ||
ep_comments.prototype.removeComment = function(className, id){ | ||
console.log('remove comment', className, id); | ||
// console.log('remove comment', className, id); | ||
} | ||
@@ -194,8 +256,5 @@ | ||
ep_comments.prototype.insertContainer = function(){ | ||
var sideDiv = this.sideDiv; | ||
// Add comments | ||
sideDiv.prepend('<div id="comments"></div>'); | ||
this.container = sideDiv.find('#comments'); | ||
$('iframe[name="ace_outer"]').contents().find("#outerdocbody").prepend('<div id="comments"></div>'); | ||
this.container = this.padOuter.find('#comments'); | ||
}; | ||
@@ -206,3 +265,3 @@ | ||
var index = 0; | ||
this.insertComment("", comment, index, true); | ||
@@ -212,3 +271,3 @@ | ||
var form = $(this).parent().parent(); | ||
form.remove(); | ||
$('iframe[name="ace_outer"]').contents().find('#comments').find('#newComment').addClass("hidden").removeClass("visible"); | ||
}); | ||
@@ -219,8 +278,8 @@ | ||
var text = form.find('.comment-content').val(); | ||
if (text.length != 0) { | ||
form.remove(); | ||
$('iframe[name="ace_outer"]').contents().find('#comments').find('#newComment').addClass("hidden").removeClass("visible"); | ||
// console.log("calling back", text, index); | ||
callback(text, index); | ||
} | ||
return false; | ||
@@ -230,5 +289,8 @@ }); | ||
// Set the top of the form to be the same Y as the target Rep | ||
console.log("oh snap"); | ||
var ace = this.ace; | ||
console.log("SUP"); // never gets here.. | ||
ace.callWithAce(function (ace){ | ||
var rep = ace.ace_getRep(); | ||
console.log("rep", rep); // doesn't fire twice | ||
var line = rep.lines.atIndex(rep.selStart[0]); | ||
@@ -240,3 +302,3 @@ var key = "#"+line.key; | ||
var y = ele[0].offsetTop; | ||
$('iframe[name="ace_outer"]').contents().find('#sidediv').contents().find('#newComment').css("top", y+"px").show(); | ||
$('iframe[name="ace_outer"]').contents().find('#comments').contents().find('#newComment').css("top", y+"px").show(); | ||
// scroll new comment form to focus | ||
@@ -260,3 +322,3 @@ $('iframe[name="ace_outer"]').contents().find('#outerdocbody').scrollTop(y); // Works in Chrome | ||
console.log('position', index, commentAfterIndex); | ||
// console.log('position', index, commentAfterIndex); | ||
if (index === 0) { | ||
@@ -278,3 +340,3 @@ content.prependTo(container); | ||
var inlineComments = padInner.contents().find(".comment"); | ||
padOuter.find("#comments").children().each(function(){ | ||
padOuter.find("#comments").children("div").each(function(){ | ||
// hide each outer comment | ||
@@ -285,3 +347,3 @@ $(this).hide(); | ||
var y = this.offsetTop; | ||
y = y+5; | ||
y = y-5; | ||
var commentId = /(?:^| )c-([A-Za-z0-9]*)/.exec(this.className); // classname is the ID of the comment | ||
@@ -291,2 +353,3 @@ var commentEle = padOuter.find('#c-'+commentId[1]) // find the comment | ||
}); | ||
}; | ||
@@ -318,2 +381,11 @@ | ||
// Get all comment replies | ||
ep_comments.prototype.getCommentReplies = function (callback){ | ||
var req = { padId: this.padId }; | ||
this.socket.emit('getCommentReplies', req, function (res){ | ||
// console.log("res.replies", res.replies); | ||
callback(res.replies); | ||
}); | ||
}; | ||
ep_comments.prototype.getCommentData = function (){ | ||
@@ -346,3 +418,3 @@ var data = {}; | ||
// Show comments | ||
$('iframe[name="ace_outer"]').contents().find('#sidediv').removeClass('sidedivhidden'); | ||
// $('iframe[name="ace_outer"]').contents().find('#comments').removeClass('sidedivhidden'); | ||
@@ -360,4 +432,6 @@ ace.callWithAce(function (ace){ | ||
// Set the top of the form | ||
// $('iframe[name="ace_outer"]').contents().find('#sidediv').contents().find('#newComment').css("top", "200px"); | ||
$('iframe[name="ace_outer"]').contents().find('#comments').find('#newComment').css("top", $('#editorcontainer').css("top")); | ||
// CAKE TODO This doesn't appear to get the Y right for the input field... | ||
this.insertNewComment(data, function (text, index){ | ||
@@ -372,3 +446,3 @@ data.comment.text = text; | ||
ace.callWithAce(function (ace){ | ||
console.log('addComment :: ', commentId); | ||
// console.log('addComment :: ', commentId); | ||
ace.ace_performSelectionChange(rep.selStart,rep.selEnd,true); | ||
@@ -384,2 +458,23 @@ ace.ace_setAttributeOnSelection('comment', commentId); | ||
// Listen for comment replies | ||
ep_comments.prototype.commentRepliesListen = function(){ | ||
var self = this; | ||
var socket = this.socket; | ||
socket.on('pushAddCommentReply', function (replyId, reply){ | ||
console.warn("pAcR response", replyId, reply); | ||
// callback(replyId, reply); | ||
// self.collectCommentReplies(); | ||
self.getCommentReplies(function (replies){ | ||
if (!$.isEmptyObject(replies)){ | ||
// console.log("collecting comment replies"); | ||
self.commentReplies = replies; | ||
self.collectCommentReplies(); | ||
self.commentRepliesListen(); | ||
} | ||
}); | ||
}); | ||
}; | ||
// Push comment from collaborators | ||
@@ -389,2 +484,4 @@ ep_comments.prototype.pushComment = function(eventType, callback){ | ||
// console.error("eventType", eventType); | ||
// On collaborator add a comment in the current pad | ||
@@ -403,2 +500,9 @@ if (eventType == 'add'){ | ||
} | ||
// On reply | ||
else if (eventType == "addCommentReply"){ | ||
socket.on('pushAddCommentReply', function (replyId, reply){ | ||
callback(replyId, reply); | ||
}); | ||
} | ||
}; | ||
@@ -414,3 +518,5 @@ | ||
postAceInit: function(hook, context){ | ||
if(!pad.plugins) pad.plugins = {}; | ||
var Comments = new ep_comments(context); | ||
pad.plugins.ep_comments_page = Comments; | ||
}, | ||
@@ -421,3 +527,3 @@ | ||
var padOuter = $('iframe[name="ace_outer"]').contents(); | ||
padOuter.find('#sidediv').removeClass("sidedivhidden"); // TEMPORARY to do removing authorship colors can add sidedivhidden class to sidesiv! | ||
// padOuter.find('#sidediv').removeClass("sidedivhidden"); // TEMPORARY to do removing authorship colors can add sidedivhidden class to sidesiv! | ||
if(!context.callstack.docTextChanged) return; | ||
@@ -437,3 +543,3 @@ // for each comment | ||
var commentEle = padOuter.find('#c-'+commentId[1]); | ||
y = y+5; | ||
y = y-5; | ||
commentEle.css("top", y+"px").show(); | ||
@@ -454,2 +560,42 @@ }); | ||
function prettyDate(time){ | ||
var time_formats = [ | ||
[60, 'seconds', 1], // 60 | ||
[120, '1 minute ago', '1 minute from now'], // 60*2 | ||
[3600, 'minutes', 60], // 60*60, 60 | ||
[7200, '1 hour ago', '1 hour from now'], // 60*60*2 | ||
[86400, 'hours', 3600], // 60*60*24, 60*60 | ||
[172800, 'yesterday', 'tomorrow'], // 60*60*24*2 | ||
[604800, 'days', 86400], // 60*60*24*7, 60*60*24 | ||
[1209600, 'last week', 'next week'], // 60*60*24*7*4*2 | ||
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7 | ||
[4838400, 'last month', 'next month'], // 60*60*24*7*4*2 | ||
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4 | ||
[58060800, 'last year', 'next year'], // 60*60*24*7*4*12*2 | ||
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12 | ||
[5806080000, 'last century', 'next century'], // 60*60*24*7*4*12*100*2 | ||
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100 | ||
]; | ||
/* | ||
var time = ('' + date_str).replace(/-/g,"/").replace(/[TZ]/g," ").replace(/^\s\s*/ /*rappel , '').replace(/\s\s*$/, ''); | ||
if(time.substr(time.length-4,1)==".") time =time.substr(0,time.length-4); | ||
*/ | ||
var seconds = (new Date - new Date(time)) / 1000; | ||
var token = 'ago', list_choice = 1; | ||
if (seconds < 0) { | ||
seconds = Math.abs(seconds); | ||
token = 'from now'; | ||
list_choice = 2; | ||
} | ||
var i = 0, format; | ||
while (format = time_formats[i++]) | ||
if (seconds < format[0]) { | ||
if (typeof format[2] == 'string') | ||
return format[list_choice]; | ||
else | ||
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token; | ||
} | ||
return time; | ||
}; | ||
exports.aceEditorCSS = hooks.aceEditorCSS; | ||
@@ -459,1 +605,2 @@ exports.postAceInit = hooks.postAceInit; | ||
exports.aceEditEvent = hooks.aceEditEvent; | ||
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
39808
16
889
1
10