From 7f613ead99f7ee196a206e88c2459cf2cbe7b8bd Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 2 May 2024 13:31:40 -0500 Subject: [PATCH] Change `InvocationsList` into a grid using `GridList` This removes the `InvocationsList` component and instead uses the `GridList`. To make this possible, a few changes were made to the Grid components (such as more field types, non-filterable grid, expandable rows etc.). The invocations list now appears uniform with the other grids due to this change. --- client/src/api/workflows.ts | 1 + .../Grid/GridElements/GridExpand.vue | 34 ++ client/src/components/Grid/GridInvocation.vue | 130 ++++++++ client/src/components/Grid/GridList.vue | 228 ++++++++++---- .../components/Grid/configs/invocations.ts | 163 ++++++++++ .../Grid/configs/invocationsHistory.ts | 151 +++++++++ .../Grid/configs/invocationsWorkflow.ts | 154 +++++++++ client/src/components/Grid/configs/types.ts | 29 +- client/src/components/HistoryImport.vue | 29 +- .../Workflow/HistoryInvocations.vue | 18 +- .../Workflow/InvocationsList.test.js | 252 --------------- .../components/Workflow/InvocationsList.vue | 291 ------------------ .../Workflow/StoredWorkflowInvocations.vue | 70 +++-- .../components/Workflow/UserInvocations.vue | 19 -- .../components/Workflow/WorkflowRunButton.vue | 2 +- .../WorkflowInvocationOverview.vue | 3 +- .../WorkflowInvocationState.vue | 12 +- .../components/admin/ActiveInvocations.vue | 17 - client/src/entry/analysis/router.js | 11 +- .../src/entry/analysis/routes/admin-routes.js | 15 +- lib/galaxy/webapps/galaxy/buildapp.py | 1 + 21 files changed, 921 insertions(+), 709 deletions(-) create mode 100644 client/src/components/Grid/GridElements/GridExpand.vue create mode 100644 client/src/components/Grid/GridInvocation.vue create mode 100644 client/src/components/Grid/configs/invocations.ts create mode 100644 client/src/components/Grid/configs/invocationsHistory.ts create mode 100644 client/src/components/Grid/configs/invocationsWorkflow.ts delete mode 100644 client/src/components/Workflow/InvocationsList.test.js delete mode 100644 client/src/components/Workflow/InvocationsList.vue delete mode 100644 client/src/components/Workflow/UserInvocations.vue delete mode 100644 client/src/components/admin/ActiveInvocations.vue diff --git a/client/src/api/workflows.ts b/client/src/api/workflows.ts index e532b4499c1b..d040f139cdfc 100644 --- a/client/src/api/workflows.ts +++ b/client/src/api/workflows.ts @@ -3,6 +3,7 @@ import { components, fetcher } from "@/api/schema"; export type StoredWorkflowDetailed = components["schemas"]["StoredWorkflowDetailed"]; export const workflowsFetcher = fetcher.path("/api/workflows").method("get").create(); +export const workflowFetcher = fetcher.path("/api/workflows/{workflow_id}").method("get").create(); export const invocationCountsFetcher = fetcher.path("/api/workflows/{workflow_id}/counts").method("get").create(); diff --git a/client/src/components/Grid/GridElements/GridExpand.vue b/client/src/components/Grid/GridElements/GridExpand.vue new file mode 100644 index 000000000000..9fcccf20d640 --- /dev/null +++ b/client/src/components/Grid/GridElements/GridExpand.vue @@ -0,0 +1,34 @@ + + + diff --git a/client/src/components/Grid/GridInvocation.vue b/client/src/components/Grid/GridInvocation.vue new file mode 100644 index 000000000000..d8997096096f --- /dev/null +++ b/client/src/components/Grid/GridInvocation.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/client/src/components/Grid/GridList.vue b/client/src/components/Grid/GridList.vue index 5669e46481c3..81d64ff638de 100644 --- a/client/src/components/Grid/GridList.vue +++ b/client/src/components/Grid/GridList.vue @@ -3,14 +3,17 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faCaretDown, faCaretUp, faShieldAlt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { useDebounceFn, useEventBus } from "@vueuse/core"; -import { BAlert, BButton, BFormCheckbox, BOverlay, BPagination } from "bootstrap-vue"; -import { computed, onMounted, onUnmounted, ref, watch } from "vue"; +import { BAlert, BButton, BCard, BFormCheckbox, BOverlay, BPagination } from "bootstrap-vue"; +import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue"; import { useRouter } from "vue-router/composables"; -import { BatchOperation, FieldHandler, GridConfig, Operation, RowData } from "./configs/types"; +import { BatchOperation, FieldEntry, FieldHandler, GridConfig, Operation, RowData } from "./configs/types"; +import HelpText from "../Help/HelpText.vue"; +import SwitchToHistoryLink from "../History/SwitchToHistoryLink.vue"; import GridBoolean from "./GridElements/GridBoolean.vue"; import GridDatasets from "./GridElements/GridDatasets.vue"; +import GridExpand from "./GridElements/GridExpand.vue"; import GridLink from "./GridElements/GridLink.vue"; import GridOperations from "./GridElements/GridOperations.vue"; import GridText from "./GridElements/GridText.vue"; @@ -31,6 +34,8 @@ interface Props { gridConfig: GridConfig; // incoming initial message gridMessage?: string; + // no data message + noDataMessage?: string; // debounce delay delay?: number; // embedded @@ -39,12 +44,18 @@ interface Props { limit?: number; // username for initial search usernameSearch?: string; + // any extra props to be passed to `getData` + extraProps?: Record; } const props = withDefaults(defineProps(), { delay: 5000, embedded: false, limit: 25, + gridMessage: "", + noDataMessage: "No data available.", + usernameSearch: "", + extraProps: undefined, }); // contains the current grid data provided by the corresponding api endpoint @@ -60,6 +71,9 @@ const selected = ref(new Set()); const selectedAll = computed(() => gridData.value.length === selected.value.size); const selectedIndeterminate = computed(() => ![0, gridData.value.length].includes(selected.value.size)); +// expand references +const expanded = ref(new Set()); + // page references const currentPage = ref(1); const totalRows = ref(0); @@ -79,11 +93,15 @@ const sortDesc = ref(props.gridConfig ? props.gridConfig.sortDesc : false); const filterText = ref(""); const showAdvanced = ref(false); const filterClass = props.gridConfig.filtering; -const rawFilters = computed(() => Object.fromEntries(filterClass.getFiltersForText(filterText.value, true, false))); -const validFilters = computed(() => filterClass.getValidFilters(rawFilters.value, true).validFilters); -const invalidFilters = computed(() => filterClass.getValidFilters(rawFilters.value, true).invalidFilters); +const rawFilters = computed(() => + Object.fromEntries(filterClass?.getFiltersForText(filterText.value, true, false) || []) +); +const validFilters = computed(() => filterClass?.getValidFilters(rawFilters.value, true).validFilters); +const invalidFilters = computed(() => filterClass?.getValidFilters(rawFilters.value, true).invalidFilters); const isSurroundedByQuotes = computed(() => /^["'].*["']$/.test(filterText.value)); -const hasInvalidFilters = computed(() => !isSurroundedByQuotes.value && Object.keys(invalidFilters.value).length > 0); +const hasInvalidFilters = computed( + () => !isSurroundedByQuotes.value && Object.keys(invalidFilters.value || {}).length > 0 +); // hide message helper const hideMessage = useDebounceFn(() => { @@ -111,6 +129,30 @@ function displayInitialMessage() { } } +/** + * Returns the appropriate text for a field entry + */ +function fieldText(fieldEntry: FieldEntry, rowData: RowData): string { + if (fieldEntry.converter) { + return fieldEntry.converter(rowData); + } else { + return rowData[fieldEntry.key] as string; + } +} + +/** + * Returns the appropriate column header title for a field entry + */ +function fieldTitle(fieldEntry: FieldEntry): string | null { + if (fieldEntry.title) { + return fieldEntry.title; + } else if (fieldEntry.title === undefined || fieldEntry.title === "") { + return fieldEntry.key.charAt(0).toUpperCase() + fieldEntry.key.slice(1).replace(/_/g, " ").toLowerCase(); + } else { + return null; + } +} + /** * Request grid data */ @@ -131,7 +173,8 @@ async function getGridData() { props.limit, validatedFilterText(), sortBy.value, - sortDesc.value + sortDesc.value, + props.extraProps ); gridData.value = responseData; totalRows.value = responseTotal; @@ -170,7 +213,11 @@ async function onOperation(operation: Operation, rowData: RowData) { * Handle router push request emitted by grid module */ function onRouterPush(route: string) { - router.push(route); + // reset expanded rows before navigating + expanded.value = new Set(); + nextTick(() => { + router.push(route); + }); } /** @@ -218,6 +265,18 @@ function onSelectAll(current: boolean): void { } } +/** + * Show details for a row + */ +function showDetails(rowData: RowData, show: boolean) { + if (show) { + expanded.value.add(rowData); + } else { + expanded.value.delete(rowData); + } + expanded.value = new Set(expanded.value); +} + /** * A valid filter/query for the backend */ @@ -230,7 +289,7 @@ function validatedFilterText() { return filterText.value; } // there are valid filters derived from the `filterText` - return filterClass.getFilterText(validFilters.value, false); + return filterClass?.getFilterText(validFilters.value || {}, false) || ""; } /** @@ -238,7 +297,7 @@ function validatedFilterText() { */ onMounted(() => { if (props.usernameSearch) { - filterText.value = filterClass.setFilterValue(filterText.value, "user", `'${props.usernameSearch}'`); + filterText.value = filterClass?.setFilterValue(filterText.value, "user", `'${props.usernameSearch}'`) || ""; } getGridData(); eventBus.on(onRouterPush); @@ -286,6 +345,7 @@ watch(operationMessage, () => { { view="compact" /> - + Nothing found with: {{ filterText }} - No entries found. + {{ noDataMessage }} - + Invalid filters in query:
  • @@ -347,7 +407,7 @@ watch(operationMessage, () => { class="text-nowrap font-weight-bold" :data-description="`grid sort key ${fieldEntry.key}`" @click="onSort(fieldEntry.key)"> - {{ fieldEntry.title || fieldEntry.key }} + {{ fieldTitle(fieldEntry) }} { - {{ fieldEntry.title || fieldEntry.key }} + {{ fieldTitle(fieldEntry) }} - - - - - -
    - - - - - - - - - Not available. -
    - - - + + + + + + +
    + + + + + + + + + {{ + fieldText(fieldEntry, rowData) + }} + + + + + + + Not available. +
    + + + + + + + + + + +
    diff --git a/client/src/components/Grid/configs/invocations.ts b/client/src/components/Grid/configs/invocations.ts new file mode 100644 index 000000000000..5a3bb0a06724 --- /dev/null +++ b/client/src/components/Grid/configs/invocations.ts @@ -0,0 +1,163 @@ +import { faPlay, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useEventBus } from "@vueuse/core"; + +import { invocationsFetcher, type WorkflowInvocation } from "@/api/invocations"; +import type { StoredWorkflowDetailed } from "@/api/workflows"; +import { useHistoryStore } from "@/stores/historyStore"; +import { useWorkflowStore } from "@/stores/workflowStore"; +import _l from "@/utils/localization"; + +import type { ActionArray, FieldArray, GridConfig } from "./types"; + +const { emit } = useEventBus("grid-router-push"); + +/** + * Local types + */ +type SortKeyLiteral = "create_time" | "update_time" | "None" | null | undefined; + +/** + * Request and return invocations from server + */ +async function getData( + offset: number, + limit: number, + search: string, + sort_by: string, + sort_desc: boolean, + extraProps?: Record +) { + const params = { + limit, + offset, + sort_by: sort_by as SortKeyLiteral, + sort_desc, + include_nested_invocations: false, + } as Record; + + if (extraProps && "include_terminal" in extraProps) { + params["include_terminal"] = extraProps["include_terminal"]; + } + if (extraProps && "user_id" in extraProps) { + params["user_id"] = extraProps["user_id"]; + } + + const { data, headers } = await invocationsFetcher(params); + fetchHistoriesAndWorkflows(data); + const totalMatches = parseInt(headers.get("total_matches") ?? "0"); + return [data, totalMatches]; +} + +/** + * Fetch histories and workflows for the given invocations, if not already loaded, + * so that they are cached and names are available for display in the grid + */ +function fetchHistoriesAndWorkflows(invocations: Array) { + const historyStore = useHistoryStore(); + const workflowStore = useWorkflowStore(); + + const historyIds: Set = new Set(); + const workflowIds: Set = new Set(); + invocations.forEach((invocation) => { + historyIds.add(invocation.history_id); + workflowIds.add(invocation.workflow_id); + }); + historyIds.forEach( + (history_id) => historyStore.getHistoryById(history_id) || historyStore.loadHistoryById(history_id) + ); + workflowIds.forEach((workflow_id) => workflowStore.fetchWorkflowForInstanceIdCached(workflow_id)); +} + +/** + * Actions are grid-wide operations + */ +const actions: ActionArray = [ + { + title: "Import New Invocation", + icon: faPlus, + handler: () => { + emit("/workflows/invocations/import"); + }, + }, +]; + +/** + * Declare columns to be displayed + */ +const fields: FieldArray = [ + { + key: "expand", + title: null, + type: "expand", + }, + { + key: "workflow_id", + title: "Workflow", + type: "link", + handler: (data) => { + const invocation = data as WorkflowInvocation; + emit(`/workflows/invocations/${invocation.id}`); + }, + converter: (data) => { + const workflowStore = useWorkflowStore(); + const invocation = data as WorkflowInvocation; + return workflowStore.getStoredWorkflowNameByInstanceId(invocation.workflow_id); + }, + }, + { + key: "history_id", + title: "History", + type: "history", + }, + { + key: "create_time", + title: "Invoked", + type: "date", + }, + { + key: "state", + title: "State", + type: "helptext", + converter: (data) => { + const invocation = data as WorkflowInvocation; + return `galaxy.invocations.states.${invocation.state}`; + }, + }, + { + key: "execute", + title: "Run", + type: "button", + icon: faPlay, + condition: (data) => { + const invocation = data as WorkflowInvocation; + const workflowStore = useWorkflowStore(); + const workflow = workflowStore.getStoredWorkflowByInstanceId( + invocation.workflow_id + ) as unknown as StoredWorkflowDetailed; + return !workflow?.deleted; + }, + handler: (data) => { + const invocation = data as WorkflowInvocation; + const workflowStore = useWorkflowStore(); + emit(`/workflows/run?id=${workflowStore.getStoredWorkflowIdByInstanceId(invocation.workflow_id)}`); + }, + converter: () => "", + }, +]; + +/** + * Grid configuration + */ +const gridConfig: GridConfig = { + id: "invocations-grid", + actions: actions, + fields: fields, + getData: getData, + plural: "Workflow Invocations", + sortBy: "create_time", + sortDesc: true, + sortKeys: ["create_time"], + title: "Workflow Invocations", +}; + +export default gridConfig; diff --git a/client/src/components/Grid/configs/invocationsHistory.ts b/client/src/components/Grid/configs/invocationsHistory.ts new file mode 100644 index 000000000000..0369b01193f4 --- /dev/null +++ b/client/src/components/Grid/configs/invocationsHistory.ts @@ -0,0 +1,151 @@ +import { faArrowLeft, faPlay } from "@fortawesome/free-solid-svg-icons"; +import { useEventBus } from "@vueuse/core"; + +import { invocationsFetcher, type WorkflowInvocation } from "@/api/invocations"; +import type { StoredWorkflowDetailed } from "@/api/workflows"; +import { useUserStore } from "@/stores/userStore"; +import { useWorkflowStore } from "@/stores/workflowStore"; +import _l from "@/utils/localization"; + +import type { ActionArray, FieldArray, GridConfig } from "./types"; + +const { emit } = useEventBus("grid-router-push"); + +/** + * Local types + */ +type SortKeyLiteral = "create_time" | "update_time" | "None" | null | undefined; + +/** + * Request and return invocations for the given history (and current user) from server + */ +async function getData( + offset: number, + limit: number, + search: string, + sort_by: string, + sort_desc: boolean, + extraProps?: Record +) { + const userStore = useUserStore(); + if (userStore.currentUser?.isAnonymous || !userStore.currentUser || !extraProps || !extraProps["history_id"]) { + // TODO: maybe raise an error here? + return [[], 0]; + } + const historyId = extraProps["history_id"] as string; + + const { data, headers } = await invocationsFetcher({ + limit, + offset, + sort_by: sort_by as SortKeyLiteral, + sort_desc, + user_id: userStore.currentUser.id, + include_nested_invocations: false, + history_id: historyId, + }); + fetchHistories(data); + const totalMatches = parseInt(headers.get("total_matches") ?? "0"); + return [data, totalMatches]; +} + +/** + * Fetch workflows for the given history's invocations, if not already loaded, + * so that they are cached and names are available for display in the grid + */ +function fetchHistories(invocations: Array) { + const workflowStore = useWorkflowStore(); + const workflowIds: Set = new Set(); + invocations.forEach((invocation) => { + workflowIds.add(invocation.workflow_id); + }); + workflowIds.forEach((workflow_id) => workflowStore.fetchWorkflowForInstanceIdCached(workflow_id)); +} + +/** + * Actions are grid-wide operations + */ +const actions: ActionArray = [ + { + title: "Invocations List", + icon: faArrowLeft, + handler: () => { + emit("/workflows/invocations"); + }, + }, +]; + +/** + * Declare columns to be displayed + */ +const fields: FieldArray = [ + { + key: "expand", + title: null, + type: "expand", + }, + { + key: "workflow_id", + title: "Workflow", + type: "link", + handler: (data) => { + const invocation = data as WorkflowInvocation; + emit(`/workflows/invocations/${invocation.id}`); + }, + converter: (data) => { + const workflowStore = useWorkflowStore(); + const invocation = data as WorkflowInvocation; + return workflowStore.getStoredWorkflowNameByInstanceId(invocation.workflow_id); + }, + }, + { + key: "create_time", + title: "Invoked", + type: "date", + }, + { + key: "state", + title: "State", + type: "helptext", + converter: (data) => { + const invocation = data as WorkflowInvocation; + return `galaxy.invocations.states.${invocation.state}`; + }, + }, + { + key: "execute", + title: "Run", + type: "button", + icon: faPlay, + condition: (data) => { + const invocation = data as WorkflowInvocation; + const workflowStore = useWorkflowStore(); + const workflow = workflowStore.getStoredWorkflowByInstanceId( + invocation.workflow_id + ) as unknown as StoredWorkflowDetailed; + return !workflow?.deleted; + }, + handler: (data) => { + const invocation = data as WorkflowInvocation; + const workflowStore = useWorkflowStore(); + emit(`/workflows/run?id=${workflowStore.getStoredWorkflowIdByInstanceId(invocation.workflow_id)}`); + }, + converter: () => "", + }, +]; + +/** + * Grid configuration + */ +const gridConfig: GridConfig = { + id: "invocations-history-grid", + actions: actions, + fields: fields, + getData: getData, + plural: "Workflow Invocations", + sortBy: "create_time", + sortDesc: true, + sortKeys: ["create_time"], + title: "Workflow Invocations For History", +}; + +export default gridConfig; diff --git a/client/src/components/Grid/configs/invocationsWorkflow.ts b/client/src/components/Grid/configs/invocationsWorkflow.ts new file mode 100644 index 000000000000..3cad8d66cc0e --- /dev/null +++ b/client/src/components/Grid/configs/invocationsWorkflow.ts @@ -0,0 +1,154 @@ +import { faArrowLeft, faEye, faPlay } from "@fortawesome/free-solid-svg-icons"; +import { useEventBus } from "@vueuse/core"; + +import { invocationsFetcher, type WorkflowInvocation } from "@/api/invocations"; +import type { StoredWorkflowDetailed } from "@/api/workflows"; +import { useHistoryStore } from "@/stores/historyStore"; +import { useUserStore } from "@/stores/userStore"; +import { useWorkflowStore } from "@/stores/workflowStore"; +import _l from "@/utils/localization"; + +import type { ActionArray, FieldArray, GridConfig } from "./types"; + +const { emit } = useEventBus("grid-router-push"); + +/** + * Local types + */ +type SortKeyLiteral = "create_time" | "update_time" | "None" | null | undefined; + +/** + * Request and return invocations for the given workflow (and current user) from server + */ +async function getData( + offset: number, + limit: number, + search: string, + sort_by: string, + sort_desc: boolean, + extraProps?: Record +) { + const userStore = useUserStore(); + if (userStore.currentUser?.isAnonymous || !userStore.currentUser || !extraProps || !extraProps["workflow_id"]) { + // TODO: maybe raise an error here? + return [[], 0]; + } + const workflowId = extraProps["workflow_id"] as string; + + const { data, headers } = await invocationsFetcher({ + limit, + offset, + sort_by: sort_by as SortKeyLiteral, + sort_desc, + user_id: userStore.currentUser.id, + workflow_id: workflowId, + }); + fetchHistories(data); + const totalMatches = parseInt(headers.get("total_matches") ?? "0"); + return [data, totalMatches]; +} + +/** + * Fetch histories for the given workflow's invocations, if not already loaded, + * so that they are cached and names are available for display in the grid + */ +function fetchHistories(invocations: Array) { + const historyStore = useHistoryStore(); + const historyIds: Set = new Set(); + invocations.forEach((invocation) => { + historyIds.add(invocation.history_id); + }); + historyIds.forEach( + (history_id) => historyStore.getHistoryById(history_id) || historyStore.loadHistoryById(history_id) + ); +} + +/** + * Actions are grid-wide operations + */ +const actions: ActionArray = [ + { + title: "Invocations List", + icon: faArrowLeft, + handler: () => { + emit("/workflows/invocations"); + }, + }, +]; + +/** + * Declare columns to be displayed + */ +const fields: FieldArray = [ + { + key: "expand", + title: null, + type: "expand", + }, + { + key: "view", + title: "View", + type: "button", + icon: faEye, + handler: (data) => { + emit(`/workflows/invocations/${(data as WorkflowInvocation).id}`); + }, + converter: () => "", + }, + { + key: "history_id", + title: "History", + type: "history", + }, + { + key: "create_time", + title: "Invoked", + type: "date", + }, + { + key: "state", + title: "State", + type: "helptext", + converter: (data) => { + const invocation = data as WorkflowInvocation; + return `galaxy.invocations.states.${invocation.state}`; + }, + }, + { + key: "execute", + title: "Run", + type: "button", + icon: faPlay, + condition: (data) => { + const invocation = data as WorkflowInvocation; + const workflowStore = useWorkflowStore(); + const workflow = workflowStore.getStoredWorkflowByInstanceId( + invocation.workflow_id + ) as unknown as StoredWorkflowDetailed; + return !workflow?.deleted; + }, + handler: (data) => { + const invocation = data as WorkflowInvocation; + const workflowStore = useWorkflowStore(); + emit(`/workflows/run?id=${workflowStore.getStoredWorkflowIdByInstanceId(invocation.workflow_id)}`); + }, + converter: () => "", + }, +]; + +/** + * Grid configuration + */ +const gridConfig: GridConfig = { + id: "invocations-workflow-grid", + actions: actions, + fields: fields, + getData: getData, + plural: "Workflow Invocations", + sortBy: "create_time", + sortDesc: true, + sortKeys: ["create_time"], + title: "Workflow Invocations For Workflow", +}; + +export default gridConfig; diff --git a/client/src/components/Grid/configs/types.ts b/client/src/components/Grid/configs/types.ts index e008e967cac6..01c60be4228f 100644 --- a/client/src/components/Grid/configs/types.ts +++ b/client/src/components/Grid/configs/types.ts @@ -15,8 +15,15 @@ export interface GridConfig { id: string; actions?: ActionArray; fields: FieldArray; - filtering: Filtering; - getData: (offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) => Promise; + filtering?: Filtering; + getData: ( + offset: number, + limit: number, + search: string, + sort_by: string, + sort_desc: boolean, + extraProps?: Record + ) => Promise; batch?: BatchOperationArray; plural: string; sortBy: string; @@ -29,12 +36,14 @@ export type FieldArray = Array; export interface FieldEntry { key: string; - title: string; + title?: string | null; condition?: (data: RowData) => boolean; disabled?: boolean; type: validTypes; operations?: Array; + icon?: IconDefinition; handler?: FieldHandler; + converter?: (data: RowData) => string; width?: number; } @@ -65,4 +74,16 @@ type OperationHandlerReturn = Promise | voi export type RowData = Record; -type validTypes = "boolean" | "date" | "datasets" | "link" | "operations" | "sharing" | "tags" | "text"; +type validTypes = + | "boolean" + | "date" + | "datasets" + | "link" + | "button" + | "operations" + | "sharing" + | "tags" + | "text" + | "expand" + | "history" + | "helptext"; diff --git a/client/src/components/HistoryImport.vue b/client/src/components/HistoryImport.vue index ff889d829399..444750dabeb0 100644 --- a/client/src/components/HistoryImport.vue +++ b/client/src/components/HistoryImport.vue @@ -1,13 +1,13 @@