import { applySnapshot, clone, destroy, flow, getRoot, getSnapshot, types } from "mobx-state-tree";
|
import { History } from "../../utils/history";
|
import { guidGenerator } from "../../utils/random";
|
import { isDefined, unique } from "../../utils/utils";
|
import { CustomJSON } from "../types";
|
import { Tab } from "./tab";
|
import { TabColumn } from "./tab_column";
|
import { TabFilterType } from "./tab_filter_type";
|
import { TabHiddenColumns } from "./tab_hidden_columns";
|
import { serializeJsonForUrl, deserializeJsonFromUrl } from "@humansignal/core";
|
import { isEmpty } from "../../utils/helpers";
|
|
const storeValue = (name, value) => {
|
window.localStorage.setItem(name, value);
|
return value;
|
};
|
|
const restoreValue = (name) => {
|
const value = window.localStorage.getItem(name);
|
|
return value ? value === "true" : false;
|
};
|
|
const dataCleanup = (tab, columns) => {
|
const { data } = tab;
|
|
if (!data) return { ...tab };
|
|
if (data.filters) {
|
data.filters.items = data.filters.items.filter(({ filter }) => {
|
return !!columns.find((c) => c.id === filter.replace(/^filter:/, ""));
|
});
|
}
|
|
["columnsDisplayType", "columnWidths"].forEach((key) => {
|
data[key] = Object.fromEntries(
|
Object.entries(data[key] ?? {}).filter(([col]) => {
|
const match = columns.find((c) => c.id === col);
|
return !!match && !match.isAnnotationResultsFilterColumn;
|
}),
|
);
|
});
|
|
Object.entries(data.hiddenColumns ?? {}).forEach(([key, list]) => {
|
data.hiddenColumns[key] = list.filter((k) => {
|
const match = columns.find((c) => c.id === k);
|
return !!match && !match.isAnnotationResultsFilterColumn;
|
});
|
});
|
|
return { ...tab, data };
|
};
|
|
const createNameCopy = (name) => {
|
let newName = name;
|
const matcher = /Copy(\s\(([\d]+)\))?/;
|
const copyNum = newName.match(matcher);
|
|
if (copyNum) {
|
newName = newName.replace(matcher, (...match) => {
|
const num = match[2];
|
|
if (num) return `Copy (${Number(num) + 1})`;
|
|
return "Copy (2)";
|
});
|
} else {
|
newName += " Copy";
|
}
|
|
return newName;
|
};
|
|
export const TabStore = types
|
.model("TabStore", {
|
selected: types.maybeNull(types.late(() => types.reference(Tab))),
|
views: types.optional(types.array(Tab), []),
|
availableFilters: types.optional(types.array(TabFilterType), []),
|
columnsTargetMap: types.map(types.array(TabColumn)),
|
columnsRaw: types.optional(CustomJSON, []),
|
sidebarVisible: restoreValue("sidebarVisible"),
|
sidebarEnabled: restoreValue("sidebarEnabled"),
|
})
|
.volatile(() => ({
|
defaultHidden: null,
|
}))
|
.views((self) => ({
|
get all() {
|
return self.views;
|
},
|
|
get canClose() {
|
return self.all.length > 1;
|
},
|
|
get columns() {
|
const cols = self.columnsTargetMap ?? new Map();
|
return cols.get(self.selected?.target ?? "tasks") ?? [];
|
},
|
|
get dataStore() {
|
return getRoot(self).dataStore;
|
},
|
|
get taskStore() {
|
return getRoot(self).taskStore;
|
},
|
|
get annotationStore() {
|
return getRoot(self).annotationStore;
|
},
|
|
get lastView() {
|
return self.views[self.views.length - 1];
|
},
|
|
serialize() {
|
return self.views.map((v) => v.serialize());
|
},
|
}))
|
.actions((self) => ({
|
setSelected: flow(function* (view, options = {}) {
|
let selected;
|
|
if (typeof view === "string") {
|
selected = yield self.getViewByKey(view);
|
} else if (typeof view === "number") {
|
selected = self.views.find((v) => v.id === view);
|
} else if (view && view.id) {
|
selected = self.views.find((v) => v.id === view.id);
|
}
|
if (!selected) {
|
selected = self.views[0];
|
}
|
|
if (self.views.length === 0 && options.createDefault !== false) {
|
view = null;
|
yield self.createDefaultView();
|
}
|
|
if (selected && self.selected !== selected) {
|
if (options.pushState !== false || !view) {
|
History.navigate({ tab: selected.tabKey }, true);
|
}
|
|
self.dataStore.clear();
|
self.selected = selected;
|
|
yield selected.reload();
|
|
const root = getRoot(self);
|
|
root.SDK.invoke("tabChanged", selected);
|
selected.selected._invokeChangeEvent();
|
}
|
}),
|
|
deleteView: flow(function* (view, { autoselect = true } = {}) {
|
if (autoselect && self.selected === view) {
|
let newView;
|
if (self.selected.opener) {
|
newView = self.opener.referrer;
|
} else {
|
const index = self.views.indexOf(view);
|
|
newView = index === 0 ? self.views[index + 1] : self.views[index - 1];
|
}
|
|
yield self.setSelected(newView.key);
|
}
|
|
if (view.saved) {
|
yield getRoot(self).apiCall("deleteTab", { tabID: view.id });
|
}
|
|
destroy(view);
|
}),
|
|
createSnapshot(viewSnapshot = {}) {
|
const isVirtual = !!viewSnapshot?.virtual;
|
const tabStorageKey = isVirtual && viewSnapshot.projectId ? `virtual-tab-${viewSnapshot.projectId}` : null;
|
const existingTabStorage = isVirtual && localStorage.getItem(tabStorageKey);
|
const existingTabStorageParsed = existingTabStorage ? JSON.parse(existingTabStorage) : null;
|
const urlTabIsVirtualCandidate = !!(viewSnapshot?.tab && isNaN(viewSnapshot.tab));
|
const existingTabUrlParsed =
|
isVirtual && urlTabIsVirtualCandidate ? self.snapshotFromUrl(viewSnapshot.tab) : null;
|
const urlTabNotEmpty = !isEmpty(existingTabUrlParsed);
|
const existingTab = urlTabNotEmpty ? existingTabUrlParsed : existingTabStorageParsed;
|
const existingTabKey = urlTabNotEmpty ? viewSnapshot.tab : existingTabStorageParsed?.tab;
|
const snapshot = {
|
...viewSnapshot,
|
key: existingTabKey,
|
tab: existingTabKey,
|
...(existingTab ?? viewSnapshot ?? {}),
|
};
|
const lastView = self.views[self.views.length - 1];
|
const newTitle = snapshot.title ?? `New Tab ${self.views.length + 1}`;
|
const newID = snapshot.id ?? (lastView?.id ? lastView.id + 1 : 0);
|
|
const defaultHiddenColumns = self.defaultHidden
|
? clone(self.defaultHidden)
|
: {
|
explore: [],
|
labeling: [],
|
};
|
|
return {
|
...snapshot,
|
id: newID,
|
title: newTitle,
|
key: snapshot.key ?? guidGenerator(),
|
hiddenColumns: snapshot.hiddenColumns ?? defaultHiddenColumns,
|
};
|
},
|
|
addView: flow(function* (viewSnapshot = {}, options) {
|
const { autoselect = true, autosave = true, reload = true } = options ?? {};
|
|
const newSnapshot = self.createSnapshot(viewSnapshot);
|
|
self.views.push(newSnapshot);
|
const newView = self.views[self.views.length - 1];
|
|
if (autosave) {
|
// with autosave it will be reloaded anyway
|
yield newView.save({ reload: !autosave && reload });
|
}
|
|
if (autoselect) {
|
const selectedView = self.views[self.views.length - 1];
|
|
self.setSelected(selectedView);
|
}
|
|
return newView;
|
}),
|
|
getViewByKey: flow(function* (key) {
|
const view = self.views.find((v) => v.key === key);
|
|
if (view) return view;
|
const viewSnapshot = self.snapshotFromUrl(key);
|
|
if (!viewSnapshot) return null;
|
|
return yield self.addVirtualView(viewSnapshot);
|
}),
|
|
addVirtualView: flow(function* (viewSnapshot) {
|
return yield self.addView(viewSnapshot, {
|
autosave: false,
|
// No need to select 'cause it's a selecting phase
|
autoselect: false,
|
});
|
}),
|
|
createDefaultView: flow(function* () {
|
self.views.push({
|
id: 0,
|
title: "Default",
|
hiddenColumns: self.defaultHidden,
|
});
|
|
let defaultView = self.views[self.views.length - 1];
|
|
yield defaultView.save(defaultView);
|
|
// at this point newly created tab does not exist
|
// so we need to take in from the list once again
|
defaultView = self.views[self.views.length - 1];
|
self.selected = defaultView;
|
getRoot(self).SDK.hasInterface("tabs") && defaultView.reload();
|
}),
|
|
snapshotFromUrl(viewQueryParam) {
|
try {
|
const viewSnapshot = deserializeJsonFromUrl(viewQueryParam);
|
|
viewSnapshot.key = viewQueryParam;
|
viewSnapshot.virtual = true;
|
return viewSnapshot;
|
} catch {
|
return null;
|
}
|
},
|
|
snapshotToUrl(snapshot) {
|
return serializeJsonForUrl(snapshot);
|
},
|
|
saveView: flow(function* (view, { reload, interaction } = {}) {
|
const needsLock = ["ordering", "filter"].includes(interaction);
|
|
if (needsLock) view.lock();
|
const { id: tabID } = view;
|
const body = { body: view.snapshot };
|
const params = { tabID };
|
|
if (interaction !== undefined) Object.assign(params, { interaction });
|
|
const root = getRoot(self);
|
const apiMethod = !view.saved && root.apiVersion === 2 ? "createTab" : "updateTab";
|
|
const result = yield root.apiCall(apiMethod, params, body, { allowToCancel: root.SDK.type === "DE" });
|
|
if (result.isCanceled) {
|
return view;
|
}
|
const viewSnapshot = getSnapshot(view);
|
const newViewSnapshot = {
|
...viewSnapshot,
|
...result,
|
saved: true,
|
filters: viewSnapshot.filters,
|
conjunction: viewSnapshot.conjunction,
|
};
|
|
if (result.id !== view.id) {
|
self.views.push({ ...newViewSnapshot, saved: true });
|
const newView = self.views[self.views.length - 1];
|
|
root.SDK.hasInterface("tabs") && newView.reload();
|
self.setSelected(newView);
|
destroy(view);
|
|
return newView;
|
}
|
applySnapshot(view, newViewSnapshot);
|
|
if (reload !== false) {
|
view.reload({ interaction });
|
}
|
|
view.unlock();
|
return view;
|
}),
|
|
updateViewOrder: flow(function* (source, destination) {
|
// Detach the view from the original position
|
const [removed] = self.views.splice(source, 1);
|
const sn = getSnapshot(removed);
|
|
// Insert the view at the new position
|
self.views.splice(destination, 0, sn);
|
|
const idList = {
|
project: getRoot(self).project.id,
|
ids: self.views.map((obj) => {
|
return obj.id;
|
}),
|
};
|
|
getRoot(self).apiCall("orderTab", {}, { body: idList }, { alwaysExpectJSON: false });
|
}),
|
duplicateView: flow(function* (view) {
|
const sn = getSnapshot(view);
|
|
self.views.push({
|
...sn,
|
id: Number.MAX_SAFE_INTEGER,
|
saved: false,
|
key: guidGenerator(),
|
title: createNameCopy(sn.title),
|
});
|
|
const newView = self.views[self.views.length - 1];
|
|
yield newView.save();
|
self.selected = self.views[self.views.length - 1];
|
self.selected.reload();
|
}),
|
|
createView(viewSnapshot) {
|
return Tab.create(viewSnapshot ?? {});
|
},
|
|
expandFilters() {
|
self.sidebarEnabled = storeValue("sidebarEnabled", true);
|
self.sidebarVisible = storeValue("sidebarVisible", true);
|
},
|
|
collapseFilters() {
|
self.sidebarEnabled = storeValue("sidebarEnabled", false);
|
self.sidebarVisible = storeValue("sidebarVisible", false);
|
},
|
|
toggleSidebar() {
|
self.sidebarVisible = storeValue("sidebarVisible", !self.sidebarVisible);
|
},
|
|
fetchColumns() {
|
const columns = self.columnsRaw;
|
const targets = unique(columns.map((c) => c.target));
|
const hiddenColumns = {};
|
const addedColumns = new Set();
|
|
const createColumnPath = (columns, column) => {
|
const result = [];
|
|
if (column && column.parent) {
|
const parentColums = columns.find((c) => {
|
return !c.parent && c.id === column.parent && c.target === column.target;
|
});
|
|
result.push(createColumnPath(columns, parentColums).columnPath);
|
}
|
|
const parentPath = result.join(".");
|
|
if (isDefined(column?.id)) {
|
result.push(column.id);
|
} else {
|
console.warn("Column or id is not defined", column);
|
console.warn("Columns", columns);
|
}
|
|
const columnPath = result.join(".");
|
|
return { parentPath, columnPath };
|
};
|
|
targets.forEach((target) => {
|
self.columnsTargetMap.set(target, []);
|
});
|
|
columns.forEach((col) => {
|
if (!isDefined(col)) return;
|
const { columnPath, parentPath } = createColumnPath(columns, col);
|
|
const { target, visibility_defaults: visibility } = col;
|
|
const columnID = `${target}:${columnPath}`;
|
|
if (addedColumns.has(columnID)) return;
|
|
const parent = parentPath ? `${target}:${parentPath}` : undefined;
|
|
const children = col.children ? col.children.map((ch) => `${target}:${columnPath}.${ch}`) : undefined;
|
|
const colsList = self.columnsTargetMap.get(col.target);
|
|
colsList.push({
|
...col,
|
id: columnID,
|
alias: col.id,
|
parent,
|
children,
|
});
|
|
const column = colsList[colsList.length - 1];
|
|
addedColumns.add(column.id);
|
|
if (!col.children && column.filterable && (col?.visibility_defaults?.filter ?? true)) {
|
self.availableFilters.push({
|
id: `filter:${columnID}`,
|
type: col.type,
|
field: columnID,
|
schema: col.schema ?? null,
|
});
|
}
|
|
Object.entries(visibility ?? {}).forEach(([key, visible]) => {
|
if (!visible) {
|
hiddenColumns[key] = hiddenColumns[key] ?? [];
|
hiddenColumns[key].push(column.id);
|
}
|
});
|
});
|
|
self.defaultHidden = TabHiddenColumns.create(hiddenColumns);
|
},
|
|
fetchTabs: flow(function* (tab, taskID, labeling) {
|
const tabId = Number.parseInt(tab);
|
const response = yield getRoot(self).apiCall("tabs");
|
const tabs = response.tabs ?? response ?? [];
|
|
const snapshots = tabs.map((t) => {
|
const { data, ...tab } = dataCleanup(t, self.columns ?? []);
|
|
return {
|
...tab,
|
...(data ?? {}),
|
saved: true,
|
hasData: !!data,
|
};
|
});
|
|
self.views.push(...snapshots);
|
|
yield self.setSelected(Number.isNaN(tabId) ? tab : tabId, {
|
pushState: tab === undefined,
|
});
|
|
yield self.selected?.save();
|
|
if (labeling) {
|
getRoot(self).startLabelStream({
|
pushState: false,
|
});
|
} else if (isDefined(taskID)) {
|
const task = { id: Number.parseInt(taskID) };
|
|
getRoot(self).startLabeling(task, {
|
pushState: false,
|
});
|
}
|
}),
|
|
fetchSingleTab: flow(function* (tabKey, selectedItems) {
|
let tab;
|
const tabId = Number.parseInt(tabKey);
|
|
if (!isNaN(tabKey) && !isNaN(tabId)) {
|
const tabData = yield getRoot(self).apiCall("tab", { tabId });
|
const { data, ...tabClean } = dataCleanup(tabData, self.columns ?? []);
|
|
self.views.push({
|
...tabClean,
|
...(data ?? {}),
|
selected: {
|
all: selectedItems?.all,
|
list: selectedItems.included ?? selectedItems.excluded ?? [],
|
},
|
saved: true,
|
hasData: !!data,
|
});
|
tab = self.views[self.views.length - 1];
|
} else {
|
const viewSnapshot = {
|
key: tabKey,
|
virtual: true,
|
title: tabKey,
|
selected: {
|
all: selectedItems?.all,
|
list: selectedItems?.included ?? selectedItems?.excluded ?? [],
|
},
|
};
|
tab = yield self.addVirtualView(viewSnapshot);
|
}
|
|
self.selected = tab;
|
}),
|
}));
|