@_linked/cli 1.4.2 → 1.4.4
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/CHANGELOG.md +16 -0
- package/defaults/app-static/src/index-static.tsx +22 -0
- package/defaults/app-with-backend/src/App.module.css +32 -0
- package/defaults/app-with-backend/src/App.tsx +30 -0
- package/defaults/app-with-backend/src/backend.ts +9 -0
- package/defaults/app-with-backend/src/components/Error.module.css +9 -0
- package/defaults/app-with-backend/src/components/Error.tsx +11 -0
- package/defaults/app-with-backend/src/components/PersonOverview.module.css +100 -0
- package/defaults/app-with-backend/src/components/PersonOverview.tsx +97 -0
- package/defaults/app-with-backend/src/components/PersonOverviewContext.tsx +12 -0
- package/defaults/app-with-backend/src/components/PersonPreview.module.css +97 -0
- package/defaults/app-with-backend/src/components/PersonPreview.tsx +107 -0
- package/defaults/app-with-backend/src/components/Spinner.module.css +17 -0
- package/defaults/app-with-backend/src/components/Spinner.tsx +12 -0
- package/defaults/app-with-backend/src/index.tsx +34 -0
- package/defaults/app-with-backend/src/layout/DefaultLayout.module.css +15 -0
- package/defaults/app-with-backend/src/layout/DefaultLayout.tsx +12 -0
- package/defaults/app-with-backend/src/layout/Header.module.css +46 -0
- package/defaults/app-with-backend/src/layout/Header.tsx +27 -0
- package/defaults/app-with-backend/src/linked.frontend.datasets.json +12 -0
- package/defaults/app-with-backend/src/linked.frontend.storage.ts +36 -0
- package/defaults/app-with-backend/src/package.ts +12 -0
- package/defaults/app-with-backend/src/pages/Home.module.css +23 -0
- package/defaults/app-with-backend/src/pages/Home.tsx +14 -0
- package/defaults/app-with-backend/src/pages/Page1.module.css +18 -0
- package/defaults/app-with-backend/src/pages/Page1.tsx +16 -0
- package/defaults/app-with-backend/src/pages/PageNotFound.module.css +19 -0
- package/defaults/app-with-backend/src/pages/PageNotFound.tsx +14 -0
- package/defaults/app-with-backend/src/pages/Signin.module.css +20 -0
- package/defaults/app-with-backend/src/pages/Signin.tsx +16 -0
- package/defaults/app-with-backend/src/routes.tsx +85 -0
- package/defaults/app-with-backend/src/theme.css +91 -0
- package/defaults/app-with-backend/src/types.ts +14 -0
- package/defaults/app-with-backend/src/utils/lazyWithPreload.tsx +42 -0
- package/defaults/app-with-backend/src/utils/preloadRoutes.ts +38 -0
- package/defaults/app-with-backend/tsconfig.json +24 -0
- package/defaults/package/package.json +7 -3
- package/lib/cjs/cli-deprecated.d.ts +2 -0
- package/lib/cjs/cli-methods.d.ts +67 -0
- package/lib/cjs/cli.d.ts +3 -0
- package/lib/cjs/commands/build-package.d.ts +9 -0
- package/lib/cjs/commands/safe-yarn.d.ts +10 -0
- package/lib/cjs/commands/setup-publish.d.ts +23 -0
- package/lib/cjs/config-webpack-app.d.ts +112 -0
- package/lib/cjs/config-webpack.d.ts +55 -0
- package/lib/cjs/defineConfig.d.ts +8 -0
- package/lib/cjs/index.d.ts +12 -0
- package/lib/cjs/interfaces.d.ts +131 -0
- package/lib/cjs/loaders/register-css-only.d.ts +2 -0
- package/lib/cjs/loaders/register.d.ts +2 -0
- package/lib/cjs/metadata.d.ts +1 -0
- package/lib/cjs/package.json +1 -1
- package/lib/cjs/plugins/check-imports.d.ts +2 -0
- package/lib/cjs/plugins/declaration-plugin.d.ts +15 -0
- package/lib/cjs/plugins/externalise-modules.d.ts +7 -0
- package/lib/cjs/plugins/lincd-tailwind-sources.d.ts +3 -0
- package/lib/cjs/plugins/watch-run.d.ts +5 -0
- package/lib/cjs/tailwind.config.d.ts +8 -0
- package/lib/cjs/utils.d.ts +77 -0
- package/lib/esm/cli-deprecated.d.ts +2 -0
- package/lib/esm/cli-methods.d.ts +67 -0
- package/lib/esm/cli.d.ts +3 -0
- package/lib/esm/commands/build-package.d.ts +9 -0
- package/lib/esm/commands/safe-yarn.d.ts +10 -0
- package/lib/esm/commands/setup-publish.d.ts +23 -0
- package/lib/esm/config-webpack-app.d.ts +112 -0
- package/lib/esm/config-webpack.d.ts +55 -0
- package/lib/esm/defineConfig.d.ts +8 -0
- package/lib/esm/index.d.ts +12 -0
- package/lib/esm/interfaces.d.ts +131 -0
- package/lib/esm/launch.d.ts +3 -0
- package/lib/esm/loaders/register-css-only.d.ts +2 -0
- package/lib/esm/loaders/register.d.ts +2 -0
- package/lib/esm/metadata.d.ts +1 -0
- package/lib/esm/package.json +1 -1
- package/lib/esm/plugins/check-imports.d.ts +2 -0
- package/lib/esm/plugins/declaration-plugin.d.ts +15 -0
- package/lib/esm/plugins/externalise-modules.d.ts +7 -0
- package/lib/esm/plugins/lincd-tailwind-sources.d.ts +3 -0
- package/lib/esm/plugins/watch-run.d.ts +5 -0
- package/lib/esm/tailwind.config.d.ts +8 -0
- package/lib/esm/utils.d.ts +77 -0
- package/package.json +1 -1
- package/tests/e2e/create-app.spec.ts +157 -0
- package/tests/unit/safeYarn.test.ts +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#30](https://github.com/linked-cm/cli/pull/30) [`420c735`](https://github.com/linked-cm/cli/commit/420c7350741e5e2c1b7b76ecbaa4608a9b7ae4f0) Thanks [@flyon](https://github.com/flyon)! - `defaults/package/package.json` (the `linked create-package` template) now uses the explicit per-step build pipeline instead of `yarn linked build`. The wrapper script was silently swallowing TS compile errors and shipping empty tarballs — the exact same bug that affected every existing `@_linked/*` package built with it.
|
|
8
|
+
|
|
9
|
+
New packages created via `linked create-package` now ship with a build script that fails loudly on real errors and produces complete `lib/esm/` + `lib/cjs/` output.
|
|
10
|
+
|
|
11
|
+
## 1.4.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#28](https://github.com/linked-cm/cli/pull/28) [`1d2338c`](https://github.com/linked-cm/cli/commit/1d2338c09a38e44caec2aa69a72b3feda7d098bf) Thanks [@flyon](https://github.com/flyon)! - `.npmignore` was excluding the starter template's `defaults/app-with-backend/src/` (and `scripts/`) because the `src` pattern matched **anywhere** in the tree, overriding the `!defaults/**/*` re-include. The published tarball was missing all the template's TypeScript source files, so `linked create-app` was producing apps without `src/`.
|
|
16
|
+
|
|
17
|
+
Anchor the cli's own-source ignore rules to the package root (`/src`, `/*.ts`, `/tsconfig.json`) and add an explicit `!defaults/**` re-include after.
|
|
18
|
+
|
|
3
19
|
## 1.4.2
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import {BrowserRouter} from 'react-router-dom';
|
|
4
|
+
import App from './App';
|
|
5
|
+
import {AppContextProvider} from 'lincd-server-utils/lib/components/AppContext';
|
|
6
|
+
|
|
7
|
+
//import the storage & file configuration for the frontend
|
|
8
|
+
import './config-frontend';
|
|
9
|
+
|
|
10
|
+
const root = ReactDOM.createRoot(
|
|
11
|
+
document.getElementById('root') as HTMLElement,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
root.render(
|
|
15
|
+
<React.StrictMode>
|
|
16
|
+
<BrowserRouter>
|
|
17
|
+
<AppContextProvider isNativeApp={true}>
|
|
18
|
+
<App />
|
|
19
|
+
</AppContextProvider>
|
|
20
|
+
</BrowserRouter>
|
|
21
|
+
</React.StrictMode>,
|
|
22
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@import '@_linked/css/package.css';
|
|
2
|
+
|
|
3
|
+
:global(body) {
|
|
4
|
+
font-family: var(--font-sans);
|
|
5
|
+
background: var(--bg-page);
|
|
6
|
+
color: var(--color-gray-900);
|
|
7
|
+
margin: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.App {
|
|
11
|
+
min-height: 100vh;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:global(h1) {
|
|
15
|
+
font-size: 1.75rem;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
margin: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
:global(h2) {
|
|
21
|
+
font-size: 1.25rem;
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
margin: 0 0 --spacing(3);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:global(a) {
|
|
27
|
+
color: var(--color-primary-600);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:global(a:hover) {
|
|
31
|
+
color: var(--color-primary-700);
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import './theme.css'; //needs to be the first import before importing other components
|
|
2
|
+
import React, { Suspense } from 'react';
|
|
3
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
4
|
+
import { Spinner } from './components/Spinner';
|
|
5
|
+
import { Error } from './components/Error';
|
|
6
|
+
import { AppRoot } from '@_linked/server-utils/components/AppRoot';
|
|
7
|
+
import { Head } from '@_linked/server-utils/components/Head';
|
|
8
|
+
import { Body } from '@_linked/server-utils/components/Body';
|
|
9
|
+
import AppRoutes, { ROUTES } from './routes';
|
|
10
|
+
import { ProvideAuth } from '@_linked/auth/hooks/useAuth';
|
|
11
|
+
import style from './App.module.css'; //import any .module.css file like this and access the classnames from the style object
|
|
12
|
+
|
|
13
|
+
export default function App() {
|
|
14
|
+
return (
|
|
15
|
+
<AppRoot>
|
|
16
|
+
<Head>
|
|
17
|
+
{/* Add tags to html <head> here, for example, a font <link href='https://fonts.someapi.com/...' />*/}
|
|
18
|
+
</Head>
|
|
19
|
+
<Body routes={ROUTES} pageStyles={style} className={style.App}>
|
|
20
|
+
<Suspense fallback={<Spinner />}>
|
|
21
|
+
<ErrorBoundary FallbackComponent={Error}>
|
|
22
|
+
<ProvideAuth>
|
|
23
|
+
<AppRoutes />
|
|
24
|
+
</ProvideAuth>
|
|
25
|
+
</ErrorBoundary>
|
|
26
|
+
</Suspense>
|
|
27
|
+
</Body>
|
|
28
|
+
</AppRoot>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//import all ShapeProviders here or define a generic BackendProvider, see https://linked.cm
|
|
2
|
+
|
|
3
|
+
import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';
|
|
4
|
+
|
|
5
|
+
export class Backend extends BackendProvider {
|
|
6
|
+
constructor(server, linkedServer) {
|
|
7
|
+
super(server, linkedServer);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
@import '@_linked/css/package.css';
|
|
2
|
+
|
|
3
|
+
.Root {
|
|
4
|
+
background: var(--bg-card);
|
|
5
|
+
border-radius: var(--radius-card);
|
|
6
|
+
box-shadow: var(--shadow-card);
|
|
7
|
+
padding: --spacing(6);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.Intro {
|
|
11
|
+
color: var(--color-gray-600);
|
|
12
|
+
font-size: 0.9rem;
|
|
13
|
+
line-height: 1.5;
|
|
14
|
+
margin: 0 0 --spacing(5);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.Intro code {
|
|
18
|
+
background: var(--bg-section);
|
|
19
|
+
padding: 0 --spacing(1);
|
|
20
|
+
border-radius: var(--radius-element);
|
|
21
|
+
font-family: var(--font-mono);
|
|
22
|
+
font-size: 0.85em;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.Empty {
|
|
26
|
+
color: var(--color-gray-500);
|
|
27
|
+
font-style: italic;
|
|
28
|
+
padding: --spacing(4) 0;
|
|
29
|
+
margin: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.Loading {
|
|
33
|
+
color: var(--color-gray-500);
|
|
34
|
+
padding: --spacing(4) 0;
|
|
35
|
+
margin: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.List {
|
|
39
|
+
list-style: none;
|
|
40
|
+
padding: 0;
|
|
41
|
+
margin: 0 0 --spacing(5);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.List > li {
|
|
45
|
+
padding: --spacing(3) 0;
|
|
46
|
+
border-bottom: 1px solid var(--color-gray-200);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.List > li:first-child {
|
|
50
|
+
padding-top: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.List > li:last-child {
|
|
54
|
+
border-bottom: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.Form {
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: --spacing(2);
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
padding-top: --spacing(4);
|
|
62
|
+
border-top: 1px solid var(--color-gray-200);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.Form input {
|
|
66
|
+
flex: 1 1 140px;
|
|
67
|
+
padding: --spacing(2) --spacing(3);
|
|
68
|
+
border: 1px solid var(--color-gray-300);
|
|
69
|
+
border-radius: var(--radius-element);
|
|
70
|
+
background: var(--bg-card);
|
|
71
|
+
color: var(--color-gray-900);
|
|
72
|
+
font: inherit;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.Form input:focus {
|
|
76
|
+
outline: none;
|
|
77
|
+
border-color: var(--color-primary-500);
|
|
78
|
+
box-shadow: 0 0 0 2px var(--color-primary-200);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.Form button {
|
|
82
|
+
padding: --spacing(2) --spacing(5);
|
|
83
|
+
background: var(--color-primary-500);
|
|
84
|
+
color: var(--color-white);
|
|
85
|
+
border: none;
|
|
86
|
+
border-radius: var(--radius-element);
|
|
87
|
+
font: inherit;
|
|
88
|
+
font-weight: 500;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
transition: background 120ms ease;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.Form button:hover:not(:disabled) {
|
|
94
|
+
background: var(--color-primary-600);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.Form button:disabled {
|
|
98
|
+
opacity: 0.6;
|
|
99
|
+
cursor: not-allowed;
|
|
100
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Example PersonOverview — demonstrates @_linked/react's data binding:
|
|
2
|
+
// linkedSetComponent for the list (auto-runs the query, injects results and
|
|
3
|
+
// an injected _refresh); linkedComponent for each row (see PersonPreview).
|
|
4
|
+
// After Person.create the form calls _refresh() to re-run the list query.
|
|
5
|
+
//
|
|
6
|
+
// Replace or extend this with your own shapes (see https://linked.cm).
|
|
7
|
+
import React, { useCallback, useRef, useState } from 'react';
|
|
8
|
+
import { linkedSetComponent } from '@_linked/react';
|
|
9
|
+
import { Person } from '@_linked/schema/shapes/Person';
|
|
10
|
+
import { PersonPreview } from './PersonPreview';
|
|
11
|
+
import { PersonListRefreshProvider } from './PersonOverviewContext';
|
|
12
|
+
import style from './PersonOverview.module.css';
|
|
13
|
+
|
|
14
|
+
const PersonList = linkedSetComponent(
|
|
15
|
+
Person.select((p) => [p.givenName, p.familyName]),
|
|
16
|
+
({ linkedData = [], _refresh, refreshRef }: any) => {
|
|
17
|
+
// Forward the list's _refresh up via a ref so the sibling form +
|
|
18
|
+
// child rows can trigger a re-fetch through context.
|
|
19
|
+
if (refreshRef) {
|
|
20
|
+
refreshRef.current = _refresh;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (linkedData.length === 0) {
|
|
24
|
+
return (
|
|
25
|
+
<p className={style.Empty}>No people yet. Add one below.</p>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<ul className={style.List} data-testid="person-list">
|
|
31
|
+
{linkedData.map((p: { id: string }) => (
|
|
32
|
+
<li key={p.id}>
|
|
33
|
+
<PersonPreview of={{ id: p.id }} />
|
|
34
|
+
</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function PersonAddForm({ onAdded }: { onAdded: () => void }) {
|
|
42
|
+
const [given, setGiven] = useState('');
|
|
43
|
+
const [family, setFamily] = useState('');
|
|
44
|
+
const [busy, setBusy] = useState(false);
|
|
45
|
+
|
|
46
|
+
async function submit(e: React.FormEvent) {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
if (!given && !family) return;
|
|
49
|
+
setBusy(true);
|
|
50
|
+
try {
|
|
51
|
+
await Person.create({ givenName: given, familyName: family });
|
|
52
|
+
setGiven('');
|
|
53
|
+
setFamily('');
|
|
54
|
+
onAdded();
|
|
55
|
+
} finally {
|
|
56
|
+
setBusy(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<form className={style.Form} onSubmit={submit}>
|
|
62
|
+
<input
|
|
63
|
+
value={given}
|
|
64
|
+
placeholder="First name"
|
|
65
|
+
onChange={(e) => setGiven(e.target.value)}
|
|
66
|
+
/>
|
|
67
|
+
<input
|
|
68
|
+
value={family}
|
|
69
|
+
placeholder="Last name"
|
|
70
|
+
onChange={(e) => setFamily(e.target.value)}
|
|
71
|
+
/>
|
|
72
|
+
<button type="submit" disabled={busy}>
|
|
73
|
+
Add
|
|
74
|
+
</button>
|
|
75
|
+
</form>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function PersonOverview() {
|
|
80
|
+
const refreshRef = useRef<() => void>(() => {});
|
|
81
|
+
const refresh = useCallback(() => refreshRef.current(), []);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<PersonListRefreshProvider value={refresh}>
|
|
85
|
+
<div className={style.Root} data-testid="person-overview">
|
|
86
|
+
<h2>People</h2>
|
|
87
|
+
<p className={style.Intro}>
|
|
88
|
+
Example code using the <code>@_linked</code> query DSL against
|
|
89
|
+
your local Fuseki dataset. Edit{' '}
|
|
90
|
+
<code>src/components/PersonOverview.tsx</code> to extend it.
|
|
91
|
+
</p>
|
|
92
|
+
<PersonList refreshRef={refreshRef} />
|
|
93
|
+
<PersonAddForm onAdded={refresh} />
|
|
94
|
+
</div>
|
|
95
|
+
</PersonListRefreshProvider>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Small context that lets PersonPreview rows trigger a list refresh on
|
|
2
|
+
// their parent PersonOverview without prop-drilling through the
|
|
3
|
+
// linkedSetComponent's wrapped renderer.
|
|
4
|
+
import React, { useContext } from 'react';
|
|
5
|
+
|
|
6
|
+
const PersonListRefreshContext = React.createContext<() => void>(() => {});
|
|
7
|
+
|
|
8
|
+
export const PersonListRefreshProvider = PersonListRefreshContext.Provider;
|
|
9
|
+
|
|
10
|
+
export function usePersonListRefresh(): () => void {
|
|
11
|
+
return useContext(PersonListRefreshContext);
|
|
12
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
@import '@_linked/css/package.css';
|
|
2
|
+
|
|
3
|
+
.Row {
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: space-between;
|
|
7
|
+
gap: --spacing(2);
|
|
8
|
+
flex-wrap: wrap;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.Name {
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
color: var(--color-gray-900);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.Actions {
|
|
17
|
+
display: flex;
|
|
18
|
+
gap: --spacing(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.EditButton,
|
|
22
|
+
.SaveButton,
|
|
23
|
+
.CancelButton,
|
|
24
|
+
.DeleteButton {
|
|
25
|
+
padding: --spacing(1) --spacing(3);
|
|
26
|
+
font: inherit;
|
|
27
|
+
font-size: 0.85rem;
|
|
28
|
+
border-radius: var(--radius-element);
|
|
29
|
+
border: 1px solid transparent;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.EditButton,
|
|
35
|
+
.CancelButton {
|
|
36
|
+
background: transparent;
|
|
37
|
+
color: var(--color-gray-700);
|
|
38
|
+
border-color: var(--color-gray-300);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.EditButton:hover:not(:disabled),
|
|
42
|
+
.CancelButton:hover:not(:disabled) {
|
|
43
|
+
background: var(--color-gray-100);
|
|
44
|
+
color: var(--color-gray-900);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.SaveButton {
|
|
48
|
+
background: var(--color-primary-500);
|
|
49
|
+
color: var(--color-white);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.SaveButton:hover:not(:disabled) {
|
|
53
|
+
background: var(--color-primary-600);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.DeleteButton {
|
|
57
|
+
background: transparent;
|
|
58
|
+
color: var(--color-red-600);
|
|
59
|
+
border-color: transparent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.DeleteButton:hover:not(:disabled) {
|
|
63
|
+
color: var(--color-red-700);
|
|
64
|
+
background: var(--color-red-50);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.EditButton:disabled,
|
|
68
|
+
.SaveButton:disabled,
|
|
69
|
+
.CancelButton:disabled,
|
|
70
|
+
.DeleteButton:disabled {
|
|
71
|
+
opacity: 0.5;
|
|
72
|
+
cursor: not-allowed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.EditRow {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: --spacing(2);
|
|
79
|
+
flex-wrap: wrap;
|
|
80
|
+
width: 100%;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.EditRow input {
|
|
84
|
+
flex: 1 1 140px;
|
|
85
|
+
padding: --spacing(2) --spacing(3);
|
|
86
|
+
border: 1px solid var(--color-gray-300);
|
|
87
|
+
border-radius: var(--radius-element);
|
|
88
|
+
background: var(--bg-card);
|
|
89
|
+
color: var(--color-gray-900);
|
|
90
|
+
font: inherit;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.EditRow input:focus {
|
|
94
|
+
outline: none;
|
|
95
|
+
border-color: var(--color-primary-500);
|
|
96
|
+
box-shadow: 0 0 0 2px var(--color-primary-200);
|
|
97
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Example PersonPreview — single-Person row. Demonstrates @_linked/react's
|
|
2
|
+
// linkedComponent: the wrapper runs the per-row query and injects query
|
|
3
|
+
// result keys + `source` + `_refresh` into the render function. _refresh
|
|
4
|
+
// patches local query state for optimistic UI; on delete, the parent
|
|
5
|
+
// list re-runs via the PersonListRefresh context.
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
import { linkedComponent } from '@_linked/react';
|
|
8
|
+
import { Person } from '@_linked/schema/shapes/Person';
|
|
9
|
+
import { usePersonListRefresh } from './PersonOverviewContext';
|
|
10
|
+
import style from './PersonPreview.module.css';
|
|
11
|
+
|
|
12
|
+
export const PersonPreview = linkedComponent(
|
|
13
|
+
Person.select((p) => [p.givenName, p.familyName]),
|
|
14
|
+
({ givenName, familyName, source, _refresh }) => {
|
|
15
|
+
const refreshList = usePersonListRefresh();
|
|
16
|
+
const [editing, setEditing] = useState(false);
|
|
17
|
+
const [draftGiven, setDraftGiven] = useState(givenName ?? '');
|
|
18
|
+
const [draftFamily, setDraftFamily] = useState(familyName ?? '');
|
|
19
|
+
const [busy, setBusy] = useState(false);
|
|
20
|
+
|
|
21
|
+
async function save() {
|
|
22
|
+
setBusy(true);
|
|
23
|
+
try {
|
|
24
|
+
await Person.update({
|
|
25
|
+
givenName: draftGiven,
|
|
26
|
+
familyName: draftFamily,
|
|
27
|
+
}).for(source);
|
|
28
|
+
// Optimistic patch — no extra network round-trip needed.
|
|
29
|
+
_refresh({ givenName: draftGiven, familyName: draftFamily });
|
|
30
|
+
setEditing(false);
|
|
31
|
+
} finally {
|
|
32
|
+
setBusy(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function remove() {
|
|
37
|
+
setBusy(true);
|
|
38
|
+
try {
|
|
39
|
+
await Person.delete(source);
|
|
40
|
+
refreshList();
|
|
41
|
+
} finally {
|
|
42
|
+
setBusy(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function startEdit() {
|
|
47
|
+
setDraftGiven(givenName ?? '');
|
|
48
|
+
setDraftFamily(familyName ?? '');
|
|
49
|
+
setEditing(true);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (editing) {
|
|
53
|
+
return (
|
|
54
|
+
<span className={style.EditRow} data-testid="person-edit">
|
|
55
|
+
<input
|
|
56
|
+
value={draftGiven}
|
|
57
|
+
onChange={(e) => setDraftGiven(e.target.value)}
|
|
58
|
+
aria-label="First name"
|
|
59
|
+
/>
|
|
60
|
+
<input
|
|
61
|
+
value={draftFamily}
|
|
62
|
+
onChange={(e) => setDraftFamily(e.target.value)}
|
|
63
|
+
aria-label="Last name"
|
|
64
|
+
/>
|
|
65
|
+
<button
|
|
66
|
+
className={style.SaveButton}
|
|
67
|
+
onClick={save}
|
|
68
|
+
disabled={busy}
|
|
69
|
+
>
|
|
70
|
+
Save
|
|
71
|
+
</button>
|
|
72
|
+
<button
|
|
73
|
+
className={style.CancelButton}
|
|
74
|
+
onClick={() => setEditing(false)}
|
|
75
|
+
disabled={busy}
|
|
76
|
+
>
|
|
77
|
+
Cancel
|
|
78
|
+
</button>
|
|
79
|
+
</span>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<span className={style.Row} data-testid="person-row">
|
|
85
|
+
<span className={style.Name} data-testid="person-name">
|
|
86
|
+
{givenName} {familyName}
|
|
87
|
+
</span>
|
|
88
|
+
<span className={style.Actions}>
|
|
89
|
+
<button
|
|
90
|
+
className={style.EditButton}
|
|
91
|
+
onClick={startEdit}
|
|
92
|
+
disabled={busy}
|
|
93
|
+
>
|
|
94
|
+
Edit
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
className={style.DeleteButton}
|
|
98
|
+
onClick={remove}
|
|
99
|
+
disabled={busy}
|
|
100
|
+
>
|
|
101
|
+
Delete
|
|
102
|
+
</button>
|
|
103
|
+
</span>
|
|
104
|
+
</span>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.spinner.active {
|
|
2
|
+
border: 4px solid transparent;
|
|
3
|
+
border-top: 4px solid #474545;
|
|
4
|
+
border-radius: 50%;
|
|
5
|
+
width: 16px;
|
|
6
|
+
height: 16px;
|
|
7
|
+
animation: spin 1s linear infinite;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@keyframes spin {
|
|
11
|
+
0% {
|
|
12
|
+
transform: rotate(0deg);
|
|
13
|
+
}
|
|
14
|
+
100% {
|
|
15
|
+
transform: rotate(360deg);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import style from './Spinner.module.css';
|
|
3
|
+
|
|
4
|
+
export function Spinner({ active = true }) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
className={[style.spinner, active && style.active].join(' ')}
|
|
8
|
+
role="progressbar"
|
|
9
|
+
aria-busy={active ? 'true' : 'false'}
|
|
10
|
+
/>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AppContextProvider } from '@_linked/server-utils/components/AppContext';
|
|
2
|
+
import { initFrontend } from '@_linked/server-utils/utils/Frontend';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
5
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
6
|
+
import App from './App';
|
|
7
|
+
import { preloadMatchedRoute } from './utils/preloadRoutes';
|
|
8
|
+
|
|
9
|
+
//import the storage & file configuration for the frontend
|
|
10
|
+
import './linked.frontend.storage';
|
|
11
|
+
|
|
12
|
+
//to avoid errors with react-refresh-webpack-plugin
|
|
13
|
+
window['$RefreshReg$'] = () => {};
|
|
14
|
+
window['$RefreshSig$'] = () => () => {};
|
|
15
|
+
|
|
16
|
+
initFrontend().then(async () => {
|
|
17
|
+
// Preload matched route before hydration to avoid Suspense mismatch
|
|
18
|
+
await preloadMatchedRoute();
|
|
19
|
+
|
|
20
|
+
hydrateRoot(
|
|
21
|
+
document.getElementById('root'), // Target the #root div for hydration
|
|
22
|
+
<React.StrictMode>
|
|
23
|
+
<BrowserRouter>
|
|
24
|
+
<AppContextProvider
|
|
25
|
+
assets={window['assetManifest']}
|
|
26
|
+
requestLD={document.getElementById('request-ld')?.innerText}
|
|
27
|
+
requestObject={JSON.parse(document.getElementById('request-json')?.innerText || '{}')}
|
|
28
|
+
>
|
|
29
|
+
<App />
|
|
30
|
+
</AppContextProvider>
|
|
31
|
+
</BrowserRouter>
|
|
32
|
+
</React.StrictMode>,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@import '@_linked/css/package.css';
|
|
2
|
+
|
|
3
|
+
.main {
|
|
4
|
+
font-family: var(--font-sans);
|
|
5
|
+
max-width: 960px;
|
|
6
|
+
margin: 0 auto;
|
|
7
|
+
padding: --spacing(6) --spacing(4);
|
|
8
|
+
|
|
9
|
+
@media (max-width: 1024px) {
|
|
10
|
+
max-width: 90%;
|
|
11
|
+
}
|
|
12
|
+
@media (max-width: 768px) {
|
|
13
|
+
padding: --spacing(4) --spacing(3);
|
|
14
|
+
}
|
|
15
|
+
}
|