Skip to content

Commit c4a6ea6

Browse files
refactor(ui): migrate Toc.vue to Vue 3 Composition API with full Type… (#13113)
1 parent a4b0bea commit c4a6ea6

File tree

1 file changed

+128
-133
lines changed

1 file changed

+128
-133
lines changed

ui/src/components/plugins/Toc.vue

Lines changed: 128 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
:name="plugin.group"
1414
:title="plugin.title?.capitalize()"
1515
:key="plugin.group"
16-
:ref="`plugin-${plugin.group}`"
16+
:ref="(el) => pluginRefs[plugin.group] = el"
1717
>
1818
<ul class="toc-h3">
1919
<li v-for="(types, namespace) in group(plugin)" :key="namespace">
2020
<h6>{{ namespace }}</h6>
2121
<ul class="toc-h4">
2222
<li v-for="(classes, type) in types" :key="type + '-' + namespace">
23-
<h6>{{ $filters.cap(type) }}</h6>
23+
<h6>{{ cap(type) }}</h6>
2424
<ul class="section-nav toc-h5">
2525
<li v-for="cls in classes" :key="cls">
2626
<router-link
@@ -35,7 +35,7 @@
3535
/>
3636
</div>
3737
<span
38-
:class="$route.params.cls === (namespace + '.' + cls) ? 'selected mx-2' : 'mx-2'"
38+
:class="route.params.cls === (namespace + '.' + cls) ? 'selected mx-2' : 'mx-2'"
3939
>{{
4040
cls
4141
}}</span>
@@ -52,142 +52,137 @@
5252
</div>
5353
</template>
5454

55-
<script>
56-
import {isEntryAPluginElementPredicate, TaskIcon} from "@kestra-io/ui-libs";
57-
import {mapStores} from "pinia";
55+
<script setup lang="ts">
56+
import {ref, computed, watch, nextTick, reactive} from "vue";
57+
import {useRoute} from "vue-router";
58+
import {isEntryAPluginElementPredicate, TaskIcon, type Plugin, type PluginElement} from "@kestra-io/ui-libs";
5859
import {usePluginsStore} from "../../stores/plugins";
60+
import {cap} from "../../utils/filters";
5961
60-
export default {
61-
emits: ["routerChange"],
62-
data() {
63-
return {
64-
offset: 0,
65-
activeNames: [],
66-
searchInput: ""
67-
}
68-
},
69-
watch: {
70-
$route: {
71-
handler() {
72-
this.plugins.forEach(plugin => {
73-
if (Object.entries(plugin).some(([key, value]) => isEntryAPluginElementPredicate(key, value) && value.map(({cls}) => cls).includes(this.$route.params.cls))) {
74-
this.activeNames = [plugin.group]
75-
localStorage.setItem("activePlugin", plugin.group);
76-
}
77-
})
78-
this.scrollToActivePlugin();
79-
},
80-
immediate: true
62+
const props = defineProps<{
63+
plugins: Plugin[];
64+
}>();
65+
66+
defineEmits<{
67+
routerChange: [];
68+
}>();
69+
70+
const route = useRoute();
71+
const pluginsStore = usePluginsStore();
72+
73+
const pluginRefs = reactive({});
74+
const activeNames = ref<string[]>([]);
75+
const searchInput = ref<string>("");
76+
77+
const countPlugin = computed(() => {
78+
return new Set(props.plugins.flatMap(plugin => pluginElements(plugin))).size;
79+
});
80+
81+
const pluginElements = (plugin: Plugin) => {
82+
return Object.entries(plugin)
83+
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
84+
.flatMap(([_, value]) => (value as PluginElement[])
85+
.filter(({deprecated}) => !deprecated)
86+
.map(({cls}) => cls)
87+
);
88+
};
89+
90+
const scrollToActivePlugin = () => {
91+
const activePlugin = localStorage.getItem("activePlugin");
92+
if (activePlugin) {
93+
const pluginElement = pluginRefs[activePlugin];
94+
if (pluginElement) {
95+
pluginElement.$el.scrollIntoView({behavior: "smooth", block: "start"});
8196
}
82-
},
83-
components: {
84-
TaskIcon
85-
},
86-
props: {
87-
plugins: {
88-
type: Array,
89-
required: true
97+
}
98+
};
99+
100+
const pluginsList = computed(() => {
101+
return props.plugins
102+
.filter((plugin, index, self) => {
103+
return index === self.findIndex((t) => (
104+
t.title === plugin.title && t.group === plugin.group
105+
));
106+
})
107+
.filter(plugin => {
108+
return plugin.title?.toLowerCase().includes(searchInput.value.toLowerCase()) ||
109+
pluginElements(plugin).some(element => element.toLowerCase().includes(searchInput.value.toLowerCase()));
110+
})
111+
.map(plugin => {
112+
return {
113+
...plugin,
114+
...Object.fromEntries(
115+
Object.entries(plugin)
116+
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
117+
.map(([elementType, elements]) => [
118+
elementType,
119+
(elements as PluginElement[]).filter(({deprecated}) => !deprecated)
120+
.filter(({cls}) => cls.toLowerCase().includes(searchInput.value.toLowerCase()))
121+
])
122+
)
123+
};
124+
});
125+
});
126+
127+
watch(route, () => {
128+
props.plugins.forEach(plugin => {
129+
if (Object.entries(plugin).some(([key, value]) => {
130+
if (isEntryAPluginElementPredicate(key, value)) {
131+
return (value as PluginElement[]).some(({cls}) => cls === route.params.cls);
132+
}
133+
return false;
134+
})) {
135+
activeNames.value = [plugin.group];
136+
localStorage.setItem("activePlugin", plugin.group);
90137
}
91-
},
92-
computed: {
93-
...mapStores(usePluginsStore),
94-
countPlugin() {
95-
return new Set(this.plugins.flatMap(plugin => this.pluginElements(plugin))).size
96-
},
97-
pluginsList() {
98-
return this.plugins
99-
// remove duplicate
100-
.filter((plugin, index, self) => {
101-
return index === self.findIndex((t) => (
102-
t.title === plugin.title && t.group === plugin.group
103-
));
104-
})
105-
// find plugin that match search input
106-
.filter(plugin => {
107-
return plugin.title.toLowerCase().includes(this.searchInput.toLowerCase()) ||
108-
this.pluginElements(plugin).some(element => element.toLowerCase().includes(this.searchInput.toLowerCase()))
109-
})
110-
// keep only task that match search input
111-
.map(plugin => {
138+
});
139+
nextTick(() => {
140+
scrollToActivePlugin();
141+
});
142+
}, {immediate: true});
143+
144+
const handlePluginChange = (pluginGroup: string) => {
145+
activeNames.value = [pluginGroup];
146+
localStorage.setItem("activePlugin", pluginGroup);
147+
};
148+
149+
const sortedPlugins = (plugins: Plugin[]) => {
150+
return plugins
151+
.sort((a, b) => {
152+
const nameA = (a.title ? a.title.toLowerCase() : ""),
153+
nameB = (b.title ? b.title.toLowerCase() : "");
154+
155+
return (nameA < nameB ? -1 : (nameA > nameB ? 1 : 0));
156+
});
157+
};
158+
159+
const group = (plugin: Plugin) => {
160+
return Object.entries(plugin)
161+
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
162+
.flatMap(([type, value]) => {
163+
return (value as PluginElement[]).filter(({deprecated}) => !deprecated)
164+
.map(({cls}) => {
165+
const namespace = cls.substring(0, cls.lastIndexOf("."));
166+
112167
return {
113-
...plugin,
114-
...Object.fromEntries(
115-
Object.entries(plugin)
116-
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
117-
.map(([elementType, elements]) => [
118-
elementType,
119-
elements.filter(({deprecated}) => !deprecated)
120-
.filter(({cls}) => cls.toLowerCase().includes(this.searchInput.toLowerCase()))
121-
])
122-
)
123-
}
124-
})
125-
}
126-
},
127-
methods: {
128-
pluginElements(plugin) {
129-
return Object.entries(plugin)
130-
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
131-
.flatMap(([_, value]) => value
132-
.filter(({deprecated}) => !deprecated)
133-
.map(({cls}) => cls)
134-
)
135-
},
136-
scrollToActivePlugin() {
137-
const activePlugin = localStorage.getItem("activePlugin");
138-
if (activePlugin) {
139-
// Use Vue's $refs to scroll to the specific plugin group
140-
this.$nextTick(() => {
141-
const pluginElement = this.$refs[`plugin-${activePlugin}`];
142-
if (pluginElement && pluginElement[0]) {
143-
pluginElement[0].$el.scrollIntoView({behavior: "smooth", block: "start"});
144-
}
168+
type,
169+
namespace: namespace,
170+
cls: cls.substring(cls.lastIndexOf(".") + 1)
171+
};
145172
});
146-
}
147-
},
148-
// When user navigates to a different plugin, save the new plugin group to localStorage
149-
handlePluginChange(pluginGroup) {
150-
this.activeNames = [pluginGroup];
151-
localStorage.setItem("activePlugin", pluginGroup); // Save to localStorage
152-
},
153-
sortedPlugins(plugins) {
154-
return plugins
155-
.sort((a, b) => {
156-
const nameA = (a.title ? a.title.toLowerCase() : ""),
157-
nameB = (b.title ? b.title.toLowerCase() : "");
158-
159-
return (nameA < nameB ? -1 : (nameA > nameB ? 1 : 0));
160-
})
161-
},
162-
group(plugin) {
163-
return Object.entries(plugin)
164-
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
165-
.flatMap(([type, value]) => {
166-
return value.filter(({deprecated}) => !deprecated)
167-
.map(({cls}) => {
168-
const namespace = cls.substring(0, cls.lastIndexOf("."));
169-
170-
return {
171-
type,
172-
namespace: namespace,
173-
cls: cls.substring(cls.lastIndexOf(".") + 1)
174-
};
175-
});
176-
})
177-
.reduce((accumulator, value) => {
178-
accumulator[value.namespace] = accumulator[value.namespace] || {};
179-
accumulator[value.namespace][value.type] = accumulator[value.namespace][value.type] || [];
180-
accumulator[value.namespace][value.type].push(value.cls);
181-
182-
return accumulator;
183-
}, Object.create(null))
184-
185-
},
186-
isVisible(plugin) {
187-
return this.pluginElements(plugin).length > 0
188-
},
189-
}
190-
}
173+
})
174+
.reduce((accumulator, value) => {
175+
accumulator[value.namespace] = accumulator[value.namespace] || {};
176+
accumulator[value.namespace][value.type] = accumulator[value.namespace][value.type] || [];
177+
accumulator[value.namespace][value.type].push(value.cls);
178+
179+
return accumulator;
180+
}, {} as Record<string, Record<string, string[]>>);
181+
};
182+
183+
const isVisible = (plugin: Plugin) => {
184+
return pluginElements(plugin).length > 0;
185+
};
191186
</script>
192187

193188
<style lang="scss" scoped>

0 commit comments

Comments
 (0)