mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
refactor(ui): migrate Toc.vue to Vue 3 Composition API with full TypeScript support (#12388)
This commit is contained in:
committed by
Piyush Bhaskar
parent
a4b0beaf63
commit
27975114c4
@@ -20,7 +20,7 @@
|
||||
<h6>{{ namespace }}</h6>
|
||||
<ul class="toc-h4">
|
||||
<li v-for="(classes, type) in types" :key="type + '-' + namespace">
|
||||
<h6>{{ $filters.cap(type) }}</h6>
|
||||
<h6>{{ capitalize(type) }}</h6>
|
||||
<ul class="section-nav toc-h5">
|
||||
<li v-for="cls in classes" :key="cls">
|
||||
<router-link
|
||||
@@ -52,142 +52,163 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {isEntryAPluginElementPredicate, TaskIcon} from "@kestra-io/ui-libs";
|
||||
import {mapStores} from "pinia";
|
||||
import {usePluginsStore} from "../../stores/plugins";
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, getCurrentInstance } from 'vue';
|
||||
import { capitalize } from 'lodash';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { isEntryAPluginElementPredicate, TaskIcon } from "@kestra-io/ui-libs";
|
||||
import { usePluginsStore } from "../../stores/plugins";
|
||||
|
||||
export default {
|
||||
emits: ["routerChange"],
|
||||
data() {
|
||||
return {
|
||||
offset: 0,
|
||||
activeNames: [],
|
||||
searchInput: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler() {
|
||||
this.plugins.forEach(plugin => {
|
||||
if (Object.entries(plugin).some(([key, value]) => isEntryAPluginElementPredicate(key, value) && value.map(({cls}) => cls).includes(this.$route.params.cls))) {
|
||||
this.activeNames = [plugin.group]
|
||||
localStorage.setItem("activePlugin", plugin.group);
|
||||
}
|
||||
})
|
||||
this.scrollToActivePlugin();
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TaskIcon
|
||||
},
|
||||
props: {
|
||||
plugins: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapStores(usePluginsStore),
|
||||
countPlugin() {
|
||||
return new Set(this.plugins.flatMap(plugin => this.pluginElements(plugin))).size
|
||||
},
|
||||
pluginsList() {
|
||||
return this.plugins
|
||||
// remove duplicate
|
||||
.filter((plugin, index, self) => {
|
||||
return index === self.findIndex((t) => (
|
||||
t.title === plugin.title && t.group === plugin.group
|
||||
));
|
||||
})
|
||||
// find plugin that match search input
|
||||
.filter(plugin => {
|
||||
return plugin.title.toLowerCase().includes(this.searchInput.toLowerCase()) ||
|
||||
this.pluginElements(plugin).some(element => element.toLowerCase().includes(this.searchInput.toLowerCase()))
|
||||
})
|
||||
// keep only task that match search input
|
||||
.map(plugin => {
|
||||
return {
|
||||
...plugin,
|
||||
...Object.fromEntries(
|
||||
Object.entries(plugin)
|
||||
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
|
||||
.map(([elementType, elements]) => [
|
||||
elementType,
|
||||
elements.filter(({deprecated}) => !deprecated)
|
||||
.filter(({cls}) => cls.toLowerCase().includes(this.searchInput.toLowerCase()))
|
||||
])
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pluginElements(plugin) {
|
||||
return Object.entries(plugin)
|
||||
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
|
||||
.flatMap(([_, value]) => value
|
||||
.filter(({deprecated}) => !deprecated)
|
||||
.map(({cls}) => cls)
|
||||
)
|
||||
},
|
||||
scrollToActivePlugin() {
|
||||
const activePlugin = localStorage.getItem("activePlugin");
|
||||
if (activePlugin) {
|
||||
// Use Vue's $refs to scroll to the specific plugin group
|
||||
this.$nextTick(() => {
|
||||
const pluginElement = this.$refs[`plugin-${activePlugin}`];
|
||||
if (pluginElement && pluginElement[0]) {
|
||||
pluginElement[0].$el.scrollIntoView({behavior: "smooth", block: "start"});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
// When user navigates to a different plugin, save the new plugin group to localStorage
|
||||
handlePluginChange(pluginGroup) {
|
||||
this.activeNames = [pluginGroup];
|
||||
localStorage.setItem("activePlugin", pluginGroup); // Save to localStorage
|
||||
},
|
||||
sortedPlugins(plugins) {
|
||||
return plugins
|
||||
.sort((a, b) => {
|
||||
const nameA = (a.title ? a.title.toLowerCase() : ""),
|
||||
nameB = (b.title ? b.title.toLowerCase() : "");
|
||||
|
||||
return (nameA < nameB ? -1 : (nameA > nameB ? 1 : 0));
|
||||
})
|
||||
},
|
||||
group(plugin) {
|
||||
return Object.entries(plugin)
|
||||
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
|
||||
.flatMap(([type, value]) => {
|
||||
return value.filter(({deprecated}) => !deprecated)
|
||||
.map(({cls}) => {
|
||||
const namespace = cls.substring(0, cls.lastIndexOf("."));
|
||||
|
||||
return {
|
||||
type,
|
||||
namespace: namespace,
|
||||
cls: cls.substring(cls.lastIndexOf(".") + 1)
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce((accumulator, value) => {
|
||||
accumulator[value.namespace] = accumulator[value.namespace] || {};
|
||||
accumulator[value.namespace][value.type] = accumulator[value.namespace][value.type] || [];
|
||||
accumulator[value.namespace][value.type].push(value.cls);
|
||||
|
||||
return accumulator;
|
||||
}, Object.create(null))
|
||||
|
||||
},
|
||||
isVisible(plugin) {
|
||||
return this.pluginElements(plugin).length > 0
|
||||
},
|
||||
}
|
||||
// Define interfaces
|
||||
interface PluginElement {
|
||||
cls: string;
|
||||
deprecated?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Plugin {
|
||||
title: string;
|
||||
group: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
plugins: Plugin[];
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'routerChange'): void;
|
||||
}>();
|
||||
|
||||
// Store and Route
|
||||
const pluginsStore = usePluginsStore();
|
||||
const route = useRoute();
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
// State
|
||||
const activeNames = ref<string[]>([]);
|
||||
const searchInput = ref("");
|
||||
|
||||
// Methods
|
||||
const pluginElements = (plugin: Plugin): string[] => {
|
||||
return Object.entries(plugin)
|
||||
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
|
||||
.flatMap(([_, value]) => (value as PluginElement[])
|
||||
.filter(({deprecated}) => !deprecated)
|
||||
.map(({cls}) => cls)
|
||||
);
|
||||
};
|
||||
|
||||
const scrollToActivePlugin = () => {
|
||||
const activePlugin = localStorage.getItem("activePlugin");
|
||||
if (activePlugin) {
|
||||
nextTick(() => {
|
||||
const refs = instance?.proxy?.$refs as Record<string, any> | undefined;
|
||||
const pluginElement = refs?.[`plugin-${activePlugin}`];
|
||||
if (pluginElement && pluginElement[0]) {
|
||||
pluginElement[0].$el.scrollIntoView({behavior: "smooth", block: "start"});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePluginChange = (pluginGroup: string) => {
|
||||
activeNames.value = [pluginGroup];
|
||||
localStorage.setItem("activePlugin", pluginGroup);
|
||||
};
|
||||
|
||||
const sortedPlugins = (plugins: Plugin[]) => {
|
||||
return [...plugins].sort((a, b) => {
|
||||
const nameA = (a.title ? a.title.toLowerCase() : "");
|
||||
const nameB = (b.title ? b.title.toLowerCase() : "");
|
||||
|
||||
return (nameA < nameB ? -1 : (nameA > nameB ? 1 : 0));
|
||||
});
|
||||
};
|
||||
|
||||
const group = (plugin: Plugin) => {
|
||||
return Object.entries(plugin)
|
||||
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
|
||||
.flatMap(([type, value]) => {
|
||||
return (value as PluginElement[])
|
||||
.filter(({deprecated}) => !deprecated)
|
||||
.map(({cls}) => {
|
||||
const namespace = cls.substring(0, cls.lastIndexOf("."));
|
||||
|
||||
return {
|
||||
type,
|
||||
namespace: namespace,
|
||||
cls: cls.substring(cls.lastIndexOf(".") + 1)
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce((accumulator, value) => {
|
||||
accumulator[value.namespace] = accumulator[value.namespace] || {};
|
||||
accumulator[value.namespace][value.type] = accumulator[value.namespace][value.type] || [];
|
||||
accumulator[value.namespace][value.type].push(value.cls);
|
||||
|
||||
return accumulator;
|
||||
}, Object.create(null) as Record<string, Record<string, string[]>>);
|
||||
};
|
||||
|
||||
const isVisible = (plugin: Plugin) => {
|
||||
return pluginElements(plugin).length > 0;
|
||||
};
|
||||
|
||||
// Computed
|
||||
const countPlugin = computed(() => {
|
||||
return new Set(props.plugins.flatMap(plugin => pluginElements(plugin))).size;
|
||||
});
|
||||
|
||||
const pluginsList = computed(() => {
|
||||
return props.plugins
|
||||
// remove duplicate
|
||||
.filter((plugin, index, self) => {
|
||||
return index === self.findIndex((t) => (
|
||||
t.title === plugin.title && t.group === plugin.group
|
||||
));
|
||||
})
|
||||
// find plugin that match search input
|
||||
.filter(plugin => {
|
||||
return plugin.title.toLowerCase().includes(searchInput.value.toLowerCase()) ||
|
||||
pluginElements(plugin).some(element => element.toLowerCase().includes(searchInput.value.toLowerCase()))
|
||||
})
|
||||
// keep only task that match search input
|
||||
.map(plugin => {
|
||||
return {
|
||||
...plugin,
|
||||
...Object.fromEntries(
|
||||
Object.entries(plugin)
|
||||
.filter(([key, value]) => isEntryAPluginElementPredicate(key, value))
|
||||
.map(([elementType, elements]) => [
|
||||
elementType,
|
||||
(elements as PluginElement[]).filter(({deprecated}) => !deprecated)
|
||||
.filter(({cls}) => cls.toLowerCase().includes(searchInput.value.toLowerCase()))
|
||||
])
|
||||
)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Watch
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
props.plugins.forEach(plugin => {
|
||||
if (Object.entries(plugin).some(([key, value]) =>
|
||||
isEntryAPluginElementPredicate(key, value) &&
|
||||
(value as PluginElement[]).map(({cls}) => cls).includes(route.params.cls as string)
|
||||
)) {
|
||||
activeNames.value = [plugin.group];
|
||||
localStorage.setItem("activePlugin", plugin.group);
|
||||
}
|
||||
})
|
||||
scrollToActivePlugin();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user