Skip to content

Commit

Permalink
feat(ui): implement CoreProgressBar - WF-60
Browse files Browse the repository at this point in the history
  • Loading branch information
madeindjs committed Dec 6, 2024
1 parent 99fcb0d commit 7fbb248
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 6 deletions.
Binary file added docs/framework/public/components/progressbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions src/ui/src/components/core/base/BaseInputWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
class="BaseInputWrapper"
:class="{ horizontal: isHorizontal }"
>
<label v-if="props.label">{{ props.label }}</label>
<label v-if="label">{{ label }}</label>
<slot></slot>
</div>
</template>

<script setup lang="ts">
const props = defineProps<{
label: string;
isHorizontal?: boolean;
}>();
defineProps({
label: { type: String, required: false, default: undefined },
isHorizontal: { type: Boolean, required: false },
});
</script>

<style scoped>
Expand Down
188 changes: 188 additions & 0 deletions src/ui/src/components/core/content/CoreProgressBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<template>
<div
ref="rootEl"
class="CoreProgressBar"
:class="{ 'CoreProgressBar--clickable': isClickable }"
role="progressbar"
:aria-valuetext="progressionText"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="value"
:tabindex="isClickable ? '0' : null"
@keydown.enter="handleClick"
@click="handleClick"
>
<div v-if="label || displayPercentage" class="CoreProgressBar__title">
<label v-if="label" class="CoreProgressBar__title__label">{{
label
}}</label>
<span
v-if="displayPercentage"
class="CoreProgressBar__title__progress"
>{{ progressionText }}</span
>
</div>
<div class="CoreProgressBar__bar">
<div
class="CoreProgressBar__bar__value"
:style="progressValueStyle"
></div>
</div>
</div>
</template>

<script lang="ts">
import {
FieldCategory,
FieldType,
WriterComponentDefinition,
} from "@/writerTypes";
import {
cssClasses,
accentColor,
primaryTextColor,
separatorColor,
} from "@/renderer/sharedStyleFields";
const clickHandlerStub = `
def handle_avatar_click():
print("The avatar was clicked")`;
const description = "A component to display a progression.";
const definition: WriterComponentDefinition = {
name: "ProgressBar",
description,
category: "Content",
allowedChildrenTypes: ["*"],
fields: {
label: {
name: "Label",
type: FieldType.Text,
default: "",
},
value: {
name: "Value",
type: FieldType.Number,
default: "0.25",
},
min: {
name: "Mininimum value",
type: FieldType.Number,
default: "0",
},
max: {
name: "Maximum value",
type: FieldType.Number,
default: "1",
},
displayPercentage: {
name: "Display percentage",
default: "no",
type: FieldType.Text,
category: FieldCategory.Style,
options: {
yes: "Yes",
no: "No",
},
},
accentColor,
primaryTextColor,
separatorColor,
cssClasses,
},
events: {
"wf-click": {
desc: "Triggered when the avatar is clicked.",
stub: clickHandlerStub.trim(),
},
},
};
export default { writer: definition };
</script>

<script setup lang="ts">
import { CSSProperties, computed, inject, ref } from "vue";
import injectionKeys from "@/injectionKeys";
import { useFieldValueAsYesNo } from "@/composables/useFieldValue";
import { getClick } from "@/renderer/syntheticEvents";
import { usePercentageFormatter } from "@/composables/useFormatter";
const rootEl = ref<HTMLElement>();
const fields = inject(injectionKeys.evaluatedFields);
const componentId = inject(injectionKeys.componentId);
const wf = inject(injectionKeys.core);
const label = computed(() => String(fields.label?.value));
const value = computed(() => Number(fields.value.value));
const min = computed(() => Number(fields.min.value));
const max = computed(() => Number(fields.max.value));
const displayPercentage = useFieldValueAsYesNo(fields, "displayPercentage");
const progression = computed(
() => (value.value - min.value) / (max.value - min.value),
);
const progressionText = usePercentageFormatter(progression);
const progressValueStyle = computed<CSSProperties>(() => ({
width: `${progression.value * 100}%`,
}));
const isClickable = computed(() => {
const component = wf.getComponentById(componentId);
return typeof component.handlers?.["wf-click"] !== "undefined";
});
function handleClick(ev: MouseEvent | KeyboardEvent) {
if (!isClickable.value) return;
const ssEv = getClick(ev);
rootEl.value.dispatchEvent(ssEv);
}
</script>

<style scoped>
@import "@/renderer/sharedStyles.css";
.CoreProgressBar--clickable {
cursor: pointer;
}
.CoreProgressBar__bar {
height: 16px;
width: 100%;
border-radius: 4px;
background-color: var(--separatorColor);
position: relative;
overflow-x: hidden;
}
.CoreProgressBar__bar__value {
background-color: var(--accentColor);
height: 100%;
border-radius: 4px;
transition: width 0.2s ease-in-out;
}
.CoreProgressBar__bar__text {
position: absolute;
left: 8px;
line-height: 24px;
}
.CoreProgressBar__title {
display: flex;
gap: 8px;
font-size: 0.875rem;
color: var(--primaryTextColor);
margin-bottom: 4px;
line-height: 150%;
}
.CoreProgressBar__title__label {
flex-grow: 1;
}
.CoreProgressBar__title__progress {
text-align: right;
}
</style>
10 changes: 10 additions & 0 deletions src/ui/src/composables/useFieldValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ComputedRef, computed } from "vue";

type Fields = Record<string, ComputedRef<unknown>>;

export function useFieldValueAsYesNo(
fields: Fields,
key: string,
): ComputedRef<boolean> {
return computed(() => fields[key].value === "yes");
}
18 changes: 18 additions & 0 deletions src/ui/src/composables/useFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { computed, ComputedRef } from "vue";

export function usePercentageFormatter(
number: ComputedRef<number>,
options: Pick<
Intl.NumberFormatOptions,
"minimumFractionDigits" | "maximumFractionDigits"
> = {
minimumFractionDigits: 0,
maximumFractionDigits: 1,
},
) {
const formatter = new Intl.NumberFormat(undefined, {
style: "percent",
...options,
});
return computed(() => formatter.format(number.value));
}
2 changes: 2 additions & 0 deletions src/ui/src/core/templateMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import CoreTags from "../components/core/content/CoreTags.vue";
import CoreAvatar from "../components/core/content/CoreAvatar.vue";
import CoreAnnotatedText from "../components/core/content/CoreAnnotatedText.vue";
import CoreJsonViewer from "../components/core/content/CoreJsonViewer.vue";
import CoreProgressBar from "../components/core/content/CoreProgressBar.vue";

// input
import CoreCheckboxInput from "../components/core/input/CoreCheckboxInput.vue";
Expand Down Expand Up @@ -136,6 +137,7 @@ const templateMap: TemplateMap = {
jsonviewer: CoreJsonViewer,
workflows_root: WorkflowsRoot,
workflows_workflow: WorkflowsWorkflow,
progressbar: CoreProgressBar,
};

const abstractTemplateMap: Record<string, AbstractTemplate> = {};
Expand Down
5 changes: 4 additions & 1 deletion src/ui/src/renderer/syntheticEvents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export function getClick(ev: MouseEvent): CustomEvent {
/**
* @param ev event from a mouse click, or a keyboard click (using tab navigation, then click with `Enter`)
*/
export function getClick(ev: MouseEvent | KeyboardEvent): CustomEvent {
const payload = {
ctrlKey: ev.ctrlKey,
shiftKey: ev.shiftKey,
Expand Down

0 comments on commit 7fbb248

Please sign in to comment.