Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dce11b7
fix(SFT-2766): conditional email queue jobs only added for matching c…
gustavs-gutmanis May 12, 2026
e4451c8
chore: updating active record eager loading
gustavs-gutmanis May 15, 2026
5aec6ff
chore: add memoization
gustavs-gutmanis May 15, 2026
cdee8fa
chore: add memoization
gustavs-gutmanis May 18, 2026
cacf384
chore: remove duplicate field builders
gustavs-gutmanis May 18, 2026
7d30a0b
chore: cache rule calls
gustavs-gutmanis May 18, 2026
3bc09a5
feat: Introduce Memo cache for IntegrationsService
gustavs-gutmanis May 18, 2026
f25117c
Merge branch 'v5' of https://github.com/solspace/craft-freeform into …
kjmartens May 18, 2026
488f862
chore: updating scripts
kjmartens May 18, 2026
c8dd5c3
Merge branch 'v5' of https://github.com/solspace/craft-freeform into …
kjmartens May 18, 2026
7de9777
feat: improving form caching in forms service
gustavs-gutmanis May 21, 2026
8c1fc2f
fix: unprefixed table join
gustavs-gutmanis May 21, 2026
a0065e9
fix: field caching per form instance
gustavs-gutmanis May 21, 2026
f4f2de3
fix: translation icon not lighting up
gustavs-gutmanis May 21, 2026
630885e
chore: add artifacts
gustavs-gutmanis May 21, 2026
8c6e298
feat: add edit links to global templates
gustavs-gutmanis May 21, 2026
1dee6d3
chore: add artifacts
gustavs-gutmanis May 21, 2026
f450586
chore: add confirm modal when clicking edit global notification
gustavs-gutmanis May 21, 2026
3c4e7a1
Merge branch 'v5' of https://github.com/solspace/craft-freeform into …
kjmartens May 21, 2026
02cd441
chore: merge
kjmartens May 21, 2026
f8e5b14
fix: fetching wrong relations in rule conditions
gustavs-gutmanis May 22, 2026
601c869
feat: add permission check for global notification edit
gustavs-gutmanis May 22, 2026
4d47296
chore: rebuilding scripts
kjmartens May 22, 2026
0ff1cfc
chore: changelog
kjmartens May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"

set -e

node -v
npm -v
pnpm -v
php -v

printf "\\n\\e[32m######### Linting Scripts #########\\e[0m\\n\\n"
npm run lint
printf "\n\e[32m######### Linting Scripts #########\e[0m\n\n"
pnpm lint

printf "\\n\\e[32m######### Running CS Fixer dry run #########\\e[0m\\n\\n"
composer run fix:dry-run || (printf \"\\e[41mCS Fixer found issues\\e[0m\\n\" && exit 1)
printf "\n\e[32m######### Running CS Fixer dry run #########\e[0m\n\n"
composer run fix:dry-run || (printf "\e[41mCS Fixer found issues\e[0m\n" && exit 1)
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Solspace Freeform Changelog

## 5.15.14 - Unreleased

- TBD

## 5.15.13 - 2026-05-18

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/client/config/freeform/freeform.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type Config = {
};
permissions: {
integrations: Permission;
notifications: Permission;
};
limitations: {
items: null | Record<string, boolean | string | string[]>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Props = {
title: string;
templates: NotificationTemplate[];
canCreate?: boolean;
canEditGlobalTemplates?: boolean;
openEditOnClick?: boolean;
onClick: NotificationSelectHandler;
onCreate?: () => void;
Expand All @@ -27,6 +28,7 @@ export const Category: React.FC<Props> = ({
title,
templates,
canCreate,
canEditGlobalTemplates,
openEditOnClick,
onClick,
onCreate,
Expand Down Expand Up @@ -63,6 +65,7 @@ export const Category: React.FC<Props> = ({
<Item
key={template.id}
openEditOnClick={openEditOnClick}
canEditGlobalTemplates={canEditGlobalTemplates}
active={value === template.id}
template={template}
onClick={onClick}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useModal } from "@components/modals/modal.context";
import { ConfirmSubmissionsModal } from "@editor/builder/tabs/modals/confirm-submissions.modal";
import { QKNotifications } from "@ff-client/queries/notifications";
import type { APIError } from "@ff-client/types/api";
import type { NotificationTemplate } from "@ff-client/types/notifications";
Expand All @@ -7,27 +9,28 @@ import translate from "@ff-client/utils/translations";
import { useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import type React from "react";

import { useNotificationEditModal } from "../../modal/template.modal.hooks";
import type { NotificationSelectHandler } from "../../notification-template";

import { Button, ButtonGroup, Name, TemplateCard } from "./item.styles";

type Props = {
active: boolean;
canEditGlobalTemplates?: boolean;
openEditOnClick?: boolean;
template: NotificationTemplate;
onClick: NotificationSelectHandler;
};

export const Item: React.FC<Props> = ({
active,
canEditGlobalTemplates,
openEditOnClick,
template,
onClick,
}) => {
const { id, name } = template;

const { openModal: openModalFn } = useModal();
const queryClient = useQueryClient();
const openModal = useNotificationEditModal();

Expand All @@ -44,6 +47,34 @@ export const Item: React.FC<Props> = ({
>
<Name title={name}>{name}</Name>

{!template.formId && canEditGlobalTemplates && (
<ButtonGroup>
<Button
title={translate("Edit")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

const isDbTemplate = /^\d+$/.test(template.id.toString());
const target = isDbTemplate ? "database" : "files";

const url = Craft.getCpUrl(
`freeform/notifications/${target}/${template.id}`,
);

// if control or command pressed, open in new tab
if (e.metaKey) {
window.open(url, "_blank")?.focus();
} else {
openModalFn(ConfirmSubmissionsModal, { url });
}
}}
>
<i className="fa-solid fa-pencil"></i>
</Button>
</ButtonGroup>
)}

{!!template.formId && (
<ButtonGroup>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const NotificationTemplate: React.FC<
const {
templates: { canCreate, method },
} = config;
const canManageGlobalTemplates =
config.permissions.notifications === "manage";

if (isFetching && !templates) {
return (
Expand Down Expand Up @@ -75,6 +77,7 @@ const NotificationTemplate: React.FC<
value={value}
title={translate("Global Templates")}
templates={templates.global}
canEditGlobalTemplates={canManageGlobalTemplates}
onClick={handleSelect}
/>
</NotificationTemplateSelector>
Expand Down
9 changes: 6 additions & 3 deletions packages/client/src/app/components/form-controls/control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,25 @@ export const Control: React.FC<PropsWithChildren<Props>> = ({
useTranslations(context as any);

const { edition, translatable, messages } = property || {};
const resolvedHandle = property?.handle || handle;

return (
<ControlBlock
edition={edition}
label={property?.label || label}
handle={property?.handle || handle}
handle={resolvedHandle}
required={property?.required || required}
instructions={property?.instructions || instructions}
width={property?.width || width}
disabled={property?.disabled || disabled}
errors={errors}
messages={messages}
translatable={isTranslationsEnabled && translatable}
hasTranslation={hasTranslation(handle)}
hasTranslation={resolvedHandle ? hasTranslation(resolvedHandle) : false}
isEncrypted={property?.flags?.includes("encrypted")}
removeTranslation={() => removeTranslation(handle)}
removeTranslation={() =>
resolvedHandle && removeTranslation(resolvedHandle)
}
preContent={preContent}
align={align}
justify={justify}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,35 @@ export const getButtonGroups = (page: Page): ButtonGroups => {
case "submit":
buttonGroup.push({
handle: "submit",
label: page.buttons.submitLabel,
label: page.buttons?.submitLabel || "Submit",
enabled: true,
assetId: page.buttons.submitIcon?.[0] || undefined,
iconPosition: page.buttons.submitIconPosition || "left",
assetId: page.buttons?.submitIcon?.[0] || undefined,
iconPosition: page.buttons?.submitIconPosition || "left",
});

break;

case "back":
if (page.buttons.back) {
if (page.buttons?.back) {
buttonGroup.push({
handle: "back",
label: page.buttons.backLabel,
enabled: page.buttons.back,
assetId: page.buttons.backIcon?.[0] || undefined,
iconPosition: page.buttons.backIconPosition || "left",
label: page.buttons?.backLabel || "Back",
enabled: page.buttons?.back || false,
assetId: page.buttons?.backIcon?.[0] || undefined,
iconPosition: page.buttons?.backIconPosition || "left",
});
}

break;

case "save":
if (page.buttons.save) {
if (page.buttons?.save) {
buttonGroup.push({
handle: "save",
label: page.buttons.saveLabel,
enabled: page.buttons.save,
assetId: page.buttons.saveIcon?.[0] || undefined,
iconPosition: page.buttons.saveIconPosition || "left",
label: page.buttons?.saveLabel || "Save",
enabled: page.buttons?.save || false,
assetId: page.buttons?.saveIcon?.[0] || undefined,
iconPosition: page.buttons?.saveIconPosition || "left",
});
}

Expand Down
6 changes: 5 additions & 1 deletion packages/client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, __dirname, "");
const host = "127.0.0.1";
const port = env.PORT ? parseInt(env.PORT, 10) : 5173;
const shouldGenerateSourceMaps = command === "build" && mode !== "production";

return {
appType: "custom",
Expand Down Expand Up @@ -51,11 +52,14 @@ export default defineConfig(({ mode, command }) => {
build: {
target: "es2020",
emptyOutDir: true,
sourcemap: false,
sourcemap: shouldGenerateSourceMaps,
manifest: "manifest.json",
outDir: path.resolve(__dirname, "../plugin/src/Resources/js/client"),
rollupOptions: {
input: path.resolve(__dirname, "./src/index.tsx"),
output: {
sourcemapExcludeSources: false,
},
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class FormAttributesTransformer implements TransformerInterface
{
public function transform($value, ?Form $form = null): FormAttributesCollection
{
return new FormAttributesCollection($value);
return new FormAttributesCollection($value ?? []);
}

public function reverseTransform($value): object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public function __construct(private FieldProvider $fieldProvider) {}

public function transform($value, ?Form $form = null): ?FieldInterface
{
if (null === $value) {
return null;
}

return $this->fieldProvider->getFieldByUid($value, $form);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ class ABTestVariantPersistence
{
public function persistVariation(AbTestVariantRecord $record): bool
{
/** @var AbTestRecord $test */
$test = $record->getAbTest()->one();
$test = $record->abTest;

$this->persistCookie($test, $record);
$this->persistUserAssignment($test, $record);
Expand All @@ -29,10 +28,7 @@ public function getPersistedVariant(AbTestRecord $test): ?AbTestVariantRecord
]);

if ($assignment) {
/** @var AbTestVariantRecord $variation */
$variation = $assignment->getAbTestVariant()->one();

return $variation;
return $assignment->abTestVariant;
}
}

Expand Down Expand Up @@ -62,16 +58,18 @@ private function persistCookie(AbTestRecord $test, AbTestVariantRecord $variant)
$name = $this->getCookieName($test);
$value = $variant->uid;

$generalConfig = \Craft::$app->getConfig()->getGeneral();

setcookie(
$name,
$value,
[
'expires' => (int) strtotime('+1 year'),
'path' => '/',
'domain' => \Craft::$app->getConfig()->getGeneral()->defaultCookieDomain,
'domain' => $generalConfig->defaultCookieDomain,
'secure' => true,
'httponly' => true,
'samesite' => \Craft::$app->getConfig()->getGeneral()->sameSiteCookieValue ?? 'Lax',
'samesite' => $generalConfig->sameSiteCookieValue ?? 'Lax',
]
);
}
Expand Down
Loading
Loading