diff options
Diffstat (limited to 'app/assets/javascripts/issuable/components')
8 files changed, 406 insertions, 0 deletions
diff --git a/app/assets/javascripts/issuable/components/time_tracking/collapsed_state.js b/app/assets/javascripts/issuable/components/time_tracking/collapsed_state.js new file mode 100644 index 00000000000..2101a4bb056 --- /dev/null +++ b/app/assets/javascripts/issuable/components/time_tracking/collapsed_state.js @@ -0,0 +1,57 @@ +import stopwatchSvg from 'icons/_icon_stopwatch.svg'; + +import '../../../lib/utils/pretty_time'; + +export default { + name: 'time-tracking-collapsed-state', + props: { + showComparisonState: { + type: Boolean, + required: true, + }, + showSpentOnlyState: { + type: Boolean, + required: true, + }, + showEstimateOnlyState: { + type: Boolean, + required: true, + }, + showNoTimeTrackingState: { + type: Boolean, + required: true, + }, + timeSpentHumanReadable: { + type: String, + required: false, + }, + timeEstimateHumanReadable: { + type: String, + required: false, + }, + }, + methods: { + abbreviateTime(timeStr) { + return gl.utils.prettyTime.abbreviateTime(timeStr); + }, + }, + template: ` + <div class='sidebar-collapsed-icon'> + ${stopwatchSvg} + <div class='time-tracking-collapsed-summary'> + <div class='compare' v-if='showComparisonState'> + <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> + </div> + <div class='estimate-only' v-if='showEstimateOnlyState'> + <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> + </div> + <div class='spend-only' v-if='showSpentOnlyState'> + <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span> + </div> + <div class='no-tracking' v-if='showNoTimeTrackingState'> + <span class='no-value'>None</span> + </div> + </div> + </div> + `, +}; diff --git a/app/assets/javascripts/issuable/components/time_tracking/comparison_pane.js b/app/assets/javascripts/issuable/components/time_tracking/comparison_pane.js new file mode 100644 index 00000000000..564f5cadcec --- /dev/null +++ b/app/assets/javascripts/issuable/components/time_tracking/comparison_pane.js @@ -0,0 +1,78 @@ +import '../../../lib/utils/pretty_time'; + +const prettyTime = gl.utils.prettyTime; + +export default { + name: 'time-tracking-comparison-pane', + props: { + timeSpent: { + type: Number, + required: true, + }, + timeEstimate: { + type: Number, + required: true, + }, + timeSpentHumanReadable: { + type: String, + required: true, + }, + timeEstimateHumanReadable: { + type: String, + required: true, + }, + }, + computed: { + parsedRemaining() { + const diffSeconds = this.timeEstimate - this.timeSpent; + return prettyTime.parseSeconds(diffSeconds); + }, + timeRemainingHumanReadable() { + return prettyTime.stringifyTime(this.parsedRemaining); + }, + timeRemainingTooltip() { + const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; + return `${prefix} ${this.timeRemainingHumanReadable}`; + }, + /* Diff values for comparison meter */ + timeRemainingMinutes() { + return this.timeEstimate - this.timeSpent; + }, + timeRemainingPercent() { + return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; + }, + timeRemainingStatusClass() { + return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; + }, + /* Parsed time values */ + parsedEstimate() { + return prettyTime.parseSeconds(this.timeEstimate); + }, + parsedSpent() { + return prettyTime.parseSeconds(this.timeSpent); + }, + }, + template: ` + <div class='time-tracking-comparison-pane'> + <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay' + :aria-valuenow='timeRemainingTooltip' + :title='timeRemainingTooltip' + :data-original-title='timeRemainingTooltip' + :class='timeRemainingStatusClass'> + <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'> + <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div> + </div> + <div class='compare-display-container'> + <div class='compare-display pull-left'> + <span class='compare-label'>Spent</span> + <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span> + </div> + <div class='compare-display estimated pull-right'> + <span class='compare-label'>Est</span> + <span class='compare-value'>{{ timeEstimateHumanReadable }}</span> + </div> + </div> + </div> + </div> + `, +}; diff --git a/app/assets/javascripts/issuable/components/time_tracking/estimate_only_pane.js b/app/assets/javascripts/issuable/components/time_tracking/estimate_only_pane.js new file mode 100644 index 00000000000..d61fb26ed0d --- /dev/null +++ b/app/assets/javascripts/issuable/components/time_tracking/estimate_only_pane.js @@ -0,0 +1,15 @@ +export default { + name: 'time-tracking-estimate-only-pane', + props: { + timeEstimateHumanReadable: { + type: String, + required: true, + }, + }, + template: ` + <div class='time-tracking-estimate-only-pane'> + <span class='bold'>Estimated:</span> + {{ timeEstimateHumanReadable }} + </div> + `, +}; diff --git a/app/assets/javascripts/issuable/components/time_tracking/help_state.js b/app/assets/javascripts/issuable/components/time_tracking/help_state.js new file mode 100644 index 00000000000..f955337b62b --- /dev/null +++ b/app/assets/javascripts/issuable/components/time_tracking/help_state.js @@ -0,0 +1,26 @@ +export default { + name: 'time-tracking-help-state', + props: { + docsUrl: { + type: String, + required: true, + }, + }, + template: ` + <div class='time-tracking-help-state'> + <div class='time-tracking-info'> + <h4>Track time with slash commands</h4> + <p>Slash commands can be used in the issues description and comment boxes.</p> + <p> + <code>/estimate</code> + will update the estimated time with the latest command. + </p> + <p> + <code>/spend</code> + will update the sum of the time spent. + </p> + <a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a> + </div> + </div> + `, +}; diff --git a/app/assets/javascripts/issuable/components/time_tracking/issuable_time_tracking.js b/app/assets/javascripts/issuable/components/time_tracking/issuable_time_tracking.js new file mode 100644 index 00000000000..62c05169666 --- /dev/null +++ b/app/assets/javascripts/issuable/components/time_tracking/issuable_time_tracking.js @@ -0,0 +1,59 @@ +import '~/smart_interval'; + +import timeTracker from './time_tracker'; +import eventHub from '../../event_hub'; + +export default { + el: '#issuable-time-tracker', + data() { + const selector = this.$options.el; + const element = document.querySelector(selector); + + const docsUrl = element.dataset.docsUrl; + + return { + issuable: {}, + docsUrl, + }; + }, + components: { + 'issuable-time-tracker': timeTracker, + }, + methods: { + fetchIssuable() { + eventHub.$emit('fetchIssuable'); + }, + updateState(data) { + this.issuable = data; + }, + listenForSlashCommands() { + $(document).on('ajax:success', '.gfm-form', (e, data) => { + const subscribedCommands = ['spend_time', 'time_estimate']; + const changedCommands = data.commands_changes + ? Object.keys(data.commands_changes) + : []; + if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { + this.fetchIssuable(); + } + }); + }, + }, + created() { + eventHub.$on('receivedIssuable', data => this.updateState(data)); + }, + mounted() { + this.fetchIssuable(); + this.listenForSlashCommands(); + }, + template: ` + <div class="block"> + <issuable-time-tracker + :time_estimate="issuable.time_estimate" + :time_spent="issuable.total_time_spent" + :human_time_estimate="issuable.human_time_estimate" + :human_time_spent="issuable.human_total_time_spent" + :docs-url="docsUrl" + /> + </div> + `, +}; diff --git a/app/assets/javascripts/issuable/components/time_tracking/no_tracking_pane.js b/app/assets/javascripts/issuable/components/time_tracking/no_tracking_pane.js new file mode 100644 index 00000000000..79f1c5a6f27 --- /dev/null +++ b/app/assets/javascripts/issuable/components/time_tracking/no_tracking_pane.js @@ -0,0 +1,8 @@ +export default { + name: 'time-tracking-no-tracking-pane', + template: ` + <div class='time-tracking-no-tracking-pane'> + <span class='no-value'>No estimate or time spent</span> + </div> + `, +}; diff --git a/app/assets/javascripts/issuable/components/time_tracking/spent_only_pane.js b/app/assets/javascripts/issuable/components/time_tracking/spent_only_pane.js new file mode 100644 index 00000000000..c71d6cd270b --- /dev/null +++ b/app/assets/javascripts/issuable/components/time_tracking/spent_only_pane.js @@ -0,0 +1,15 @@ +export default { + name: 'time-tracking-spent-only-pane', + props: { + timeSpentHumanReadable: { + type: String, + required: true, + }, + }, + template: ` + <div class='time-tracking-spend-only-pane'> + <span class='bold'>Spent:</span> + {{ timeSpentHumanReadable }} + </div> + `, +}; diff --git a/app/assets/javascripts/issuable/components/time_tracking/time_tracker.js b/app/assets/javascripts/issuable/components/time_tracking/time_tracker.js new file mode 100644 index 00000000000..57c72f258c6 --- /dev/null +++ b/app/assets/javascripts/issuable/components/time_tracking/time_tracker.js @@ -0,0 +1,148 @@ +import timeTrackingHelpState from './help_state'; +import timeTrackingCollapsedState from './collapsed_state'; +import timeTrackingSpentOnlyPane from './spent_only_pane'; +import timeTrackingNoTrackingPane from './no_tracking_pane'; +import timeTrackingEstimateOnlyPane from './estimate_only_pane'; +import timeTrackingComparisonPane from './comparison_pane'; + +export default { + name: 'issuable-time-tracker', + props: { + time_estimate: { + type: Number, + required: true, + default: 0, + }, + time_spent: { + type: Number, + required: true, + default: 0, + }, + human_time_estimate: { + type: String, + required: false, + }, + human_time_spent: { + type: String, + required: false, + }, + docsUrl: { + type: String, + required: true, + }, + }, + data() { + return { + showHelp: false, + }; + }, + components: { + 'time-tracking-collapsed-state': timeTrackingCollapsedState, + 'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane, + 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, + 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane, + 'time-tracking-comparison-pane': timeTrackingComparisonPane, + 'time-tracking-help-state': timeTrackingHelpState, + }, + computed: { + timeSpent() { + return this.time_spent; + }, + timeEstimate() { + return this.time_estimate; + }, + timeEstimateHumanReadable() { + return this.human_time_estimate; + }, + timeSpentHumanReadable() { + return this.human_time_spent; + }, + hasTimeSpent() { + return !!this.timeSpent; + }, + hasTimeEstimate() { + return !!this.timeEstimate; + }, + showComparisonState() { + return this.hasTimeEstimate && this.hasTimeSpent; + }, + showEstimateOnlyState() { + return this.hasTimeEstimate && !this.hasTimeSpent; + }, + showSpentOnlyState() { + return this.hasTimeSpent && !this.hasTimeEstimate; + }, + showNoTimeTrackingState() { + return !this.hasTimeEstimate && !this.hasTimeSpent; + }, + showHelpState() { + return !!this.showHelp; + }, + }, + methods: { + toggleHelpState(show) { + this.showHelp = show; + }, + }, + template: ` + <div class='time_tracker time-tracking-component-wrap' v-cloak> + <time-tracking-collapsed-state + :show-comparison-state='showComparisonState' + :show-no-time-tracking-state='showNoTimeTrackingState' + :show-help-state='showHelpState' + :show-spent-only-state='showSpentOnlyState' + :show-estimate-only-state='showEstimateOnlyState' + :time-spent-human-readable='timeSpentHumanReadable' + :time-estimate-human-readable='timeEstimateHumanReadable' + /> + <div class='title hide-collapsed'> + Time tracking + <div + class='help-button pull-right' + v-if='!showHelpState' + @click='toggleHelpState(true)' + > + <i + class='fa fa-question-circle' + aria-hidden='true' + /> + </div> + <div + class='close-help-button pull-right' + v-if='showHelpState' + @click='toggleHelpState(false)'> + <i + class='fa fa-close' + aria-hidden='true' + /> + </div> + </div> + <div class='time-tracking-content hide-collapsed'> + <time-tracking-estimate-only-pane + v-if='showEstimateOnlyState' + :time-estimate-human-readable='timeEstimateHumanReadable' + /> + <time-tracking-spent-only-pane + v-if='showSpentOnlyState' + :time-spent-human-readable='timeSpentHumanReadable' + /> + <time-tracking-no-tracking-pane + v-if='showNoTimeTrackingState' + /> + <time-tracking-comparison-pane + v-if='showComparisonState' + :time-estimate='timeEstimate' + :time-spent='timeSpent' + :time-spent-human-readable='timeSpentHumanReadable' + :time-estimate-human-readable='timeEstimateHumanReadable' + /> + <transition name='help-state-toggle'> + <time-tracking-help-state + v-if='showHelpState' + :docs-url='docsUrl' + /> + </transition> + </div> + </div> + `, +}; |