@nocobase/portal-template-default 1.0.0

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 (123) hide show
  1. package/.env.example +10 -0
  2. package/Dockerfile +36 -0
  3. package/README.MD +50 -0
  4. package/components.json +25 -0
  5. package/eslint.config.js +28 -0
  6. package/index.html +39 -0
  7. package/package.json +84 -0
  8. package/pnpm-workspace.yaml +2 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/logo-dark.png +0 -0
  11. package/public/logo-mark-dark.png +0 -0
  12. package/public/logo-mark.png +0 -0
  13. package/public/logo.png +0 -0
  14. package/src/App.css +158 -0
  15. package/src/App.tsx +151 -0
  16. package/src/components/app-shell/brand.tsx +70 -0
  17. package/src/components/app-shell/breadcrumb.tsx +81 -0
  18. package/src/components/app-shell/document-title-handler.tsx +169 -0
  19. package/src/components/app-shell/error-component.tsx +134 -0
  20. package/src/components/app-shell/header.tsx +127 -0
  21. package/src/components/app-shell/layout.tsx +39 -0
  22. package/src/components/app-shell/loading-overlay.tsx +36 -0
  23. package/src/components/app-shell/sidebar.tsx +351 -0
  24. package/src/components/app-shell/user-avatar.tsx +42 -0
  25. package/src/components/app-shell/user-info.tsx +53 -0
  26. package/src/components/auth/auth-layout.tsx +82 -0
  27. package/src/components/auth/forgot-password-form.tsx +58 -0
  28. package/src/components/auth/input-password.tsx +38 -0
  29. package/src/components/auth/sign-in-form.tsx +111 -0
  30. package/src/components/auth/sign-up-form.tsx +127 -0
  31. package/src/components/data-table/data-table-filter.tsx +994 -0
  32. package/src/components/data-table/data-table-pagination.tsx +146 -0
  33. package/src/components/data-table/data-table-sorter.tsx +47 -0
  34. package/src/components/data-table/data-table.tsx +321 -0
  35. package/src/components/notifications/toaster.tsx +23 -0
  36. package/src/components/notifications/undoable-notification.tsx +84 -0
  37. package/src/components/notifications/use-notification-provider.tsx +62 -0
  38. package/src/components/resources/buttons/clone.tsx +81 -0
  39. package/src/components/resources/buttons/create.tsx +70 -0
  40. package/src/components/resources/buttons/delete.tsx +113 -0
  41. package/src/components/resources/buttons/edit.tsx +81 -0
  42. package/src/components/resources/buttons/list.tsx +70 -0
  43. package/src/components/resources/buttons/refresh.tsx +81 -0
  44. package/src/components/resources/buttons/show.tsx +81 -0
  45. package/src/components/resources/status-badge.tsx +37 -0
  46. package/src/components/resources/views/create-view.tsx +81 -0
  47. package/src/components/resources/views/edit-view.tsx +104 -0
  48. package/src/components/resources/views/list-view.tsx +86 -0
  49. package/src/components/resources/views/show-view.tsx +108 -0
  50. package/src/components/theme/theme-provider.tsx +71 -0
  51. package/src/components/theme/theme-select.tsx +97 -0
  52. package/src/components/theme/theme-toggle.tsx +66 -0
  53. package/src/components/ui/accordion.tsx +72 -0
  54. package/src/components/ui/alert-dialog.tsx +185 -0
  55. package/src/components/ui/alert.tsx +76 -0
  56. package/src/components/ui/aspect-ratio.tsx +22 -0
  57. package/src/components/ui/avatar.tsx +107 -0
  58. package/src/components/ui/badge.tsx +52 -0
  59. package/src/components/ui/breadcrumb.tsx +125 -0
  60. package/src/components/ui/button.tsx +58 -0
  61. package/src/components/ui/calendar.tsx +221 -0
  62. package/src/components/ui/card.tsx +103 -0
  63. package/src/components/ui/carousel.tsx +240 -0
  64. package/src/components/ui/chart.tsx +373 -0
  65. package/src/components/ui/checkbox.tsx +29 -0
  66. package/src/components/ui/collapsible.tsx +19 -0
  67. package/src/components/ui/command.tsx +192 -0
  68. package/src/components/ui/context-menu.tsx +271 -0
  69. package/src/components/ui/dialog.tsx +160 -0
  70. package/src/components/ui/drawer.tsx +226 -0
  71. package/src/components/ui/dropdown-menu.tsx +268 -0
  72. package/src/components/ui/form.tsx +174 -0
  73. package/src/components/ui/hover-card.tsx +49 -0
  74. package/src/components/ui/input-group.tsx +158 -0
  75. package/src/components/ui/input-otp.tsx +87 -0
  76. package/src/components/ui/input.tsx +20 -0
  77. package/src/components/ui/label.tsx +18 -0
  78. package/src/components/ui/menubar.tsx +278 -0
  79. package/src/components/ui/navigation-menu.tsx +168 -0
  80. package/src/components/ui/pagination.tsx +130 -0
  81. package/src/components/ui/popover.tsx +91 -0
  82. package/src/components/ui/progress.tsx +81 -0
  83. package/src/components/ui/radio-group.tsx +38 -0
  84. package/src/components/ui/resizable.tsx +48 -0
  85. package/src/components/ui/scroll-area.tsx +55 -0
  86. package/src/components/ui/select.tsx +199 -0
  87. package/src/components/ui/separator.tsx +25 -0
  88. package/src/components/ui/sheet.tsx +136 -0
  89. package/src/components/ui/sidebar.tsx +723 -0
  90. package/src/components/ui/skeleton.tsx +13 -0
  91. package/src/components/ui/slider.tsx +52 -0
  92. package/src/components/ui/sonner.tsx +47 -0
  93. package/src/components/ui/switch.tsx +32 -0
  94. package/src/components/ui/table.tsx +114 -0
  95. package/src/components/ui/tabs.tsx +82 -0
  96. package/src/components/ui/textarea.tsx +18 -0
  97. package/src/components/ui/toggle-group.tsx +87 -0
  98. package/src/components/ui/toggle.tsx +43 -0
  99. package/src/components/ui/tooltip.tsx +66 -0
  100. package/src/hooks/use-mobile.ts +19 -0
  101. package/src/index.tsx +17 -0
  102. package/src/lib/utils.ts +19 -0
  103. package/src/pages/blog-posts/create.tsx +177 -0
  104. package/src/pages/blog-posts/edit.tsx +197 -0
  105. package/src/pages/blog-posts/index.ts +4 -0
  106. package/src/pages/blog-posts/list.tsx +131 -0
  107. package/src/pages/blog-posts/show.tsx +81 -0
  108. package/src/pages/categories/create.tsx +73 -0
  109. package/src/pages/categories/edit.tsx +73 -0
  110. package/src/pages/categories/index.ts +4 -0
  111. package/src/pages/categories/list.tsx +86 -0
  112. package/src/pages/categories/show.tsx +37 -0
  113. package/src/pages/forgot-password/index.tsx +6 -0
  114. package/src/pages/forgotPassword/index.tsx +5 -0
  115. package/src/pages/login/index.tsx +5 -0
  116. package/src/pages/register/index.tsx +5 -0
  117. package/src/providers/auth.ts +204 -0
  118. package/src/providers/constants.ts +75 -0
  119. package/src/providers/data.ts +232 -0
  120. package/src/vite-env.d.ts +1 -0
  121. package/tsconfig.json +25 -0
  122. package/tsconfig.node.json +21 -0
  123. package/vite.config.ts +74 -0
@@ -0,0 +1,232 @@
1
+ import type {
2
+ BaseRecord,
3
+ CreateManyParams,
4
+ CreateManyResponse,
5
+ CreateParams,
6
+ CreateResponse,
7
+ DataProvider,
8
+ DeleteManyParams,
9
+ DeleteManyResponse,
10
+ DeleteOneParams,
11
+ DeleteOneResponse,
12
+ GetListParams,
13
+ GetListResponse,
14
+ GetManyParams,
15
+ GetManyResponse,
16
+ GetOneParams,
17
+ GetOneResponse,
18
+ MetaQuery,
19
+ UpdateManyParams,
20
+ UpdateManyResponse,
21
+ UpdateParams,
22
+ UpdateResponse,
23
+ } from "@refinedev/core";
24
+
25
+ import { API_URL, getNocoBaseHeaders, NOCOBASE_TOKEN_KEY } from "./constants";
26
+
27
+ type NocoBaseListResponse<T> = {
28
+ rows?: T[];
29
+ count?: number;
30
+ data?: T[] | NocoBaseListResponse<T>;
31
+ meta?: {
32
+ count?: number;
33
+ };
34
+ };
35
+
36
+ type NocoBaseMeta = MetaQuery & {
37
+ appends?: string[];
38
+ token?: string;
39
+ };
40
+
41
+ type NocoBaseFilter = Record<string, unknown>;
42
+
43
+ const getToken = (meta?: NocoBaseMeta) =>
44
+ meta?.token ??
45
+ localStorage.getItem(NOCOBASE_TOKEN_KEY) ??
46
+ import.meta.env.NOCOBASE_API_TOKEN;
47
+
48
+ const toNocoBaseFilter = (
49
+ filters: GetListParams["filters"] = [],
50
+ ): NocoBaseFilter | undefined => {
51
+ const filterItems: NocoBaseFilter[] = filters.flatMap((filter) => {
52
+ if ("field" in filter) {
53
+ const operatorMap: Record<string, string> = {
54
+ eq: "$eq",
55
+ ne: "$ne",
56
+ lt: "$lt",
57
+ gt: "$gt",
58
+ lte: "$lte",
59
+ gte: "$gte",
60
+ in: "$in",
61
+ nin: "$notIn",
62
+ contains: "$includes",
63
+ containss: "$includes",
64
+ startswith: "$startsWith",
65
+ endswith: "$endsWith",
66
+ null: "$null",
67
+ nnull: "$notNull",
68
+ between: "$between",
69
+ nbetween: "$notBetween",
70
+ };
71
+
72
+ const operator = operatorMap[filter.operator] ?? "$eq";
73
+ return [{ [filter.field]: { [operator]: filter.value } }];
74
+ }
75
+
76
+ const value = toNocoBaseFilter(filter.value as GetListParams["filters"]);
77
+ return value ? [{ [`$${filter.operator}`]: value.$and ?? [value] }] : [];
78
+ });
79
+
80
+ if (!filterItems.length) return undefined;
81
+ return filterItems.length === 1 ? filterItems[0] : { $and: filterItems };
82
+ };
83
+
84
+ const buildUrl = (
85
+ resource: string,
86
+ action: string,
87
+ query: Record<string, string | number | undefined> = {},
88
+ meta?: NocoBaseMeta,
89
+ ) => {
90
+ const endpoint = `${API_URL.replace(/\/$/, "")}/${resource}:${action}`;
91
+ const url = endpoint.startsWith("http://") || endpoint.startsWith("https://")
92
+ ? new URL(endpoint)
93
+ : new URL(endpoint, window.location.origin);
94
+
95
+ for (const [key, value] of Object.entries(query)) {
96
+ if (value !== undefined) url.searchParams.set(key, String(value));
97
+ }
98
+
99
+ meta?.appends?.forEach((append) => url.searchParams.append("appends[]", append));
100
+ if (Array.isArray(meta?.fields)) {
101
+ meta.fields
102
+ .filter((field): field is string => typeof field === "string")
103
+ .forEach((field) => url.searchParams.append("fields[]", field));
104
+ }
105
+
106
+ return url;
107
+ };
108
+
109
+ const request = async <T>(
110
+ resource: string,
111
+ action: string,
112
+ options: {
113
+ method?: "GET" | "POST";
114
+ query?: Record<string, string | number | undefined>;
115
+ body?: unknown;
116
+ meta?: NocoBaseMeta;
117
+ unwrapData?: boolean;
118
+ } = {},
119
+ ): Promise<T> => {
120
+ const token = getToken(options.meta);
121
+ const response = await fetch(buildUrl(resource, action, options.query, options.meta), {
122
+ method: options.method ?? "GET",
123
+ headers: getNocoBaseHeaders({
124
+ token,
125
+ includeContentType: Boolean(options.body),
126
+ }),
127
+ body: options.body ? JSON.stringify(options.body) : undefined,
128
+ });
129
+
130
+ const payload = await response.json().catch(() => undefined);
131
+ if (!response.ok) {
132
+ throw new Error(
133
+ payload?.errors?.[0]?.message ?? payload?.message ?? `NocoBase request failed (${response.status})`,
134
+ );
135
+ }
136
+
137
+ // Most NocoBase resource actions return their record in `data`. List endpoints
138
+ // can also return `{ data: [...], meta: { count } }`, which must stay intact.
139
+ return (options.unwrapData === false ? payload : payload?.data ?? payload) as T;
140
+ };
141
+
142
+ export const dataProvider: DataProvider = {
143
+ async getList<TData extends BaseRecord = BaseRecord>({
144
+ resource,
145
+ pagination,
146
+ sorters,
147
+ filters,
148
+ meta,
149
+ }: GetListParams): Promise<GetListResponse<TData>> {
150
+ const page = pagination?.currentPage ?? 1;
151
+ const pageSize = pagination?.pageSize ?? 10;
152
+ const filter = toNocoBaseFilter(filters);
153
+ const sort = sorters?.map((sorter) => `${sorter.order === "desc" ? "-" : ""}${sorter.field}`);
154
+ const response = await request<NocoBaseListResponse<TData>>(resource, "list", {
155
+ query: {
156
+ page,
157
+ pageSize,
158
+ ...(filter ? { filter: JSON.stringify(filter) } : {}),
159
+ ...(sort?.length ? { sort: sort.join(",") } : {}),
160
+ },
161
+ meta,
162
+ unwrapData: false,
163
+ });
164
+
165
+ const list = Array.isArray(response.data)
166
+ ? { rows: response.data, count: response.meta?.count }
167
+ : response.data ?? response;
168
+
169
+ return {
170
+ data: list.rows ?? [],
171
+ total: list.count ?? list.rows?.length ?? 0,
172
+ };
173
+ },
174
+
175
+ async getOne<TData extends BaseRecord = BaseRecord>({ resource, id, meta }: GetOneParams): Promise<GetOneResponse<TData>> {
176
+ return { data: await request<TData>(resource, "get", { query: { filterByTk: id }, meta }) };
177
+ },
178
+
179
+ async getMany<TData extends BaseRecord = BaseRecord>({ resource, ids, meta }: GetManyParams): Promise<GetManyResponse<TData>> {
180
+ const data = await Promise.all(
181
+ ids.map((id) => request<TData>(resource, "get", { query: { filterByTk: id }, meta })),
182
+ );
183
+ return { data };
184
+ },
185
+
186
+ async create<TData extends BaseRecord = BaseRecord, TVariables = {}>({ resource, variables, meta }: CreateParams<TVariables>): Promise<CreateResponse<TData>> {
187
+ return { data: await request<TData>(resource, "create", { method: "POST", body: variables, meta }) };
188
+ },
189
+
190
+ async createMany<TData extends BaseRecord = BaseRecord, TVariables = {}>({ resource, variables, meta }: CreateManyParams<TVariables>): Promise<CreateManyResponse<TData>> {
191
+ const data = await Promise.all(
192
+ variables.map((values) => request<TData>(resource, "create", { method: "POST", body: values, meta })),
193
+ );
194
+ return { data };
195
+ },
196
+
197
+ async update<TData extends BaseRecord = BaseRecord, TVariables = {}>({ resource, id, variables, meta }: UpdateParams<TVariables>): Promise<UpdateResponse<TData>> {
198
+ return {
199
+ data: await request<TData>(resource, "update", {
200
+ method: "POST",
201
+ query: { filterByTk: id },
202
+ body: variables,
203
+ meta,
204
+ }),
205
+ };
206
+ },
207
+
208
+ async updateMany<TData extends BaseRecord = BaseRecord, TVariables = {}>({ resource, ids, variables, meta }: UpdateManyParams<TVariables>): Promise<UpdateManyResponse<TData>> {
209
+ const data = await Promise.all(
210
+ ids.map((id) => request<TData>(resource, "update", {
211
+ method: "POST",
212
+ query: { filterByTk: id },
213
+ body: variables,
214
+ meta,
215
+ })),
216
+ );
217
+ return { data };
218
+ },
219
+
220
+ async deleteOne<TData extends BaseRecord = BaseRecord, TVariables = {}>({ resource, id, meta }: DeleteOneParams<TVariables>): Promise<DeleteOneResponse<TData>> {
221
+ return { data: await request<TData>(resource, "destroy", { method: "POST", query: { filterByTk: id }, meta }) };
222
+ },
223
+
224
+ async deleteMany<TData extends BaseRecord = BaseRecord, TVariables = {}>({ resource, ids, meta }: DeleteManyParams<TVariables>): Promise<DeleteManyResponse<TData>> {
225
+ const data = await Promise.all(
226
+ ids.map((id) => request<TData>(resource, "destroy", { method: "POST", query: { filterByTk: id }, meta })),
227
+ );
228
+ return { data };
229
+ },
230
+
231
+ getApiUrl: () => API_URL,
232
+ };
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": false,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true,
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@/*": ["./src/*"]
22
+ }
23
+ },
24
+ "include": ["src"]
25
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true
19
+ },
20
+ "include": ["vite.config.ts"]
21
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,74 @@
1
+ import tailwindcss from "@tailwindcss/vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import path from "path";
4
+ import { defineConfig, loadEnv } from "vite";
5
+
6
+ const getDefaultProxyTarget = (apiUrl?: string) => {
7
+ if (!apiUrl || apiUrl.startsWith("/")) return undefined;
8
+
9
+ try {
10
+ return new URL(apiUrl).origin;
11
+ } catch {
12
+ return undefined;
13
+ }
14
+ };
15
+
16
+ const getProxyPath = (apiUrl?: string) => {
17
+ if (!apiUrl) return "/api";
18
+ if (apiUrl.startsWith("/")) return apiUrl;
19
+
20
+ try {
21
+ return new URL(apiUrl).pathname || "/api";
22
+ } catch {
23
+ return "/api";
24
+ }
25
+ };
26
+
27
+ const normalizeBase = (base?: string) => {
28
+ const normalized = String(base || "/").trim();
29
+ if (!normalized || normalized === "/") return "/";
30
+ return `/${normalized.replace(/^\/+|\/+$/g, "")}/`;
31
+ };
32
+
33
+ // https://vite.dev/config/
34
+ export default defineConfig(({ mode }) => {
35
+ const env = loadEnv(mode, process.cwd(), "");
36
+ const proxyTarget = getDefaultProxyTarget(env.NOCOBASE_API_URL);
37
+ const proxyOrigin = proxyTarget
38
+ ? (() => {
39
+ try {
40
+ return new URL(proxyTarget).origin;
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ })()
45
+ : undefined;
46
+
47
+ return {
48
+ base: normalizeBase(env.NOCOBASE_PORTAL_BASE),
49
+ envPrefix: ["VITE_", "NOCOBASE_"],
50
+ plugins: [react(), tailwindcss()],
51
+ resolve: {
52
+ alias: {
53
+ "@": path.resolve(__dirname, "./src"),
54
+ },
55
+ },
56
+ server: proxyTarget
57
+ ? {
58
+ proxy: {
59
+ [getProxyPath(env.NOCOBASE_API_URL)]: {
60
+ target: proxyTarget,
61
+ changeOrigin: true,
62
+ secure: false,
63
+ headers: proxyOrigin
64
+ ? {
65
+ origin: proxyOrigin,
66
+ referer: `${proxyOrigin}/`,
67
+ }
68
+ : undefined,
69
+ },
70
+ },
71
+ }
72
+ : undefined,
73
+ };
74
+ });