From 1a9724d790e6f3f8787afb604c9567761e4b2ab1 Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Mon, 12 Dec 2022 07:28:37 -0800 Subject: [PATCH] Added inline tree attributes Summary: This is temporary solution to get to parity with the old plugin. In future would like to make this more flexible on the desktop side Additionally getData was renamed to getAttributes for consistency Reviewed By: lblasa Differential Revision: D41845248 fbshipit-source-id: 50e94a7712f5d42938229134e212cef5d379475d --- .../descriptors/DebugComponentDescriptor.kt | 17 ++++++++++- .../litho/descriptors/LithoViewDescriptor.kt | 2 +- .../descriptors/TextDrawableDescriptor.kt | 2 +- .../descriptors/ChainedDescriptor.kt | 25 +++++++++++++--- .../descriptors/ColorDrawableDescriptor.kt | 2 +- .../descriptors/DrawableDescriptor.kt | 2 +- .../FragmentFrameworkDescriptor.kt | 2 +- .../descriptors/FragmentSupportDescriptor.kt | 2 +- .../descriptors/ImageViewDescriptor.kt | 2 +- .../uidebugger/descriptors/NodeDescriptor.kt | 12 ++++++-- .../descriptors/ObjectDescriptor.kt | 2 +- .../descriptors/OffsetChildDescriptor.kt | 4 +-- .../descriptors/TextViewDescriptor.kt | 2 +- .../uidebugger/descriptors/ViewDescriptor.kt | 15 +++++++++- .../descriptors/ViewGroupDescriptor.kt | 2 +- .../descriptors/ViewPagerDescriptor.kt | 2 +- .../descriptors/WindowDescriptor.kt | 2 +- .../flipper/plugins/uidebugger/model/Node.kt | 1 + .../traversal/PartialLayoutTraversal.kt | 4 ++- .../public/ui-debugger/components/Tree.tsx | 1 + .../public/ui-debugger/components/Tree2.tsx | 29 +++++++++++++++++-- desktop/plugins/public/ui-debugger/types.tsx | 1 + 22 files changed, 107 insertions(+), 26 deletions(-) diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt index 34dfeb592..ffaeedd65 100644 --- a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/DebugComponentDescriptor.kt @@ -72,7 +72,9 @@ class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescripto private val StateId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "Litho State") - override fun getData(node: DebugComponent): MaybeDeferred> { + override fun getAttributes( + node: DebugComponent + ): MaybeDeferred> { return Deferred { val attributeSections = mutableMapOf() @@ -101,4 +103,17 @@ class DebugComponentDescriptor(val register: DescriptorRegister) : NodeDescripto override fun getTags(node: DebugComponent): Set = setOf(BaseTags.Declarative, LithoTag) override fun getSnapshot(node: DebugComponent, bitmap: Bitmap?): Bitmap? = null + + override fun getInlineAttributes(node: DebugComponent): Map { + val attributes = mutableMapOf() + val key = node.key + val testKey = node.testKey + if (key != null && key.trim { it <= ' ' }.length > 0) { + attributes["key"] = key + } + if (testKey != null && testKey.trim { it <= ' ' }.length > 0) { + attributes["testKey"] = testKey + } + return attributes + } } diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt index 264e62c6f..48ae601e0 100644 --- a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/LithoViewDescriptor.kt @@ -36,7 +36,7 @@ object LithoViewDescriptor : ChainedDescriptor() { MetadataRegister.register( MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isIncrementalMountEnabled") - override fun onGetData( + override fun onGetAttributes( node: LithoView, attributeSections: MutableMap ) { diff --git a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt index 91262cc50..842603704 100644 --- a/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt +++ b/android/plugins/litho/src/main/java/com/facebook/flipper/plugins/uidebugger/litho/descriptors/TextDrawableDescriptor.kt @@ -25,7 +25,7 @@ object TextDrawableDescriptor : ChainedDescriptor() { private val TextAttributeId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "text") - override fun onGetData( + override fun onGetAttributes( node: TextDrawable, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ChainedDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ChainedDescriptor.kt index 42667e259..194778089 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ChainedDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ChainedDescriptor.kt @@ -83,14 +83,14 @@ abstract class ChainedDescriptor : NodeDescriptor { open fun onGetChildren(node: T): List? = null - final override fun getData(node: T): MaybeDeferred> { + final override fun getAttributes(node: T): MaybeDeferred> { val builder = mutableMapOf() - onGetData(node, builder) + onGetAttributes(node, builder) var curDescriptor: ChainedDescriptor? = mSuper while (curDescriptor != null) { - curDescriptor.onGetData(node, builder) + curDescriptor.onGetAttributes(node, builder) curDescriptor = curDescriptor.mSuper } @@ -101,7 +101,7 @@ abstract class ChainedDescriptor : NodeDescriptor { * Get the data to show for this node in the sidebar of the inspector. Each key will be a have its * own section */ - open fun onGetData(node: T, attributeSections: MutableMap) {} + open fun onGetAttributes(node: T, attributeSections: MutableMap) {} /** Get a snapshot of the node. */ final override fun getSnapshot(node: T, bitmap: Bitmap?): Bitmap? { @@ -111,4 +111,21 @@ abstract class ChainedDescriptor : NodeDescriptor { open fun onGetSnapshot(node: T, bitmap: Bitmap?): Bitmap? { return null } + + final override fun getInlineAttributes(node: T): Map { + + val builder = mutableMapOf() + onGetInlineAttributes(node, builder) + + var curDescriptor: ChainedDescriptor? = mSuper + + while (curDescriptor != null) { + curDescriptor.onGetInlineAttributes(node, builder) + curDescriptor = curDescriptor.mSuper + } + + return builder + } + + open fun onGetInlineAttributes(node: T, attributes: MutableMap) {} } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ColorDrawableDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ColorDrawableDescriptor.kt index a485e1ff3..0297ab923 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ColorDrawableDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ColorDrawableDescriptor.kt @@ -20,7 +20,7 @@ object ColorDrawableDescriptor : ChainedDescriptor() { override fun onGetName(node: ColorDrawable): String = node.javaClass.simpleName - override fun onGetData( + override fun onGetAttributes( node: ColorDrawable, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/DrawableDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/DrawableDescriptor.kt index a8f21fd9c..520c82a7a 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/DrawableDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/DrawableDescriptor.kt @@ -26,7 +26,7 @@ object DrawableDescriptor : ChainedDescriptor() { override fun onGetBounds(node: Drawable): Bounds = Bounds(node.bounds.left, node.bounds.top, node.bounds.width(), node.bounds.height()) - override fun onGetData( + override fun onGetAttributes( node: Drawable, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentFrameworkDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentFrameworkDescriptor.kt index 136317ef7..5b30654a8 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentFrameworkDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentFrameworkDescriptor.kt @@ -32,7 +32,7 @@ class FragmentFrameworkDescriptor(val register: DescriptorRegister) : override fun onGetChildren(node: android.app.Fragment): List = node.view?.let { view -> listOf(view) } ?: listOf() - override fun onGetData( + override fun onGetAttributes( node: android.app.Fragment, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentSupportDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentSupportDescriptor.kt index 73b50b69a..e9116b4aa 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentSupportDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/FragmentSupportDescriptor.kt @@ -58,7 +58,7 @@ class FragmentSupportDescriptor(val register: DescriptorRegister) : } } - override fun onGetData( + override fun onGetAttributes( node: androidx.fragment.app.Fragment, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ImageViewDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ImageViewDescriptor.kt index 475041910..a6b37146e 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ImageViewDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ImageViewDescriptor.kt @@ -32,7 +32,7 @@ object ImageViewDescriptor : ChainedDescriptor() { override fun onGetName(node: ImageView): String = node.javaClass.simpleName - override fun onGetData( + override fun onGetAttributes( node: ImageView, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/NodeDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/NodeDescriptor.kt index af52ebf63..1f1f5dd43 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/NodeDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/NodeDescriptor.kt @@ -71,14 +71,20 @@ interface NodeDescriptor { fun getActiveChild(node: T): Any? /** - * Get the data to show for this node in the sidebar of the inspector. The object will be shown in - * order and with a header matching the given name. + * Get the attribute to show for this node in the sidebar of the inspector. The object first level + * is a section and subsequent objects within are the first level of that section. Nested objects + * will nest in the sidebar */ - fun getData(node: T): MaybeDeferred> + fun getAttributes(node: T): MaybeDeferred> /** * Set of tags to describe this node in an abstract way for the UI Unfortunately this can't be an * enum as we have to plugin 3rd party frameworks dynamically */ fun getTags(node: T): Set + + /** + * These are shown inline in the tree view on the desktop, will likely be removed in the future + */ + fun getInlineAttributes(node: T): Map = mutableMapOf() } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ObjectDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ObjectDescriptor.kt index 910a2b2e7..78d4ea80f 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ObjectDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ObjectDescriptor.kt @@ -27,7 +27,7 @@ object ObjectDescriptor : NodeDescriptor { override fun getChildren(node: Any) = listOf() - override fun getData(node: Any) = Immediate(mapOf()) + override fun getAttributes(node: Any) = Immediate(mapOf()) override fun getBounds(node: Any): Bounds = Bounds(0, 0, 0, 0) diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/OffsetChildDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/OffsetChildDescriptor.kt index 4278ca8e8..59c1a4511 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/OffsetChildDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/OffsetChildDescriptor.kt @@ -38,8 +38,8 @@ object OffsetChildDescriptor : NodeDescriptor { override fun getActiveChild(node: OffsetChild): Any? = node.descriptor.getActiveChild(node.child) - override fun getData(node: OffsetChild): MaybeDeferred> = - node.descriptor.getData(node.child) + override fun getAttributes(node: OffsetChild): MaybeDeferred> = + node.descriptor.getAttributes(node.child) override fun getTags(node: OffsetChild): Set = node.descriptor.getTags(node.child) override fun getSnapshot(node: OffsetChild, bitmap: Bitmap?): Bitmap? = diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/TextViewDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/TextViewDescriptor.kt index c0d2ff273..5e8bca7c1 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/TextViewDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/TextViewDescriptor.kt @@ -46,7 +46,7 @@ object TextViewDescriptor : ChainedDescriptor() { override fun onGetName(node: TextView): String = node.javaClass.simpleName - override fun onGetData( + override fun onGetAttributes( node: TextView, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewDescriptor.kt index 50a2823bc..d8573087f 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewDescriptor.kt @@ -273,7 +273,10 @@ object ViewDescriptor : ChainedDescriptor() { override fun onGetTags(node: View): Set = BaseTags.NativeAndroid - override fun onGetData(node: View, attributeSections: MutableMap) { + override fun onGetAttributes( + node: View, + attributeSections: MutableMap + ) { val props = mutableMapOf() @@ -365,6 +368,16 @@ object ViewDescriptor : ChainedDescriptor() { attributeSections[SectionId] = InspectableObject(props.toMap()) } + override fun onGetInlineAttributes(node: View, attributes: MutableMap) { + val id = node.id + if (id == View.NO_ID) { + return + } + + val value = ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id) + attributes["id"] = value + } + override fun onGetSnapshot(node: View, bitmap: Bitmap?): Bitmap? { if (node.width <= 0 || node.height <= 0) { return null diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewGroupDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewGroupDescriptor.kt index 7e6bee089..05a19ce49 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewGroupDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewGroupDescriptor.kt @@ -60,7 +60,7 @@ object ViewGroupDescriptor : ChainedDescriptor() { private val ClipToPaddingAttributeId = MetadataRegister.register(MetadataRegister.TYPE_LAYOUT, NAMESPACE, "clipToPadding") - override fun onGetData( + override fun onGetAttributes( node: ViewGroup, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewPagerDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewPagerDescriptor.kt index 2b6a8b618..cdd10c885 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewPagerDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/ViewPagerDescriptor.kt @@ -31,7 +31,7 @@ object ViewPagerDescriptor : ChainedDescriptor() { private val CurrentItemIndexAttributeId = MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "currentItemIndex") - override fun onGetData( + override fun onGetAttributes( node: ViewPager, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/WindowDescriptor.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/WindowDescriptor.kt index 91a942274..de8e62440 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/WindowDescriptor.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/descriptors/WindowDescriptor.kt @@ -38,7 +38,7 @@ object WindowDescriptor : ChainedDescriptor() { override fun onGetChildren(node: Window): List = listOf(node.decorView) @SuppressLint("PrivateApi") - override fun onGetData( + override fun onGetAttributes( node: Window, attributeSections: MutableMap ) { diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Node.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Node.kt index ed44d3936..b6083ea15 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Node.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Node.kt @@ -15,6 +15,7 @@ data class Node( val qualifiedName: String, val name: String, val attributes: Map, + val inlineAttributes: Map, val bounds: Bounds, val tags: Set, val children: List, diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/traversal/PartialLayoutTraversal.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/traversal/PartialLayoutTraversal.kt index 1dde63542..b86e21cab 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/traversal/PartialLayoutTraversal.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/traversal/PartialLayoutTraversal.kt @@ -61,6 +61,7 @@ class PartialLayoutTraversal( descriptor.getQualifiedName(node), descriptor.getName(node), emptyMap(), + emptyMap(), descriptor.getBounds(node), emptySet(), emptyList(), @@ -92,7 +93,7 @@ class PartialLayoutTraversal( } } - val attributes = descriptor.getData(node) + val attributes = descriptor.getAttributes(node) val bounds = descriptor.getBounds(node) val tags = descriptor.getTags(node) visited.add( @@ -102,6 +103,7 @@ class PartialLayoutTraversal( descriptor.getQualifiedName(node), descriptor.getName(node), attrs, + descriptor.getInlineAttributes(node), bounds, tags, childrenIds, diff --git a/desktop/plugins/public/ui-debugger/components/Tree.tsx b/desktop/plugins/public/ui-debugger/components/Tree.tsx index 009ff10dc..c02292cb6 100644 --- a/desktop/plugins/public/ui-debugger/components/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/Tree.tsx @@ -273,6 +273,7 @@ const FakeNode: UINode = { id: 'Fakeroot', qualifiedName: 'Fakeroot', name: 'Fakeroot', + inlineAttributes: {}, children: [], attributes: {}, bounds: {x: 0, y: 0, height: 0, width: 0}, diff --git a/desktop/plugins/public/ui-debugger/components/Tree2.tsx b/desktop/plugins/public/ui-debugger/components/Tree2.tsx index 50ca804b3..afec50794 100644 --- a/desktop/plugins/public/ui-debugger/components/Tree2.tsx +++ b/desktop/plugins/public/ui-debugger/components/Tree2.tsx @@ -30,9 +30,11 @@ import {plugin} from '../index'; import {Glyph} from 'flipper'; import {head} from 'lodash'; import {reverse} from 'lodash/fp'; -import {Dropdown, Menu} from 'antd'; +import {Dropdown, Menu, Typography} from 'antd'; import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem'; +const {Text} = Typography; + export function Tree2({ nodes, rootId, @@ -168,11 +170,34 @@ function TreeItemContainer({ /> {nodeIcon(treeNode)} + ); } +const TreeAttributeContainer = styled(Text)({ + color: theme.textColorSecondary, + fontWeight: 300, + marginLeft: 5, + fontSize: 12, +}); + +function InlineAttributes({attributes}: {attributes: Record}) { + return ( + <> + {Object.entries(attributes ?? {}).map(([key, value]) => ( + <> + + {key} + ={value} + + + ))} + + ); +} + function useIsHovered(nodeId: Id) { const instance = usePlugin(plugin); const [isHovered, setIsHovered] = useState(false); @@ -200,7 +225,7 @@ const TreeItem = styled.li<{ isSelected: boolean; }>(({item, isHovered, isSelected}) => ({ display: 'flex', - alignItems: 'center', + alignItems: 'baseline', height: '26px', paddingLeft: `${(item.depth + 1) * renderDepthOffset}px`, borderWidth: '1px', diff --git a/desktop/plugins/public/ui-debugger/types.tsx b/desktop/plugins/public/ui-debugger/types.tsx index cc8371803..37f916fbf 100644 --- a/desktop/plugins/public/ui-debugger/types.tsx +++ b/desktop/plugins/public/ui-debugger/types.tsx @@ -67,6 +67,7 @@ export type UINode = { qualifiedName: string; name: string; attributes: Record; + inlineAttributes: Record; children: Id[]; bounds: Bounds; tags: Tag[];