mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-10 23:50:12 +00:00
Merge branch '256-show-field-name-when-hovering-over-a-field_30-in-the-generated-api-docs' into 'develop'
Resolve "Show field name when hovering over a field_30 in the generated API docs" Closes #256 See merge request bramw/baserow!169
This commit is contained in:
commit
44fc99372a
17 changed files with 296 additions and 50 deletions
|
@ -19,6 +19,7 @@
|
|||
* Use UTC time in the date picker.
|
||||
* Refactored handler get_* methods so that they never check for permissions.
|
||||
* Made it possible to configure SMTP settings via environment variables.
|
||||
* Added field name to the public REST API docs.
|
||||
|
||||
## Released (2021-02-04)
|
||||
|
||||
|
|
|
@ -10,6 +10,6 @@ stylelint:
|
|||
lint: eslint stylelint
|
||||
|
||||
jest:
|
||||
yarn run jest || exit;
|
||||
yarn run jest-all || exit;
|
||||
|
||||
test: jest
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
&::after {
|
||||
content: '';
|
||||
width: calc(45% - #{$api-docs-nav-width * 0.45});
|
||||
width: calc(50% - #{$api-docs-nav-width * 0.5});
|
||||
background-color: $color-neutral-600;
|
||||
z-index: $api-docs-background-z-index;
|
||||
|
||||
|
@ -101,12 +101,12 @@
|
|||
}
|
||||
|
||||
.api-docs__left {
|
||||
width: 55%;
|
||||
width: 50%;
|
||||
padding: 0 34px;
|
||||
}
|
||||
|
||||
.api-docs__right {
|
||||
width: 45%;
|
||||
width: 50%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,14 @@
|
|||
}
|
||||
|
||||
.api-docs__example {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
background-color: $color-neutral-100;
|
||||
|
||||
&.api-docs__example--with-padding {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.api-docs__copy {
|
||||
|
@ -65,11 +70,41 @@
|
|||
.api-docs__example-type {
|
||||
width: 200px;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.api-docs__example-content-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.api-docs__example-content-side {
|
||||
position: relative;
|
||||
flex: 0 0 120px;
|
||||
padding: 16px 0;
|
||||
background-color: $color-neutral-200;
|
||||
}
|
||||
|
||||
.api-docs__example-content-line {
|
||||
@extend %ellipsis;
|
||||
|
||||
margin-top: 16px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
color: $color-neutral-700;
|
||||
|
||||
@include absolute(auto, 0, auto, 0);
|
||||
}
|
||||
|
||||
.api-docs__example-content-wrapper {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.api-docs__example-content {
|
||||
margin: 0;
|
||||
line-height: 160%;
|
||||
line-height: 21px;
|
||||
color: $color-neutral-700;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
|
|
@ -27,8 +27,13 @@
|
|||
}
|
||||
|
||||
.api-docs__parameter-name {
|
||||
flex: 0 140px;
|
||||
margin-right: 20px;
|
||||
flex: 0 200px;
|
||||
margin: 0 20px 20px 0;
|
||||
}
|
||||
|
||||
.api-docs__parameter-visible-name {
|
||||
color: $color-neutral-700;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.api-docs__parameter-optional {
|
||||
|
|
|
@ -87,7 +87,7 @@ $font-awesome-font-family: 'Font Awesome 5 Free', sans-serif !default;
|
|||
$font-awesome-font-weight: 900 !default;
|
||||
|
||||
// API docs variables
|
||||
$api-docs-nav-width: 240px !default;
|
||||
$api-docs-nav-width: 220px !default;
|
||||
$api-docs-header-height: 52px !default;
|
||||
$api-docs-header-z-index: 4 !default;
|
||||
$api-docs-databases-z-index: 5 !default;
|
||||
|
|
|
@ -7,3 +7,57 @@
|
|||
export function clone(o) {
|
||||
return JSON.parse(JSON.stringify(o))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an object where the key indicates the line number and the value is
|
||||
* the string that must be shown on that line number. The line number matches
|
||||
* the line number if the value would be stringified with an indent of 4
|
||||
* characters `JSON.stringify(value, null, 4)`. The correct value is matched
|
||||
* if a value (recursive) object key matches a key of the mapping.
|
||||
*
|
||||
* Example:
|
||||
* mappingToStringifiedJSONLines(
|
||||
* { key_2: 'Value' },
|
||||
* {
|
||||
* key_1: 'A random value',
|
||||
* key_2: 'Another value'
|
||||
* }
|
||||
* ) === {
|
||||
* 3: 'Value'
|
||||
* }
|
||||
*/
|
||||
export function mappingToStringifiedJSONLines(
|
||||
mapping,
|
||||
value,
|
||||
index = 1,
|
||||
lines = {},
|
||||
first = true
|
||||
) {
|
||||
if (Array.isArray(value)) {
|
||||
index += 1
|
||||
value.forEach((v, i) => {
|
||||
index = mappingToStringifiedJSONLines(mapping, v, index, lines, false)
|
||||
})
|
||||
index += 1
|
||||
return first ? lines : index
|
||||
} else if (value instanceof Object) {
|
||||
index += 1
|
||||
Object.keys(value).forEach((k) => {
|
||||
if (Object.prototype.hasOwnProperty.call(mapping, k)) {
|
||||
lines[index] = mapping[k]
|
||||
}
|
||||
index = mappingToStringifiedJSONLines(
|
||||
mapping,
|
||||
value[k],
|
||||
index,
|
||||
lines,
|
||||
false
|
||||
)
|
||||
})
|
||||
index += 1
|
||||
return first ? lines : index
|
||||
} else {
|
||||
index += 1
|
||||
return first ? lines : index
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="type !== '' && url !== ''" class="api-docs__example">
|
||||
<div
|
||||
v-if="type !== '' && url !== ''"
|
||||
class="api-docs__example api-docs__example--with-padding"
|
||||
>
|
||||
<a
|
||||
class="api-docs__copy"
|
||||
@click.prevent=";[copyToClipboard(url), $refs.urlCopied.show()]"
|
||||
|
@ -34,7 +37,10 @@
|
|||
<a
|
||||
class="api-docs__copy"
|
||||
@click.prevent="
|
||||
;[copyToClipboard(getFormattedRequest()), $refs.requestCopied.show()]
|
||||
;[
|
||||
copyToClipboard(getFormattedRequest().example),
|
||||
$refs.requestCopied.show(),
|
||||
]
|
||||
"
|
||||
>
|
||||
Copy
|
||||
|
@ -51,9 +57,29 @@
|
|||
<DropdownItem value="python" name="Python (requests)"></DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<pre
|
||||
class="api-docs__example-content"
|
||||
><code>{{ formattedRequest }}</code></pre>
|
||||
<div class="api-docs__example-content-container">
|
||||
<div
|
||||
v-if="Object.keys(mapping).length > 0"
|
||||
class="api-docs__example-content-side"
|
||||
>
|
||||
<div
|
||||
v-for="(lineValue, line) in formattedRequest.lines"
|
||||
:key="'response-info-line-' + line"
|
||||
class="api-docs__example-content-line"
|
||||
:style="'top:' + (line - 1) * 21 + 'px;'"
|
||||
:title="lineValue"
|
||||
>
|
||||
{{ lineValue }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-docs__example-content-wrapper">
|
||||
<div class="api-docs__example-content">
|
||||
<pre
|
||||
class="api-docs__example-content"
|
||||
><code>{{ formattedRequest.example }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="response !== false">
|
||||
<div class="api-docs__example-title">Response sample</div>
|
||||
|
@ -62,7 +88,7 @@
|
|||
class="api-docs__copy"
|
||||
@click.prevent="
|
||||
;[
|
||||
copyToClipboard(getFormattedResponse()),
|
||||
copyToClipboard(getFormattedResponse().example),
|
||||
$refs.responseCopied.show(),
|
||||
]
|
||||
"
|
||||
|
@ -70,9 +96,29 @@
|
|||
Copy
|
||||
<Copied ref="responseCopied"></Copied>
|
||||
</a>
|
||||
<pre
|
||||
class="api-docs__example-content"
|
||||
><code>{{ formattedResponse }}</code></pre>
|
||||
<div class="api-docs__example-content-container">
|
||||
<div
|
||||
v-if="Object.keys(mapping).length > 0"
|
||||
class="api-docs__example-content-side"
|
||||
>
|
||||
<div
|
||||
v-for="(lineValue, line) in formattedResponse.lines"
|
||||
:key="'response-info-line-' + line"
|
||||
class="api-docs__example-content-line"
|
||||
:style="'top:' + (line - 1) * 21 + 'px;'"
|
||||
:title="lineValue"
|
||||
>
|
||||
{{ lineValue }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-docs__example-content-wrapper">
|
||||
<div class="api-docs__example-content">
|
||||
<pre
|
||||
class="api-docs__example-content"
|
||||
><code>{{ formattedResponse.example }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -80,6 +126,7 @@
|
|||
|
||||
<script>
|
||||
import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
|
||||
import { mappingToStringifiedJSONLines } from '@baserow/modules/core/utils/object'
|
||||
|
||||
export default {
|
||||
name: 'APIDocsExample',
|
||||
|
@ -108,6 +155,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
mapping: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formattedResponse() {
|
||||
|
@ -134,30 +186,39 @@ export default {
|
|||
return ''
|
||||
},
|
||||
getCURLRequestExample() {
|
||||
let index = 3
|
||||
let example = 'curl \\'
|
||||
|
||||
if (this.type !== '') {
|
||||
index++
|
||||
example += `\n-X ${this.type} \\`
|
||||
}
|
||||
|
||||
example += '\n-H "Authorization: Token YOUR_API_KEY" \\'
|
||||
|
||||
if (this.request !== false) {
|
||||
index++
|
||||
example += '\n-H "Content-Type: application/json" \\'
|
||||
}
|
||||
|
||||
example += `\n${this.url}`
|
||||
|
||||
if (this.request !== false) {
|
||||
index++
|
||||
example += ` \\\n--data '${this.format(this.request)}'`
|
||||
}
|
||||
|
||||
return example
|
||||
return {
|
||||
example,
|
||||
lines: mappingToStringifiedJSONLines(this.mapping, this.request, index),
|
||||
}
|
||||
},
|
||||
getHTTPRequestExample() {
|
||||
let index = 2
|
||||
let example = ''
|
||||
|
||||
if (this.type !== '') {
|
||||
index++
|
||||
example += `${this.type.toUpperCase()} `
|
||||
}
|
||||
|
||||
|
@ -165,16 +226,22 @@ export default {
|
|||
example += '\nAuthorization: Token YOUR_API_KEY'
|
||||
|
||||
if (this.request !== false) {
|
||||
index += 2
|
||||
example += '\nContent-Type: application/json'
|
||||
example += `\n\n${this.format(this.request)}`
|
||||
}
|
||||
|
||||
return example
|
||||
return {
|
||||
example,
|
||||
lines: mappingToStringifiedJSONLines(this.mapping, this.request, index),
|
||||
}
|
||||
},
|
||||
getJavaScriptExample() {
|
||||
let index = 5
|
||||
let example = 'axios({'
|
||||
|
||||
if (this.type !== '') {
|
||||
index++
|
||||
example += `\n method: "${this.type.toUpperCase()}",`
|
||||
}
|
||||
|
||||
|
@ -183,20 +250,26 @@ export default {
|
|||
example += '\n Authorization: "Token YOUR_API_KEY"'
|
||||
|
||||
if (this.request !== false) {
|
||||
index++
|
||||
example += ',\n "Content-Type": "application/json"'
|
||||
}
|
||||
|
||||
example += '\n }'
|
||||
|
||||
if (this.request !== false) {
|
||||
index++
|
||||
const data = this.format(this.request).slice(0, -1) + ' }'
|
||||
example += `,\n data: ${data}`
|
||||
}
|
||||
|
||||
example += '\n})'
|
||||
return example
|
||||
return {
|
||||
example,
|
||||
lines: mappingToStringifiedJSONLines(this.mapping, this.request, index),
|
||||
}
|
||||
},
|
||||
getPythonExample() {
|
||||
let index = 5
|
||||
const type = (this.type || 'get').toLowerCase()
|
||||
let example = `requests.${type}(`
|
||||
example += `\n "${this.url}",`
|
||||
|
@ -205,21 +278,29 @@ export default {
|
|||
example += `\n "Authorization": "Token YOUR_API_KEY"`
|
||||
|
||||
if (this.request !== false) {
|
||||
index++
|
||||
example += `,\n "Content-Type": "application/json"`
|
||||
}
|
||||
|
||||
example += '\n }'
|
||||
|
||||
if (this.request !== false) {
|
||||
index++
|
||||
const data = this.format(this.request).split('\n').join('\n ')
|
||||
example += `,\n json=${data}`
|
||||
}
|
||||
|
||||
example += '\n)'
|
||||
return example
|
||||
return {
|
||||
example,
|
||||
lines: mappingToStringifiedJSONLines(this.mapping, this.request, index),
|
||||
}
|
||||
},
|
||||
getFormattedResponse() {
|
||||
return this.format(this.response)
|
||||
return {
|
||||
example: this.format(this.response),
|
||||
lines: mappingToStringifiedJSONLines(this.mapping, this.response),
|
||||
}
|
||||
},
|
||||
copyToClipboard(value) {
|
||||
copyToClipboard(value)
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
<template>
|
||||
<li class="api-docs__parameter">
|
||||
<div class="api-docs__parameter-name">
|
||||
<div>{{ name }}</div>
|
||||
<div>
|
||||
{{ name }}
|
||||
<span
|
||||
v-if="visibleName !== null"
|
||||
class="api-docs__parameter-visible-name"
|
||||
>{{ visibleName }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="optional" class="api-docs__parameter-optional">optional</div>
|
||||
</div>
|
||||
<div class="api-docs__parameter-description">
|
||||
|
@ -24,6 +31,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
visibleName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
optional: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
|
@ -409,6 +409,7 @@
|
|||
previous: null,
|
||||
results: [getResponseItem(table)],
|
||||
}"
|
||||
:mapping="getFieldMapping(table)"
|
||||
></APIDocsExample>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -436,6 +437,7 @@
|
|||
type="GET"
|
||||
:url="getItemURL(table)"
|
||||
:response="getResponseItem(table)"
|
||||
:mapping="getFieldMapping(table)"
|
||||
></APIDocsExample>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -454,6 +456,7 @@
|
|||
v-for="field in fields[table.id]"
|
||||
:key="field.id"
|
||||
:name="'field_' + field.id"
|
||||
:visible-name="field.name"
|
||||
:optional="true"
|
||||
:type="field._.type"
|
||||
>
|
||||
|
@ -466,8 +469,9 @@
|
|||
v-model="exampleType"
|
||||
type="POST"
|
||||
:url="getListURL(table)"
|
||||
:request="getRequestItem(table)"
|
||||
:request="getRequestExample(table)"
|
||||
:response="getResponseItem(table)"
|
||||
:mapping="getFieldMapping(table)"
|
||||
></APIDocsExample>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -494,6 +498,7 @@
|
|||
v-for="field in fields[table.id]"
|
||||
:key="field.id"
|
||||
:name="'field_' + field.id"
|
||||
:visible-name="field.name"
|
||||
:optional="true"
|
||||
:type="field._.type"
|
||||
>
|
||||
|
@ -506,8 +511,9 @@
|
|||
v-model="exampleType"
|
||||
type="PATCH"
|
||||
:url="getItemURL(table)"
|
||||
:request="getRequestItem(table)"
|
||||
:request="getRequestExample(table)"
|
||||
:response="getResponseItem(table)"
|
||||
:mapping="getFieldMapping(table)"
|
||||
></APIDocsExample>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -763,7 +769,7 @@ export default {
|
|||
/**
|
||||
* Generates an example request object based on the available fields of the table.
|
||||
*/
|
||||
getRequestItem(table, response = false) {
|
||||
getRequestExample(table, response = false) {
|
||||
const item = {}
|
||||
this.fields[table.id].forEach((field) => {
|
||||
const example = response
|
||||
|
@ -778,9 +784,19 @@ export default {
|
|||
*/
|
||||
getResponseItem(table) {
|
||||
const item = { id: 0 }
|
||||
Object.assign(item, this.getRequestItem(table, true))
|
||||
Object.assign(item, this.getRequestExample(table, true))
|
||||
return item
|
||||
},
|
||||
/**
|
||||
* Returns the mapping of the field id as key and the field name as value.
|
||||
*/
|
||||
getFieldMapping(table) {
|
||||
const mapping = {}
|
||||
this.fields[table.id].forEach((field) => {
|
||||
mapping[`field_${field.id}`] = field.name
|
||||
})
|
||||
return mapping
|
||||
},
|
||||
getListURL(table) {
|
||||
return `${this.$env.PUBLIC_BACKEND_URL}/api/database/rows/table/${table.id}/`
|
||||
},
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"demo": "nuxt start --hostname 0.0.0.0 --config-file config/nuxt.config.demo.js",
|
||||
"eslint": "eslint --ext .js,.vue .",
|
||||
"stylelint": "stylelint **/*.scss --syntax scss",
|
||||
"jest": "jest -i --verbose false test/"
|
||||
"jest": "jest -i --verbose false",
|
||||
"jest-all": "jest -i --verbose false test/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.0",
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import createNuxt from '@/test/helpers/create-nuxt'
|
||||
|
||||
let nuxt = null
|
||||
|
||||
describe('Config', () => {
|
||||
beforeAll(async (done) => {
|
||||
nuxt = await createNuxt(true)
|
||||
done()
|
||||
}, 120000)
|
||||
|
||||
// @TODO fix this test.
|
||||
test('Plugins', () => {
|
||||
// const plugins = nuxt.options.plugins.map(option => option.src)
|
||||
// expect(plugins.includes('@/modules/core/plugins/auth.js')).toBe(true)
|
||||
// expect(plugins.includes('@/modules/core/plugins/vuelidate.js')).toBe(true)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await nuxt.close()
|
||||
})
|
||||
})
|
62
web-frontend/test/core/utils/object.spec.js
Normal file
62
web-frontend/test/core/utils/object.spec.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import {
|
||||
clone,
|
||||
mappingToStringifiedJSONLines,
|
||||
} from '@/modules/core/utils/object'
|
||||
|
||||
describe('test utils object', () => {
|
||||
test('test clone', () => {
|
||||
const o = {
|
||||
a: '1',
|
||||
b: '2',
|
||||
c: {
|
||||
d: '3',
|
||||
},
|
||||
}
|
||||
const cloned = clone(o)
|
||||
|
||||
expect(o !== cloned).toBe(true)
|
||||
o.a = '5'
|
||||
o.c.d = '6'
|
||||
expect(cloned.a).toBe('1')
|
||||
expect(cloned.c.d).toBe('3')
|
||||
})
|
||||
|
||||
test('test mappingToStringifiedJSONLines', () => {
|
||||
expect(
|
||||
mappingToStringifiedJSONLines(
|
||||
{
|
||||
key_1: 'Value 1',
|
||||
key_2: 'Value 2',
|
||||
key_3: 'Value 3',
|
||||
},
|
||||
{
|
||||
key_1: '',
|
||||
key_a: [
|
||||
{
|
||||
key_b: '',
|
||||
key_2: '',
|
||||
key_3: '',
|
||||
key_c: {
|
||||
key_d: {
|
||||
key_1: '',
|
||||
key_e: [
|
||||
{
|
||||
key_3: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
key_2: '',
|
||||
}
|
||||
)
|
||||
).toMatchObject({
|
||||
2: 'Value 1',
|
||||
6: 'Value 2',
|
||||
10: 'Value 1',
|
||||
13: 'Value 3',
|
||||
20: 'Value 2',
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue