diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-14 00:08:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-14 00:08:20 +0300 |
commit | 87731a5333142f369829341116ab2d4445e47d66 (patch) | |
tree | 7fa601e799d5e06c2d3eb79543bf4b22522ea3ce /app | |
parent | e9606d7f51144274f9a390c9dd683200daab8eed (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
14 files changed, 240 insertions, 80 deletions
diff --git a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue new file mode 100644 index 00000000000..08e3b250832 --- /dev/null +++ b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue @@ -0,0 +1,91 @@ +<script> +import { GlDropdown, GlDropdownDivider, GlDropdownForm, GlButton } from '@gitlab/ui'; +import { Editor as TiptapEditor } from '@tiptap/vue-2'; +import { __, sprintf } from '~/locale'; +import { clamp } from '../services/utils'; + +export const tableContentType = 'table'; + +const MIN_ROWS = 3; +const MIN_COLS = 3; +const MAX_ROWS = 8; +const MAX_COLS = 8; + +export default { + components: { + GlDropdown, + GlDropdownDivider, + GlDropdownForm, + GlButton, + }, + props: { + tiptapEditor: { + type: TiptapEditor, + required: true, + }, + }, + data() { + return { + maxRows: MIN_ROWS, + maxCols: MIN_COLS, + rows: 1, + cols: 1, + }; + }, + methods: { + list(n) { + return new Array(n).fill().map((_, i) => i + 1); + }, + setRowsAndCols(rows, cols) { + this.rows = rows; + this.cols = cols; + this.maxRows = clamp(rows + 1, MIN_ROWS, MAX_ROWS); + this.maxCols = clamp(cols + 1, MIN_COLS, MAX_COLS); + }, + resetState() { + this.rows = 1; + this.cols = 1; + }, + insertTable() { + this.tiptapEditor + .chain() + .focus() + .insertTable({ + rows: this.rows, + cols: this.cols, + withHeaderRow: true, + }) + .run(); + + this.resetState(); + + this.$emit('execute', { contentType: 'table' }); + }, + getButtonLabel(rows, cols) { + return sprintf(__('Insert a %{rows}x%{cols} table.'), { rows, cols }); + }, + }, +}; +</script> +<template> + <gl-dropdown size="small" category="tertiary" icon="table"> + <gl-dropdown-form class="gl-px-3! gl-w-auto!"> + <div class="gl-w-auto!"> + <div v-for="c of list(maxCols)" :key="c" class="gl-display-flex"> + <gl-button + v-for="r of list(maxRows)" + :key="r" + :data-testid="`table-${r}-${c}`" + :class="{ 'gl-bg-blue-50!': r <= rows && c <= cols }" + :aria-label="getButtonLabel(r, c)" + class="gl-display-inline! gl-px-0! gl-w-5! gl-h-5! gl-rounded-0!" + @mouseover="setRowsAndCols(r, c)" + @click="insertTable()" + /> + </div> + <gl-dropdown-divider /> + {{ getButtonLabel(rows, cols) }} + </div> + </gl-dropdown-form> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue index 398a9610fb5..e1bc26d50fc 100644 --- a/app/assets/javascripts/content_editor/components/top_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue @@ -5,6 +5,7 @@ import { ContentEditor } from '../services/content_editor'; import Divider from './divider.vue'; import ToolbarButton from './toolbar_button.vue'; import ToolbarLinkButton from './toolbar_link_button.vue'; +import ToolbarTableButton from './toolbar_table_button.vue'; import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue'; const trackingMixin = Tracking.mixin({ @@ -16,6 +17,7 @@ export default { ToolbarButton, ToolbarTextStyleDropdown, ToolbarLinkButton, + ToolbarTableButton, Divider, }, mixins: [trackingMixin], @@ -132,5 +134,9 @@ export default { :tiptap-editor="contentEditor.tiptapEditor" @execute="trackToolbarControlExecution" /> + <toolbar-table-button + :tiptap-editor="contentEditor.tiptapEditor" + @execute="trackToolbarControlExecution" + /> </div> </template> diff --git a/app/assets/javascripts/content_editor/extensions/table.js b/app/assets/javascripts/content_editor/extensions/table.js new file mode 100644 index 00000000000..566f7a21a85 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/table.js @@ -0,0 +1,7 @@ +import { Table } from '@tiptap/extension-table'; + +export const tiptapExtension = Table; + +export function serializer(state, node) { + state.renderContent(node); +} diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js new file mode 100644 index 00000000000..6c25b867466 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/table_cell.js @@ -0,0 +1,9 @@ +import { TableCell } from '@tiptap/extension-table-cell'; + +export const tiptapExtension = TableCell.extend({ + content: 'inline*', +}); + +export function serializer(state, node) { + state.renderInline(node); +} diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js new file mode 100644 index 00000000000..3475857b9e6 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/table_header.js @@ -0,0 +1,9 @@ +import { TableHeader } from '@tiptap/extension-table-header'; + +export const tiptapExtension = TableHeader.extend({ + content: 'inline*', +}); + +export function serializer(state, node) { + state.renderInline(node); +} diff --git a/app/assets/javascripts/content_editor/extensions/table_row.js b/app/assets/javascripts/content_editor/extensions/table_row.js new file mode 100644 index 00000000000..07d2eb4faa2 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/table_row.js @@ -0,0 +1,51 @@ +import { TableRow } from '@tiptap/extension-table-row'; + +export const tiptapExtension = TableRow.extend({ + allowGapCursor: false, +}); + +export function serializer(state, node) { + const isHeaderRow = node.child(0).type.name === 'tableHeader'; + + const renderRow = () => { + const cellWidths = []; + + state.flushClose(1); + + state.write('| '); + node.forEach((cell, _, i) => { + if (i) state.write(' | '); + + const { length } = state.out; + state.render(cell, node, i); + cellWidths.push(state.out.length - length); + }); + state.write(' |'); + + state.closeBlock(node); + + return cellWidths; + }; + + const renderHeaderRow = (cellWidths) => { + state.flushClose(1); + + state.write('|'); + node.forEach((cell, _, i) => { + if (i) state.write('|'); + + state.write(cell.attrs.align === 'center' ? ':' : '-'); + state.write(state.repeat('-', cellWidths[i])); + state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-'); + }); + state.write('|'); + + state.closeBlock(node); + }; + + if (isHeaderRow) { + renderHeaderRow(renderRow()); + } else { + renderRow(); + } +} diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index 03ddae2aa7c..9251fdbbdc5 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -20,6 +20,10 @@ import * as ListItem from '../extensions/list_item'; import * as OrderedList from '../extensions/ordered_list'; import * as Paragraph from '../extensions/paragraph'; import * as Strike from '../extensions/strike'; +import * as Table from '../extensions/table'; +import * as TableCell from '../extensions/table_cell'; +import * as TableHeader from '../extensions/table_header'; +import * as TableRow from '../extensions/table_row'; import * as Text from '../extensions/text'; import buildSerializerConfig from './build_serializer_config'; import { ContentEditor } from './content_editor'; @@ -70,6 +74,10 @@ export const createContentEditor = ({ OrderedList, Paragraph, Strike, + TableCell, + TableHeader, + TableRow, + Table, Text, ]; diff --git a/app/assets/javascripts/content_editor/services/utils.js b/app/assets/javascripts/content_editor/services/utils.js index ca2f9762ff8..2a2c7f617da 100644 --- a/app/assets/javascripts/content_editor/services/utils.js +++ b/app/assets/javascripts/content_editor/services/utils.js @@ -15,3 +15,5 @@ export const readFileAsDataURL = (file) => { reader.readAsDataURL(file); }); }; + +export const clamp = (n, min, max) => Math.max(Math.min(n, max), min); diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js index 122f23a5bb5..1ccecf3eb53 100644 --- a/app/assets/javascripts/jobs/utils.js +++ b/app/assets/javascripts/jobs/utils.js @@ -3,10 +3,10 @@ * https?:\/\/ * * up until a disallowed character or whitespace - * [^"<>\\^`{|}\s]+ + * [^"<>()\\^`{|}\s]+ * * and a disallowed character or whitespace, including non-ending chars .,:;!? - * [^"<>\\^`{|}\s.,:;!?] + * [^"<>()\\^`{|}\s.,:;!?] */ -export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+[^"<>\\^`{|}\s.,:;!?])/g; +export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])/g; export default { linkRegex }; diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue index 0229d226258..24565c441d8 100644 --- a/app/assets/javascripts/token_access/components/token_access.vue +++ b/app/assets/javascripts/token_access/components/token_access.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlCard, GlFormGroup, GlFormInput, GlLoadingIcon, GlToggle } from '@gitlab/ui'; +import { GlButton, GlCard, GlFormInput, GlLoadingIcon, GlToggle } from '@gitlab/ui'; import createFlash from '~/flash'; import { __, s__ } from '~/locale'; import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql'; @@ -16,7 +16,6 @@ export default { `CICD|Select projects that can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable.`, ), cardHeaderTitle: s__('CICD|Add an existing project to the scope'), - formGroupLabel: __('Search for project'), addProject: __('Add project'), cancel: __('Cancel'), addProjectPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'), @@ -26,7 +25,6 @@ export default { components: { GlButton, GlCard, - GlFormGroup, GlFormInput, GlLoadingIcon, GlToggle, @@ -183,13 +181,10 @@ export default { <h5 class="gl-my-0">{{ $options.i18n.cardHeaderTitle }}</h5> </template> <template #default> - <gl-form-group :label="$options.i18n.formGroupLabel" label-for="token-project-search"> - <gl-form-input - id="token-project-search" - v-model="targetProjectPath" - :placeholder="$options.i18n.addProjectPlaceholder" - /> - </gl-form-group> + <gl-form-input + v-model="targetProjectPath" + :placeholder="$options.i18n.addProjectPlaceholder" + /> </template> <template #footer> <gl-button diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 581dcf5c1a1..3fa9a484b0c 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -14,19 +14,7 @@ module Ci belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id - default_value_for :data_store do |chunk| - # We're using the safe operator here to get to the project for which we're - # creating a TraceChunk because the build attribute would not be populated - # when the chunk was initialized by FactoryBot: - # https://github.com/thoughtbot/factory_bot/wiki/How-factory_bot-interacts-with-ActiveRecord - # While the `default_value_for` gem depends on an `after_initialize` - # callback. - if Feature.enabled?(:dedicated_redis_trace_chunks, chunk.build&.project, type: :ops) - :redis_trace_chunks - else - :redis - end - end + default_value_for :data_store, :redis_trace_chunks after_create { metrics.increment_trace_operation(operation: :chunked) } diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index 259e96901fd..a6302569187 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -1,25 +1,23 @@ = form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f| = form_errors(@milestone) - .row - .col-md-6 - .form-group.row - .col-form-label.col-sm-2 - = f.label :title, _("Title") - .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control", data: { qa_selector: "milestone_title_field" }, required: true, autofocus: true - .form-group.row.milestone-description - .col-form-label.col-sm-2 - = f.label :description, _("Description") - .col-sm-10 - = render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do - = render 'shared/zen', f: f, attr: :description, - classes: 'note-textarea', - qa_selector: 'milestone_description_field', - supports_autocomplete: true, - placeholder: _('Write milestone description...') - .clearfix - .error-alert - = render "shared/milestones/form_dates", f: f + .form-group.row + .col-form-label.col-sm-2 + = f.label :title, _("Title") + .col-sm-10 + = f.text_field :title, maxlength: 255, class: "form-control", data: { qa_selector: "milestone_title_field" }, required: true, autofocus: true + = render "shared/milestones/form_dates", f: f + .form-group.row.milestone-description + .col-form-label.col-sm-2 + = f.label :description, _("Description") + .col-sm-10 + = render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do + = render 'shared/zen', f: f, attr: :description, + classes: 'note-textarea', + qa_selector: 'milestone_description_field', + supports_autocomplete: true, + placeholder: _('Write milestone description...') + .clearfix + .error-alert .form-actions - if @milestone.new_record? diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index dfb9defb91c..5f2057df4aa 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,27 +1,25 @@ = form_for [@project, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f| = form_errors(@milestone) - .row - .col-md-6 - .form-group.row - .col-form-label.col-sm-2 - = f.label :title, _('Title') - .col-sm-10 - = f.text_field :title, maxlength: 255, class: 'form-control gl-form-input', data: { qa_selector: 'milestone_title_field' }, required: true, autofocus: true - .form-group.row.milestone-description - .col-form-label.col-sm-2 - = f.label :description, _('Description') - .col-sm-10 - = render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do - = render 'shared/zen', f: f, attr: :description, - classes: 'note-textarea', - qa_selector: 'milestone_description_field', - supports_autocomplete: true, - placeholder: _('Write milestone description...') - = render 'shared/notes/hints' - .clearfix - .error-alert - = render 'shared/milestones/form_dates', f: f + .form-group.row + .col-form-label.col-sm-2 + = f.label :title, _('Title') + .col-sm-10 + = f.text_field :title, maxlength: 255, class: 'form-control gl-form-input', data: { qa_selector: 'milestone_title_field' }, required: true, autofocus: true + = render 'shared/milestones/form_dates', f: f + .form-group.row.milestone-description + .col-form-label.col-sm-2 + = f.label :description, _('Description') + .col-sm-10 + = render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do + = render 'shared/zen', f: f, attr: :description, + classes: 'note-textarea', + qa_selector: 'milestone_description_field', + supports_autocomplete: true, + placeholder: _('Write milestone description...') + = render 'shared/notes/hints' + .clearfix + .error-alert .form-actions - if @milestone.new_record? diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml index e0664c1feba..7a41e381a96 100644 --- a/app/views/shared/milestones/_form_dates.html.haml +++ b/app/views/shared/milestones/_form_dates.html.haml @@ -1,13 +1,11 @@ -.col-md-6 - .form-group.row - .col-form-label.col-sm-2 - = f.label :start_date, _('Start Date') - .col-sm-10 - = f.text_field :start_date, class: "datepicker form-control gl-form-input", data: { qa_selector: "start_date_field" }, placeholder: _('Select start date'), autocomplete: 'off' - %a.inline.float-right.gl-mt-2.js-clear-start-date{ href: "#" }= _('Clear start date') - .form-group.row - .col-form-label.col-sm-2 - = f.label :due_date, _('Due Date') - .col-sm-10 - = f.text_field :due_date, class: "datepicker form-control gl-form-input", data: { qa_selector: "due_date_field" }, placeholder: _('Select due date'), autocomplete: 'off' - %a.inline.float-right.gl-mt-2.js-clear-due-date{ href: "#" }= _('Clear due date') +.form-group.row + .col-form-label.col-sm-2 + = f.label :start_date, _('Start Date') + .col-sm-4 + = f.text_field :start_date, class: "datepicker form-control gl-form-input", data: { qa_selector: "start_date_field" }, placeholder: _('Select start date'), autocomplete: 'off' + %a.inline.float-right.gl-mt-2.js-clear-start-date{ href: "#" }= _('Clear start date') + .col-form-label.col-sm-2 + = f.label :due_date, _('Due Date') + .col-sm-4 + = f.text_field :due_date, class: "datepicker form-control gl-form-input", data: { qa_selector: "due_date_field" }, placeholder: _('Select due date'), autocomplete: 'off' + %a.inline.float-right.gl-mt-2.js-clear-due-date{ href: "#" }= _('Clear due date') |