1
0
Fork 0
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 

See merge request 
This commit is contained in:
Bram Wiepjes 2021-02-13 13:12:19 +00:00
commit 44fc99372a
17 changed files with 296 additions and 50 deletions

View file

@ -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)

View file

@ -10,6 +10,6 @@ stylelint:
lint: eslint stylelint
jest:
yarn run jest || exit;
yarn run jest-all || exit;
test: jest

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;

View file

@ -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
}
}

View file

@ -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)

View file

@ -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,

View file

@ -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}/`
},

View file

@ -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",

View file

@ -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()
})
})

View 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',
})
})
})