Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import { Table } from "antd";
import { inject, observer } from "mobx-react";
import { flow, getEnv, types } from "mobx-state-tree";
import Papa from "papaparse";
 
import { errorBuilder } from "../../core/DataValidator/ConfigValidator";
import Registry from "../../core/Registry";
import { AnnotationMixin } from "../../mixins/AnnotationMixin";
import ProcessAttrsMixin from "../../mixins/ProcessAttrs";
import Base from "./Base";
import { parseTypeAndOption, parseValue } from "../../utils/data";
 
/**
 * The `Table` tag is used to display object keys and values in a table.
 * @example
 * <!-- Basic labeling configuration for text in a table -->
 * <View>
 *   <Table name="text-1" value="$text"></Table>
 * </View>
 * @name Table
 * @meta_title Table Tag to Display Keys & Values in Tables
 * @meta_description Customize Label Studio by displaying key-value pairs in tasks for machine learning and data science projects.
 * @param {string} value Data field value containing JSON type for Table
 * @param {string} [valueType] Value to define the data type in Table
 */
const Model = types
  .model({
    type: "table",
    value: types.maybeNull(types.string),
    _value: types.frozen([]),
    valuetype: types.optional(types.string, "json"),
  })
  .views((self) => ({
    get isJsonArrayOfObjects() {
      const value = self._value;
      return (
        Array.isArray(value) &&
        value.length > 0 &&
        typeof value[0] === "object" &&
        value[0] !== null &&
        !Array.isArray(value[0])
      );
    },
    get dataSource() {
      const { type } = parseTypeAndOption(self.valuetype);
 
      if (type === "json") {
        const value = self._value;
 
        // If JSON is an array of objects, treat each object as a row for a normal table
        if (self.isJsonArrayOfObjects) {
          return value;
        }
 
        // Array of primitives or mixed values → key/value representation with index as key
        if (Array.isArray(value)) {
          return value.map((v, idx) => {
            let val = v;
            if (typeof val === "object") val = JSON.stringify(val);
            return { type: String(idx), value: val };
          });
        }
 
        // Object case → key/value representation
        return Object.keys(value)
          .sort((a, b) => {
            return a.toLowerCase().localeCompare(b.toLowerCase());
          })
          .map((k) => {
            let val = value[k];
 
            if (typeof val === "object") val = JSON.stringify(val);
            return { type: k, value: val };
          });
      }
      return self._value;
    },
    get columns() {
      const { type } = parseTypeAndOption(self.valuetype);
      if (type === "json") {
        const value = self._value;
        // JSON array of objects → derive columns from union of keys across all rows
        if (self.isJsonArrayOfObjects) {
          const allKeys = Array.from(
            new Set(
              value.reduce((acc, row) => {
                if (row && typeof row === "object" && !Array.isArray(row)) {
                  acc.push(...Object.keys(row));
                }
                return acc;
              }, []),
            ),
          );
 
          return allKeys.map((key) => ({ title: key, dataIndex: key }));
        }
        // Otherwise, use generic Name/Value columns
        return [
          { title: "Name", dataIndex: "type" },
          { title: "Value", dataIndex: "value" },
        ];
      }
      if (!self._value[0]) {
        return [
          { title: "Name", dataIndex: "type" },
          { title: "Value", dataIndex: "value" },
        ];
      }
      return Object.keys(self._value[0]).map((value) => ({ title: value, dataIndex: value }));
    },
  }))
  .actions((self) => ({
    updateValue: flow(function* (store) {
      const { type, options } = parseTypeAndOption(self.valuetype);
      let originData = parseValue(self.value, store.task.dataObj);
 
      if (options.url) {
        try {
          const response = yield fetch(originData);
          const { ok, status, statusText } = response;
 
          if (!ok) throw new Error(`${status} ${statusText}`);
 
          originData = yield response.text();
        } catch (error) {
          const message = getEnv(self).messages.ERR_LOADING_HTTP({
            attr: self.value,
            error: String(error),
            url: originData,
          });
 
          self.annotationStore.addErrors([errorBuilder.generalError(message)]);
        }
      }
 
      switch (type) {
        case "csv":
          {
            Papa.parse(originData, {
              delimiter: options.separator,
              header: !options.headless,
              download: false,
              complete: ({ data }) => {
                self._value = data;
              },
            });
          }
          break;
        default:
          self._value = typeof originData === "string" ? JSON.parse(originData) : originData;
          break;
      }
    }),
  }));
 
const TableModel = types.compose("TableModel", Base, ProcessAttrsMixin, AnnotationMixin, Model);
 
const HtxTable = inject("store")(
  observer(({ item }) => {
    return (
      <Table bordered dataSource={item.dataSource} columns={item.columns} pagination={{ hideOnSinglePage: true }} />
    );
  }),
);
 
Registry.addTag("table", TableModel, HtxTable);
Registry.addObjectType(TableModel);
 
export { HtxTable, TableModel };