@morscherlab/mint-sdk 1.0.12 → 1.0.14
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/{ExperimentPopover-CCYB1oWp.js → ExperimentPopover-B29fIHQz.js} +6 -6
- package/dist/ExperimentPopover-B29fIHQz.js.map +1 -0
- package/dist/{ExperimentPopover-D0bg_fqM.js → ExperimentPopover-gdSA9ZCF.js} +1 -1
- package/dist/{ExperimentSelectorModal-DeBE0YKT.js → ExperimentSelectorModal-CHsU-LIh.js} +3 -3
- package/dist/{ExperimentSelectorModal-DeBE0YKT.js.map → ExperimentSelectorModal-CHsU-LIh.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-BXf-XnQw.js → ExperimentSelectorModal-DIFyL5ta.js} +1 -1
- package/dist/__tests__/composables/useCommandHistory.test.d.ts +1 -0
- package/dist/__tests__/composables/useFileImport.test.d.ts +1 -0
- package/dist/__tests__/composables/useOptimisticMutation.test.d.ts +1 -0
- package/dist/__tests__/composables/useResourceCrud.test.d.ts +1 -0
- package/dist/__tests__/composables/useWellPainting.test.d.ts +1 -0
- package/dist/__tests__/composables/useWellPlateAdapter.test.d.ts +1 -0
- package/dist/__tests__/composables/useWellPlateValidation.test.d.ts +1 -0
- package/dist/components/index.js +3 -3
- package/dist/{components-CqdBz8DI.js → components-Dq02EVZH.js} +10 -10
- package/dist/components-Dq02EVZH.js.map +1 -0
- package/dist/composables/index.d.ts +7 -0
- package/dist/composables/index.js +5 -5
- package/dist/composables/useCommandHistory.d.ts +29 -0
- package/dist/composables/useFileImport.d.ts +35 -0
- package/dist/composables/useOptimisticMutation.d.ts +28 -0
- package/dist/composables/useResourceCrud.d.ts +40 -0
- package/dist/composables/useWellPainting.d.ts +35 -0
- package/dist/composables/useWellPlateAdapter.d.ts +36 -0
- package/dist/composables/useWellPlateValidation.d.ts +27 -0
- package/dist/{composables-BWh0MpcK.js → composables-D9mexHSW.js} +668 -5
- package/dist/composables-D9mexHSW.js.map +1 -0
- package/dist/{experiment-utils-hGXMHlAc.js → experiment-utils-D11yT3AR.js} +1 -2
- package/dist/{experiment-utils-hGXMHlAc.js.map → experiment-utils-D11yT3AR.js.map} +1 -1
- package/dist/index.js +8 -8
- package/dist/install.js +3 -3
- package/dist/styles.css +4 -8
- package/dist/{useExperimentSelector-B3hAGvL4.js → useExperimentSelector-BBaz0w51.js} +2 -2
- package/dist/{useExperimentSelector-B3hAGvL4.js.map → useExperimentSelector-BBaz0w51.js.map} +1 -1
- package/dist/{useProtocolTemplates-BJxS5F0_.js → useProtocolTemplates-DODHlhxr.js} +3 -3
- package/dist/{useProtocolTemplates-BJxS5F0_.js.map → useProtocolTemplates-DODHlhxr.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/ExperimentPopover.test.ts +23 -0
- package/src/__tests__/composables/experiment-utils.test.ts +2 -2
- package/src/__tests__/composables/useAppExperiment.test.ts +2 -1
- package/src/__tests__/composables/useCommandHistory.test.ts +43 -0
- package/src/__tests__/composables/useFileImport.test.ts +44 -0
- package/src/__tests__/composables/useOptimisticMutation.test.ts +40 -0
- package/src/__tests__/composables/useResourceCrud.test.ts +56 -0
- package/src/__tests__/composables/useWellPainting.test.ts +52 -0
- package/src/__tests__/composables/useWellPlateAdapter.test.ts +50 -0
- package/src/__tests__/composables/useWellPlateValidation.test.ts +42 -0
- package/src/components/ExperimentPopover.story.vue +3 -4
- package/src/components/ExperimentPopover.vue +6 -6
- package/src/components/PluginWorkspaceView.vue +3 -3
- package/src/composables/experiment-utils.ts +1 -1
- package/src/composables/index.ts +67 -0
- package/src/composables/useCommandHistory.ts +113 -0
- package/src/composables/useFileImport.ts +231 -0
- package/src/composables/useOptimisticMutation.ts +107 -0
- package/src/composables/useResourceCrud.ts +245 -0
- package/src/composables/useWellPainting.ts +187 -0
- package/src/composables/useWellPlateAdapter.ts +147 -0
- package/src/composables/useWellPlateValidation.ts +85 -0
- package/src/styles/components/experiment-popover.css +2 -4
- package/dist/ExperimentPopover-CCYB1oWp.js.map +0 -1
- package/dist/components-CqdBz8DI.js.map +0 -1
- package/dist/composables-BWh0MpcK.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { S as resolveCurrentExperimentId, b as currentExperimentFromContext, x as getInjectedPlatformContext } from "./useProtocolTemplates-
|
|
1
|
+
import { S as resolveCurrentExperimentId, b as currentExperimentFromContext, x as getInjectedPlatformContext } from "./useProtocolTemplates-DODHlhxr.js";
|
|
2
2
|
import { r as useSettingsStore, t as useAuthStore } from "./auth-D9q2GIcv.js";
|
|
3
3
|
import { i as usePlatformContext } from "./useFormBuilder-COfYWDuC.js";
|
|
4
|
-
import { i as useApi, r as useRequestSyncState } from "./useExperimentSelector-
|
|
5
|
-
import { computed, getCurrentInstance, onMounted, onUnmounted, ref, shallowRef, watch } from "vue";
|
|
4
|
+
import { i as useApi, r as useRequestSyncState } from "./useExperimentSelector-BBaz0w51.js";
|
|
5
|
+
import { computed, getCurrentInstance, onMounted, onUnmounted, ref, shallowRef, toValue, watch } from "vue";
|
|
6
6
|
import axios from "axios";
|
|
7
7
|
//#region src/composables/useAuth.ts
|
|
8
8
|
var TOKEN_REFRESH_MARGIN_MS = 300 * 1e3;
|
|
@@ -665,6 +665,669 @@ function usePluginConfig(pluginName) {
|
|
|
665
665
|
};
|
|
666
666
|
}
|
|
667
667
|
//#endregion
|
|
668
|
+
//#region src/composables/useCommandHistory.ts
|
|
669
|
+
/** Generic command-stack state for plugin editors with undo/redo controls. */
|
|
670
|
+
function useCommandHistory(options = {}) {
|
|
671
|
+
const maxDepth = Math.max(1, options.maxDepth ?? 50);
|
|
672
|
+
const undoStack = ref([]);
|
|
673
|
+
const redoStack = ref([]);
|
|
674
|
+
const canUndo = computed(() => undoStack.value.length > 0);
|
|
675
|
+
const canRedo = computed(() => redoStack.value.length > 0);
|
|
676
|
+
const undoDescription = computed(() => undoStack.value[undoStack.value.length - 1]?.description ?? "");
|
|
677
|
+
const redoDescription = computed(() => redoStack.value[redoStack.value.length - 1]?.description ?? "");
|
|
678
|
+
function push(command) {
|
|
679
|
+
undoStack.value = [...undoStack.value.slice(-(maxDepth - 1)), command];
|
|
680
|
+
redoStack.value = [];
|
|
681
|
+
}
|
|
682
|
+
function execute(command, executeOptions = {}) {
|
|
683
|
+
if (executeOptions.skipExecute) {
|
|
684
|
+
push(command);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
return afterMaybePromise(command.execute(), () => push(command));
|
|
688
|
+
}
|
|
689
|
+
function undo() {
|
|
690
|
+
const command = undoStack.value[undoStack.value.length - 1];
|
|
691
|
+
if (!command) return;
|
|
692
|
+
return afterMaybePromise(command.undo(), () => {
|
|
693
|
+
undoStack.value = undoStack.value.slice(0, -1);
|
|
694
|
+
redoStack.value = [...redoStack.value, command];
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function redo() {
|
|
698
|
+
const command = redoStack.value[redoStack.value.length - 1];
|
|
699
|
+
if (!command) return;
|
|
700
|
+
return afterMaybePromise((command.redo ?? command.execute)(), () => {
|
|
701
|
+
redoStack.value = redoStack.value.slice(0, -1);
|
|
702
|
+
undoStack.value = [...undoStack.value.slice(-(maxDepth - 1)), command];
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function clear() {
|
|
706
|
+
undoStack.value = [];
|
|
707
|
+
redoStack.value = [];
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
undoStack,
|
|
711
|
+
redoStack,
|
|
712
|
+
canUndo,
|
|
713
|
+
canRedo,
|
|
714
|
+
undoDescription,
|
|
715
|
+
redoDescription,
|
|
716
|
+
execute,
|
|
717
|
+
push,
|
|
718
|
+
undo,
|
|
719
|
+
redo,
|
|
720
|
+
clear
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function afterMaybePromise(result, callback) {
|
|
724
|
+
if (isPromiseLike(result)) return result.then(callback);
|
|
725
|
+
callback();
|
|
726
|
+
}
|
|
727
|
+
function isPromiseLike(value) {
|
|
728
|
+
return !!value && typeof value.then === "function";
|
|
729
|
+
}
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/composables/useOptimisticMutation.ts
|
|
732
|
+
/** Run async mutations with optional optimistic local updates and rollback. */
|
|
733
|
+
function useOptimisticMutation(options) {
|
|
734
|
+
const loading = shallowRef(false);
|
|
735
|
+
const error = ref(null);
|
|
736
|
+
const data = ref(null);
|
|
737
|
+
const lastSnapshot = ref(void 0);
|
|
738
|
+
const isIdle = computed(() => !loading.value && data.value === null && error.value === null);
|
|
739
|
+
function clearError() {
|
|
740
|
+
error.value = null;
|
|
741
|
+
}
|
|
742
|
+
function reset() {
|
|
743
|
+
loading.value = false;
|
|
744
|
+
error.value = null;
|
|
745
|
+
data.value = null;
|
|
746
|
+
lastSnapshot.value = void 0;
|
|
747
|
+
}
|
|
748
|
+
async function run(variables) {
|
|
749
|
+
const snapshot = options.snapshot?.(variables);
|
|
750
|
+
const context = {
|
|
751
|
+
variables,
|
|
752
|
+
snapshot
|
|
753
|
+
};
|
|
754
|
+
loading.value = true;
|
|
755
|
+
error.value = null;
|
|
756
|
+
lastSnapshot.value = snapshot;
|
|
757
|
+
options.apply?.(variables, snapshot);
|
|
758
|
+
try {
|
|
759
|
+
const result = await options.mutation(variables);
|
|
760
|
+
data.value = result;
|
|
761
|
+
options.commit?.(result, context);
|
|
762
|
+
options.onSuccess?.(result, context);
|
|
763
|
+
return result;
|
|
764
|
+
} catch (err) {
|
|
765
|
+
options.rollback?.(snapshot, variables, err);
|
|
766
|
+
error.value = options.readErrorMessage?.(err) ?? readErrorMessage$2(err);
|
|
767
|
+
options.onError?.(err, context);
|
|
768
|
+
throw err;
|
|
769
|
+
} finally {
|
|
770
|
+
loading.value = false;
|
|
771
|
+
options.onSettled?.(context);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
loading,
|
|
776
|
+
error,
|
|
777
|
+
data,
|
|
778
|
+
lastSnapshot,
|
|
779
|
+
isIdle,
|
|
780
|
+
run,
|
|
781
|
+
clearError,
|
|
782
|
+
reset
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function readErrorMessage$2(value) {
|
|
786
|
+
if (value instanceof Error && value.message) return value.message;
|
|
787
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
788
|
+
if (typeof value === "object" && value !== null && "message" in value && typeof value.message === "string" && value.message.trim()) return value.message;
|
|
789
|
+
return "Request failed.";
|
|
790
|
+
}
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/composables/useResourceCrud.ts
|
|
793
|
+
/** Common list/create/update/delete state wrapper for generated plugin clients. */
|
|
794
|
+
function useResourceCrud(options) {
|
|
795
|
+
const items = ref([...options.initialItems ?? []]);
|
|
796
|
+
const loading = shallowRef(false);
|
|
797
|
+
const saving = shallowRef(false);
|
|
798
|
+
const deleting = shallowRef(false);
|
|
799
|
+
const error = ref(null);
|
|
800
|
+
const isBusy = computed(() => loading.value || saving.value || deleting.value);
|
|
801
|
+
const readMessage = options.readErrorMessage ?? readErrorMessage$1;
|
|
802
|
+
const getKey = options.getKey ?? defaultGetKey;
|
|
803
|
+
function clearError() {
|
|
804
|
+
error.value = null;
|
|
805
|
+
}
|
|
806
|
+
function setError(err) {
|
|
807
|
+
error.value = readMessage(err);
|
|
808
|
+
}
|
|
809
|
+
function setItems(nextItems) {
|
|
810
|
+
items.value = [...nextItems];
|
|
811
|
+
}
|
|
812
|
+
function upsertLocal(item) {
|
|
813
|
+
const key = getKey(item);
|
|
814
|
+
const index = items.value.findIndex((existing) => getKey(existing) === key);
|
|
815
|
+
if (index === -1) {
|
|
816
|
+
items.value = [...items.value, item];
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
items.value = [
|
|
820
|
+
...items.value.slice(0, index),
|
|
821
|
+
item,
|
|
822
|
+
...items.value.slice(index + 1)
|
|
823
|
+
];
|
|
824
|
+
}
|
|
825
|
+
function removeLocal(keyOrItem) {
|
|
826
|
+
const key = resolveKey(keyOrItem, getKey);
|
|
827
|
+
items.value = items.value.filter((item) => getKey(item) !== key);
|
|
828
|
+
}
|
|
829
|
+
async function load() {
|
|
830
|
+
loading.value = true;
|
|
831
|
+
clearError();
|
|
832
|
+
try {
|
|
833
|
+
const result = await options.adapter.list();
|
|
834
|
+
setItems(result);
|
|
835
|
+
return result;
|
|
836
|
+
} catch (err) {
|
|
837
|
+
setError(err);
|
|
838
|
+
throw err;
|
|
839
|
+
} finally {
|
|
840
|
+
loading.value = false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function createItem(payload) {
|
|
844
|
+
if (!options.adapter.create) throw new Error("[MINT SDK] Resource adapter does not implement create().");
|
|
845
|
+
saving.value = true;
|
|
846
|
+
clearError();
|
|
847
|
+
try {
|
|
848
|
+
const result = await options.adapter.create(payload);
|
|
849
|
+
upsertLocal(result);
|
|
850
|
+
return result;
|
|
851
|
+
} catch (err) {
|
|
852
|
+
setError(err);
|
|
853
|
+
throw err;
|
|
854
|
+
} finally {
|
|
855
|
+
saving.value = false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async function updateItem(key, payload) {
|
|
859
|
+
if (!options.adapter.update) throw new Error("[MINT SDK] Resource adapter does not implement update().");
|
|
860
|
+
saving.value = true;
|
|
861
|
+
clearError();
|
|
862
|
+
try {
|
|
863
|
+
const result = await options.adapter.update(key, payload);
|
|
864
|
+
upsertLocal(result);
|
|
865
|
+
return result;
|
|
866
|
+
} catch (err) {
|
|
867
|
+
setError(err);
|
|
868
|
+
throw err;
|
|
869
|
+
} finally {
|
|
870
|
+
saving.value = false;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function deleteItem(keyOrItem) {
|
|
874
|
+
if (!options.adapter.remove) throw new Error("[MINT SDK] Resource adapter does not implement remove().");
|
|
875
|
+
const key = resolveKey(keyOrItem, getKey);
|
|
876
|
+
deleting.value = true;
|
|
877
|
+
clearError();
|
|
878
|
+
try {
|
|
879
|
+
await options.adapter.remove(key);
|
|
880
|
+
removeLocal(key);
|
|
881
|
+
} catch (err) {
|
|
882
|
+
setError(err);
|
|
883
|
+
throw err;
|
|
884
|
+
} finally {
|
|
885
|
+
deleting.value = false;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
items,
|
|
890
|
+
loading,
|
|
891
|
+
saving,
|
|
892
|
+
deleting,
|
|
893
|
+
error,
|
|
894
|
+
isBusy,
|
|
895
|
+
load,
|
|
896
|
+
createItem,
|
|
897
|
+
updateItem,
|
|
898
|
+
deleteItem,
|
|
899
|
+
setItems,
|
|
900
|
+
upsertLocal,
|
|
901
|
+
removeLocal,
|
|
902
|
+
clearError
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function createPluginResourceClient(options) {
|
|
906
|
+
return {
|
|
907
|
+
list: () => options.list(options.client),
|
|
908
|
+
create: options.create ? (payload) => options.create(options.client, payload) : void 0,
|
|
909
|
+
update: options.update ? (key, payload) => options.update(options.client, key, payload) : void 0,
|
|
910
|
+
remove: options.remove ? (key) => options.remove(options.client, key) : void 0
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function defaultGetKey(item) {
|
|
914
|
+
if (item && typeof item === "object" && "id" in item) return item.id;
|
|
915
|
+
throw new Error("[MINT SDK] Resource items need an id field or a getKey option.");
|
|
916
|
+
}
|
|
917
|
+
function resolveKey(keyOrItem, getKey) {
|
|
918
|
+
if (typeof keyOrItem === "string" || typeof keyOrItem === "number") return keyOrItem;
|
|
919
|
+
return getKey(keyOrItem);
|
|
920
|
+
}
|
|
921
|
+
function readErrorMessage$1(value) {
|
|
922
|
+
if (value instanceof Error && value.message) return value.message;
|
|
923
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
924
|
+
if (typeof value === "object" && value !== null && "message" in value && typeof value.message === "string" && value.message.trim()) return value.message;
|
|
925
|
+
return "Request failed.";
|
|
926
|
+
}
|
|
927
|
+
//#endregion
|
|
928
|
+
//#region src/composables/useFileImport.ts
|
|
929
|
+
/** Stateful file reader for plugin import flows with shared validation and error handling. */
|
|
930
|
+
function useFileImport(options = {}) {
|
|
931
|
+
const loading = shallowRef(false);
|
|
932
|
+
const error = ref(null);
|
|
933
|
+
const fileName = ref("");
|
|
934
|
+
const result = ref(null);
|
|
935
|
+
function clear() {
|
|
936
|
+
loading.value = false;
|
|
937
|
+
error.value = null;
|
|
938
|
+
fileName.value = "";
|
|
939
|
+
result.value = null;
|
|
940
|
+
}
|
|
941
|
+
async function importFile(file) {
|
|
942
|
+
loading.value = true;
|
|
943
|
+
error.value = null;
|
|
944
|
+
fileName.value = file.name;
|
|
945
|
+
try {
|
|
946
|
+
validateImportFile(file, options);
|
|
947
|
+
const text = await readFileAsText(file, options.encoding);
|
|
948
|
+
const parsed = options.parse ? await options.parse(text, file) : text;
|
|
949
|
+
result.value = parsed;
|
|
950
|
+
return parsed;
|
|
951
|
+
} catch (err) {
|
|
952
|
+
error.value = readErrorMessage(err);
|
|
953
|
+
throw err;
|
|
954
|
+
} finally {
|
|
955
|
+
loading.value = false;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
loading,
|
|
960
|
+
error,
|
|
961
|
+
fileName,
|
|
962
|
+
result,
|
|
963
|
+
importFile,
|
|
964
|
+
clear
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function validateImportFile(file, options = {}) {
|
|
968
|
+
if (options.maxSizeBytes !== void 0 && file.size > options.maxSizeBytes) throw new Error(`File is too large. Maximum size is ${formatBytes(options.maxSizeBytes)}.`);
|
|
969
|
+
const accept = normalizeAccept(options.accept);
|
|
970
|
+
if (accept.length === 0) return;
|
|
971
|
+
const name = file.name.toLowerCase();
|
|
972
|
+
const type = file.type.toLowerCase();
|
|
973
|
+
if (!accept.some((entry) => {
|
|
974
|
+
if (entry.startsWith(".")) return name.endsWith(entry);
|
|
975
|
+
if (entry.endsWith("/*")) return type.startsWith(entry.slice(0, -1));
|
|
976
|
+
return type === entry;
|
|
977
|
+
})) throw new Error(`Unsupported file type. Expected ${accept.join(", ")}.`);
|
|
978
|
+
}
|
|
979
|
+
function readFileAsText(file, encoding) {
|
|
980
|
+
if (typeof FileReader === "undefined" && typeof file.text === "function") return file.text();
|
|
981
|
+
return new Promise((resolve, reject) => {
|
|
982
|
+
const reader = new FileReader();
|
|
983
|
+
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
984
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("Failed to read file."));
|
|
985
|
+
reader.readAsText(file, encoding);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
function parseDelimitedText(text, options = {}) {
|
|
989
|
+
const trim = options.trim ?? true;
|
|
990
|
+
const delimiter = options.delimiter ?? detectDelimiter(text);
|
|
991
|
+
const rawRows = parseDelimitedRows(text, delimiter).filter((row) => row.some((cell) => cell.trim().length > 0)).map((row) => trim ? row.map((cell) => cell.trim()) : row);
|
|
992
|
+
const hasHeaderRow = options.hasHeaderRow ?? !options.headers;
|
|
993
|
+
const columns = options.headers ? [...options.headers] : hasHeaderRow ? rawRows[0] ?? [] : createColumnNames(maxRowLength(rawRows));
|
|
994
|
+
return {
|
|
995
|
+
columns,
|
|
996
|
+
rows: (hasHeaderRow && !options.headers ? rawRows.slice(1) : rawRows).map((row) => Object.fromEntries(columns.map((column, index) => [column, row[index] ?? ""]))),
|
|
997
|
+
rawRows,
|
|
998
|
+
delimiter
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function detectDelimiter(text) {
|
|
1002
|
+
const firstLine = text.split(/\r?\n/, 1)[0] ?? "";
|
|
1003
|
+
const commaCount = countChar(firstLine, ",");
|
|
1004
|
+
const tabCount = countChar(firstLine, " ");
|
|
1005
|
+
const semicolonCount = countChar(firstLine, ";");
|
|
1006
|
+
if (tabCount > commaCount && tabCount >= semicolonCount) return " ";
|
|
1007
|
+
if (semicolonCount > commaCount) return ";";
|
|
1008
|
+
return ",";
|
|
1009
|
+
}
|
|
1010
|
+
function parseDelimitedRows(text, delimiter) {
|
|
1011
|
+
const rows = [];
|
|
1012
|
+
let row = [];
|
|
1013
|
+
let field = "";
|
|
1014
|
+
let quoted = false;
|
|
1015
|
+
for (let index = 0; index < text.length; index++) {
|
|
1016
|
+
const char = text[index];
|
|
1017
|
+
const next = text[index + 1];
|
|
1018
|
+
if (char === "\"") {
|
|
1019
|
+
if (quoted && next === "\"") {
|
|
1020
|
+
field += "\"";
|
|
1021
|
+
index++;
|
|
1022
|
+
} else quoted = !quoted;
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (!quoted && char === delimiter) {
|
|
1026
|
+
row.push(field);
|
|
1027
|
+
field = "";
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (!quoted && (char === "\n" || char === "\r")) {
|
|
1031
|
+
if (char === "\r" && next === "\n") index++;
|
|
1032
|
+
row.push(field);
|
|
1033
|
+
rows.push(row);
|
|
1034
|
+
row = [];
|
|
1035
|
+
field = "";
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
field += char;
|
|
1039
|
+
}
|
|
1040
|
+
row.push(field);
|
|
1041
|
+
rows.push(row);
|
|
1042
|
+
return rows;
|
|
1043
|
+
}
|
|
1044
|
+
function normalizeAccept(accept) {
|
|
1045
|
+
if (!accept) return [];
|
|
1046
|
+
return (typeof accept === "string" ? accept.split(",") : [...accept]).map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
1047
|
+
}
|
|
1048
|
+
function createColumnNames(count) {
|
|
1049
|
+
return Array.from({ length: count }, (_, index) => `column_${index + 1}`);
|
|
1050
|
+
}
|
|
1051
|
+
function maxRowLength(rows) {
|
|
1052
|
+
return rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
1053
|
+
}
|
|
1054
|
+
function countChar(value, char) {
|
|
1055
|
+
return [...value].filter((candidate) => candidate === char).length;
|
|
1056
|
+
}
|
|
1057
|
+
function formatBytes(bytes) {
|
|
1058
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1059
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
1060
|
+
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
1061
|
+
}
|
|
1062
|
+
function readErrorMessage(value) {
|
|
1063
|
+
if (value instanceof Error && value.message) return value.message;
|
|
1064
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
1065
|
+
return "Failed to import file.";
|
|
1066
|
+
}
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region src/composables/useWellPainting.ts
|
|
1069
|
+
var ROW_LABELS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
1070
|
+
var PLATE_DIMENSIONS = {
|
|
1071
|
+
6: {
|
|
1072
|
+
rows: 2,
|
|
1073
|
+
cols: 3
|
|
1074
|
+
},
|
|
1075
|
+
12: {
|
|
1076
|
+
rows: 3,
|
|
1077
|
+
cols: 4
|
|
1078
|
+
},
|
|
1079
|
+
24: {
|
|
1080
|
+
rows: 4,
|
|
1081
|
+
cols: 6
|
|
1082
|
+
},
|
|
1083
|
+
48: {
|
|
1084
|
+
rows: 6,
|
|
1085
|
+
cols: 8
|
|
1086
|
+
},
|
|
1087
|
+
54: {
|
|
1088
|
+
rows: 6,
|
|
1089
|
+
cols: 9
|
|
1090
|
+
},
|
|
1091
|
+
96: {
|
|
1092
|
+
rows: 8,
|
|
1093
|
+
cols: 12
|
|
1094
|
+
},
|
|
1095
|
+
384: {
|
|
1096
|
+
rows: 16,
|
|
1097
|
+
cols: 24
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
/** Pointer painting/rectangle selection state for WellPlate-based editors. */
|
|
1101
|
+
function useWellPainting(options) {
|
|
1102
|
+
const activeMode = ref(options.initialMode ?? "select");
|
|
1103
|
+
const activePayload = ref(options.initialPayload);
|
|
1104
|
+
const isDragging = ref(false);
|
|
1105
|
+
const dragStart = ref(null);
|
|
1106
|
+
const dragEnd = ref(null);
|
|
1107
|
+
const paintTrail = ref(/* @__PURE__ */ new Set());
|
|
1108
|
+
const previewWellIds = computed(() => {
|
|
1109
|
+
if (!isDragging.value) return [];
|
|
1110
|
+
if (activeMode.value === "select") {
|
|
1111
|
+
if (!dragStart.value || !dragEnd.value) return [];
|
|
1112
|
+
return wellIdsInRectangle(dragStart.value, dragEnd.value, toValue(options.format));
|
|
1113
|
+
}
|
|
1114
|
+
return [...paintTrail.value];
|
|
1115
|
+
});
|
|
1116
|
+
function setMode(mode, payload) {
|
|
1117
|
+
activeMode.value = mode;
|
|
1118
|
+
activePayload.value = payload;
|
|
1119
|
+
}
|
|
1120
|
+
function setPayload(payload) {
|
|
1121
|
+
activePayload.value = payload;
|
|
1122
|
+
}
|
|
1123
|
+
function onWellMouseDown(wellId) {
|
|
1124
|
+
const coord = wellIdToCoordinate(wellId);
|
|
1125
|
+
if (!coord) return;
|
|
1126
|
+
isDragging.value = true;
|
|
1127
|
+
dragStart.value = coord;
|
|
1128
|
+
dragEnd.value = coord;
|
|
1129
|
+
paintTrail.value = new Set([wellId]);
|
|
1130
|
+
}
|
|
1131
|
+
function onWellMouseMove(wellId) {
|
|
1132
|
+
if (!isDragging.value) return;
|
|
1133
|
+
const coord = wellIdToCoordinate(wellId);
|
|
1134
|
+
if (!coord) return;
|
|
1135
|
+
dragEnd.value = coord;
|
|
1136
|
+
if (activeMode.value !== "select") paintTrail.value = new Set([...paintTrail.value, wellId]);
|
|
1137
|
+
}
|
|
1138
|
+
function onWellMouseUp() {
|
|
1139
|
+
if (!isDragging.value) return;
|
|
1140
|
+
const wellIds = previewWellIds.value;
|
|
1141
|
+
isDragging.value = false;
|
|
1142
|
+
if (wellIds.length > 0) options.onComplete?.({
|
|
1143
|
+
wellIds,
|
|
1144
|
+
mode: activeMode.value,
|
|
1145
|
+
payload: activePayload.value
|
|
1146
|
+
});
|
|
1147
|
+
clearDragState();
|
|
1148
|
+
}
|
|
1149
|
+
function cancel() {
|
|
1150
|
+
isDragging.value = false;
|
|
1151
|
+
clearDragState();
|
|
1152
|
+
}
|
|
1153
|
+
function clearDragState() {
|
|
1154
|
+
dragStart.value = null;
|
|
1155
|
+
dragEnd.value = null;
|
|
1156
|
+
paintTrail.value = /* @__PURE__ */ new Set();
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
activeMode,
|
|
1160
|
+
activePayload,
|
|
1161
|
+
isDragging,
|
|
1162
|
+
previewWellIds,
|
|
1163
|
+
setMode,
|
|
1164
|
+
setPayload,
|
|
1165
|
+
onWellMouseDown,
|
|
1166
|
+
onWellMouseMove,
|
|
1167
|
+
onWellMouseUp,
|
|
1168
|
+
cancel
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function wellIdToCoordinate(wellId) {
|
|
1172
|
+
const match = /^([A-Z]+)(\d+)$/i.exec(wellId.trim());
|
|
1173
|
+
if (!match) return null;
|
|
1174
|
+
return {
|
|
1175
|
+
row: rowLabelToIndex$1(match[1].toUpperCase()),
|
|
1176
|
+
col: Number(match[2]) - 1
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
function coordinateToWellId(coord) {
|
|
1180
|
+
return `${indexToRowLabel$1(coord.row)}${coord.col + 1}`;
|
|
1181
|
+
}
|
|
1182
|
+
function wellIdsInRectangle(start, end, format) {
|
|
1183
|
+
const dimensions = PLATE_DIMENSIONS[format];
|
|
1184
|
+
const minRow = Math.max(0, Math.min(start.row, end.row));
|
|
1185
|
+
const maxRow = Math.min(dimensions.rows - 1, Math.max(start.row, end.row));
|
|
1186
|
+
const minCol = Math.max(0, Math.min(start.col, end.col));
|
|
1187
|
+
const maxCol = Math.min(dimensions.cols - 1, Math.max(start.col, end.col));
|
|
1188
|
+
const ids = [];
|
|
1189
|
+
for (let row = minRow; row <= maxRow; row++) for (let col = minCol; col <= maxCol; col++) ids.push(coordinateToWellId({
|
|
1190
|
+
row,
|
|
1191
|
+
col
|
|
1192
|
+
}));
|
|
1193
|
+
return ids;
|
|
1194
|
+
}
|
|
1195
|
+
function rowLabelToIndex$1(label) {
|
|
1196
|
+
let result = 0;
|
|
1197
|
+
for (const char of label) result = result * 26 + (char.charCodeAt(0) - 64);
|
|
1198
|
+
return result - 1;
|
|
1199
|
+
}
|
|
1200
|
+
function indexToRowLabel$1(index) {
|
|
1201
|
+
if (index < 26) return ROW_LABELS[index];
|
|
1202
|
+
let value = index + 1;
|
|
1203
|
+
let label = "";
|
|
1204
|
+
while (value > 0) {
|
|
1205
|
+
value--;
|
|
1206
|
+
label = ROW_LABELS[value % 26] + label;
|
|
1207
|
+
value = Math.floor(value / 26);
|
|
1208
|
+
}
|
|
1209
|
+
return label;
|
|
1210
|
+
}
|
|
1211
|
+
//#endregion
|
|
1212
|
+
//#region src/composables/useWellPlateAdapter.ts
|
|
1213
|
+
/** Convert domain rows into WellPlate's wells prop without hand-written adapters. */
|
|
1214
|
+
function useWellPlateAdapter(options) {
|
|
1215
|
+
return { wells: computed(() => createWellPlateWells(toValue(options.entries), options)) };
|
|
1216
|
+
}
|
|
1217
|
+
function createWellPlateWells(entries, options) {
|
|
1218
|
+
return Object.fromEntries(entries.map((entry) => {
|
|
1219
|
+
const wellId = options.getWellId(entry);
|
|
1220
|
+
const coord = parseWellId(wellId);
|
|
1221
|
+
return [wellId, {
|
|
1222
|
+
id: wellId,
|
|
1223
|
+
row: coord.row,
|
|
1224
|
+
col: coord.col,
|
|
1225
|
+
state: options.getState?.(entry) ?? "filled",
|
|
1226
|
+
sampleType: options.getSampleType?.(entry),
|
|
1227
|
+
value: options.getValue?.(entry),
|
|
1228
|
+
metadata: options.getMetadata?.(entry)
|
|
1229
|
+
}];
|
|
1230
|
+
}));
|
|
1231
|
+
}
|
|
1232
|
+
function createColumnConditions(entries) {
|
|
1233
|
+
return entries.map((entry) => {
|
|
1234
|
+
const startCol = typeof entry.start === "number" ? entry.start : Number.parseInt(entry.start, 10);
|
|
1235
|
+
const cols = [];
|
|
1236
|
+
const concentrations = [];
|
|
1237
|
+
for (const concentration of entry.concentrations) {
|
|
1238
|
+
const replicates = concentration.replicates ?? 1;
|
|
1239
|
+
for (let index = 0; index < replicates; index++) {
|
|
1240
|
+
cols.push(startCol + cols.length);
|
|
1241
|
+
concentrations.push(concentration.value);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
label: entry.label,
|
|
1246
|
+
color: entry.color,
|
|
1247
|
+
unit: entry.unit,
|
|
1248
|
+
cols,
|
|
1249
|
+
concentrations
|
|
1250
|
+
};
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
function createRowConditions(entries) {
|
|
1254
|
+
return entries.map((entry) => {
|
|
1255
|
+
const startRow = typeof entry.start === "number" ? entry.start : rowLabelToIndex(entry.start);
|
|
1256
|
+
const rows = [];
|
|
1257
|
+
const concentrations = [];
|
|
1258
|
+
for (const concentration of entry.concentrations) {
|
|
1259
|
+
const replicates = concentration.replicates ?? 1;
|
|
1260
|
+
for (let index = 0; index < replicates; index++) {
|
|
1261
|
+
rows.push(indexToRowLabel(startRow + rows.length));
|
|
1262
|
+
concentrations.push(concentration.value);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
label: entry.label,
|
|
1267
|
+
color: entry.color,
|
|
1268
|
+
unit: entry.unit,
|
|
1269
|
+
rows,
|
|
1270
|
+
concentrations
|
|
1271
|
+
};
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
function parseWellId(wellId) {
|
|
1275
|
+
const match = /^([A-Z]+)(\d+)$/i.exec(wellId.trim());
|
|
1276
|
+
if (!match) throw new Error(`[MINT SDK] Invalid well id "${wellId}".`);
|
|
1277
|
+
return {
|
|
1278
|
+
row: rowLabelToIndex(match[1].toUpperCase()),
|
|
1279
|
+
col: Number(match[2]) - 1
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function rowLabelToIndex(label) {
|
|
1283
|
+
let result = 0;
|
|
1284
|
+
for (const char of label.toUpperCase()) result = result * 26 + (char.charCodeAt(0) - 64);
|
|
1285
|
+
return result - 1;
|
|
1286
|
+
}
|
|
1287
|
+
function indexToRowLabel(index) {
|
|
1288
|
+
let value = index + 1;
|
|
1289
|
+
let label = "";
|
|
1290
|
+
while (value > 0) {
|
|
1291
|
+
value--;
|
|
1292
|
+
label = String.fromCharCode(65 + value % 26) + label;
|
|
1293
|
+
value = Math.floor(value / 26);
|
|
1294
|
+
}
|
|
1295
|
+
return label;
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/composables/useWellPlateValidation.ts
|
|
1299
|
+
/** Rule-driven validation state for WellPlate layout/design screens. */
|
|
1300
|
+
function useWellPlateValidation(options) {
|
|
1301
|
+
const warnings = computed(() => runWellPlateValidation(toValue(options.context), toValue(options.rules)));
|
|
1302
|
+
return {
|
|
1303
|
+
warnings,
|
|
1304
|
+
hasBlockingIssues: computed(() => warnings.value.some((warning) => warning.severity === "error")),
|
|
1305
|
+
warningCount: computed(() => warnings.value.length),
|
|
1306
|
+
errorCount: computed(() => warnings.value.filter((warning) => warning.severity === "error").length)
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function runWellPlateValidation(context, rules) {
|
|
1310
|
+
const warnings = [];
|
|
1311
|
+
for (const rule of rules) {
|
|
1312
|
+
const result = rule.validate(context);
|
|
1313
|
+
if (!result) continue;
|
|
1314
|
+
warnings.push(normalizeWarning(rule, result));
|
|
1315
|
+
}
|
|
1316
|
+
return warnings;
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeWarning(rule, result) {
|
|
1319
|
+
if (typeof result === "string") return {
|
|
1320
|
+
type: rule.type,
|
|
1321
|
+
severity: rule.severity,
|
|
1322
|
+
message: result
|
|
1323
|
+
};
|
|
1324
|
+
return {
|
|
1325
|
+
type: rule.type,
|
|
1326
|
+
severity: rule.severity,
|
|
1327
|
+
...result
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
668
1331
|
//#region src/composables/pluginEndpointBuilder.ts
|
|
669
1332
|
function normalizeApiBaseUrl(baseUrl) {
|
|
670
1333
|
if (!baseUrl) return void 0;
|
|
@@ -987,6 +1650,6 @@ function useCurrentExperiment(options = {}) {
|
|
|
987
1650
|
};
|
|
988
1651
|
}
|
|
989
1652
|
//#endregion
|
|
990
|
-
export { pluginFormDataFromPayload as a, usePluginClient as c, resolvePluginBaseUrl as d,
|
|
1653
|
+
export { useCommandHistory as A, parseDelimitedText as C, createPluginResourceClient as D, validateImportFile as E, useAuth as F, useAsync as M, useAsyncBatch as N, useResourceCrud as O, usePasskey as P, detectDelimiter as S, useFileImport as T, useWellPlateAdapter as _, pluginFormDataFromPayload as a, wellIdToCoordinate as b, usePluginClient as c, resolvePluginBaseUrl as d, runWellPlateValidation as f, createWellPlateWells as g, createRowConditions as h, getPluginPageSelectorItems as i, usePluginConfig as j, useOptimisticMutation as k, usePluginSettings as l, createColumnConditions as m, downloadBlob as n, uploadPluginEndpoint as o, useWellPlateValidation as p, downloadPluginEndpoint as r, useCurrentExperiment as s, createPluginClient as t, buildPluginEndpointUrl as u, coordinateToWellId as v, readFileAsText as w, wellIdsInRectangle as x, useWellPainting as y };
|
|
991
1654
|
|
|
992
|
-
//# sourceMappingURL=composables-
|
|
1655
|
+
//# sourceMappingURL=composables-D9mexHSW.js.map
|