github如何导出github private 价格jira

Formatting CSV Data For JIRA Tables Using AngularJS And Plupload - 推酷
Formatting CSV Data For JIRA Tables Using AngularJS And Plupload
At my company,InVision App, I'm often tasked with running SQL reports against the database. I love this (for serious). Writing SQL is sometimes one of the major highlights of my day. But, unfortunately, reporting those results in a JIRA ticket seems to be a non-trivial task. Of course, I suck at JIRA, so there may be some native functionality that I just don't know about. That said, I wanted to see if I could create a little utility app, self-hosted on GitHub Pages, that would help me prepare my SQL findings for JIRA comments.
As a fun experiment, I wanted to take this opportunity to learn a little bit more about the JavaScript File API. So, in addition to being able to copy-n-paste CSV data into the app, I wanted to be able to drag-n-drop a CSV file onto the app. When doing so, the app catches the drop event, thanks to Plupload, reads in the CSV data, and pipes it into the rendering engine.
Once the data is available, I parse it into a two-dimensional array of records, which is subsequently converted into a recordset. This recordset is then used to generate the JIRA markup as well as generate an actual HTML preview of the table. If the first record is being used as the header, it will be sliced off. If there is no header record, the column list will be auto-generated.
Right now, there is no way (in the user interface) to tell the parser what the field delimiter is. But, the parser will do its best to guess the most appropriate delimiter by calculating the most frequently used character in the data. If commas are used more than tabs, it assumes a comma delimiter. If tabs are used more than commas, it assumes a tab delimiter. Under the hood, the CSV parser is defined as a provider that will allow a delimiter to be defined during the application's bootstrapping phase.
If I grab a CSV file and drag-n-drop it onto the app, it looks like this:
And, here's the code:
&!doctype html&
&html ng-app=&App&&
&meta charset=&utf-8& /&
JIRA CSV Formatter by Ben Nadel
&link rel=&stylesheet& type=&text/css& href=&///css?family=Open+Sans:300,400,600,700&&&/link&
&link rel=&stylesheet& type=&text/css& href=&./app.css&&&/link&
&!-- CAUTION: Using Body tag as a component directive. --&
JIRA CSV Formatter
&section class=&csv-input&&
CSV Content
&label class=&has-header&&
&input type=&checkbox& ng-model=&vm.form.hasHeader& ng-change=&vm.processCSV()& /&
Use first row as header column list.
ng-model=&vm.form.csv&
ng-change=&vm.processCSV()&
placeholder=&Paste CSV content here... (or drag-n-drop a CSV file)&&
&/textarea&
&/section&
&section class=&jira-output&&
JIRA Table Markup
&textarea id=&jira& ng-model=&vm.form.jira& readonly=&readonly&&&/textarea&
&/section&
&section class=&data-output&&
Table Preview
&div class=&preview& ng-switch=&!! vm.recordset&&
&table ng-switch-when=&true&&
&th ng-repeat=&column in vm.recordset.columns track by $index&&
{{ column }}
&tr ng-repeat=&row in vm.recordset.rows track by $index&&
&td ng-repeat=&value in row track by $index&&
{{ value }}
&div ng-switch-when=&false& class=&no-data&&
No data yet to preview.
&/section&
&section ng-if=&vm.isShowingDropzone& class=&drop-cover&&
&div class=&lasso&&
&span class=&label&&
Drop CSV File Here...
&/section&
&!-- Load vendor scripts. --&
&script type=&text/javascript& src=&./vendor/angular-1.4.8/angular.min.js&&&/script&
&script type=&text/javascript& src=&./vendor/plupload-2.1.8/moxie.min.js&&&/script&
&script type=&text/javascript&&
// Define our application module.
angular.module( &App&, [ &ng& ] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I manage the application component directive.
angular.module( &App& ).directive(
function bodyDirective( $document, $timeout, $log, readFile ) {
// Return the directive definition object.
controller: BodyController,
controllerAs: &vm&,
link: link,
restrict: &E&
// I bind the JavaScript events to the view-model.
function link( scope, element, attributes, controller ) {
// Set up our dropzone (using the Body).
var dropzone = new mOxie.FileDrop({
drop_zone: element[ 0 ]
dropzone.ondrop = handleD
dropzone.init();
// Because the drag API is basically a nightmare wrapped in an insult,
// we need to listen for various drag events to make sense of what the
// user is actually doing.
element.on( &dragenter&, handleDragenter );
element.on( &dragleave&, handleDragleave );
element.on( &dragover&, handleDragover );
// Part of the drag-n-drop dance requires a timer to prevent
// prematurely closing the dropzone overlay while the user is actively
// dragging a file onto the window.
var leaveTimer =
// When the user clicks on the JIRA Output textarea, let's highlight
// it for easy copy-n-paste operations.
var jiraOutput = angular
.element( $document[ 0 ].getElementById( &jira& ) )
.on( &click&, highlightOutput )
// PRIVATE METHODS.
// I handle the drag-enter event.
function handleDragenter( event ) {
// If we're not showing the dropzone overlay yet, show it.
if ( ! controller.isShowingDropzone ) {
scope.$apply( controller.showDropzone );
// I handle the drag-leave event.
function handleDragleave( event ) {
// Because the drag events fire in a horrible horrible horrible
// order, we can't believe that the leave event really indicates
// that the user has intended to leave. As such, we have to put
// the &hide& command in a timeout, giving the &dragover& event
// a chance to cancel this timer.
leaveTimer = $timeout( controller.hideDropzone, 50 );
// I handle the drag-over event.
function handleDragover( event ) {
// Clear any running leave-timer so that we don't prematurely
// close the dropzone overlay.
$timeout.cancel( leaveTimer );
// I handle the drop event for the file objects and read the file
// content if it is applicable.
function handleDrop( event ) {
var file = dropzone.files[ 0 ];
// If the file type looks valid, read in the content and pass
// it off to the controller for view-model integration.
if ( isValidFilename( file.name ) ) {
readFile( file.getSource() )
.then( controller.setCSV, $log.error )
.then( highlightOutput )
// No matter what happens with the file, close the overlay.
scope.$apply( controller.hideDropzone );
// Destroy all of the dropped files to prevent memory leaks.
for ( var i = 0, length = dropzone.files. i & i++ ) {
dropzone.files[ i ].destroy();
// I highlight the JIRA output text for easy copy-paste action.
function highlightOutput() {
// Since the JIRA output is controlled by an ngModel binding, we
// need to give the HTML time to catch up with the change in the
// view-model. As such, wrap highlight in a timeout.
setTimeout(
function waitForHTMLToCatchUp() {
jiraOutput[ 0 ].focus();
jiraOutput[ 0 ].select();
// I determine if the given filename matches a file type that is
// likely to have parsable content.
function isValidFilename( name ) {
return( /\.(csv|txt)$/i.test( name ) );
// I control the application component.
function BodyController( $scope, parseCSV ) {
// I hold the ng-model bindings for the form fields.
vm.form = {
hasHeader: true
// I determine if the dropzone overlay is showing.
vm.isShowingDropzone =
// I hold the recordset instance that we'd like to render as a table
// in JIRA markdown.
vm.recordset =
// Expose the public methods.
vm.hideDropzone = hideD
vm.processCSV = processCSV;
vm.setCSV = setCSV;
vm.showDropzone = showD
// PUBLIC METHODS.
// I hide the dropzone overlay.
function hideDropzone() {
vm.isShowingDropzone =
// I process the CSV data that is currently in the csv binding.
function processCSV() {
// If there is no CSV data, nullify the recordset.
if ( ! vm.form.csv ) {
vm.form.jira = &&;
vm.recordset =
// If we've gotten this far, we have content to parse. First,
// let's parse the content into a simple two-dimensional array.
var rows = parseCSV( vm.form.csv );
// Now, let's convert the two-dimensional array into an actual
// recordset that we can render in the markup.
if ( vm.form.hasHeader ) {
vm.recordset = {
columns: rows[ 0 ],
rows: rows.slice( 1 )
vm.recordset = {
columns: buildColumns( rows[ 0 ].length ),
rows: rows
// Calculate the JIRA markup required to render the recordset.
// This the *actual data* that we are trying to get at.
vm.form.jira = recordsetToJIRA( vm.recordset );
// I set the CSV content and then process it.
function setCSV( newCSV ) {
vm.form.csv = newCSV;
processCSV();
// I show the dropzone overlay.
function showDropzone() {
vm.isShowingDropzone =
// PRIVATE METHODS.
// I build a columns collection with the given column count.
function buildColumns( count ) {
var letters = &ABCDEFGHIJKLMNOPQRSTUVWXYZ&.match( /./g );
var columns = [];
for ( var i = 0 ; i & i++ ) {
var groupIndex = Math.floor( i / letters.length );
var letter = letters[ i % letters.length ]
var label = groupIndex
? ( groupIndex + 1 + letter )
columns.push( label );
return( columns );
// I convert the given recordset into JIRA table markdown.
function recordsetToJIRA( recordset ) {
var newline = &\n&;
// We can get the JIRA markdown by concatenating the markdown
// representation for the Header and the Body of the table.
columnsToString( recordset.columns ) +
rowsToString( recordset.rows )
// I convert the given columns collection to a JIRA markdown string.
function columnsToString( columns ) {
return( &||& + escapePipes( columns ).join( &||& ) + &||& );
// I take the given collection and escape all embedded pipes in
// each item. We need to do this because the pipe is a special
// character in the JIRA markdown.
function escapePipes( collection ) {
collection.map(
function operator( value ) {
return( value.replace( /\|/g, &\\|& ) );
// I convert the given rows collection to a JIRA markdown string.
function rowsToString( rows ) {
var rowBuffer = rows.map(
function operator( values ) {
return( &|& + escapePipes( values ).join( &|& ) + &|& );
return( rowBuffer.join( newline ) );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a service that reads a File object and returns a promise.
angular.module( &App& ).factory(
&readFile&,
function readFileFactory( $q ) {
return( readFile );
// I read the the given file object and return promise that will either
// resolve with the text content or will reject with the error object
// provided by the File API.
function readFile( source ) {
var deferred = $q.defer();
var reader = new FileReader();
reader.onload = handleL
reader.onerror = handleE
reader.readAsText( source );
return( deferred.promise );
// I handle file IO errors.
function handleError( event ) {
deferred.reject( event.target.error );
// I handle a successful file read.
function handleLoad( event ) {
deferred.resolve( event.target.result );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a service that parses CSV text content.
angular.module( &App& ).provider(
&parseCSV&,
function parseCSVProvider() {
// I am the default field delimiter for the CSV content. By default,
// we'll use a best-guess approach to the delimiter. However, if this
// delimiter is defined, we'll use it instead of guessing.
var defaultDelimiter = &&;
var fallbackDelimiter = &,&;
// Return the public API.
getDelimiter: getDelimiter,
setDelimiter: setDelimiter,
// The underlying factory.
$get: parseCSVFactory
// PULIC METHODS.
// I get the current default delimiter.
function getDelimiter() {
return( defaultDelimiter );
// I set the new default delimiter.
function setDelimiter( newDelimiter ) {
// Only use the first character as the delimiter.
defaultDelimiter = String( newDelimiter || &,& ).slice( 0, 1 );
// FACTORY METHOD.
// I build the CSV parsing service.
function parseCSVFactory() {
return( parseCSV );
// PUBLIC METHODS.
// I parse the given CSV data using the optional delimiter. The
// parsed payload is an array of arrays.
function parseCSV( data, delimiter ) {
delimiter = ( delimiter || defaultDelimiter || guessDelimiter( data ) );
var rows = [];
var values =
var matches =
var pattern = new RegExp(
// Delimiters.
&(^|\\& + delimiter + &|\\r?\\n|\\r)& +
// Quoted fields.
&\&([^\&]*(?:\&\&[^\&]*)*)\&& +
// Standard fields.
&([^\&\\& + delimiter + &\\r\\n]*)& +
// Keep looping over the matches until we've processed the
// entire CSV content.
while ( matches = pattern.exec( data ) ) {
// Extract the groups for short-hand access.
var $delimiter = matches[ 1 ];
var $quotedField = matches[ 2 ];
var $nakedField = matches[ 3 ];
// If we have a non-field delimiter, start a new row of values.
// NOTE: Since we are aggressively matching on &^&, our first
// match should always be the start of the content and therefore
// the start of the first row.
if ( $delimiter !== delimiter ) {
rows.push( values = [] );
// If it's a quoted field, escape un-escape embedded quotes.
if ( $quotedField ) {
values.push( $quotedField.replace( /&&/g, &\&& ) );
// If it's a naked field, just add as-is.
values.push( $nakedField );
return( rows );
// PRIVATE METHODS.
// I guess the correct delimiter by counting the possible delimiters
// and choosing the most frequent one. If no clear winner can be found,
// I fall back to using the fallbackDelimiter.
function guessDelimiter( data ) {
var commaCount = ( data + &,& ).match( /,/g ).
var tabCount = ( data + &\t& ).match( /\t/g ).
var colonCount = ( data + &:& ).match( /:/g ).
// If all the counts are equal, use the fallback since we couldn't
// make an educated decision.
if ( ( commaCount === tabCount ) && ( commaCount === colonCount ) ) {
return( fallbackDelimiter );
// Since we know that one of the delimiters was a winner, check to
// see if the comma (most common) is the winner.
} else if ( ( commaCount & tabCount ) && ( commaCount & colonCount ) ) {
return( &,& );
// If the comma didn't win, check to see if the tab is the winner.
} else if ( tabCount &= colonCount ) {
return( &\t& );
return( &:& );
For me personally, I'll get a lot of use out of this in JIRA. But, more than anything, this was just a fun little application to build.
Thanks my man —
you rock the party that rocks the body!
已发表评论数()
已收藏到推刊!
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
没有分页内容
图片无法显示
视频无法显示
与原文不一致小团队协作,有哪些值得推荐的 Web 应用和工具软件?有什么好的做法可以作为最佳实践?
【李楠的回答(119票)】:
我可能比较“老派”。。
一个付费的 github 我认为是最必要的。它可以一下子解决代码托管, issues , wiki 三个小团队最需要的东西。
2 testflight
团队管理中最容易忽视的,却最重要的东西是。。。
无论一切多么井井有条,不能频繁发布的团队也是危险的。
所以,可控制灰度的空中测试工具也很重要。移动项目导入 testflight 是必要的。
3 invision
另外, github 不适合设计的交流,和 basecamp 比较,个人更喜欢 invision 。
相信我,你们需要一个微信群 - 他非常有利于实时的消息共享。
5 最后的话
github + invision + testflight + 微信群基本可以解决:
需求文档( github wiki ) -& 课题跟踪( github issues ) -& 设计讨论( invision ) -& 代码托管( github ) -& 灰度发布( testflight ) 的整个的流程。
6 工具之外的话
但是,工具之外,更重要的是你的团队组织和管理方法。
团队都有什么角色?如何分工?需要那些规定和流程(会议安排,迭代周期等等)?
个人的推荐是 SCRUM 。
其实,只要组织和管理方法定了,工具反而不重要了,Low-tech 也可以很高效:
有没有把“方法”和“工具”更好的结合在一起的团队协作工具?目前真的没有看到很好的。
不过,也许过一段我们会拿出一个。。。敬请期待。
【马力的回答(36票)】:
小团队协作,首先要明确的是协作的目的是为了提高效率,辅助沟通,帮助记录,而不是为了流程而流程,为了看起来理想的套路,盲目的希望通过工具来标准化大家的行为,这样往往会适得其反,最终难以推行下去。
这里的关键点在于,根据团队和工作的特点,把握好人机分工的度,哪些是需要通过流程解决,用计算机来辅助解决,哪些是留给人的空间,给人足够的自由度,避免工具反过来成为制约。常犯的错误是对后者的忽视。
我们在选择工具时,伙伴做了很多探索(感谢我们的 Geek伙伴),几乎主流的工具都品味了一下,最终选择了 Trello。我自己现在的体会是,Trello 好在你以什么样的程度去用都可以,简单当看板用也可以,再复杂点支持更多内容也能用,没有一个高学习成本和固化的套路,这很好。
我们非常注意何时将其用作流程工具、何时用作沟通工具、何时用作记录工具。例如,昨天我们还进行了一个小的讨论,某件工作本身是流程化的,我们在探讨是否应该使用工具来固化下来流程,在每一个步骤大家应该在工具里做什么样的操作、标记等等。但是后来发现,在这里其实有好几个主要环节,由人直接去线下实施,而不是先让信息在计算机里绕一圈,会更有效率,只需要将结果记录在工具中即可,过程可以非常有弹性。最终我们的结论是,只需要明确流程(哪怕是以邮件或口头通知的形式),在线下流程已经有效率的情况下,无需再使用工具来看起来「很规范」。
对于小团队,中国的团队,工具越简单越好,越轻量级越好,越灵活越好,越能够根据团队和实际工作选择性使用越好,而不是盲目追求强大,追求自动化。
其他工具,例如 Github 等,也非常有帮助。
【李会军的回答(28票)】:
以前回答过类似的问题 ,我觉的答案非常适合这个问题,等有机会再写一篇文章专门讨论。
大部分朋友都推荐一堆的工具,我个人认为对于创业团队未必适合使用这么多的沟通交流以及团队协作工具:
1. 工具比团队人数多
简单统计一下,有邮件Email、即时通讯IM、日程管理 、文档协作Google Docs、项目管理、群组聊天
、文件共享Dropbox、缺陷跟踪 ,这就已经有八个工具了,试想对于小创业公司来说,团队成员人数都不一定能有这么多,少则两三人、多则七八人。每个成员还要分别注册不同的系统帐号,才能跟大家一起工作,这是在提高效率,还是在降低效率?
2. 各个工具的数据无法共享
使用太多的工具造成的另外一个问题就是各个系统之间的数据无法共享,邮件分散的各自的邮箱里,IM聊天记录在各个成员的电脑里,Dropbox里的文件怎么跟Basecamp结合,怎么跟缺陷跟踪工具结合?经常想起一件事的时候,不知道应该去Dropbox里找文件还是去Email里面查找附件,又或者去Basecamp里找。工具应该是为人服务,人不应该被工具所牵绊。
3. 简单够用就好
结合以上两点,我认为在创业团队中,不需要太多的工具,下面是我们团队的使用情况:
1)Email:Email的地位始终无法取代,Email是必须的,我们使用的是 ,Gmail由于众所周知的原因,无奈放弃。
2) :源码管理,放弃自己搭建git、svn服务器的想法吧,在Github每月支付几美元就够用了,一年也花不了几个钱。我们所有的源代码都放在Github。
3) :我们团队自己的产品,自产自销。用它做项目管理、缺陷跟踪、文件共享、群组聊天和文档协作,各个元素之间可以很好的互相配合。有朋友会问,Worktile做了这么多,估计哪个都做不专。这点我承认,文件共享我们不如Dropbox,文档协作不如Google Docs,但是仔细想想,我们真的用了Google Docs的全部功能了吗,还是那么句话,够用就好。再补充一点,Worktile可以很好的跟Email结合,通过Email创建任务、发起讨论,这样不就把把团队的资源连接起来了吗?
总结,我们内心始终在追求强大的工具,却忽略了寻找工具是为了解决团队问题。选择适合你的团队、满足需要的少数几个工具足以,用好工具才是关键,对于创业团队尤为重要。切记,别让你的团队使用的工具数比团队人数还多!
【warfalcon的回答(19票)】:
小团队协作必不可少的工具就是ITS。(Issue Tracking system )
一套合格的 Issue Tracking system 的 Issue 至少要可以纪录这些内容:
Issue 的主题Issue 的内容Issue 现在的状态 (新建立、已指派、已解决、已响应、已结束、已搁置…etc)Issue 优先权 (正常、重要、紧急、轻微、会挡路…etc.)Issue 发生日期Issue 希望解决日期Issue 实际解决日期Issue 被分派给谁Issue 的附件Isuue 的观察者有谁Project Management Tool
其中 Redmine 、JIRA 、Basecamp 并不仅止是 Issue Tracking System,更精确的来说,它们应该被称为「项目管理工具」。
它们多半能够提供以下作用:
一个地方可以透明的列出所有需要被执行的项目 (Issue List)一个地方可以列出阶段内需要被执行的项目 ( Issue Milestone )一个可以记载 内容,状态、优先权、日期、分派者、观察者,且具有「permalink」、「权限控管」,且让大家可以讨论执行项目细节的地方。(Issue Ticket)可以 cross reference 或具有子票功能一个地方可以整理统合项目现在所有的相关信息。( Wiki 功能)一个地方可以看到自己今天需要 Focus 进行哪些项目(Issue Personal Dashboard)一个地方能让 Manager 可以看到自己的 Member 正在进行哪些项目,这些项目目前的状态是什么。(Issue Query)
项目需要一个工具能提供以下功能:
一个可以记载 内容,状态、优先权、预计完成日期、分派者、执行者,且让大家可以讨论执行项目细节的地方。(Issue Ticket)可以平行讨论,而不是信件顺序往来明确的完成时间一个地方可以整理统合项目现在所有的相关信息。( Wiki 功能)提供项目相关的信息以及 SOP 同时,项目最好能够搭配举行每日的 Standup Meeting,确保每个人正在进
【朱磊的回答(8票)】:
@ 的理论很强,实际操作太复杂了,尤其是小团队10人左右推荐Trello.
【Siva的回答(6票)】:
对新人来说,了解工具是必要的, 了解工具产生的上下文尤为必要。
彩程设计的fish最近的一篇日志(“任务”让项目顺利运行 )可以帮小白们快速了解必要的上下文。ps:彩程设计自家的tower也很不错,
豌豆荚的工作方式是个大问题(),里面挺全面真实的讲述了他们在选择工具和使用工具中的一些实践,值得参考。
一下是来自于web app successs中groundwork篇,对团队合作工具的小小论述:
一套团队人员公认的工具是很有必要的。
项目文件共享:
对于团队成员间的文档和其他项目文件的共享的需求,有很工具能够满足。 团队环境决定了最终的选择,
考虑: D ShareP B 或者一个简单的共享网络硬盘(samba等)
异步通信:
对于非时间迫切的通信,需要一个非侵入式的交流方式,同时保留交流记录。
如:私人邮件列表;日常的电子邮件或Basecamp(内嵌在团队项目管理中的聊天功能)。
即时通信:
有些问题需要在很短时间内给出答复。
考虑: QQ(和其他即时通讯软件);面对面(如果团队成员都是在同一个地方);电话。
代码库管理:
理想情况下你的开发人员应该有一个工具,使他们能够轻松浏览代码库、监控和跟踪问题的发展。
考虑:Trac1;GitHub2;
协作工具:
一个问题通常需要一个更加结构化的或视觉协作的解决方案,而不是一系列的电子邮件。
考虑:MediaW 谷歌文档,具体的协作工具,如。MindMeister4
把团队公认的工具的网站链接加入到浏览器的书签中,然后坚持使用。
同时,订阅工具的RSS,保持更新,跟上时代步伐。
Google Docs
对于团队合作来说, 链接分享和导出,revision版本历史,修改记录,comment同事评论,权限管理,几乎是必须的。
它的特殊功能,很实用:presentation的live presentation,spreadsheet的creating a form to gather data,documentation的Google’s research tools等
项目管理:
Redmine, jira, trello, tower,basecamp
对于普通的,简单的项目管理需求,trello做的很好。
idea(issue), list(project),和idea detail(指派,checklist,投票,订阅等)功能层级清晰明了
而对于软件|web项目,需要很好的支持和集成BUG跟踪系统,版本控制系统,wiki文档等。
jira或redmine可以按团队成员的口味和资金预算权衡下。
其实还有很多槽想吐的。。 (因为本童鞋先在大公司实习半年时间,最近准备创业小公司的入职,对比起来,到的确发现了挺多有趣的事)
【钟小兔的回答(4票)】:
【知乎用户的回答(3票)】:
只用一个Matis,自己架设了以后,稍稍的改一下。
让这个Bug管理工具,也能兼容需求的功能在这里面。
这样,每一个版本要完成哪些功能,哪些Bug,都一目了然了。
其他的方面,就是流程图。计划时间表等。
日常协作也很多是邮件。
【知乎用户的回答(1票)】:
俺也推荐一个
简单的协作、记录、沟通、白板功能都很不错,全程所见即所得
【agen的回答(1票)】:
总感觉目前的这些项目协作工具都过于复杂了,一个十人以内的小团队,范得上去架JIRA这样的应用作问题跟踪、团队协作吗?本来人就少,再整上两三个协作软件,小团队的优势还能体现出来吗?使用了这些重量级的产品,小团队的灵活就打了折扣,还有什么竞争力呢?小舢板后面拖了一个大型补给船,快不起来了。
【刘博云的回答(0票)】:
推荐科研在线文档库,,免费的团队协作与管理工具,云服务模式。适合中小团队。简洁、实用。
【张健飞的回答(1票)】:
如果是程序类的,我推荐github。 它里面的issues功能用着也很方便。
【PengWang的回答(0票)】:
, 这个挺有意思。
【汪慧的回答(0票)】:
Redmain吧,功能不强大,但是简单,基本的也有
【知乎用户的回答(1票)】:
【细雨满地的回答(0票)】:
(该回答有部分广告目的)
其他所有回答都是针对团队内的封闭协作的回答。
如果你们的工作内容是可以公开的,推荐:
它提供了整套的团队协作模块,且操作很简单。
更重要的是它让你们的工作内容开放,让任何对你们的工作感兴趣的人都可以参与工作,贡献智慧。
利用大众的智慧来让你们的工作更出色。
开放还有利于你们产品的传播,我们的消息流通流机制会把你们的工作推荐感兴趣的人。
也有利于用户反馈,可以让你的产品在还为发布之前就直面用户,倾听他们的真实需求。
【周畅的回答(0票)】:
非常好用,简洁明了,像白板一样,适合敏捷团队。
【郭宽的回答(0票)】:
Bitbucket/Github + Trello 很好用,如果远程协作还可以加上skype.
【mava的回答(0票)】:
小团队是怎么定义的? 如果是10个人以内的小团队,一张白板做项目管理,一个SVN做代码和文档存档足矣。
【知乎用户的回答(0票)】:
小团队推荐组合:Confluence + JIRA + GTalk + GMail,其中Confluence 和 JIRA 小团队套餐价格非常便宜, GTalk 和 GMail是免费提供的。
&&&&&本文固定链接:
【上一篇】
【下一篇】
您可能还会对这些文章感兴趣!
最新日志热评日志随机日志

我要回帖

更多关于 github private key 的文章

 

随机推荐