opacacms 0.1.12 → 0.1.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.
Files changed (217) hide show
  1. package/dist/admin/index.js +0 -4
  2. package/dist/admin/webcomponent.d.ts +0 -1
  3. package/dist/admin/webcomponent.js +0 -4
  4. package/dist/admin.css +1 -0
  5. package/package.json +8 -2
  6. package/bun.lock +0 -34
  7. package/dist/admin/index.css +0 -47
  8. package/dist/admin/webcomponent.css +0 -47
  9. package/global.d.ts +0 -11
  10. package/src/admin/api-client.ts +0 -63
  11. package/src/admin/auth-client.ts +0 -40
  12. package/src/admin/custom-field.ts +0 -179
  13. package/src/admin/index.ts +0 -15
  14. package/src/admin/react.tsx +0 -72
  15. package/src/admin/router.ts +0 -9
  16. package/src/admin/stores/admin-queries.ts +0 -121
  17. package/src/admin/stores/auth.ts +0 -61
  18. package/src/admin/stores/column-visibility.ts +0 -67
  19. package/src/admin/stores/config.ts +0 -15
  20. package/src/admin/stores/media.ts +0 -95
  21. package/src/admin/stores/query.ts +0 -13
  22. package/src/admin/stores/ui.ts +0 -29
  23. package/src/admin/ui/admin-client.tsx +0 -283
  24. package/src/admin/ui/admin-layout.tsx +0 -276
  25. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  26. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  27. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  28. package/src/admin/ui/components/Table.tsx +0 -67
  29. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  30. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  31. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  32. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  33. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  34. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  35. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  36. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  37. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  38. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  39. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  40. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  41. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  42. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  43. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  44. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  45. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  46. package/src/admin/ui/components/fields/index.tsx +0 -371
  47. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  48. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  49. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  50. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  51. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  52. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  53. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  54. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  55. package/src/admin/ui/components/fields/utils.ts +0 -1
  56. package/src/admin/ui/components/link.tsx +0 -41
  57. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  58. package/src/admin/ui/components/toast.tsx +0 -72
  59. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  60. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  61. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  62. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  63. package/src/admin/ui/components/ui/button.tsx +0 -26
  64. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  65. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  66. package/src/admin/ui/components/ui/group.tsx +0 -20
  67. package/src/admin/ui/components/ui/index.ts +0 -17
  68. package/src/admin/ui/components/ui/input.tsx +0 -12
  69. package/src/admin/ui/components/ui/join.tsx +0 -53
  70. package/src/admin/ui/components/ui/label.tsx +0 -11
  71. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  72. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  73. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  74. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  75. package/src/admin/ui/components/ui/select.tsx +0 -187
  76. package/src/admin/ui/components/ui/separator.tsx +0 -21
  77. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  78. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  79. package/src/admin/ui/components/ui/utils.ts +0 -3
  80. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  81. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  82. package/src/admin/ui/styles/accordion.scss +0 -60
  83. package/src/admin/ui/styles/animations.scss +0 -41
  84. package/src/admin/ui/styles/asset-manager.scss +0 -547
  85. package/src/admin/ui/styles/badge.scss +0 -13
  86. package/src/admin/ui/styles/base.scss +0 -22
  87. package/src/admin/ui/styles/button.scss +0 -161
  88. package/src/admin/ui/styles/card.scss +0 -13
  89. package/src/admin/ui/styles/collapsible.scss +0 -75
  90. package/src/admin/ui/styles/data-detail.scss +0 -92
  91. package/src/admin/ui/styles/dialog.scss +0 -102
  92. package/src/admin/ui/styles/empty-state.scss +0 -22
  93. package/src/admin/ui/styles/group.scss +0 -19
  94. package/src/admin/ui/styles/index.scss +0 -33
  95. package/src/admin/ui/styles/input.scss +0 -80
  96. package/src/admin/ui/styles/label.scss +0 -12
  97. package/src/admin/ui/styles/layout.scss +0 -56
  98. package/src/admin/ui/styles/lexical.scss +0 -469
  99. package/src/admin/ui/styles/loading.scss +0 -102
  100. package/src/admin/ui/styles/media-registry.scss +0 -597
  101. package/src/admin/ui/styles/pagination.scss +0 -20
  102. package/src/admin/ui/styles/radio-group.scss +0 -66
  103. package/src/admin/ui/styles/row.scss +0 -17
  104. package/src/admin/ui/styles/scrollbar.scss +0 -36
  105. package/src/admin/ui/styles/select.scss +0 -121
  106. package/src/admin/ui/styles/separator.scss +0 -14
  107. package/src/admin/ui/styles/sheet.scss +0 -152
  108. package/src/admin/ui/styles/sidebar.scss +0 -148
  109. package/src/admin/ui/styles/switch.scss +0 -59
  110. package/src/admin/ui/styles/table.scss +0 -207
  111. package/src/admin/ui/styles/tabs.scss +0 -62
  112. package/src/admin/ui/styles/toast.scss +0 -45
  113. package/src/admin/ui/styles/variables.scss +0 -24
  114. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  115. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  116. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  117. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  118. package/src/admin/ui/views/init-view.tsx +0 -182
  119. package/src/admin/ui/views/login-view.tsx +0 -123
  120. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  121. package/src/admin/ui/views/settings-view.tsx +0 -729
  122. package/src/admin/webcomponent.tsx +0 -15
  123. package/src/auth/index.ts +0 -194
  124. package/src/auth/migrations.ts +0 -87
  125. package/src/auth/premissions.ts +0 -46
  126. package/src/cli/commands/generate-types.ts +0 -116
  127. package/src/cli/commands/init.ts +0 -95
  128. package/src/cli/commands/migrate-commands.ts +0 -160
  129. package/src/cli/commands/seed-command.ts +0 -11
  130. package/src/cli/d1-mock.ts +0 -101
  131. package/src/cli/index.test.ts +0 -84
  132. package/src/cli/index.ts +0 -183
  133. package/src/cli/r2-mock.ts +0 -217
  134. package/src/cli/seeding.ts +0 -409
  135. package/src/client.ts +0 -181
  136. package/src/config-utils.ts +0 -102
  137. package/src/config.ts +0 -49
  138. package/src/db/adapter.ts +0 -53
  139. package/src/db/better-sqlite.ts +0 -657
  140. package/src/db/bun-sqlite.ts +0 -666
  141. package/src/db/d1.ts +0 -721
  142. package/src/db/index.ts +0 -10
  143. package/src/db/kysely/data-mapper.ts +0 -142
  144. package/src/db/kysely/field-mapper.ts +0 -149
  145. package/src/db/kysely/migration-generator.ts +0 -223
  146. package/src/db/kysely/query-builder.ts +0 -92
  147. package/src/db/kysely/schema-builder.ts +0 -439
  148. package/src/db/kysely/sql-utils.ts +0 -13
  149. package/src/db/postgres.ts +0 -631
  150. package/src/db/sqlite.ts +0 -670
  151. package/src/db/system-schema.ts +0 -121
  152. package/src/index.ts +0 -13
  153. package/src/runtimes/README.md +0 -59
  154. package/src/runtimes/bun.ts +0 -49
  155. package/src/runtimes/cloudflare-workers.ts +0 -38
  156. package/src/runtimes/next.ts +0 -26
  157. package/src/runtimes/node.ts +0 -52
  158. package/src/schema/collection.ts +0 -184
  159. package/src/schema/fields/base.ts +0 -164
  160. package/src/schema/fields/index.ts +0 -427
  161. package/src/schema/global.ts +0 -145
  162. package/src/schema/index.ts +0 -4
  163. package/src/schema/infer.ts +0 -72
  164. package/src/server/admin-router.ts +0 -20
  165. package/src/server/admin.ts +0 -142
  166. package/src/server/assets.ts +0 -306
  167. package/src/server/collection-router.ts +0 -55
  168. package/src/server/handlers.ts +0 -722
  169. package/src/server/middlewares/admin.ts +0 -27
  170. package/src/server/middlewares/auth.ts +0 -89
  171. package/src/server/middlewares/context.ts +0 -17
  172. package/src/server/middlewares/cors.ts +0 -24
  173. package/src/server/middlewares/database-init.ts +0 -74
  174. package/src/server/middlewares/rate-limit.ts +0 -77
  175. package/src/server/router.ts +0 -47
  176. package/src/server/setup-middlewares.ts +0 -58
  177. package/src/server/system-router.ts +0 -35
  178. package/src/server.ts +0 -9
  179. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  180. package/src/storage/adapters/local.ts +0 -146
  181. package/src/storage/adapters/s3.ts +0 -186
  182. package/src/storage/errors.ts +0 -46
  183. package/src/storage/index.ts +0 -5
  184. package/src/storage/types.ts +0 -39
  185. package/src/types.ts +0 -577
  186. package/src/utils/lexical.ts +0 -37
  187. package/src/utils/logger.ts +0 -73
  188. package/src/validation.ts +0 -429
  189. package/src/validator.ts +0 -179
  190. package/test/admin-custom-field.test.ts +0 -162
  191. package/test/admin-react-field.test.tsx +0 -134
  192. package/test/api-features.test.ts +0 -78
  193. package/test/api.test.ts +0 -178
  194. package/test/auth.test.ts +0 -62
  195. package/test/cli-integration.test.ts +0 -148
  196. package/test/cli.test.ts +0 -25
  197. package/test/db/postgres.test.ts +0 -95
  198. package/test/db/sqlite-filter.test.ts +0 -53
  199. package/test/db/sqlite.test.ts +0 -82
  200. package/test/engine-features.test.ts +0 -79
  201. package/test/globals.test.ts +0 -74
  202. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  203. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  204. package/test/integration-tmp/my-test-app/index.ts +0 -8
  205. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  206. package/test/integration-tmp/my-test-app/package.json +0 -12
  207. package/test/populate.test.ts +0 -79
  208. package/test/runtimes.test.ts +0 -43
  209. package/test/schema-builder.test.ts +0 -107
  210. package/test/schema-features.test.ts +0 -63
  211. package/test/seeding.test.ts +0 -68
  212. package/test/storage/local.test.ts +0 -72
  213. package/test/storage/s3.test.ts +0 -60
  214. package/test/structural-data.test.ts +0 -100
  215. package/test/test-setup.ts +0 -11
  216. package/test/validation.test.ts +0 -162
  217. package/tsconfig.json +0 -42
@@ -1,202 +0,0 @@
1
- import { Plus, Trash2 } from "lucide-react";
2
- import type React from "react";
3
- import { Blocks } from "../ui/blocks";
4
- import { Button } from "../ui/button";
5
- import { capitalize } from "./utils";
6
-
7
- interface BlocksFieldProps {
8
- name: string;
9
- label?: string;
10
- blocks: Array<{
11
- slug: string;
12
- fields: any[];
13
- label?: string;
14
- }>;
15
- value: any[];
16
- onChange: (val: any[]) => void;
17
- disabled?: boolean;
18
- readOnly?: boolean;
19
- renderField: (field: any, value: any, onChange: (val: any) => void) => React.ReactNode;
20
- }
21
-
22
- export const BlocksField: React.FC<BlocksFieldProps> = ({
23
- name,
24
- label,
25
- blocks,
26
- value = [],
27
- onChange,
28
- disabled,
29
- readOnly,
30
- renderField,
31
- }) => {
32
- const addBlock = (blockSlug: string) => {
33
- const currentValue = Array.isArray(value) ? value : [];
34
- const blockDef = blocks.find((b) => b.slug === blockSlug);
35
-
36
- const initialData: Record<string, any> = { blockType: blockSlug };
37
-
38
- if (blockDef) {
39
- blockDef.fields.forEach((f) => {
40
- if (f.name) {
41
- if (f.defaultValue !== undefined) {
42
- initialData[f.name] = f.defaultValue;
43
- } else if (f.type === "boolean") {
44
- initialData[f.name] = false;
45
- } else if (f.type === "number") {
46
- initialData[f.name] = 0;
47
- } else if (f.type === "relationship" && f.hasMany) {
48
- initialData[f.name] = [];
49
- } else if (f.type === "blocks") {
50
- initialData[f.name] = [];
51
- } else {
52
- initialData[f.name] = "";
53
- }
54
- }
55
- });
56
- }
57
-
58
- onChange([...currentValue, initialData]);
59
- };
60
-
61
- const removeBlock = (index: number) => {
62
- const currentValue = Array.isArray(value) ? value : [];
63
- const newValue = [...currentValue];
64
- newValue.splice(index, 1);
65
- onChange(newValue);
66
- };
67
-
68
- const updateBlockData = (index: number, fieldName: string, fieldValue: any) => {
69
- const currentValue = Array.isArray(value) ? value : [];
70
- const newValue = [...currentValue];
71
- newValue[index] = {
72
- ...newValue[index],
73
- [fieldName]: fieldValue,
74
- };
75
- onChange(newValue);
76
- };
77
-
78
- return (
79
- <Blocks label={label || name} className="opaca-blocks-field">
80
- <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
81
- {(Array.isArray(value) ? value : []).map((item, index) => {
82
- const blockDef = blocks.find((b) => b.slug === item.blockType);
83
- if (!blockDef) return null;
84
-
85
- return (
86
- <div
87
- key={`${item.blockType}-${index}`}
88
- style={{
89
- border: "1px solid var(--opaca-border)",
90
- borderRadius: "var(--opaca-radius-lg)",
91
- backgroundColor: "rgba(255, 255, 255, 0.02)",
92
- padding: "1.25rem",
93
- position: "relative",
94
- transition: "border-color var(--opaca-transition)",
95
- }}
96
- className="opaca-block-item"
97
- >
98
- <div
99
- style={{
100
- display: "flex",
101
- justifyContent: "space-between",
102
- alignItems: "center",
103
- marginBottom: "1.25rem",
104
- borderBottom: "1px solid var(--opaca-border)",
105
- paddingBottom: "0.75rem",
106
- }}
107
- >
108
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
109
- <div
110
- style={{
111
- width: "8px",
112
- height: "8px",
113
- borderRadius: "50%",
114
- backgroundColor: "var(--opaca-primary)",
115
- }}
116
- ></div>
117
- <h4
118
- style={{
119
- margin: 0,
120
- fontSize: "0.8125rem",
121
- fontWeight: 600,
122
- color: "var(--opaca-text)",
123
- }}
124
- >
125
- {blockDef.label || capitalize(blockDef.slug)}
126
- </h4>
127
- </div>
128
- {!disabled && !readOnly && (
129
- <Button
130
- type="button"
131
- variant="ghost"
132
- size="icon"
133
- onClick={() => removeBlock(index)}
134
- style={{ color: "var(--opaca-text-dim)", height: "24px", width: "24px" }}
135
- >
136
- <Trash2 size={14} />
137
- </Button>
138
- )}
139
- </div>
140
- <div style={{ display: "flex", flexDirection: "column", gap: "1.25rem" }}>
141
- {blockDef.fields.map((field) => (
142
- <div key={field.name} className="block-field-item">
143
- {renderField(field, item[field.name], (val) =>
144
- updateBlockData(index, field.name, val),
145
- )}
146
- </div>
147
- ))}
148
- </div>
149
- </div>
150
- );
151
- })}
152
-
153
- {!disabled && !readOnly && (
154
- <div
155
- style={{
156
- marginTop: "0.5rem",
157
- padding: "1.25rem",
158
- border: "1px dashed var(--opaca-border)",
159
- borderRadius: "var(--opaca-radius-lg)",
160
- textAlign: "center",
161
- }}
162
- >
163
- <p
164
- style={{
165
- fontSize: "0.75rem",
166
- fontWeight: 500,
167
- color: "var(--opaca-text-muted)",
168
- textTransform: "uppercase",
169
- letterSpacing: "0.03em",
170
- marginBottom: "1rem",
171
- }}
172
- >
173
- Add a new block:
174
- </p>
175
- <div
176
- style={{
177
- display: "flex",
178
- flexWrap: "wrap",
179
- gap: "0.75rem",
180
- justifyContent: "center",
181
- }}
182
- >
183
- {blocks.map((block) => (
184
- <Button
185
- type="button"
186
- key={block.slug}
187
- variant="outline"
188
- size="sm"
189
- onClick={() => addBlock(block.slug)}
190
- style={{ display: "flex", alignItems: "center", gap: "6px", fontSize: "0.75rem" }}
191
- >
192
- <Plus size={12} />
193
- {block.label || capitalize(block.slug)}
194
- </Button>
195
- ))}
196
- </div>
197
- </div>
198
- )}
199
- </div>
200
- </Blocks>
201
- );
202
- };
@@ -1,50 +0,0 @@
1
- import type React from "react";
2
-
3
- interface BooleanFieldProps {
4
- name: string;
5
- label?: string;
6
- value: boolean;
7
- onChange: (val: boolean) => void;
8
- disabled?: boolean;
9
- readOnly?: boolean;
10
- error?: string;
11
- renderType?: "switch" | "toggle" | "checkbox";
12
- }
13
-
14
- export const BooleanField: React.FC<BooleanFieldProps> = ({
15
- name,
16
- label,
17
- value,
18
- onChange,
19
- disabled,
20
- readOnly,
21
- error,
22
- renderType = "switch",
23
- }) => {
24
- const isDisabled = disabled || readOnly;
25
-
26
- return (
27
- <div className="opaca-form-group">
28
- <label className="opaca-label" htmlFor={`field-${name}`}>
29
- {label || name}
30
- </label>
31
-
32
- <label className="opaca-switch">
33
- <input
34
- id={`field-${name}`}
35
- type="checkbox"
36
- checked={!!value}
37
- disabled={isDisabled}
38
- onChange={(e) => onChange(e.target.checked)}
39
- />
40
- <div className="opaca-switch-track">
41
- <div className="opaca-switch-thumb" />
42
- </div>
43
- <span className="opaca-switch-label" style={{ minWidth: "60px" }}>
44
- {value ? "Enabled" : "Disabled"}
45
- </span>
46
- </label>
47
- {error && <span className="opaca-field-error">{error}</span>}
48
- </div>
49
- );
50
- };
@@ -1,75 +0,0 @@
1
- import type React from "react";
2
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible";
3
-
4
- interface CollapsibleFieldProps {
5
- label?: string;
6
- fields: any[];
7
- parentData: any;
8
- disabled?: boolean;
9
- readOnly?: boolean;
10
- options?: {
11
- initiallyCollapsed?: boolean;
12
- useAsTitle?: string;
13
- };
14
- onChange: (fieldName: string, value: any) => void;
15
- renderField: (field: any, value: any, onChange: (val: any) => void) => React.ReactNode;
16
- }
17
-
18
- export const CollapsibleField: React.FC<CollapsibleFieldProps> = ({
19
- label,
20
- fields,
21
- parentData,
22
- disabled,
23
- readOnly,
24
- options,
25
- onChange,
26
- renderField,
27
- }) => {
28
- const getValue = (data: any, path: string) => {
29
- return path.split(".").reduce((obj, key) => obj?.[key], data);
30
- };
31
-
32
- const resolveTitle = (template: string, data: any) => {
33
- if (!template.includes("{{")) return getValue(data, template);
34
- return template.replace(/\{\{(.+?)\}\}/g, (_, path) => {
35
- const val = getValue(data, path.trim());
36
- return val !== undefined && val !== null ? String(val) : "";
37
- });
38
- };
39
-
40
- const dynamicTitle = options?.useAsTitle
41
- ? resolveTitle(options.useAsTitle, parentData)
42
- : undefined;
43
-
44
- return (
45
- <Collapsible
46
- defaultOpen={!options?.initiallyCollapsed}
47
- className="opaca-collapsible-field"
48
- disabled={disabled}
49
- >
50
- <CollapsibleTrigger className="opaca-collapsible-field-trigger">
51
- {dynamicTitle || label || "Section"}
52
- </CollapsibleTrigger>
53
- <CollapsibleContent>
54
- <div
55
- style={{
56
- padding: "0",
57
- display: "flex",
58
- flexDirection: "column",
59
- gap: "1.25rem",
60
- }}
61
- >
62
- {(fields || []).map((field, index) => (
63
- <div key={field.name || `collapsible-item-${index}`} className="collapsible-field-item">
64
- {renderField(
65
- field,
66
- field.name ? parentData?.[field.name] : undefined,
67
- (val) => field.name && onChange(field.name, val),
68
- )}
69
- </div>
70
- ))}
71
- </div>
72
- </CollapsibleContent>
73
- </Collapsible>
74
- );
75
- };
@@ -1,45 +0,0 @@
1
- import type React from "react";
2
-
3
- interface DateFieldProps {
4
- name: string;
5
- label?: string;
6
- value: string | number | Date;
7
- onChange: (val: string) => void;
8
- required?: boolean;
9
- disabled?: boolean;
10
- readOnly?: boolean;
11
- error?: string;
12
- }
13
-
14
- export const DateField: React.FC<DateFieldProps> = ({
15
- name,
16
- label,
17
- value,
18
- onChange,
19
- required,
20
- disabled,
21
- readOnly,
22
- error,
23
- }) => {
24
- const formattedValue = value ? new Date(value).toISOString().split("T")[0] : "";
25
-
26
- return (
27
- <div className="opaca-form-group">
28
- <label className="opaca-label" htmlFor={`field-${name}`}>
29
- {label || name}
30
- {required && <span style={{ color: "var(--opaca-error)", marginLeft: "4px" }}>*</span>}
31
- </label>
32
- <input
33
- id={`field-${name}`}
34
- type="date"
35
- className={`opaca-input ${error ? "error" : ""}`}
36
- value={formattedValue}
37
- readOnly={readOnly}
38
- disabled={disabled}
39
- onChange={(e) => onChange(e.target.value)}
40
- required={required}
41
- />
42
- {error && <span className="opaca-field-error">{error}</span>}
43
- </div>
44
- );
45
- };
@@ -1,322 +0,0 @@
1
- import type React from "react";
2
- import { useEffect, useState } from "react";
3
- import { api, getCurrentBaseURL } from "../../../api-client";
4
- import { AssetManagerModal } from "../media/AssetManagerModal";
5
- import "../../styles/asset-manager.scss";
6
-
7
- interface FileFieldProps {
8
- name: string;
9
- label?: string;
10
- value: any; // { assetId, url, filename, mime_type, filesize, meta: {} }
11
- onChange: (value: any) => void;
12
- options?: {
13
- allowedmime_types?: string[];
14
- maxFileSize?: number;
15
- metaFields?: Array<{
16
- name: string;
17
- type: string;
18
- label?: string;
19
- required?: boolean;
20
- }>;
21
- };
22
- disabled?: boolean;
23
- readOnly?: boolean;
24
- bucket?: string;
25
- }
26
-
27
- export const FileField: React.FC<FileFieldProps> = ({
28
- name,
29
- label,
30
- value,
31
- onChange,
32
- options,
33
- disabled,
34
- readOnly,
35
- bucket = "default",
36
- }) => {
37
- const [isModalOpen, setIsModalOpen] = useState(false);
38
- const [assetMetadata, setAssetMetadata] = useState<any>(null);
39
- const [isLoadingMetadata, setIsLoadingMetadata] = useState(false);
40
- const [optimisticFile, setOptimisticFile] = useState<{
41
- url: string;
42
- file: File;
43
- progress: number;
44
- } | null>(null);
45
- const [uploadError, setUploadError] = useState<string | null>(null);
46
-
47
- const isRestricted = disabled || readOnly;
48
-
49
- // Extract ID from value (could be string ID or older object format)
50
- const assetId = typeof value === "string" ? value : value?.assetId;
51
-
52
- // Fetch metadata if we only have an ID
53
- useEffect(() => {
54
- if (assetId && (!value || typeof value === "string" || !value.filename)) {
55
- setIsLoadingMetadata(true);
56
- api
57
- .get(`api/assets/${assetId}`)
58
- .json<any>()
59
- .then((data) => {
60
- setAssetMetadata(data);
61
- })
62
- .catch((err) => {
63
- console.error("Failed to fetch asset metadata", err);
64
- })
65
- .finally(() => {
66
- setIsLoadingMetadata(false);
67
- });
68
- } else if (value && typeof value === "object" && value.filename) {
69
- setAssetMetadata(value);
70
- } else {
71
- setAssetMetadata(null);
72
- }
73
- }, [assetId, value]);
74
-
75
- // Cleanup object URLs to prevent memory leaks when the component unmounts
76
- useEffect(() => {
77
- return () => {
78
- if (optimisticFile?.url) {
79
- URL.revokeObjectURL(optimisticFile.url);
80
- }
81
- };
82
- }, [optimisticFile]);
83
-
84
- // Handle a direct file drop on the field (bypassing the global modal)
85
- const handleDirectDrop = async (e: React.DragEvent) => {
86
- e.preventDefault();
87
- if (isRestricted) return;
88
-
89
- const file = e.dataTransfer.files[0];
90
- if (!file) return;
91
-
92
- if (options?.allowedmime_types && !options.allowedmime_types.includes(file.type)) {
93
- setUploadError(`Invalid file type. Allowed: ${options.allowedmime_types.join(", ")}`);
94
- return;
95
- }
96
- if (options?.maxFileSize && file.size > options.maxFileSize) {
97
- setUploadError(`File too large. Max size: ${options.maxFileSize / 1024 / 1024}MB`);
98
- return;
99
- }
100
-
101
- // 1. Create Optimistic Preview Instantly
102
- const tempUrl = URL.createObjectURL(file);
103
- setOptimisticFile({ url: tempUrl, file, progress: 0 });
104
- setUploadError(null);
105
-
106
- // 2. Start Background Upload
107
- const formData = new FormData();
108
- formData.append("file", file);
109
-
110
- try {
111
- const response = await api
112
- .post(`api/__system/assets/upload?bucket=${bucket}`, {
113
- body: formData,
114
- })
115
- .json<any>();
116
-
117
- // 3. Success! Update with the ID/Key only as requested
118
- const id = response.id || response.assetId;
119
- setOptimisticFile(null); // Clear optimistic state
120
- onChange(id); // Update the real form state with JUST the ID
121
- } catch (err: any) {
122
- // 4. Failure! Revert the UI
123
- setOptimisticFile(null);
124
- setUploadError(err.message);
125
- }
126
- };
127
-
128
- const handleRemove = () => {
129
- if (isRestricted) return;
130
- onChange(null);
131
- };
132
-
133
- // Note: Local metadata for the field itself is currently not handled if we only store ID.
134
- // We might want to store a wrapper object if metadata is needed: { assetId, meta: {} }
135
- const handleMetaChange = (fieldName: string, fieldValue: any) => {
136
- if (isRestricted) return;
137
-
138
- // If we need to support meta, we might need to keep a small object
139
- // But per user request, ID only is preferred for the asset itself.
140
- // For now, if meta is present, we'll store as an object.
141
- const currentMeta = typeof value === "object" ? value.meta || {} : {};
142
- onChange({
143
- assetId: assetId,
144
- meta: {
145
- ...currentMeta,
146
- [fieldName]: fieldValue,
147
- },
148
- });
149
- };
150
-
151
- const getResolvedUrl = (asset: any) => {
152
- if (!asset) return "";
153
- if (asset.url) return asset.url;
154
- return `${getCurrentBaseURL()}/api/assets/${asset.id || asset.assetId}/view`;
155
- };
156
-
157
- return (
158
- <div
159
- className={`file-field-container ${isRestricted ? "is-restricted" : ""}`}
160
- onDragOver={(e) => !isRestricted && e.preventDefault()}
161
- onDrop={handleDirectDrop}
162
- >
163
- <div className="file-field-label-row">
164
- <label>{label || name}</label>
165
- {uploadError && <span className="upload-error">{uploadError}</span>}
166
- </div>
167
-
168
- {/* State 1: Optimistic Uploading */}
169
- {optimisticFile ? (
170
- <div className="file-field-optimistic-card">
171
- <div className="optimistic-info">
172
- {optimisticFile.file.type.startsWith("image/") ? (
173
- <img src={optimisticFile.url} alt="Uploading..." />
174
- ) : (
175
- <div className="file-icon-placeholder">
176
- <span>FILE</span>
177
- </div>
178
- )}
179
- <div className="optimistic-details">
180
- <span className="filename">{optimisticFile.file.name}</span>
181
- <span className="progress-text">Uploading...</span>
182
- <div className="progress-bar-bg">
183
- <div className="progress-bar-fill" style={{ width: `50%` }}></div>
184
- </div>
185
- </div>
186
- </div>
187
- </div>
188
- ) : assetMetadata ? (
189
- <>
190
- <div className="file-field-asset-card">
191
- <div className="file-field-asset-info">
192
- <div className="asset-preview">
193
- {(() => {
194
- const mime = assetMetadata.mimeType || assetMetadata.mime_type;
195
- if (mime?.startsWith("image/")) {
196
- return <img src={getResolvedUrl(assetMetadata)} alt={assetMetadata.filename} />;
197
- }
198
- return (
199
- <div className="file-icon-placeholder">
200
- <span>{mime?.split("/")[1]?.toUpperCase() || "FILE"}</span>
201
- </div>
202
- );
203
- })()}
204
- </div>
205
- <div className="asset-details">
206
- <span className="filename" title={assetMetadata.filename}>
207
- {assetMetadata.filename}
208
- </span>
209
- <span className="filesize">
210
- {assetMetadata.filesize
211
- ? `${(assetMetadata.filesize / 1024).toFixed(1)} KB`
212
- : "..."}
213
- </span>
214
- </div>
215
- </div>
216
- {!isRestricted && (
217
- <div className="asset-actions">
218
- <button
219
- type="button"
220
- onClick={() => setIsModalOpen(true)}
221
- className="replace-button"
222
- >
223
- Replace
224
- </button>
225
- <button type="button" onClick={handleRemove} className="remove-button">
226
- Remove
227
- </button>
228
- </div>
229
- )}
230
- </div>
231
-
232
- {/* Contextual Metadata Fields */}
233
- {options?.metaFields && options.metaFields.length > 0 && (
234
- <div className="file-field-metadata">
235
- <h4>Contextual Metadata</h4>
236
- {options.metaFields.map((metaField) => (
237
- <div key={metaField.name} className="metadata-field">
238
- <label htmlFor={metaField.name}>
239
- {metaField.label || metaField.name}{" "}
240
- {metaField.required && <span className="required">*</span>}
241
- </label>
242
- {metaField.type === "textarea" ? (
243
- <textarea
244
- id={metaField.name}
245
- rows={3}
246
- value={(value?.meta || {})[metaField.name] || ""}
247
- onChange={(e) => handleMetaChange(metaField.name, e.target.value)}
248
- readOnly={readOnly}
249
- disabled={disabled}
250
- placeholder={
251
- isRestricted
252
- ? ""
253
- : `Enter ${metaField.label?.toLowerCase() || metaField.name}...`
254
- }
255
- />
256
- ) : (
257
- <input
258
- type={metaField.type === "number" ? "number" : "text"}
259
- value={(value?.meta || {})[metaField.name] || ""}
260
- onChange={(e) =>
261
- handleMetaChange(
262
- metaField.name,
263
- metaField.type === "number" ? Number(e.target.value) : e.target.value,
264
- )
265
- }
266
- readOnly={readOnly}
267
- disabled={disabled}
268
- placeholder={
269
- isRestricted
270
- ? ""
271
- : `Enter ${metaField.label?.toLowerCase() || metaField.name}...`
272
- }
273
- />
274
- )}
275
- </div>
276
- ))}
277
- </div>
278
- )}
279
- </>
280
- ) : isLoadingMetadata ? (
281
- <div className="file-field-optimistic-card">
282
- <div className="status-text">Loading asset details...</div>
283
- </div>
284
- ) : (
285
- // State 3: Empty state
286
- <button
287
- type="button"
288
- onClick={() => !isRestricted && setIsModalOpen(true)}
289
- disabled={isRestricted}
290
- className={`file-field-empty-button ${isRestricted ? "disabled" : ""}`}
291
- >
292
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
293
- <path
294
- strokeLinecap="round"
295
- strokeLinejoin="round"
296
- strokeWidth="2"
297
- d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
298
- ></path>
299
- </svg>
300
- <span>{isRestricted ? "No file selected" : "Drag a file here or click to select"}</span>
301
- </button>
302
- )}
303
-
304
- {/* Render the Global Modal if Open */}
305
- {isModalOpen && !isRestricted && (
306
- <AssetManagerModal
307
- bucket={bucket}
308
- onClose={() => setIsModalOpen(false)}
309
- onSelect={(asset) => {
310
- // Per user request, save only the ID/Key
311
- if (!isRestricted) {
312
- onChange(asset.assetId);
313
- setIsModalOpen(false);
314
- }
315
- }}
316
- allowedmime_types={options?.allowedmime_types}
317
- maxFileSize={options?.maxFileSize}
318
- />
319
- )}
320
- </div>
321
- );
322
- };