@malloydata/malloy-explorer 0.0.278-dev250515234639 → 0.0.278-dev250516210719

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -17,7 +17,7 @@ import { jsx, Fragment, jsxs } from "react/jsx-runtime";
17
17
  import * as React from "react";
18
18
  import React__default, { useLayoutEffect, useEffect, useMemo, useState, useRef, createElement, useContext, useCallback, createContext, memo, useReducer } from "react";
19
19
  import * as QB from "@malloydata/malloy-query-builder";
20
- import { ASTArrowQueryDefinition, ASTSegmentViewDefinition, ASTRefinementViewDefinition, ASTOrderByViewOperation, ASTLimitViewOperation, ASTNestViewOperation, ASTAggregateViewOperation, ASTGroupByViewOperation, ASTTimeTruncationExpression, ASTWhereViewOperation, ASTHavingViewOperation, ASTArrowViewDefinition } from "@malloydata/malloy-query-builder";
20
+ import { ASTArrowQueryDefinition, ASTSegmentViewDefinition, ASTRefinementViewDefinition, ASTOrderByViewOperation, ASTGroupByViewOperation, ASTAggregateViewOperation, ASTLimitViewOperation, ASTNestViewOperation, ASTTimeTruncationExpression, ASTWhereViewOperation, ASTHavingViewOperation, ASTArrowViewDefinition } from "@malloydata/malloy-query-builder";
21
21
  import { Tag } from "@malloydata/malloy-tag";
22
22
  import { TemporalFilterExpression, BooleanFilterExpression, NumberFilterExpression, StringFilterExpression } from "@malloydata/malloy-filter";
23
23
  import "@malloydata/render/webcomponent";
@@ -29932,6 +29932,11 @@ function getViewDefinition(parent) {
29932
29932
  return parent instanceof ASTArrowQueryDefinition ? parent.view : parent.definition;
29933
29933
  }
29934
29934
  function getInputSchemaFromViewParent(parent) {
29935
+ if (!parent) {
29936
+ return {
29937
+ fields: []
29938
+ };
29939
+ }
29935
29940
  const definition = getViewDefinition(parent);
29936
29941
  return definition.getInputSchema();
29937
29942
  }
@@ -32382,12 +32387,49 @@ const styles$g = {
32382
32387
  $$css: true
32383
32388
  }
32384
32389
  };
32390
+ function toFullName(path, name) {
32391
+ return [...path || [], name].join(".");
32392
+ }
32385
32393
  function segmentHasLimit(segment) {
32386
32394
  return segment.operations.items.find((operation) => operation instanceof ASTLimitViewOperation) !== void 0;
32387
32395
  }
32388
32396
  function segmentHasOrderBy(segment, name) {
32389
32397
  return segment.operations.items.find((operation) => operation instanceof ASTOrderByViewOperation && operation.name === name) !== void 0;
32390
32398
  }
32399
+ function getOutputNameToInputNameMap(segment) {
32400
+ const nameMap = /* @__PURE__ */ new Map();
32401
+ for (const operation of segment.operations.items) {
32402
+ if (operation instanceof ASTGroupByViewOperation || operation instanceof ASTAggregateViewOperation) {
32403
+ const reference = operation.field.getReference();
32404
+ nameMap.set(operation.name, toFullName(reference.path, reference.name));
32405
+ }
32406
+ }
32407
+ return nameMap;
32408
+ }
32409
+ function segmentHasOrderBySourceField(segment, path, name) {
32410
+ const nameMap = getOutputNameToInputNameMap(segment);
32411
+ const fullInputName = toFullName(path, name);
32412
+ return !!segment.operations.items.find((operation) => {
32413
+ if (operation instanceof ASTOrderByViewOperation && nameMap.has(operation.name)) {
32414
+ return fullInputName === nameMap.get(operation.name);
32415
+ }
32416
+ return false;
32417
+ });
32418
+ }
32419
+ function areReferencesEqual(path1, name1, path2, name2) {
32420
+ return name1 === name2 && (path1 || []).join(".") === (path2 || []).join(".");
32421
+ }
32422
+ function segmentHasFieldInOutputSpace(segment, path, name) {
32423
+ const match = segment.operations.items.find((operation) => {
32424
+ if (operation instanceof ASTGroupByViewOperation || operation instanceof ASTAggregateViewOperation) {
32425
+ const reference = operation.field.getReference();
32426
+ const isEqual = areReferencesEqual(path, name, reference.path, reference.name);
32427
+ return isEqual;
32428
+ }
32429
+ return false;
32430
+ });
32431
+ return !!match;
32432
+ }
32391
32433
  function segmentNestNo(segment, name) {
32392
32434
  return segment.operations.items.reduce((acc, operation) => {
32393
32435
  if (operation instanceof ASTNestViewOperation) {
@@ -32442,6 +32484,19 @@ function addNest(view, field) {
32442
32484
  }
32443
32485
  segment.addNest(field.name, rename);
32444
32486
  }
32487
+ function addOrderByFromSource(view, path, name, direction = "desc") {
32488
+ const fullInputName = toFullName(path, name);
32489
+ let orderByName = name;
32490
+ const segment = view.getOrAddDefaultSegment();
32491
+ const nameMap = getOutputNameToInputNameMap(segment);
32492
+ for (const entry of nameMap.entries()) {
32493
+ if (entry[1] === fullInputName) {
32494
+ orderByName = entry[0];
32495
+ break;
32496
+ }
32497
+ }
32498
+ segment.addOrderBy(orderByName, direction);
32499
+ }
32445
32500
  function addOrderBy(view, field, direction = "desc") {
32446
32501
  const segment = view.getOrAddDefaultSegment();
32447
32502
  segment.addOrderBy(field.name, direction);
@@ -32455,6 +32510,9 @@ function addFilter(view, field, path, filter) {
32455
32510
  }
32456
32511
  }
32457
32512
  function getSegmentIfPresent(parent) {
32513
+ if (!parent) {
32514
+ return void 0;
32515
+ }
32458
32516
  const definition = getViewDefinition(parent);
32459
32517
  if (definition instanceof ASTSegmentViewDefinition) {
32460
32518
  return definition;
@@ -35896,62 +35954,83 @@ const FIELD_KIND_TO_TITLE = {
35896
35954
  dimension: "Dimensions"
35897
35955
  };
35898
35956
  function useOperations(view, field, path) {
35899
- const dimensionFields = useMemo(() => {
35957
+ const fullName = toFullName(path, field.name);
35958
+ const flattenedFields = useMemo(() => {
35900
35959
  const {
35901
35960
  fields
35902
35961
  } = getInputSchemaFromViewParent(view);
35903
- return new Set(flattenFieldsTree(fields).filter(({
35904
- field: field2
35905
- }) => field2.kind === "dimension").map(({
35906
- field: field2
35907
- }) => field2.name));
35908
- }, [view]);
35909
- const measureFields = useMemo(() => {
35910
- const {
35911
- fields
35912
- } = getInputSchemaFromViewParent(view);
35913
- return new Set(flattenFieldsTree(fields).filter(({
35914
- field: field2
35915
- }) => field2.kind === "measure").map(({
35916
- field: field2
35917
- }) => field2.name));
35918
- }, [view]);
35919
- const isGroupByAllowed = useMemo(() => {
35920
- if (!view) {
35921
- return false;
35922
- }
35962
+ const inputPath = path.join(".");
35963
+ return flattenFieldsTree(fields).filter((fieldItem) => {
35964
+ return fieldItem.path.join(".") === inputPath;
35965
+ });
35966
+ }, [path, view]);
35967
+ const matchingFieldItem = flattenedFields.find((fieldItem) => field.name === fieldItem.field.name);
35968
+ const groupByDisabledReason = useMemo(() => {
35923
35969
  const segment = getSegmentIfPresent(view);
35924
- return dimensionFields.has(field.name) && !(segment == null ? void 0 : segment.hasField(field.name, path)) && isNotAnnotatedFilteredField(field);
35925
- }, [view, field, path, dimensionFields]);
35926
- const isAggregateAllowed = useMemo(() => {
35927
- if (!view) {
35928
- return false;
35970
+ if ((matchingFieldItem == null ? void 0 : matchingFieldItem.field.kind) !== "dimension") {
35971
+ return "Grouping is only available on a dimenion.";
35972
+ }
35973
+ if (segment == null ? void 0 : segment.hasField(field.name, path)) {
35974
+ return "Cannot group by a field already in the view.";
35975
+ }
35976
+ if (!isNotAnnotatedFilteredField(field)) {
35977
+ return "This field is annotated with #NO_UI.";
35978
+ }
35979
+ return "";
35980
+ }, [view, matchingFieldItem == null ? void 0 : matchingFieldItem.field.kind, field, path]);
35981
+ const aggregateDisabledReason = useMemo(() => {
35982
+ if ((matchingFieldItem == null ? void 0 : matchingFieldItem.field.kind) !== "measure") {
35983
+ return "Aggregation only supports measure fields.";
35929
35984
  }
35930
35985
  const segment = getSegmentIfPresent(view);
35931
- return measureFields.has(field.name) && !(segment == null ? void 0 : segment.hasField(field.name, path)) && isNotAnnotatedFilteredField(field);
35932
- }, [view, field, path, measureFields]);
35933
- const isFilterAllowed = useMemo(() => {
35934
- if (!view) {
35935
- return false;
35986
+ if (segment == null ? void 0 : segment.hasField(field.name, path)) {
35987
+ return "This field is already used in the query.";
35936
35988
  }
35937
- const fieldName = field.name;
35938
- const inputSchemaFields = getInputSchemaFromViewParent(view).fields;
35939
- return inputSchemaFields.filter((field2) => field2.kind === "dimension" || field2.kind === "measure").filter((field2) => FILTERABLE_TYPES.includes(field2.type.kind)).some((field2) => field2.name === fieldName);
35940
- }, [view, field]);
35941
- const isOrderByAllowed = useMemo(() => {
35942
- if (!view) {
35943
- return false;
35989
+ if (!isNotAnnotatedFilteredField(field)) {
35990
+ return "This field is annotated with #NO_UI.";
35991
+ }
35992
+ return "";
35993
+ }, [matchingFieldItem == null ? void 0 : matchingFieldItem.field.kind, view, field, path]);
35994
+ const filterDisabledReason = useMemo(() => {
35995
+ if (!matchingFieldItem) {
35996
+ return `Unexpected Error: Could not find a field ${fullName}.`;
35997
+ }
35998
+ if (!["dimension", "measure"].includes(matchingFieldItem.field.kind)) {
35999
+ return `Filtering is only available for a dimension or measure.`;
36000
+ }
36001
+ if (!FILTERABLE_TYPES.includes(matchingFieldItem.field.type.kind)) {
36002
+ return "Filtering only supports string, boolean, number, date and time fields.";
36003
+ }
36004
+ return "";
36005
+ }, [fullName, matchingFieldItem]);
36006
+ const orderByDisabledReason = useMemo(() => {
36007
+ if (!matchingFieldItem) {
36008
+ return `Unexpected Error: Could not find a field ${fullName}.`;
35944
36009
  }
35945
- const fieldName = field.name;
35946
- const outputSchemaFields = view.getOutputSchema().fields;
35947
36010
  const segment = getSegmentIfPresent(view);
35948
- return outputSchemaFields.filter((field2) => field2.kind === "dimension").filter((field2) => ORDERABLE_TYPES.includes(field2.type.kind)).filter((field2) => !segment || !segmentHasOrderBy(segment, field2.name)).some((field2) => field2.name === fieldName);
35949
- }, [view, field]);
36011
+ if (segment && segmentHasOrderBySourceField(segment, path, field.name)) {
36012
+ return "Query is already ordered by this field.";
36013
+ }
36014
+ if (!segment || !segmentHasFieldInOutputSpace(segment, path, field.name)) {
36015
+ return "Order by is only available for fields in the output.";
36016
+ }
36017
+ if (!["dimension", "measure"].includes(matchingFieldItem.field.kind)) {
36018
+ return "Order By is only available for dimension or measure fields.";
36019
+ }
36020
+ if (!ORDERABLE_TYPES.includes(matchingFieldItem.field.type.kind)) {
36021
+ return "Order By only supports string, boolean, number, date and time fields.";
36022
+ }
36023
+ return "";
36024
+ }, [matchingFieldItem, view, path, field.name, fullName]);
35950
36025
  return {
35951
- isGroupByAllowed,
35952
- isAggregateAllowed,
35953
- isFilterAllowed,
35954
- isOrderByAllowed
36026
+ isGroupByAllowed: !groupByDisabledReason,
36027
+ groupByDisabledReason,
36028
+ isAggregateAllowed: !aggregateDisabledReason,
36029
+ aggregateDisabledReason,
36030
+ isFilterAllowed: !filterDisabledReason,
36031
+ filterDisabledReason,
36032
+ isOrderByAllowed: !orderByDisabledReason,
36033
+ orderByDisabledReason
35955
36034
  };
35956
36035
  }
35957
36036
  const FILTERABLE_TYPES = ["string_type", "boolean_type", "number_type", "date_type", "timestamp_type"];
@@ -35968,22 +36047,22 @@ function FieldTokenWithActions({
35968
36047
  } = React__default.useContext(QueryEditorContext);
35969
36048
  const view = currentNestView ?? viewDef;
35970
36049
  const {
35971
- isGroupByAllowed,
35972
- isAggregateAllowed,
35973
- isFilterAllowed,
35974
- isOrderByAllowed
36050
+ groupByDisabledReason,
36051
+ aggregateDisabledReason,
36052
+ filterDisabledReason,
36053
+ orderByDisabledReason
35975
36054
  } = useOperations(view, field, path);
35976
36055
  const [isFilterPopoverOpen, setIsFilterPopoverOpen] = useState();
35977
36056
  const [isTooltipOpen, setIsTooltipOpen] = useState(false);
35978
36057
  const handleAddOperationAction = (operation, filter) => {
35979
36058
  if (field.kind === "dimension" || field.kind === "measure") {
35980
- if (operation === "groupBy" && isGroupByAllowed) {
36059
+ if (operation === "groupBy" && !groupByDisabledReason) {
35981
36060
  addGroupBy(view, field, path);
35982
- } else if (operation === "aggregate" && isAggregateAllowed) {
36061
+ } else if (operation === "aggregate" && !aggregateDisabledReason) {
35983
36062
  addAggregate(view, field, path);
35984
- } else if (operation === "orderBy" && isOrderByAllowed) {
35985
- addOrderBy(view, field);
35986
- } else if (operation === "filter" && isFilterAllowed && filter) {
36063
+ } else if (operation === "orderBy" && !orderByDisabledReason) {
36064
+ addOrderByFromSource(view, path, field.name);
36065
+ } else if (operation === "filter" && !filterDisabledReason && filter) {
35987
36066
  addFilter(view, field, path, filter);
35988
36067
  }
35989
36068
  setQuery == null ? void 0 : setQuery(rootQuery == null ? void 0 : rootQuery.build());
@@ -36008,7 +36087,7 @@ function FieldTokenWithActions({
36008
36087
  icon: "insert",
36009
36088
  disabled: !(rootQuery == null ? void 0 : rootQuery.isEmpty()),
36010
36089
  onClick: handleSetView,
36011
- tooltip: "Add view",
36090
+ tooltip: !(rootQuery == null ? void 0 : rootQuery.isEmpty()) ? "Can only add a view to an empty query." : "Add view",
36012
36091
  onTooltipOpenChange: setIsTooltipOpen
36013
36092
  }), /* @__PURE__ */ jsx(ActionButton, {
36014
36093
  icon: "nest",
@@ -36019,8 +36098,8 @@ function FieldTokenWithActions({
36019
36098
  }) : field.kind === "measure" ? /* @__PURE__ */ jsxs(Fragment, {
36020
36099
  children: [/* @__PURE__ */ jsx(ActionButton, {
36021
36100
  icon: "aggregate",
36022
- tooltip: "Add as aggregate",
36023
- disabled: !isAggregateAllowed,
36101
+ tooltip: aggregateDisabledReason || "Add as aggregate",
36102
+ disabled: !!aggregateDisabledReason,
36024
36103
  onClick: () => handleAddOperationAction("aggregate"),
36025
36104
  onTooltipOpenChange: setIsTooltipOpen
36026
36105
  }), /* @__PURE__ */ jsx(FilterPopover, {
@@ -36029,23 +36108,23 @@ function FieldTokenWithActions({
36029
36108
  setFilter: (filter) => handleAddOperationAction("filter", filter),
36030
36109
  trigger: /* @__PURE__ */ jsx(ActionButton, {
36031
36110
  icon: "filter",
36032
- tooltip: "Add as filter",
36033
- disabled: !isFilterAllowed,
36111
+ tooltip: filterDisabledReason || "Add as filter",
36112
+ disabled: !!filterDisabledReason,
36034
36113
  onTooltipOpenChange: setIsTooltipOpen
36035
36114
  }),
36036
36115
  onOpenChange: setIsFilterPopoverOpen
36037
36116
  }), /* @__PURE__ */ jsx(ActionButton, {
36038
36117
  icon: "orderBy",
36039
- tooltip: "Add as order by",
36040
- disabled: !isOrderByAllowed,
36118
+ tooltip: orderByDisabledReason || "Add as order by",
36119
+ disabled: !!orderByDisabledReason,
36041
36120
  onClick: () => handleAddOperationAction("orderBy"),
36042
36121
  onTooltipOpenChange: setIsTooltipOpen
36043
36122
  })]
36044
36123
  }) : field.kind === "dimension" ? /* @__PURE__ */ jsxs(Fragment, {
36045
36124
  children: [/* @__PURE__ */ jsx(ActionButton, {
36046
36125
  icon: "groupBy",
36047
- tooltip: "Add as group by",
36048
- disabled: !isGroupByAllowed,
36126
+ tooltip: groupByDisabledReason || "Add as group by",
36127
+ disabled: !!groupByDisabledReason,
36049
36128
  onClick: () => handleAddOperationAction("groupBy"),
36050
36129
  onTooltipOpenChange: setIsTooltipOpen
36051
36130
  }), /* @__PURE__ */ jsx(FilterPopover, {
@@ -36054,20 +36133,20 @@ function FieldTokenWithActions({
36054
36133
  setFilter: (filter) => handleAddOperationAction("filter", filter),
36055
36134
  trigger: /* @__PURE__ */ jsx(ActionButton, {
36056
36135
  icon: "filter",
36057
- tooltip: "Add as filter",
36058
- disabled: !isFilterAllowed,
36136
+ tooltip: filterDisabledReason || "Add as filter",
36137
+ disabled: !!filterDisabledReason,
36059
36138
  onTooltipOpenChange: setIsTooltipOpen
36060
36139
  }),
36061
36140
  onOpenChange: setIsFilterPopoverOpen
36062
36141
  }), /* @__PURE__ */ jsx(ActionButton, {
36063
36142
  icon: "orderBy",
36064
- tooltip: "Add as order by",
36065
- disabled: !isOrderByAllowed,
36143
+ tooltip: orderByDisabledReason || "Add as order by",
36144
+ disabled: !!orderByDisabledReason,
36066
36145
  onClick: () => handleAddOperationAction("orderBy"),
36067
36146
  onTooltipOpenChange: setIsTooltipOpen
36068
36147
  })]
36069
36148
  }) : null,
36070
- onClick: field.kind === "dimension" && isGroupByAllowed ? () => handleAddOperationAction("groupBy") : field.kind === "measure" && isAggregateAllowed ? () => handleAddOperationAction("aggregate") : field.kind === "view" ? () => handleAddView() : void 0,
36149
+ onClick: field.kind === "dimension" && !groupByDisabledReason ? () => handleAddOperationAction("groupBy") : field.kind === "measure" && !aggregateDisabledReason ? () => handleAddOperationAction("aggregate") : field.kind === "view" ? () => handleAddView() : void 0,
36071
36150
  hoverActionsVisible: isFilterPopoverOpen || isTooltipOpen,
36072
36151
  tooltip: /* @__PURE__ */ jsx(FieldHoverCard, {
36073
36152
  field,