Skip to content

Commit

Permalink
Merge pull request #293 from streamsync-cloud/feat-tags-component
Browse files Browse the repository at this point in the history
feat: Tags component
  • Loading branch information
ramedina86 authored Mar 11, 2024
2 parents b53161f + 3f5207b commit 08d48ef
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 1 deletion.
Binary file added docs/docs/public/components/tags.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/streamsync/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,17 @@ def transform(self, ev: StreamsyncEvent) -> None:
else:
ev.payload = tf_payload

def _transform_tag_click(self, ev: StreamsyncEvent) -> Optional[str]:
payload = ev.payload
instance_path = ev.instancePath
options = self.evaluator.evaluate_field(
instance_path, "tags", True, "{ }")
if not isinstance(options, dict):
raise ValueError("Invalid value for tags")
if payload not in options.keys():
raise ValueError("Unauthorised option")
return payload

def _transform_option_change(self, ev: StreamsyncEvent) -> Optional[str]:
payload = ev.payload
instance_path = ev.instancePath
Expand Down
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@monaco-editor/loader": "^1.3.3",
"@tato30/vue-pdf": "1.9.4",
"arquero": "^5.2.0",
"chroma-js": "^2.4.2",
"mapbox-gl": "3.1.2",
"marked": "^7.0.2",
"monaco-editor": "^0.41.0",
Expand Down
2 changes: 2 additions & 0 deletions ui/src/core/templateMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CoreVegaLiteChart from "../core_components/content/CoreVegaLiteChart.vue"
import CoreVideoPlayer from "../core_components/content/CoreVideoPlayer.vue";
import CoreLink from "../core_components/content/CoreLink.vue";
import CoreChat from "../core_components/content/CoreChat.vue";
import CoreTags from "../core_components/content/CoreTags.vue";
// input
import CoreCheckboxInput from "../core_components/input/CoreCheckboxInput.vue";
import CoreDateInput from "../core_components/input/CoreDateInput.vue";
Expand Down Expand Up @@ -106,6 +107,7 @@ const templateMap = {
step: CoreStep,
steps: CoreSteps,
ratinginput: CoreRating,
tags: CoreTags,
switchinput: CoreSwitchInput,
};

Expand Down
3 changes: 2 additions & 1 deletion ui/src/core_components/content/CoreImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ const componentId = inject(injectionKeys.componentId);
const rootStyle = computed(() => {
const component = ss.getComponentById(componentId);
const isClickHandled = typeof component.handlers?.["click"] !== "undefined";
const isClickHandled =
typeof component.handlers?.["ss-click"] !== "undefined";
return {
cursor: isClickHandled ? "pointer" : "unset",
Expand Down
143 changes: 143 additions & 0 deletions ui/src/core_components/content/CoreTags.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template>
<div ref="rootEl" class="CoreTags">
<div
v-for="(tagDesc, tagId) in fields.tags.value"
:key="tagId"
class="tag"
:style="{ background: generateColor(tagId) }"
@click="() => handleTagClick(tagId)"
>
{{ tagDesc }}
</div>
</div>
</template>

<script lang="ts">
import { FieldCategory, FieldType } from "../../streamsyncTypes";
import { cssClasses, primaryTextColor } from "../../renderer/sharedStyleFields";
const clickHandlerStub = `
def handle_tag_click(state, payload):
state["selected_tag_id"] = payload`;
const description = "A component to display coloured tag pills.";
export default {
streamsync: {
name: "Tags",
description,
category: "Content",
fields: {
tags: {
name: "Tags",
desc: "Key-value object with tags. Must be a JSON string or a state reference to a dictionary.",
type: FieldType.KeyValue,
default: "{}",
},
referenceColor: {
name: "Reference",
desc: "The colour to be used as reference for chroma and luminance, and as the starting point for hue rotation.",
type: FieldType.Color,
default: "#29cf00",
category: FieldCategory.Style,
},
seed: {
name: "Seed value",
desc: "Choose a different value to reshuffle colours.",
type: FieldType.Number,
default: "1",
category: FieldCategory.Style,
},
rotateHue: {
name: "Rotate hue",
desc: "If active, rotates the hue depending on the content of the string. If turned off, the reference colour is always used.",
type: FieldType.Text,
options: {
yes: "yes",
no: "no",
},
default: "yes",
},
primaryTextColor: {
...primaryTextColor,
default: "#ffffff",
},
cssClasses,
},
events: {
"ss-tag-click": {
desc: "Triggered when a tag is clicked.",
stub: clickHandlerStub.trim(),
},
},
previewField: "text",
},
};
</script>
<script setup lang="ts">
import { Ref, computed, inject, ref } from "vue";
import injectionKeys from "../../injectionKeys";
import chroma from "chroma-js";
const COLOR_STEPS = 18;
const rootEl: Ref<HTMLElement> = ref(null);
const fields = inject(injectionKeys.evaluatedFields);
const componentId = inject(injectionKeys.componentId);
const ss = inject(injectionKeys.core);
const isClickable = computed(() => {
const component = ss.getComponentById(componentId);
return typeof component.handlers?.["ss-tag-click"] !== "undefined";
});
function generateColor(s: string) {
if (fields.rotateHue.value == "no") {
return fields.referenceColor.value;
}
const baseColor = chroma(fields.referenceColor.value);
let genColor = baseColor.set(
"hcl.h",
`+${calculateColorStep(s) * (360 / COLOR_STEPS)}`,
);
return genColor.css();
}
function calculateColorStep(s: string) {
let hash = (fields.seed.value * 52673) & 0xffffffff;
for (let i = 0; i < s.length; i++) {
hash = ((i + 1) * s.charCodeAt(i)) ^ (hash & 0xffffffff);
}
const step = Math.abs(hash) % COLOR_STEPS;
return step;
}
function handleTagClick(tagId: string) {
const event = new CustomEvent("ss-tag-click", {
detail: {
payload: tagId,
},
});
rootEl.value.dispatchEvent(event);
}
</script>

<style scoped>
@import "../../renderer/sharedStyles.css";
.CoreTags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tag {
padding: 4px 8px 4px 8px;
border-radius: 16px;
font-size: 0.65rem;
color: var(--primaryTextColor);
display: flex;
align-items: center;
gap: 4px;
cursor: v-bind("isClickable ? 'pointer' : 'auto'");
}
</style>

0 comments on commit 08d48ef

Please sign in to comment.