@solidxai/core 0.1.13-beta.12 → 0.1.13-beta.13
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/.claude/settings.local.json +16 -0
- package/CHANGELOG.md +570 -0
- package/CLAUDE.md +26 -0
- package/dist/seeders/seed-data/solid-core-metadata.json +1 -1
- package/dist/services/chatter-message.service.d.ts +4 -1
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +4 -1
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/crud-helper.service.d.ts +1 -2
- package/dist/services/crud-helper.service.d.ts.map +1 -1
- package/dist/services/crud-helper.service.js +2 -16
- package/dist/services/crud-helper.service.js.map +1 -1
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +1 -1
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/draft-publish-helper.service.d.ts +1 -0
- package/dist/services/draft-publish-helper.service.d.ts.map +1 -1
- package/dist/services/draft-publish-helper.service.js +38 -4
- package/dist/services/draft-publish-helper.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +62 -6
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +34 -3
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/theme/theme-registry.d.ts +14 -0
- package/dist/theme/theme-registry.d.ts.map +1 -0
- package/dist/theme/theme-registry.js +147 -0
- package/dist/theme/theme-registry.js.map +1 -0
- package/package.json +1 -1
- package/src/seeders/seed-data/solid-core-metadata.json +1 -1
- package/src/services/1.js +6 -0
- package/src/services/chatter-message.service.ts +22 -1
- package/src/services/crud-helper.service.ts +19 -16
- package/src/services/crud.service.ts +2 -1
- package/src/services/draft-publish-helper.service.ts +56 -4
- package/src/services/settings/default-settings-provider.service.ts +34 -3
- package/src/theme/theme-registry.ts +181 -0
- package/dist-tests/api/authenticate.spec.js +0 -119
- package/dist-tests/api/authenticate.spec.js.map +0 -1
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
- package/dist-tests/api/ping.spec.js +0 -21
- package/dist-tests/api/ping.spec.js.map +0 -1
- package/dist-tests/helpers/auth.js +0 -41
- package/dist-tests/helpers/auth.js.map +0 -1
- package/dist-tests/helpers/env.js +0 -11
- package/dist-tests/helpers/env.js.map +0 -1
- package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
- package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
- package/docs/agent-hub-grooming.md +0 -301
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
- package/docs/datasource-introspection-ddl-analysis.md +0 -326
- package/docs/datasource-introspection-implementation-plan.md +0 -306
- package/docs/grouping-enhancements.md +0 -89
- package/docs/java-spring/README.md +0 -3
- package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
- package/docs/module-package-import-handoff.md +0 -691
- package/docs/seed-changes.md +0 -65
- package/docs/test-data-workflow.md +0 -200
- package/docs/type-declaration-import-issue.md +0 -24
- package/docs/workflow/business-automation-example-notes.md +0 -309
- package/docs/workflow/control-flow-node-addition-sop.md +0 -324
- package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
- package/docs/workflow/foreach-example-notes.md +0 -187
- package/docs/workflow/hello-world-example-notes.md +0 -271
- package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
- package/docs/workflow/loop-until-example-notes.md +0 -198
- package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
- package/docs/workflow/samples.md +0 -25
- package/docs/workflow/what-is-kestra.md +0 -79
- package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
- package/docs/workflow/workflow-module-brd.md +0 -185
- package/docs/workflow/workflow-module-domain-model.md +0 -252
- package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
- package/docs/workflow/workflow-module-technical-spec.md +0 -293
- package/docs/workflow/workflow-node-type-contracts.md +0 -635
- package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
- package/docs/workflow/workflow-solidx-model-field-spec.md +0 -284
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getThemeModeFromThemeKey = getThemeModeFromThemeKey;
|
|
7
|
+
exports.getCoreThemes = getCoreThemes;
|
|
8
|
+
exports.getRegisteredThemes = getRegisteredThemes;
|
|
9
|
+
exports.getThemesByMode = getThemesByMode;
|
|
10
|
+
exports.getDefaultThemeKey = getDefaultThemeKey;
|
|
11
|
+
exports.isRegisteredThemeKey = isRegisteredThemeKey;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const DEFAULT_THEME_KEYS = {
|
|
15
|
+
light: "solid-light-purple",
|
|
16
|
+
dark: "solid-dark-purple",
|
|
17
|
+
};
|
|
18
|
+
const THEME_MODE_TOKENS = {
|
|
19
|
+
light: "-light-",
|
|
20
|
+
dark: "-dark-",
|
|
21
|
+
};
|
|
22
|
+
const THEME_MODE_SUFFIXES = {
|
|
23
|
+
light: "-light",
|
|
24
|
+
dark: "-dark",
|
|
25
|
+
};
|
|
26
|
+
function normalizeThemeKey(value) {
|
|
27
|
+
const normalized = value?.trim().toLowerCase();
|
|
28
|
+
return normalized || null;
|
|
29
|
+
}
|
|
30
|
+
function matchesThemeMode(value, mode) {
|
|
31
|
+
return (value.includes(THEME_MODE_TOKENS[mode])
|
|
32
|
+
|| value.endsWith(THEME_MODE_SUFFIXES[mode])
|
|
33
|
+
|| value === mode);
|
|
34
|
+
}
|
|
35
|
+
function titleCase(value) {
|
|
36
|
+
return value
|
|
37
|
+
.split(/[-_\s]+/)
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((token) => {
|
|
40
|
+
if (token.toLowerCase() === "solid")
|
|
41
|
+
return "SolidX";
|
|
42
|
+
return token.charAt(0).toUpperCase() + token.slice(1);
|
|
43
|
+
})
|
|
44
|
+
.join(" ");
|
|
45
|
+
}
|
|
46
|
+
function getThemeModeFromThemeKey(value) {
|
|
47
|
+
const normalized = normalizeThemeKey(value);
|
|
48
|
+
if (!normalized)
|
|
49
|
+
return null;
|
|
50
|
+
if (matchesThemeMode(normalized, "light"))
|
|
51
|
+
return "light";
|
|
52
|
+
if (matchesThemeMode(normalized, "dark"))
|
|
53
|
+
return "dark";
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function createThemeDefinition(key, defaultTheme = false) {
|
|
57
|
+
const mode = getThemeModeFromThemeKey(key);
|
|
58
|
+
if (!mode) {
|
|
59
|
+
throw new Error(`Invalid theme key: ${key}`);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
key,
|
|
63
|
+
label: titleCase(key),
|
|
64
|
+
mode,
|
|
65
|
+
default: defaultTheme,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createDefaultTheme(mode) {
|
|
69
|
+
return createThemeDefinition(DEFAULT_THEME_KEYS[mode], true);
|
|
70
|
+
}
|
|
71
|
+
function groupThemesByMode(themes, mode) {
|
|
72
|
+
return themes.filter((theme) => theme.mode === mode);
|
|
73
|
+
}
|
|
74
|
+
function pickDefaultThemeKey(themes, mode) {
|
|
75
|
+
const themesForMode = groupThemesByMode(themes, mode);
|
|
76
|
+
return (themesForMode.find((theme) => theme.default)?.key
|
|
77
|
+
?? themesForMode[0]?.key
|
|
78
|
+
?? DEFAULT_THEME_KEYS[mode]);
|
|
79
|
+
}
|
|
80
|
+
function resolveThemeDirectory() {
|
|
81
|
+
const themeDirectory = path_1.default.resolve(process.cwd(), "..", "solid-ui", "public", "themes");
|
|
82
|
+
try {
|
|
83
|
+
if (fs_1.default.existsSync(themeDirectory) && fs_1.default.statSync(themeDirectory).isDirectory()) {
|
|
84
|
+
return path_1.default.resolve(themeDirectory);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function isThemeDirectory(themeDirectory, entryName) {
|
|
93
|
+
return fs_1.default.existsSync(path_1.default.join(themeDirectory, entryName, "theme.css"));
|
|
94
|
+
}
|
|
95
|
+
function readThemesFromPath() {
|
|
96
|
+
const themeDirectory = resolveThemeDirectory();
|
|
97
|
+
if (!themeDirectory) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const directoryEntries = fs_1.default.readdirSync(themeDirectory, { withFileTypes: true });
|
|
102
|
+
return directoryEntries
|
|
103
|
+
.filter((entry) => entry.isDirectory())
|
|
104
|
+
.map((entry) => entry.name.trim())
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.filter((entryName) => isThemeDirectory(themeDirectory, entryName))
|
|
107
|
+
.flatMap((entryName) => {
|
|
108
|
+
const mode = getThemeModeFromThemeKey(entryName);
|
|
109
|
+
if (!mode)
|
|
110
|
+
return [];
|
|
111
|
+
return [createThemeDefinition(entryName, entryName === DEFAULT_THEME_KEYS[mode])];
|
|
112
|
+
})
|
|
113
|
+
.sort((left, right) => left.label.localeCompare(right.label));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function getThemes() {
|
|
120
|
+
const discoveredThemes = readThemesFromPath();
|
|
121
|
+
if (discoveredThemes.length) {
|
|
122
|
+
return discoveredThemes;
|
|
123
|
+
}
|
|
124
|
+
return [createDefaultTheme("light"), createDefaultTheme("dark")];
|
|
125
|
+
}
|
|
126
|
+
function getCoreThemes() {
|
|
127
|
+
return getThemes();
|
|
128
|
+
}
|
|
129
|
+
function getRegisteredThemes() {
|
|
130
|
+
return getThemes();
|
|
131
|
+
}
|
|
132
|
+
function getThemesByMode(mode) {
|
|
133
|
+
return groupThemesByMode(getRegisteredThemes(), mode);
|
|
134
|
+
}
|
|
135
|
+
function getDefaultThemeKey(mode = "light") {
|
|
136
|
+
return pickDefaultThemeKey(getRegisteredThemes(), mode);
|
|
137
|
+
}
|
|
138
|
+
function isRegisteredThemeKey(value, mode) {
|
|
139
|
+
if (!value)
|
|
140
|
+
return false;
|
|
141
|
+
return getRegisteredThemes().some((theme) => {
|
|
142
|
+
if (theme.key !== value)
|
|
143
|
+
return false;
|
|
144
|
+
return mode ? theme.mode === mode : true;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=theme-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-registry.js","sourceRoot":"","sources":["../../src/theme/theme-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\n\n/**\n * Backend theme registry.\n *\n * Responsibilities:\n * - discover theme folders from the filesystem\n * - classify each theme as light/dark from its key\n * - expose default and grouped theme helpers for backend settings\n *\n * Shared pure parsing helpers are intentionally mirrored in solid-core-ui,\n * but all filesystem discovery stays here.\n */\nexport type ThemeMode = \"light\" | \"dark\";\n\nexport type AppThemeDefinition = {\n key: string;\n label: string;\n mode: ThemeMode;\n default?: boolean;\n};\n\nconst DEFAULT_THEME_KEYS: Record<ThemeMode, string> = {\n light: \"solid-light-purple\",\n dark: \"solid-dark-purple\",\n};\n\nconst THEME_MODE_TOKENS: Record<ThemeMode, string> = {\n light: \"-light-\",\n dark: \"-dark-\",\n};\n\nconst THEME_MODE_SUFFIXES: Record<ThemeMode, string> = {\n light: \"-light\",\n dark: \"-dark\",\n};\n\nfunction normalizeThemeKey(value?: string | null): string | null {\n const normalized = value?.trim().toLowerCase();\n return normalized || null;\n}\n\nfunction matchesThemeMode(value: string, mode: ThemeMode): boolean {\n return (\n value.includes(THEME_MODE_TOKENS[mode])\n || value.endsWith(THEME_MODE_SUFFIXES[mode])\n || value === mode\n );\n}\n\nfunction titleCase(value: string): string {\n return value\n .split(/[-_\\s]+/)\n .filter(Boolean)\n .map((token) => {\n if (token.toLowerCase() === \"solid\") return \"SolidX\";\n return token.charAt(0).toUpperCase() + token.slice(1);\n })\n .join(\" \");\n}\n\nexport function getThemeModeFromThemeKey(value?: string | null): ThemeMode | null {\n const normalized = normalizeThemeKey(value);\n if (!normalized) return null;\n\n if (matchesThemeMode(normalized, \"light\")) return \"light\";\n if (matchesThemeMode(normalized, \"dark\")) return \"dark\";\n\n return null;\n}\n\nfunction createThemeDefinition(key: string, defaultTheme = false): AppThemeDefinition {\n const mode = getThemeModeFromThemeKey(key);\n if (!mode) {\n throw new Error(`Invalid theme key: ${key}`);\n }\n\n return {\n key,\n label: titleCase(key),\n mode,\n default: defaultTheme,\n };\n}\n\nfunction createDefaultTheme(mode: ThemeMode): AppThemeDefinition {\n return createThemeDefinition(DEFAULT_THEME_KEYS[mode], true);\n}\n\nfunction groupThemesByMode(themes: AppThemeDefinition[], mode: ThemeMode): AppThemeDefinition[] {\n return themes.filter((theme) => theme.mode === mode);\n}\n\nfunction pickDefaultThemeKey(themes: AppThemeDefinition[], mode: ThemeMode): string {\n const themesForMode = groupThemesByMode(themes, mode);\n\n return (\n themesForMode.find((theme) => theme.default)?.key\n ?? themesForMode[0]?.key\n ?? DEFAULT_THEME_KEYS[mode]\n );\n}\n\nfunction resolveThemeDirectory(): string | null {\n const themeDirectory = path.resolve(process.cwd(), \"..\", \"solid-ui\", \"public\", \"themes\");\n try {\n if (fs.existsSync(themeDirectory) && fs.statSync(themeDirectory).isDirectory()) {\n return path.resolve(themeDirectory);\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nfunction isThemeDirectory(themeDirectory: string, entryName: string): boolean {\n return fs.existsSync(path.join(themeDirectory, entryName, \"theme.css\"));\n}\n\nfunction readThemesFromPath(): AppThemeDefinition[] {\n const themeDirectory = resolveThemeDirectory();\n if (!themeDirectory) {\n return [];\n }\n\n try {\n const directoryEntries = fs.readdirSync(themeDirectory, { withFileTypes: true });\n\n return directoryEntries\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name.trim())\n .filter(Boolean)\n .filter((entryName) => isThemeDirectory(themeDirectory, entryName))\n .flatMap((entryName) => {\n const mode = getThemeModeFromThemeKey(entryName);\n if (!mode) return [];\n\n return [createThemeDefinition(entryName, entryName === DEFAULT_THEME_KEYS[mode])];\n })\n .sort((left, right) => left.label.localeCompare(right.label));\n } catch {\n return [];\n }\n}\n\nfunction getThemes(): AppThemeDefinition[] {\n const discoveredThemes = readThemesFromPath();\n\n if (discoveredThemes.length) {\n return discoveredThemes;\n }\n\n return [createDefaultTheme(\"light\"), createDefaultTheme(\"dark\")];\n}\n\nexport function getCoreThemes(): AppThemeDefinition[] {\n return getThemes();\n}\n\nexport function getRegisteredThemes(): AppThemeDefinition[] {\n return getThemes();\n}\n\nexport function getThemesByMode(mode: ThemeMode): AppThemeDefinition[] {\n return groupThemesByMode(getRegisteredThemes(), mode);\n}\n\nexport function getDefaultThemeKey(mode: ThemeMode = \"light\"): string {\n return pickDefaultThemeKey(getRegisteredThemes(), mode);\n}\n\nexport function isRegisteredThemeKey(value?: string | null, mode?: ThemeMode): value is string {\n if (!value) return false;\n\n return getRegisteredThemes().some((theme) => {\n if (theme.key !== value) return false;\n return mode ? theme.mode === mode : true;\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidxai/core",
|
|
3
|
-
"version": "0.1.13-beta.
|
|
3
|
+
"version": "0.1.13-beta.13",
|
|
4
4
|
"description": "This module is a NestJS module containing all the required core providers required by a Solid application",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12591,7 +12591,7 @@
|
|
|
12591
12591
|
50
|
|
12592
12592
|
],
|
|
12593
12593
|
"enableGlobalSearch": true,
|
|
12594
|
-
"create":
|
|
12594
|
+
"create": false,
|
|
12595
12595
|
"edit": true,
|
|
12596
12596
|
"delete": true,
|
|
12597
12597
|
"import": true,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
1. Do i need to create a storeStreams method for aws service too?
|
|
2
|
+
- Handle later
|
|
3
|
+
2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
|
|
4
|
+
- startExport should either return the data or return the transaction id
|
|
5
|
+
3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
|
|
6
|
+
- show the userKey
|
|
@@ -37,6 +37,16 @@ interface ChatterMention {
|
|
|
37
37
|
id?: string | number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Server-side-only overrides for `postMessage`. Not part of `PostChatterMessageDto`, so
|
|
42
|
+
* nothing here is settable over HTTP - only in-process callers such as migration or
|
|
43
|
+
* back-fill services can supply it.
|
|
44
|
+
*/
|
|
45
|
+
export interface PostChatterMessageOptions {
|
|
46
|
+
/** Backdate the message instead of stamping it with the current time. */
|
|
47
|
+
createdAt?: Date;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
@Injectable()
|
|
41
51
|
export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
42
52
|
private readonly _logger = new Logger(ChatterMessageService.name);
|
|
@@ -389,7 +399,14 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
389
399
|
return this.trimMessageUser(savedMessage);
|
|
390
400
|
}
|
|
391
401
|
|
|
392
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Post a chatter message.
|
|
404
|
+
*
|
|
405
|
+
* `options` is deliberately kept out of `PostChatterMessageDto` so it can only be supplied
|
|
406
|
+
* by server-side callers (migrations, back-fills). The controller binds the DTO alone, so
|
|
407
|
+
* an API client has no way to backdate a message.
|
|
408
|
+
*/
|
|
409
|
+
async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = [], options: PostChatterMessageOptions = {}) {
|
|
393
410
|
const coModelName = lowerFirst(postDto.coModelName);
|
|
394
411
|
await this.assertRecordAccess(coModelName, postDto.coModelEntityId);
|
|
395
412
|
|
|
@@ -404,6 +421,10 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
404
421
|
chatterMessage.coModelEntityId = postDto.coModelEntityId;
|
|
405
422
|
chatterMessage.coModelName = coModelName;
|
|
406
423
|
chatterMessage.modelUserKey = postDto.modelUserKey ?? null;
|
|
424
|
+
// Left unset otherwise, so the @CreateDateColumn default still applies.
|
|
425
|
+
if (options.createdAt) {
|
|
426
|
+
chatterMessage.createdAt = options.createdAt;
|
|
427
|
+
}
|
|
407
428
|
|
|
408
429
|
const model = await this.modelMetadataRepo.findOne({
|
|
409
430
|
where: { singularName: coModelName },
|
|
@@ -889,25 +889,25 @@ export class CrudHelperService {
|
|
|
889
889
|
};
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
-
async countGroupedRecords(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string) { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
|
|
893
|
-
|
|
894
|
-
|
|
892
|
+
// async countGroupedRecords(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string, internationalisation?: boolean, draftPublishWorkflow?: boolean, moduleRef?: any) { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
|
|
893
|
+
// const { limit, offset, ...rest } = basicFilterDto;
|
|
894
|
+
// const filteredDto = { ...rest, limit: undefined, offset: undefined };
|
|
895
895
|
|
|
896
|
-
|
|
896
|
+
// const filteredQB = this.buildFilterQuery(qb, filteredDto as BasicFilterDto, entityAlias, internationalisation, draftPublishWorkflow, moduleRef, FilterCombinator.AND, false, false);
|
|
897
897
|
|
|
898
|
-
|
|
898
|
+
// const groupByFields = this.normalize(filteredDto.groupBy);
|
|
899
899
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
900
|
+
// if (!groupByFields || groupByFields.length === 0) {
|
|
901
|
+
// throw new Error(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
|
|
902
|
+
// }
|
|
903
903
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
904
|
+
// this.applyGroupBySelections(filteredQB, groupByFields, entityAlias);
|
|
905
|
+
// this.applyAggregates(filteredQB, ['count'], entityAlias);
|
|
906
|
+
// filteredQB.limit(undefined).offset(undefined).take(undefined).skip(undefined);
|
|
907
907
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}
|
|
908
|
+
// const rawResults = await filteredQB.getRawMany();
|
|
909
|
+
// return rawResults.length;
|
|
910
|
+
// }
|
|
911
911
|
|
|
912
912
|
hasReadPermissionOnModel = (activeUser: ActiveUserData, modelName: string) => {
|
|
913
913
|
const permissionNames = [`${classify(modelName)}Controller.findOne`, `${classify(modelName)}Controller.findMany`];
|
|
@@ -980,7 +980,10 @@ export class CrudHelperService {
|
|
|
980
980
|
basicFilterDto: BasicFilterDto,
|
|
981
981
|
alias: string,
|
|
982
982
|
createQbFn: () => Promise<SelectQueryBuilder<T>>,
|
|
983
|
-
postProcessEntities?: (entities: T[]) => Promise<void
|
|
983
|
+
postProcessEntities?: (entities: T[]) => Promise<void>,
|
|
984
|
+
internationalisation?: boolean,
|
|
985
|
+
draftPublishWorkflow?: boolean,
|
|
986
|
+
moduleRef?: any
|
|
984
987
|
): Promise<{ meta: { totalRecords: number }; groupMeta: any[]; groupRecords: any[] }> {
|
|
985
988
|
const groupByFields = this.normalize(basicFilterDto.groupBy);
|
|
986
989
|
if (!groupByFields.length) throw new BadRequestException(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
|
|
@@ -1015,7 +1018,7 @@ export class CrudHelperService {
|
|
|
1015
1018
|
aggregates: undefined,
|
|
1016
1019
|
sort: basicFilterDto.groupFilter?.sort,
|
|
1017
1020
|
};
|
|
1018
|
-
groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias);
|
|
1021
|
+
groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias, internationalisation, draftPublishWorkflow, moduleRef);
|
|
1019
1022
|
groupQb = this.buildGroupByRecordsQuery(groupQb, group, alias, groupAliasMap, aggregateAliasMap, groupExpressionMap);
|
|
1020
1023
|
const [entities, count] = await groupQb.getManyAndCount();
|
|
1021
1024
|
if (postProcessEntities) await postProcessEntities(entities);
|
|
@@ -583,7 +583,8 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
|
|
|
583
583
|
async (entities) => {
|
|
584
584
|
if (populateUserIdFields?.length) await this.handlePopulateUserIdFields(populateUserIdFields, entities);
|
|
585
585
|
if (populateMedia?.length) await this.handlePopulateMedia(populateMedia, entities);
|
|
586
|
-
}
|
|
586
|
+
},
|
|
587
|
+
internationalisation, draftPublishWorkflow, this.moduleRef
|
|
587
588
|
);
|
|
588
589
|
}
|
|
589
590
|
else {
|
|
@@ -333,13 +333,65 @@ export class DraftPublishHelperService {
|
|
|
333
333
|
throw new BadRequestException(`${modelName} with id ${id} is already unpublished`);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
const chainId = entity.initialEntityVersionId || entity.id;
|
|
337
|
+
|
|
338
|
+
return repo.manager.transaction(async (manager) => {
|
|
339
|
+
const transactionalRepo = manager.getRepository(repo.metadata.target);
|
|
340
|
+
|
|
341
|
+
// Release the "na" published tracker before anything else can claim it, mirroring
|
|
342
|
+
// publishRecord's archive-others-then-publish ordering in reverse. unpublishRecord
|
|
343
|
+
// only ever acts on the latest version (see loadLatestEntityForPublishAction), so
|
|
344
|
+
// this always becomes a Draft rather than an Archived version — clear publishedAt
|
|
345
|
+
// to match applyCreateDefaults' Draft convention instead of leaving a stale date.
|
|
346
|
+
const unpublishedEntity = await transactionalRepo.save({
|
|
347
|
+
...entity,
|
|
348
|
+
isPublished: false,
|
|
349
|
+
publishedAt: null,
|
|
350
|
+
publishedTracker: this.createPublishedVersionTracker(entity.id),
|
|
351
|
+
updatedBy: activeUser?.sub ?? entity.updatedBy,
|
|
352
|
+
} as any) as unknown as T;
|
|
353
|
+
|
|
354
|
+
const chainVersions = await transactionalRepo.find({
|
|
355
|
+
where: [
|
|
356
|
+
{ initialEntityVersionId: chainId },
|
|
357
|
+
{ id: chainId },
|
|
358
|
+
] as any,
|
|
359
|
+
}) as unknown as T[];
|
|
360
|
+
|
|
361
|
+
const versionToRestore = this.pickMostRecentlyPublishedVersion(
|
|
362
|
+
chainVersions.filter(version => version.id !== entity.id),
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (versionToRestore) {
|
|
366
|
+
await transactionalRepo.save({
|
|
367
|
+
...versionToRestore,
|
|
368
|
+
isPublished: true,
|
|
369
|
+
publishedAt: new Date(),
|
|
370
|
+
publishedTracker: "na",
|
|
371
|
+
} as any);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return unpublishedEntity;
|
|
340
375
|
});
|
|
341
376
|
}
|
|
342
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Among the other versions in a chain, find the one that was live most recently before
|
|
380
|
+
* being superseded (i.e. the "Archived" version unpublishRecord should restore). Ties on
|
|
381
|
+
* publishedAt fall back to the highest id, matching pickCurrentLocaleRecord's ordering.
|
|
382
|
+
*/
|
|
383
|
+
private pickMostRecentlyPublishedVersion<T extends CommonEntity>(versions: T[]): T | null {
|
|
384
|
+
const previouslyPublished = versions.filter(version => Boolean(version.publishedAt));
|
|
385
|
+
if (previouslyPublished.length === 0) return null;
|
|
386
|
+
|
|
387
|
+
return previouslyPublished.sort((a, b) => {
|
|
388
|
+
const aPublishedAt = new Date(a.publishedAt as any).getTime();
|
|
389
|
+
const bPublishedAt = new Date(b.publishedAt as any).getTime();
|
|
390
|
+
if (aPublishedAt !== bPublishedAt) return bPublishedAt - aPublishedAt;
|
|
391
|
+
return Number(b.id) - Number(a.id);
|
|
392
|
+
})[0];
|
|
393
|
+
}
|
|
394
|
+
|
|
343
395
|
/**
|
|
344
396
|
* Load the latest version of a draft/publish-enabled record for a publish/unpublish
|
|
345
397
|
* action, applying the two checks both actions share (workflow enabled, version is latest).
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SettingDefinition,
|
|
7
7
|
SettingLevel,
|
|
8
8
|
} from "src/interfaces";
|
|
9
|
+
import { getDefaultThemeKey, getThemesByMode } from "src/theme/theme-registry";
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_MEDIA_UPLOAD_DIR = "media-uploads";
|
|
11
12
|
export const DEFAULT_MEDIA_FILE_STORAGE_DIR = "media-files-storage";
|
|
@@ -224,14 +225,44 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
224
225
|
},
|
|
225
226
|
{
|
|
226
227
|
moduleName: "solid-core",
|
|
227
|
-
key: "
|
|
228
|
+
key: "showThemeToggle",
|
|
228
229
|
value: true,
|
|
229
230
|
level: SettingLevel.SystemAdminEditable,
|
|
230
|
-
label: "
|
|
231
|
+
label: "Show Theme Toggle",
|
|
231
232
|
group: "app-settings",
|
|
232
233
|
sortOrder: 120,
|
|
233
234
|
controlType: "boolean",
|
|
234
|
-
helpText: "
|
|
235
|
+
helpText: "Shows the theme toggle in the frontend so users can switch between light and dark themes.",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
moduleName: "solid-core",
|
|
239
|
+
key: "lightTheme",
|
|
240
|
+
value: getDefaultThemeKey("light"),
|
|
241
|
+
level: SettingLevel.SystemAdminEditable,
|
|
242
|
+
label: "Light Theme",
|
|
243
|
+
group: "app-settings",
|
|
244
|
+
sortOrder: 125,
|
|
245
|
+
controlType: "selectionStatic",
|
|
246
|
+
options: getThemesByMode("light").map((theme) => ({
|
|
247
|
+
label: theme.label,
|
|
248
|
+
value: theme.key,
|
|
249
|
+
})),
|
|
250
|
+
helpText: "Selects the theme used whenever the application is in light mode.",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
moduleName: "solid-core",
|
|
254
|
+
key: "darkTheme",
|
|
255
|
+
value: getDefaultThemeKey("dark"),
|
|
256
|
+
level: SettingLevel.SystemAdminEditable,
|
|
257
|
+
label: "Dark Theme",
|
|
258
|
+
group: "app-settings",
|
|
259
|
+
sortOrder: 126,
|
|
260
|
+
controlType: "selectionStatic",
|
|
261
|
+
options: getThemesByMode("dark").map((theme) => ({
|
|
262
|
+
label: theme.label,
|
|
263
|
+
value: theme.key,
|
|
264
|
+
})),
|
|
265
|
+
helpText: "Selects the theme used whenever the application is in dark mode.",
|
|
235
266
|
},
|
|
236
267
|
{
|
|
237
268
|
moduleName: "solid-core",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Backend theme registry.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - discover theme folders from the filesystem
|
|
9
|
+
* - classify each theme as light/dark from its key
|
|
10
|
+
* - expose default and grouped theme helpers for backend settings
|
|
11
|
+
*
|
|
12
|
+
* Shared pure parsing helpers are intentionally mirrored in solid-core-ui,
|
|
13
|
+
* but all filesystem discovery stays here.
|
|
14
|
+
*/
|
|
15
|
+
export type ThemeMode = "light" | "dark";
|
|
16
|
+
|
|
17
|
+
export type AppThemeDefinition = {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
mode: ThemeMode;
|
|
21
|
+
default?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const DEFAULT_THEME_KEYS: Record<ThemeMode, string> = {
|
|
25
|
+
light: "solid-light-purple",
|
|
26
|
+
dark: "solid-dark-purple",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const THEME_MODE_TOKENS: Record<ThemeMode, string> = {
|
|
30
|
+
light: "-light-",
|
|
31
|
+
dark: "-dark-",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const THEME_MODE_SUFFIXES: Record<ThemeMode, string> = {
|
|
35
|
+
light: "-light",
|
|
36
|
+
dark: "-dark",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function normalizeThemeKey(value?: string | null): string | null {
|
|
40
|
+
const normalized = value?.trim().toLowerCase();
|
|
41
|
+
return normalized || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function matchesThemeMode(value: string, mode: ThemeMode): boolean {
|
|
45
|
+
return (
|
|
46
|
+
value.includes(THEME_MODE_TOKENS[mode])
|
|
47
|
+
|| value.endsWith(THEME_MODE_SUFFIXES[mode])
|
|
48
|
+
|| value === mode
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function titleCase(value: string): string {
|
|
53
|
+
return value
|
|
54
|
+
.split(/[-_\s]+/)
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map((token) => {
|
|
57
|
+
if (token.toLowerCase() === "solid") return "SolidX";
|
|
58
|
+
return token.charAt(0).toUpperCase() + token.slice(1);
|
|
59
|
+
})
|
|
60
|
+
.join(" ");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getThemeModeFromThemeKey(value?: string | null): ThemeMode | null {
|
|
64
|
+
const normalized = normalizeThemeKey(value);
|
|
65
|
+
if (!normalized) return null;
|
|
66
|
+
|
|
67
|
+
if (matchesThemeMode(normalized, "light")) return "light";
|
|
68
|
+
if (matchesThemeMode(normalized, "dark")) return "dark";
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createThemeDefinition(key: string, defaultTheme = false): AppThemeDefinition {
|
|
74
|
+
const mode = getThemeModeFromThemeKey(key);
|
|
75
|
+
if (!mode) {
|
|
76
|
+
throw new Error(`Invalid theme key: ${key}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
key,
|
|
81
|
+
label: titleCase(key),
|
|
82
|
+
mode,
|
|
83
|
+
default: defaultTheme,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createDefaultTheme(mode: ThemeMode): AppThemeDefinition {
|
|
88
|
+
return createThemeDefinition(DEFAULT_THEME_KEYS[mode], true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function groupThemesByMode(themes: AppThemeDefinition[], mode: ThemeMode): AppThemeDefinition[] {
|
|
92
|
+
return themes.filter((theme) => theme.mode === mode);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pickDefaultThemeKey(themes: AppThemeDefinition[], mode: ThemeMode): string {
|
|
96
|
+
const themesForMode = groupThemesByMode(themes, mode);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
themesForMode.find((theme) => theme.default)?.key
|
|
100
|
+
?? themesForMode[0]?.key
|
|
101
|
+
?? DEFAULT_THEME_KEYS[mode]
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveThemeDirectory(): string | null {
|
|
106
|
+
const themeDirectory = path.resolve(process.cwd(), "..", "solid-ui", "public", "themes");
|
|
107
|
+
try {
|
|
108
|
+
if (fs.existsSync(themeDirectory) && fs.statSync(themeDirectory).isDirectory()) {
|
|
109
|
+
return path.resolve(themeDirectory);
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isThemeDirectory(themeDirectory: string, entryName: string): boolean {
|
|
119
|
+
return fs.existsSync(path.join(themeDirectory, entryName, "theme.css"));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readThemesFromPath(): AppThemeDefinition[] {
|
|
123
|
+
const themeDirectory = resolveThemeDirectory();
|
|
124
|
+
if (!themeDirectory) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const directoryEntries = fs.readdirSync(themeDirectory, { withFileTypes: true });
|
|
130
|
+
|
|
131
|
+
return directoryEntries
|
|
132
|
+
.filter((entry) => entry.isDirectory())
|
|
133
|
+
.map((entry) => entry.name.trim())
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.filter((entryName) => isThemeDirectory(themeDirectory, entryName))
|
|
136
|
+
.flatMap((entryName) => {
|
|
137
|
+
const mode = getThemeModeFromThemeKey(entryName);
|
|
138
|
+
if (!mode) return [];
|
|
139
|
+
|
|
140
|
+
return [createThemeDefinition(entryName, entryName === DEFAULT_THEME_KEYS[mode])];
|
|
141
|
+
})
|
|
142
|
+
.sort((left, right) => left.label.localeCompare(right.label));
|
|
143
|
+
} catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getThemes(): AppThemeDefinition[] {
|
|
149
|
+
const discoveredThemes = readThemesFromPath();
|
|
150
|
+
|
|
151
|
+
if (discoveredThemes.length) {
|
|
152
|
+
return discoveredThemes;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [createDefaultTheme("light"), createDefaultTheme("dark")];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getCoreThemes(): AppThemeDefinition[] {
|
|
159
|
+
return getThemes();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getRegisteredThemes(): AppThemeDefinition[] {
|
|
163
|
+
return getThemes();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function getThemesByMode(mode: ThemeMode): AppThemeDefinition[] {
|
|
167
|
+
return groupThemesByMode(getRegisteredThemes(), mode);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getDefaultThemeKey(mode: ThemeMode = "light"): string {
|
|
171
|
+
return pickDefaultThemeKey(getRegisteredThemes(), mode);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function isRegisteredThemeKey(value?: string | null, mode?: ThemeMode): value is string {
|
|
175
|
+
if (!value) return false;
|
|
176
|
+
|
|
177
|
+
return getRegisteredThemes().some((theme) => {
|
|
178
|
+
if (theme.key !== value) return false;
|
|
179
|
+
return mode ? theme.mode === mode : true;
|
|
180
|
+
});
|
|
181
|
+
}
|