Skip to content

Commit

Permalink
feat: implement JsonViewer widget
Browse files Browse the repository at this point in the history
  • Loading branch information
madeindjs committed Jul 9, 2024
1 parent 7ca166b commit 98096ae
Show file tree
Hide file tree
Showing 10 changed files with 541 additions and 1 deletion.
Binary file added docs/framework/public/components/jsonviewer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/ui/src/core/templateMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import CoreLink from "../core_components/content/CoreLink.vue";
import CoreChatbot from "../core_components/content/CoreChatbot.vue";
import CoreTags from "../core_components/content/CoreTags.vue";
import CoreAvatar from "../core_components/content/CoreAvatar.vue";
import CoreJsonViewer from "../core_components/content/CoreJsonViewer.vue";
// input
import CoreCheckboxInput from "../core_components/input/CoreCheckboxInput.vue";
import CoreDateInput from "../core_components/input/CoreDateInput.vue";
Expand Down Expand Up @@ -115,12 +116,13 @@ const templateMap = {
switchinput: CoreSwitchInput,
reuse: CoreReuse,
avatar: CoreAvatar,
jsonviewer: CoreJsonViewer,
};

if (WRITER_LIVE_CCT === "yes") {
/*
Assigns the components in custom_components to the template map,
allowing for live updates when developing custom component templates.
allowing for live updates when developing custom component templates.
*/

const liveCCT: Record<string, any> = (await import("../custom_components"))
Expand Down
99 changes: 99 additions & 0 deletions src/ui/src/core_components/base/BaseCollapsible.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<details
class="collapsible"
:open="open"
:disabled="disabled"
@toggle="onToggle"
>
<summary><slot name="title" /></summary>
<div class="content">
<slot name="content" />
</div>
</details>
</template>

<script setup lang="ts">
defineProps({
open: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
});
const emit = defineEmits({
toggle: (open: boolean) => typeof open === "boolean",
});
function onToggle(event) {
emit("toggle", event.newState === "open");
}
</script>

<style scoped>
/* customize the triangle and animate it */
details {
box-sizing: border-box;
}
details summary::-webkit-details-marker {
display: none;
}
details[open] > summary:before {
transform: rotate(90deg);
}
summary {
outline: none;
display: flex;
padding-left: 16px;
position: relative;
cursor: pointer;
}
summary:before {
content: "";
border-width: 6px;
border-style: solid;
border-color: transparent transparent transparent var(--accentColor);
position: absolute;
left: 4px;
top: 2px;
transform: rotate(0);
transform-origin: 3px 50%;
transition: 0.3s transform ease;
}
summary:focus-visible:before {
border-color: transparent transparent transparent var(--primaryTextColor);
}
@media (prefers-reduced-motion) {
summary:before {
transition: unset;
}
}
/* small animation on the content */
details[open] summary ~ .content {
animation: sweep 0.2s ease-in-out;
}
@media (prefers-reduced-motion) {
details[open] summary ~ .content {
animation: unset;
}
}
@keyframes sweep {
0% {
opacity: 0;
margin-top: -12px;
}
100% {
opacity: 1;
margin-top: 0;
}
}
</style>
32 changes: 32 additions & 0 deletions src/ui/src/core_components/base/BaseJsonViewer.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {
JsonData,
JsonValue,
JsonViewerTogglePayload,
} from "./BaseJsonViewer.vue";

export function isJSONValue(data: JsonData): data is JsonValue {
if (["string", "number", "boolean"].includes(typeof data)) return true;
if (data === null) return true;
return false;
}

export function isJSONArray(data: JsonData): data is JsonData[] {
if (isJSONValue(data)) return false;
return Array.isArray(data);
}

export function isJSONObject(
data: JsonData,
): data is { [x: string]: JsonData } {
return !isJSONArray(data) && typeof data === "object" && data !== null;
}

export function getJSONLength(data: JsonData): number {
return isJSONValue(data) ? 1 : Object.keys(data).length;
}

export function jsonViewerToggleEmitDefinition(
payload: JsonViewerTogglePayload,
) {
return typeof payload.open === "boolean" && Array.isArray(payload.path);
}
72 changes: 72 additions & 0 deletions src/ui/src/core_components/base/BaseJsonViewer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<template v-if="isJSONObject(data) || isJSONArray(data)">
<BaseJsonViewerCollapsible
v-if="isRoot"
:open="isRootOpen"
:data="data"
@toggle="$emit('toggle', { path: [], open: $event })"
>
<BaseJsonViewerObject
:data="data"
:path="path"
:initial-depth="initialDepth"
@toggle="$emit('toggle', $event)"
/>
</BaseJsonViewerCollapsible>
<BaseJsonViewerObject
v-else
:data="data"
:path="path"
:initial-depth="initialDepth"
@toggle="$emit('toggle', $event)"
/>
</template>
<BaseJsonViewerValue v-else-if="isJSONValue(data)" :data="data" />
</template>

<script lang="ts">
export type JsonValue = string | number | boolean | null;
export type JsonData = JsonValue | { [x: string]: JsonData } | JsonData[];
export type JsonPath = string[];
export type JsonViewerTogglePayload = { path: JsonPath; open: boolean };
</script>

<script setup lang="ts">
/**
* This component will detect the shape of the JSON and redirect the right dedicated component.
*/
import { PropType, computed } from "vue";
import {
isJSONArray,
isJSONObject,
isJSONValue,
jsonViewerToggleEmitDefinition,
} from "./BaseJsonViewer.utils";
import BaseJsonViewerCollapsible from "./BaseJsonViewerCollapsible.vue";
import BaseJsonViewerObject from "./BaseJsonViewerObject.vue";
import BaseJsonViewerValue from "./BaseJsonViewerValue.vue";
const props = defineProps({
data: {
type: Object as PropType<JsonData>,
required: true,
},
path: {
type: Array as PropType<JsonPath>,
default: () => [],
},
initialDepth: { type: Number, default: 0 },
});
defineEmits({
toggle: jsonViewerToggleEmitDefinition,
});
const isRoot = computed(() => props.path.length === 0);
const isRootOpen = computed(
() => props.initialDepth === -1 || props.initialDepth > 0,
);
</script>
40 changes: 40 additions & 0 deletions src/ui/src/core_components/base/BaseJsonViewerChildrenCounter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<span>{{ text }}</span>
</template>

<script setup lang="ts">
import { PropType, computed } from "vue";
import {
getJSONLength,
isJSONArray,
isJSONObject,
} from "./BaseJsonViewer.utils";
import type { JsonData } from "./BaseJsonViewer.vue";
const props = defineProps({
data: {
type: Object as PropType<JsonData>,
required: true,
},
});
const printObject = (length: number) => `Object{${length}}`;
const printArray = (length: number) => `Array[${length}]`;
const text = computed(() => {
const count = getJSONLength(props.data);
if (count === 0) return printObject(0);
if (isJSONArray(props.data)) return printArray(count);
if (isJSONObject(props.data)) return printObject(count);
return printObject(0);
});
</script>

<style scoped>
span {
color: var(--secondaryTextColor);
font-family: monospace;
font-size: 12px;
}
</style>
58 changes: 58 additions & 0 deletions src/ui/src/core_components/base/BaseJsonViewerCollapsible.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<BaseCollapsible
:open="open"
:disabled="disabled"
@toggle="$emit('toggle', $event)"
>
<template #title>
<div class="BaseJsonViewerCollapsible__title">
<span v-if="title">{{ title }}</span>
<BaseJsonViewerChildrenCounter v-if="data" :data="data" />
</div>
</template>
<template #content>
<div class="BaseJsonViewerCollapsible__content">
<slot />
</div>
</template>
</BaseCollapsible>
</template>

<script setup lang="ts">
import type { PropType } from "vue";
import BaseCollapsible from "./BaseCollapsible.vue";
import type { JsonData } from "./BaseJsonViewer.vue";
import BaseJsonViewerChildrenCounter from "./BaseJsonViewerChildrenCounter.vue";
defineProps({
open: { type: Boolean, required: false },
disabled: { type: Boolean, required: false },
title: { type: String, required: false, default: undefined },
data: {
type: [Object, Array] as PropType<JsonData>,
required: false,
default: undefined,
},
});
defineEmits({
toggle: (open: boolean) => typeof open === "boolean",
});
</script>

<style scoped>
.BaseJsonViewerCollapsible__title {
font-family: monospace;
font-size: 12px;
display: flex;
gap: 8px;
}
.BaseJsonViewerCollapsible__content {
margin-left: 7px;
padding-left: var(--jsonViewerIndentationSpacing, 8px);
padding-top: 4px;
padding-bottom: 4px;
border-left: 1px solid var(--separatorColor);
}
</style>
Loading

0 comments on commit 98096ae

Please sign in to comment.