mirror of
https://github.com/penpot/penpot.git
synced 2026-06-19 05:42:08 +00:00
🐛 Fix circular reference error on token edition
This commit is contained in:
parent
585d6944fc
commit
c96c84af5c
@ -48,6 +48,32 @@
|
||||
self-reference? (get token-references token-name)]
|
||||
(boolean self-reference?)))
|
||||
|
||||
;; TODO: Review this function when tokenscript is implemented.
|
||||
(defn token-circular-reference?
|
||||
"Checks if the given `tokens` map contains a circular reference reachable from
|
||||
`token-name`. Uses DFS with 3-color marking (:in-progress / :done) to detect
|
||||
cycles without false positives on diamond dependencies (A->B, A->C, B->C).
|
||||
Returns the token name that closes the cycle, or nil."
|
||||
[tokens token-name]
|
||||
(let [find-refs (fn [v] (when (string? v) (find-token-value-references v)))
|
||||
state (atom {})]
|
||||
(letfn [(visit [name]
|
||||
(let [mark (get @state name)]
|
||||
(if (= mark :in-progress)
|
||||
name
|
||||
(when-not (= mark :done)
|
||||
(swap! state assoc name :in-progress)
|
||||
(let [token (get tokens name)
|
||||
result (when token
|
||||
(let [refs (find-refs (:value token))]
|
||||
(some visit refs)))]
|
||||
(swap! state assoc name :done)
|
||||
result)))))]
|
||||
(let [token (get tokens token-name)]
|
||||
(when token
|
||||
(let [refs (find-refs (:value token))]
|
||||
(some visit refs)))))))
|
||||
|
||||
(defn references-token?
|
||||
"Recursively check if a value references the token name. Handles strings, maps, and sequences."
|
||||
[value token-name]
|
||||
|
||||
@ -95,7 +95,7 @@ test.describe("Tokens - creation", () => {
|
||||
await toggleDropdownButton.click();
|
||||
const option = page.getByRole("option", { name: "my-token" });
|
||||
await expect(option).toBeVisible();
|
||||
const resolvedValue = option.getByText('3');
|
||||
const resolvedValue = option.getByText("3");
|
||||
await expect(resolvedValue).toBeVisible();
|
||||
await option.click();
|
||||
await expect(
|
||||
@ -1956,8 +1956,9 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await valueField.fill(value);
|
||||
|
||||
// Check that the value has an error
|
||||
const errorNode =
|
||||
tokensUpdateCreateModal.getByText(`Group name of ${name} conflicts with a token of the same name in another active set.`);
|
||||
const errorNode = tokensUpdateCreateModal.getByText(
|
||||
`Group name of ${name} conflicts with a token of the same name in another active set.`,
|
||||
);
|
||||
|
||||
await expect(errorNode).toBeVisible();
|
||||
|
||||
@ -1968,10 +1969,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await expect(submitButton).toBeDisabled();
|
||||
};
|
||||
|
||||
test("User can't create Border Radius token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Border Radius token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -1981,10 +1984,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Border Radius", "rad1.bad", "Value", "10");
|
||||
});
|
||||
|
||||
test("User can't create Color token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Color token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -1994,10 +1999,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Color", "col1.bad", "Value", "red");
|
||||
});
|
||||
|
||||
test("User can't create Dimensions token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Dimensions token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2007,10 +2014,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Dimensions", "dim1.bad", "Value", "100");
|
||||
});
|
||||
|
||||
test("User can't create Font Size token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Font Size token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2020,10 +2029,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Font Size", "fsiz1.bad", "Value", "16");
|
||||
});
|
||||
|
||||
test("User can't create Font Weight token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Font Weight token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2033,10 +2044,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Font Weight", "wei1.bad", "Value", "400");
|
||||
});
|
||||
|
||||
test("User can't create Letter Spacing token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Letter Spacing token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2046,10 +2059,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Letter Spacing", "lspa1.bad", "Value", "1");
|
||||
});
|
||||
|
||||
test("User can't create Number token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Number token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2059,10 +2074,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Number", "num1.bad", "Value", "10");
|
||||
});
|
||||
|
||||
test("User can't create Rotation token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Rotation token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2072,10 +2089,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Rotation", "rot1.bad", "Value", "90");
|
||||
});
|
||||
|
||||
test("User can't create Sizing token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Sizing token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2085,10 +2104,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Sizing", "siz1.bad", "Value", "100");
|
||||
});
|
||||
|
||||
test("User can't create Spacing token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Spacing token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2098,10 +2119,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Spacing", "spa1.bad", "Value", "10");
|
||||
});
|
||||
|
||||
test("User can't create Stroke Width token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Stroke Width token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2111,10 +2134,12 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Stroke Width", "str1.bad", "Value", "2");
|
||||
});
|
||||
|
||||
test("User can't create Text Case token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Text Case token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2124,23 +2149,33 @@ test.describe("User can't create groups that clash with token names", () => {
|
||||
await createBadToken(page, "Text Case", "cas1.bad", "Value", "uppercase");
|
||||
});
|
||||
|
||||
test("User can't create Text Decoration token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Text Decoration token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
await createSet(tokenThemesSetsSidebar, "Second set");
|
||||
|
||||
await createBadToken(page, "Text Decoration", "dec1.bad", "Value", "strike-through");
|
||||
await createBadToken(
|
||||
page,
|
||||
"Text Decoration",
|
||||
"dec1.bad",
|
||||
"Value",
|
||||
"strike-through",
|
||||
);
|
||||
});
|
||||
|
||||
test("User can't create Typography token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Typography token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2200,15 +2235,19 @@ test("User can't create Text Decoration token with group name that clashes with
|
||||
name: "Save",
|
||||
});
|
||||
|
||||
const errorNode = tokensUpdateCreateModal.getByText("Group name of typ1.bad conflicts with a token of the same name in another active set.");
|
||||
const errorNode = tokensUpdateCreateModal.getByText(
|
||||
"Group name of typ1.bad conflicts with a token of the same name in another active set.",
|
||||
);
|
||||
await expect(errorNode).toHaveCount(1);
|
||||
await expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("User can't create Shadow token with group name that clashes with existing token", async ({ page }) => {
|
||||
test("User can't create Shadow token with group name that clashes with existing token", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-tokens-all-types.json"
|
||||
file: "workspace/get-file-tokens-all-types.json",
|
||||
});
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
@ -2258,7 +2297,9 @@ test("User can't create Text Decoration token with group name that clashes with
|
||||
name: "Save",
|
||||
});
|
||||
|
||||
const errorNode = tokensUpdateCreateModal.getByText("Group name of sha1.bad conflicts with a token of the same name in another active set.");
|
||||
const errorNode = tokensUpdateCreateModal.getByText(
|
||||
"Group name of sha1.bad conflicts with a token of the same name in another active set.",
|
||||
);
|
||||
await expect(errorNode).toHaveCount(1);
|
||||
await expect(submitButton).toBeDisabled();
|
||||
});
|
||||
@ -2462,6 +2503,139 @@ test.describe("Tokens tab - edition", () => {
|
||||
await valueSaturationSelector.click({ position: { x: 0, y: 0 } });
|
||||
await expect(valueField).toHaveValue(/^rgba(.*)$/);
|
||||
});
|
||||
|
||||
test("User sees self-reference error when editing a token to create a circular reference", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupEmptyTokensFileRender(page, {
|
||||
flags: ["enable-token-combobox", "enable-feature-token-input"],
|
||||
});
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
// Create first token "base" with value "10"
|
||||
const addTokenButton = tokensTabPanel.getByRole("button", {
|
||||
name: "Add Token: Border Radius",
|
||||
});
|
||||
await addTokenButton.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
await createToken(page, "Border radius", "base", "Value", "combobox", "10");
|
||||
|
||||
await unfoldTokenType(tokensTabPanel, "border radius");
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", { name: "base" }),
|
||||
).toBeEnabled();
|
||||
|
||||
// Create second token "linear" referencing "base"
|
||||
await createToken(
|
||||
page,
|
||||
"Border radius",
|
||||
"linear",
|
||||
"Value",
|
||||
"combobox",
|
||||
"{base} * 0.25",
|
||||
);
|
||||
|
||||
await expect(
|
||||
tokensTabPanel.getByRole("button", { name: "linear" }),
|
||||
).toBeEnabled();
|
||||
|
||||
// Open the edit modal for "base"
|
||||
const baseToken = tokensTabPanel.getByRole("button", { name: "base" });
|
||||
await expect(baseToken).toBeVisible();
|
||||
await baseToken.click({ button: "right" });
|
||||
await expect(tokenContextMenuForToken).toBeVisible();
|
||||
const editOption = tokenContextMenuForToken
|
||||
.getByRole("listitem")
|
||||
.filter({ hasText: "Edit token" });
|
||||
await expect(editOption).toBeVisible();
|
||||
await editOption.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
// Change value to reference "linear.0.25", creating a circular reference
|
||||
const editValueField = tokensUpdateCreateModal.getByRole("combobox", {
|
||||
name: "Value",
|
||||
});
|
||||
await editValueField.fill("{linear}");
|
||||
|
||||
// The circular reference error should appear gracefully
|
||||
const selfRefError = tokensUpdateCreateModal.getByText(
|
||||
"Token has self reference",
|
||||
);
|
||||
await expect(selfRefError).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Save button should be disabled
|
||||
const editSubmitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await expect(editSubmitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("User sees self-reference error for a three-step circular reference (A → B → C → A)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupEmptyTokensFileRender(page, {
|
||||
flags: ["enable-token-combobox", "enable-feature-token-input"],
|
||||
});
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
// Create first token "a" with value "10"
|
||||
const addTokenButton = tokensTabPanel.getByRole("button", {
|
||||
name: "Add Token: Border Radius",
|
||||
});
|
||||
|
||||
await createToken(
|
||||
page,
|
||||
"Border radius",
|
||||
"First",
|
||||
"Value",
|
||||
"combobox",
|
||||
"10",
|
||||
);
|
||||
await createToken(
|
||||
page,
|
||||
"Border radius",
|
||||
"Second",
|
||||
"Value",
|
||||
"combobox",
|
||||
"{First}",
|
||||
);
|
||||
await createToken(
|
||||
page,
|
||||
"Border radius",
|
||||
"Third",
|
||||
"Value",
|
||||
"combobox",
|
||||
"{Second}",
|
||||
);
|
||||
|
||||
// Edit "First" to reference "Third", completing the cycle A → B → C → A
|
||||
const tokenA = tokensTabPanel.getByRole("button", { name: "First" });
|
||||
await tokenA.click({ button: "right" });
|
||||
await expect(tokenContextMenuForToken).toBeVisible();
|
||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
const editValueField = tokensUpdateCreateModal.getByRole("combobox", {
|
||||
name: "Value",
|
||||
});
|
||||
await editValueField.fill("{Third}");
|
||||
|
||||
// The circular reference error should appear gracefully
|
||||
const selfRefError = tokensUpdateCreateModal.getByText(
|
||||
"Token has self reference",
|
||||
);
|
||||
await expect(selfRefError).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Save button should be disabled
|
||||
const editSubmitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await expect(editSubmitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Tokens tab - delete", () => {
|
||||
@ -2496,7 +2670,14 @@ test("BUG: 14262 Token pill must be highlighted when value references a token in
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
await unfoldTokenType(tokensSidebar, "Border radius");
|
||||
await createToken(page, "Border radius", "base-radius", "Value", "textbox", "20");
|
||||
await createToken(
|
||||
page,
|
||||
"Border radius",
|
||||
"base-radius",
|
||||
"Value",
|
||||
"textbox",
|
||||
"20",
|
||||
);
|
||||
await createToken(
|
||||
page,
|
||||
"Border radius",
|
||||
@ -2529,7 +2710,14 @@ test("BUG: 14262 Token pill must be highlighted when value references a token in
|
||||
.getByRole("button", { name: "New set" })
|
||||
.getByRole("checkbox")
|
||||
.click();
|
||||
await createToken(page, "Border radius", "new-ref", "Value", "textbox", "{base-radius}");
|
||||
await createToken(
|
||||
page,
|
||||
"Border radius",
|
||||
"new-ref",
|
||||
"Value",
|
||||
"textbox",
|
||||
"{base-radius}",
|
||||
);
|
||||
|
||||
// Pill is highlighted if the referenced token is in a different disabled set than the token with the reference
|
||||
const newBrokenTokenPill = tokensSidebar.getByRole("button", {
|
||||
|
||||
@ -76,6 +76,7 @@ const setupEmptyTokensFileRender = async (page, options = {}) => {
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokensSidebar: workspacePage.tokensSidebar,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
tokenContextMenuForToken: workspacePage.tokenContextMenuForToken,
|
||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||
};
|
||||
};
|
||||
|
||||
@ -73,18 +73,20 @@
|
||||
(dissoc (:name prev-token))
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
(if (cto/token-circular-reference? tokens (:name token))
|
||||
(rx/of {:error (wte/error-with-value :error.token/direct-self-reference nil)})
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)})))))))))
|
||||
|
||||
(defn- hex->color-obj
|
||||
[hex]
|
||||
|
||||
@ -50,20 +50,22 @@
|
||||
(dissoc (:name prev-token))
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
(first errors)
|
||||
(wte/error-with-value :error/unknown value))}))))))))
|
||||
(if (cto/token-circular-reference? tokens (:name token))
|
||||
(rx/of {:error (wte/error-with-value :error.token/direct-self-reference nil)})
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
(first errors)
|
||||
(wte/error-with-value :error/unknown value))})))))))))
|
||||
|
||||
(mf/defc value-combobox*
|
||||
[{:keys [name tokens token token-type empty-to-end ref] :rest props}]
|
||||
|
||||
@ -68,20 +68,22 @@
|
||||
tokens
|
||||
(update tokens (:name token) #(ctob/make-token (merge % prev-token token)))]
|
||||
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
(first errors)
|
||||
(wte/error-with-value :error/unknown value))}))))))))
|
||||
(if (cto/token-circular-reference? tokens (:name token))
|
||||
(rx/of {:error (wte/error-with-value :error.token/direct-self-reference nil)})
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
(first errors)
|
||||
(wte/error-with-value :error/unknown value))})))))))))
|
||||
|
||||
(mf/defc fonts-combobox*
|
||||
[{:keys [token tokens name] :rest props}]
|
||||
|
||||
@ -172,19 +172,21 @@
|
||||
(dissoc (:name prev-token))
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
|
||||
(->> tokens
|
||||
(sd/resolve-tokens-interactive)
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
(first errors)
|
||||
(wte/error-with-value :error/unknown value))}))))))))
|
||||
(if (cto/token-circular-reference? tokens (:name token))
|
||||
(rx/of {:error (wte/error-with-value :error.token/direct-self-reference nil)})
|
||||
(->> tokens
|
||||
(sd/resolve-tokens-interactive)
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
(first errors)
|
||||
(wte/error-with-value :error/unknown value))})))))))))
|
||||
|
||||
(mf/defc input*
|
||||
[{:keys [name tokens token] :rest props}]
|
||||
|
||||
@ -39,20 +39,22 @@
|
||||
:always
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens'))
|
||||
(sd/resolve-tokens-interactive tokens'))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [resolved-token (cond-> (get resolved-tokens (:name token))
|
||||
(contains? cf/flags :tokenscript)
|
||||
(update :resolved-value ts/tokenscript-symbols->penpot-unit))]
|
||||
(cond
|
||||
(:resolved-value resolved-token)
|
||||
(rx/of resolved-token)
|
||||
(if (cto/token-circular-reference? tokens' (:name token))
|
||||
(rx/throw {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens'))
|
||||
(sd/resolve-tokens-interactive tokens'))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [resolved-token (cond-> (get resolved-tokens (:name token))
|
||||
(contains? cf/flags :tokenscript)
|
||||
(update :resolved-value ts/tokenscript-symbols->penpot-unit))]
|
||||
(cond
|
||||
(:resolved-value resolved-token)
|
||||
(rx/of resolved-token)
|
||||
|
||||
:else (rx/throw {:errors (or (seq (:errors resolved-token))
|
||||
[(wte/get-error-code :error/unknown-error)])}))))))))
|
||||
:else (rx/throw {:errors (or (seq (:errors resolved-token))
|
||||
[(wte/get-error-code :error/unknown-error)])})))))))))
|
||||
|
||||
(defn- validate-token-with [token validators]
|
||||
(if-let [error (some (fn [validate] (validate token)) validators)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user