create-twenty-app 2.4.2 → 2.5.1

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.
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none">
2
+ <rect width="80" height="80" rx="16" fill="#141414"/>
3
+ <rect x="20" y="20" width="16" height="16" rx="4" fill="#FAFAFA"/>
4
+ <rect x="44" y="20" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
5
+ <rect x="20" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
6
+ <rect x="44" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.3"/>
7
+ </svg>
@@ -2,3 +2,10 @@ export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
2
2
  export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED';
3
3
  export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
4
4
  export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
5
+ export const MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
6
+ 'UUID-TO-BE-GENERATED';
7
+ export const MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
8
+ export const MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
9
+ export const MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
10
+ export const MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
11
+ 'UUID-TO-BE-GENERATED';
@@ -0,0 +1,279 @@
1
+ import { defineFrontComponent } from 'twenty-sdk/define';
2
+ import { useState } from 'react';
3
+
4
+ import {
5
+ APP_DISPLAY_NAME,
6
+ MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
7
+ } from 'src/constants/universal-identifiers';
8
+
9
+ const DOCS_BASE_URL = 'https://docs.twenty.com/developers/extend/apps';
10
+
11
+ const CATEGORIES = [
12
+ {
13
+ title: 'Data model',
14
+ color: '#73D08D',
15
+ items: [
16
+ { label: 'CUSTOM OBJECT', href: `${DOCS_BASE_URL}/data/objects` },
17
+ {
18
+ label: 'CUSTOM FIELDS',
19
+ href: `${DOCS_BASE_URL}/data/extending-objects`,
20
+ },
21
+ ],
22
+ rotation: '2.4deg',
23
+ },
24
+ {
25
+ title: 'Logic',
26
+ color: '#F4D345',
27
+ items: [
28
+ {
29
+ label: 'TOOLS',
30
+ href: `${DOCS_BASE_URL}/logic/logic-functions`,
31
+ },
32
+ {
33
+ label: 'SERVERLESS FUNCT.',
34
+ href: `${DOCS_BASE_URL}/logic/logic-functions`,
35
+ },
36
+ {
37
+ label: 'SKILLS',
38
+ href: `${DOCS_BASE_URL}/logic/skills-and-agents`,
39
+ },
40
+ ],
41
+ rotation: '0deg',
42
+ },
43
+ {
44
+ title: 'Layout',
45
+ color: '#C4A2E0',
46
+ items: [
47
+ { label: 'VIEWS', href: `${DOCS_BASE_URL}/layout/views` },
48
+ { label: 'WIDGETS', href: `${DOCS_BASE_URL}/layout/page-layouts` },
49
+ {
50
+ label: 'LAYOUT PAGES',
51
+ href: `${DOCS_BASE_URL}/layout/page-layouts`,
52
+ },
53
+ {
54
+ label: 'COMMANDS',
55
+ href: `${DOCS_BASE_URL}/layout/command-menu-items`,
56
+ },
57
+ ],
58
+ rotation: '-2.8deg',
59
+ },
60
+ ] as const;
61
+
62
+ const ItemIcon = ({ color }: { color: string }) => (
63
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
64
+ <rect x="2" y="2" width="5" height="5" rx="1" fill={color} />
65
+ <rect x="9" y="2" width="5" height="5" rx="1" fill={color} opacity="0.6" />
66
+ <rect x="2" y="9" width="5" height="5" rx="1" fill={color} opacity="0.6" />
67
+ <rect x="9" y="9" width="5" height="5" rx="1" fill={color} opacity="0.3" />
68
+ </svg>
69
+ );
70
+
71
+ const ArrowUpRight = ({ color = '#999' }: { color?: string }) => (
72
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
73
+ <path
74
+ d="M4.5 3.5H10.5V9.5M10.5 3.5L3.5 10.5"
75
+ stroke={color}
76
+ strokeWidth="1.2"
77
+ strokeLinecap="round"
78
+ strokeLinejoin="round"
79
+ />
80
+ </svg>
81
+ );
82
+
83
+ const CategoryCard = ({
84
+ title,
85
+ color,
86
+ items,
87
+ rotation,
88
+ }: {
89
+ title: string;
90
+ color: string;
91
+ items: ReadonlyArray<{ label: string; href: string }>;
92
+ rotation: string;
93
+ }) => {
94
+ const [hoveredItem, setHoveredItem] = useState<string | null>(null);
95
+
96
+ return (
97
+ <div
98
+ style={{
99
+ display: 'flex',
100
+ flexDirection: 'column',
101
+ border: `1px solid ${color}80`,
102
+ borderRadius: '12px',
103
+ overflow: 'hidden',
104
+ width: '240px',
105
+ background: '#FFFFFF',
106
+ transform: `rotate(${rotation})`,
107
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
108
+ }}
109
+ >
110
+ <div
111
+ style={{
112
+ padding: '16px 20px',
113
+ background: `${color}22`,
114
+ }}
115
+ >
116
+ <span
117
+ style={{
118
+ fontSize: '16px',
119
+ fontWeight: 600,
120
+ color: color,
121
+ }}
122
+ >
123
+ {title}
124
+ </span>
125
+ </div>
126
+ <div
127
+ style={{
128
+ display: 'flex',
129
+ flexDirection: 'column',
130
+ padding: '8px',
131
+ gap: '4px',
132
+ }}
133
+ >
134
+ {items.map((item) => {
135
+ const isHovered = hoveredItem === item.label;
136
+
137
+ return (
138
+ <a
139
+ key={item.label}
140
+ href={item.href}
141
+ target="_blank"
142
+ rel="noopener noreferrer"
143
+ onMouseEnter={() => setHoveredItem(item.label)}
144
+ onMouseLeave={() => setHoveredItem(null)}
145
+ style={{
146
+ display: 'flex',
147
+ alignItems: 'center',
148
+ gap: '10px',
149
+ textDecoration: 'none',
150
+ cursor: 'pointer',
151
+ padding: '10px 12px',
152
+ borderRadius: '8px',
153
+ background: isHovered ? '#0000000A' : 'transparent',
154
+ transition: 'background 0.15s',
155
+ }}
156
+ >
157
+ <ItemIcon color={color} />
158
+ <span
159
+ style={{
160
+ fontSize: '13px',
161
+ fontWeight: 300,
162
+ color: '#333',
163
+ letterSpacing: '0.5px',
164
+ flex: 1,
165
+ }}
166
+ >
167
+ {item.label}
168
+ </span>
169
+ {isHovered && <ArrowUpRight />}
170
+ </a>
171
+ );
172
+ })}
173
+ </div>
174
+ </div>
175
+ );
176
+ };
177
+
178
+ const MainPage = () => {
179
+ return (
180
+ <div
181
+ style={{
182
+ display: 'flex',
183
+ flexDirection: 'column',
184
+ alignItems: 'center',
185
+ justifyContent: 'center',
186
+ height: '100%',
187
+ fontFamily:
188
+ 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
189
+ gap: '8px',
190
+ padding: '40px',
191
+ }}
192
+ >
193
+ {/*<Avatar
194
+ avatarUrl={getPublicAssetUrl('logo.svg')}
195
+ placeholder={APP_DISPLAY_NAME}
196
+ placeholderColorSeed={APP_DISPLAY_NAME}
197
+ type="squared"
198
+ size="xl"
199
+ />*/}
200
+ <span
201
+ style={{
202
+ fontSize: '24px',
203
+ fontWeight: 600,
204
+ color: '#333',
205
+ marginTop: '8px',
206
+ }}
207
+ >
208
+ {APP_DISPLAY_NAME}
209
+ </span>
210
+ <span
211
+ style={{
212
+ fontSize: '13px',
213
+ color: '#888',
214
+ textAlign: 'center',
215
+ lineHeight: '1.5',
216
+ }}
217
+ >
218
+ Was installed successfully.
219
+ <br />
220
+ You can now add content to your app.
221
+ </span>
222
+ <a
223
+ href="/settings/applications"
224
+ style={{
225
+ display: 'inline-flex',
226
+ alignItems: 'center',
227
+ gap: '6px',
228
+ marginTop: '16px',
229
+ fontSize: '13px',
230
+ color: '#333',
231
+ textDecoration: 'none',
232
+ padding: '8px 16px',
233
+ borderRadius: '8px',
234
+ border: '1px solid #e0e0e0',
235
+ background: '#fafafa',
236
+ transition: 'background 0.15s, border-color 0.15s',
237
+ }}
238
+ onMouseEnter={(e) => {
239
+ e.currentTarget.style.background = '#f0f0f0';
240
+ e.currentTarget.style.borderColor = '#ccc';
241
+ }}
242
+ onMouseLeave={(e) => {
243
+ e.currentTarget.style.background = '#fafafa';
244
+ e.currentTarget.style.borderColor = '#e0e0e0';
245
+ }}
246
+ >
247
+ Open app settings
248
+ <ArrowUpRight color="#333" />
249
+ </a>
250
+ <div
251
+ style={{
252
+ display: 'flex',
253
+ gap: '16px',
254
+ marginTop: '32px',
255
+ flexWrap: 'wrap',
256
+ justifyContent: 'center',
257
+ alignItems: 'flex-start',
258
+ }}
259
+ >
260
+ {CATEGORIES.map((category) => (
261
+ <CategoryCard
262
+ key={category.title}
263
+ title={category.title}
264
+ color={category.color}
265
+ items={category.items}
266
+ rotation={category.rotation}
267
+ />
268
+ ))}
269
+ </div>
270
+ </div>
271
+ );
272
+ };
273
+
274
+ export default defineFrontComponent({
275
+ universalIdentifier: MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
276
+ name: APP_DISPLAY_NAME,
277
+ description: `${APP_DISPLAY_NAME} front component displaying the app logo and name`,
278
+ component: MainPage,
279
+ });
@@ -0,0 +1,19 @@
1
+ import {
2
+ defineNavigationMenuItem,
3
+ NavigationMenuItemType,
4
+ } from 'twenty-sdk/define';
5
+
6
+ import {
7
+ APP_DISPLAY_NAME,
8
+ MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
9
+ MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
10
+ } from 'src/constants/universal-identifiers';
11
+
12
+ export default defineNavigationMenuItem({
13
+ universalIdentifier: MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
14
+ name: APP_DISPLAY_NAME,
15
+ icon: 'IconFile',
16
+ position: -1,
17
+ type: NavigationMenuItemType.PAGE_LAYOUT,
18
+ pageLayoutUniversalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
19
+ });
@@ -0,0 +1,37 @@
1
+ import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
2
+
3
+ import {
4
+ APP_DISPLAY_NAME,
5
+ MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
6
+ MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
7
+ MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
8
+ MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
9
+ } from 'src/constants/universal-identifiers';
10
+
11
+ export default definePageLayout({
12
+ universalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
13
+ name: APP_DISPLAY_NAME,
14
+ type: 'STANDALONE_PAGE',
15
+ tabs: [
16
+ {
17
+ universalIdentifier: MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
18
+ title: 'Overview',
19
+ position: 0,
20
+ icon: 'IconApps',
21
+ layoutMode: PageLayoutTabLayoutMode.CANVAS,
22
+ widgets: [
23
+ {
24
+ universalIdentifier: MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
25
+ title: ' ',
26
+ type: 'FRONT_COMPONENT',
27
+ gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
28
+ configuration: {
29
+ configurationType: 'FRONT_COMPONENT',
30
+ frontComponentUniversalIdentifier:
31
+ MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
32
+ },
33
+ },
34
+ ],
35
+ },
36
+ ],
37
+ });
@@ -1,11 +1,10 @@
1
1
  export type AuthenticationMethod = 'oauth' | 'apiKey';
2
2
  type CreateAppOptions = {
3
3
  directory?: string;
4
- example?: string;
5
4
  name?: string;
6
5
  displayName?: string;
7
6
  description?: string;
8
- apiUrl?: string;
7
+ workspaceUrl?: string;
9
8
  authenticationMethod?: AuthenticationMethod;
10
9
  };
11
10
  export declare class CreateAppCommand {
@@ -15,12 +14,19 @@ export declare class CreateAppCommand {
15
14
  private computeTotalSteps;
16
15
  private getAppInfos;
17
16
  private validateDirectory;
18
- private tryDownloadExample;
19
17
  private logPlan;
20
18
  private logNextStep;
21
19
  private logDetail;
22
20
  private pullImageInBackground;
23
21
  private ensureDockerServer;
22
+ private openMainPage;
23
+ private resolveWorkspaceFrontUrl;
24
+ private readMainPageLayoutUniversalIdentifier;
25
+ private resolvePageLayoutId;
26
+ private sanitizeBrowserUrl;
27
+ private openInBrowser;
28
+ private syncApplication;
29
+ private tryExistingAuth;
24
30
  private authenticateWithDevKey;
25
31
  private deriveRemoteName;
26
32
  private authenticateWithOAuth;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-twenty-app",
3
- "version": "2.4.2",
3
+ "version": "2.5.1",
4
4
  "description": "Command-line interface to create Twenty application",
5
5
  "main": "dist/cli.cjs",
6
6
  "bin": "dist/cli.cjs",
@@ -35,7 +35,7 @@
35
35
  "lodash.camelcase": "^4.3.0",
36
36
  "lodash.kebabcase": "^4.1.1",
37
37
  "lodash.startcase": "^4.4.0",
38
- "twenty-sdk": "2.4.2",
38
+ "twenty-sdk": "2.5.1",
39
39
  "uuid": "^13.0.0"
40
40
  },
41
41
  "devDependencies": {
@@ -1 +0,0 @@
1
- export declare const downloadExample: (exampleName: string, targetDirectory: string) => Promise<void>;