feat: dui3 package (#1585)
This commit is contained in:
Родитель
1668cf6a65
Коммит
2eb5f51af3
|
@ -26,6 +26,9 @@ workflows:
|
|||
- test-frontend-2:
|
||||
filters: *filters-allow-all
|
||||
|
||||
- test-dui-3:
|
||||
filters: *filters-allow-all
|
||||
|
||||
- test-ui-components:
|
||||
filters: *filters-allow-all
|
||||
|
||||
|
@ -420,6 +423,32 @@ jobs:
|
|||
command: yarn storybook:test:ci
|
||||
working_directory: 'packages/frontend-2'
|
||||
|
||||
test-dui-3:
|
||||
docker:
|
||||
- image: cimg/node:18.16.0
|
||||
resource_class: medium
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
name: Restore Yarn Package Cache
|
||||
keys:
|
||||
- yarn-packages-server-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn
|
||||
|
||||
- save_cache:
|
||||
name: Save Yarn Package Cache
|
||||
key: yarn-packages-server-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- .yarn/cache
|
||||
- .yarn/unplugged
|
||||
|
||||
- run:
|
||||
name: Lint everything
|
||||
command: yarn lint
|
||||
working_directory: 'packages/dui3'
|
||||
|
||||
test-ui-components:
|
||||
docker:
|
||||
- image: cimg/node:18.16.0-browsers
|
||||
|
|
13
.eslintrc.js
13
.eslintrc.js
|
@ -1,8 +1,11 @@
|
|||
/** @type {import("eslint").Linter.Config} */
|
||||
const config = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022
|
||||
},
|
||||
env: {
|
||||
es2021: true,
|
||||
es2022: true,
|
||||
node: true,
|
||||
commonjs: true
|
||||
},
|
||||
|
@ -20,6 +23,14 @@ const config = {
|
|||
'prefer-const': 'warn',
|
||||
'object-shorthand': 'warn'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: '*.mjs',
|
||||
parserOptions: {
|
||||
sourceType: 'module'
|
||||
}
|
||||
}
|
||||
],
|
||||
ignorePatterns: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
|
|
|
@ -60,4 +60,5 @@ packages/server/.vscode/*.log
|
|||
.cache_ggshield
|
||||
|
||||
storybook-static
|
||||
build-storybook.log
|
||||
build-storybook.log
|
||||
ensure-tailwind-deps.mjs.lock
|
|
@ -10,10 +10,11 @@ packages/preview-service/public/render/**/*
|
|||
packages/objectloader/examples/browser/objectloader.web.js
|
||||
packages/viewer/example/speckleviewer.web.js
|
||||
|
||||
packages/frontend-2/.output
|
||||
packages/frontend-2/.nuxt
|
||||
packages/frontend-2/lib/core/nuxt-modules/**/templates/*.js
|
||||
.output
|
||||
.nuxt
|
||||
**/nuxt-modules/**/templates/*.js
|
||||
packages/frontend-2/lib/common/generated/**/*
|
||||
packages/dui3/lib/common/generated/**/*
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"build": "yarn workspaces foreach -ptv run build",
|
||||
"build:public": "yarn workspaces foreach -ptv --no-private run build",
|
||||
"build:tailwind-deps": "yarn workspaces foreach -iv -j unlimited --include '{@speckle/shared,@speckle/tailwind-theme,@speckle/ui-components}' run build",
|
||||
"ensure:tailwind-deps": "node ./utils/ensure-tailwind-deps.mjs",
|
||||
"lint": "eslint . --ext .js,.ts,.vue --max-warnings=0",
|
||||
"helm:readme:generate": "./utils/helm/update-documentation.sh",
|
||||
"prettier:check": "prettier --check .",
|
||||
|
@ -37,12 +38,14 @@
|
|||
"@rollup/plugin-typescript": "^11.1.0",
|
||||
"@swc/core": "^1.2.222",
|
||||
"@types/eslint": "^8.4.1",
|
||||
"@types/lockfile": "^1.0.2",
|
||||
"commitizen": "^4.2.5",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.3.7",
|
||||
"lockfile": "^1.0.4",
|
||||
"pino-pretty": "^9.1.1",
|
||||
"prettier": "^2.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
HOST=0.0.0.0
|
||||
PORT=8082
|
||||
|
||||
NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
|
||||
NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
|
|
@ -0,0 +1,119 @@
|
|||
const mainExtends = [
|
||||
'plugin:nuxt/recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'prettier'
|
||||
]
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parser: '@typescript-eslint/parser',
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.eslint.json'],
|
||||
extraFileExtensions: ['.vue']
|
||||
},
|
||||
extends: [...mainExtends],
|
||||
plugins: ['@typescript-eslint'],
|
||||
ignorePatterns: [
|
||||
'**/templates/*',
|
||||
'coverage',
|
||||
'lib/common/generated/**/*',
|
||||
'storybook-static',
|
||||
'!.storybook',
|
||||
'.nuxt',
|
||||
'.output'
|
||||
],
|
||||
rules: {
|
||||
camelcase: [
|
||||
'error',
|
||||
{
|
||||
properties: 'always',
|
||||
allow: ['^[\\w]+_[\\w]+Fragment$']
|
||||
}
|
||||
],
|
||||
'no-alert': 'error',
|
||||
eqeqeq: ['error', 'always', { null: 'always' }],
|
||||
'no-console': 'off',
|
||||
'no-var': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: '*.test.{ts,js}',
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
},
|
||||
{
|
||||
files: './{components|pages|store|lib}/*.{js,ts,vue}',
|
||||
env: {
|
||||
node: false,
|
||||
browser: true
|
||||
}
|
||||
},
|
||||
{
|
||||
files: '*.{ts,tsx,vue}',
|
||||
extends: ['plugin:@typescript-eslint/recommended', ...mainExtends],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': ['error'],
|
||||
'@typescript-eslint/no-unsafe-argument': ['error'],
|
||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
||||
'@typescript-eslint/no-unsafe-call': 'error',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'error',
|
||||
'@typescript-eslint/no-unsafe-return': 'error',
|
||||
'@typescript-eslint/no-for-in-array': ['error'],
|
||||
'@typescript-eslint/restrict-template-expressions': ['error'],
|
||||
'@typescript-eslint/restrict-plus-operands': ['error'],
|
||||
'@typescript-eslint/await-thenable': ['warn'],
|
||||
'@typescript-eslint/ban-types': ['warn'],
|
||||
'require-await': 'off',
|
||||
'@typescript-eslint/require-await': 'error',
|
||||
'no-undef': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: '*.vue',
|
||||
plugins: ['vuejs-accessibility'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
...mainExtends,
|
||||
'plugin:vuejs-accessibility/recommended'
|
||||
],
|
||||
rules: {
|
||||
'vue/component-tags-order': [
|
||||
'error',
|
||||
{ order: ['docs', 'template', 'script', 'style'] }
|
||||
],
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/component-name-in-template-casing': [
|
||||
'error',
|
||||
'PascalCase',
|
||||
{ registeredComponentsOnly: false }
|
||||
],
|
||||
'vuejs-accessibility/label-has-for': [
|
||||
'error',
|
||||
{
|
||||
required: {
|
||||
some: ['nesting', 'id']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: '*.d.ts',
|
||||
rules: {
|
||||
'no-var': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-types': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = config
|
|
@ -0,0 +1,10 @@
|
|||
node_modules
|
||||
*.log*
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
.output
|
||||
.env
|
||||
dist
|
||||
.DS_Store
|
||||
.env
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"css.validate": false,
|
||||
"less.validate": false,
|
||||
"scss.validate": false,
|
||||
"stylelint.validate": ["css", "scss", "vue", "postcss"],
|
||||
"stylelint.enable": true,
|
||||
"stylelint.configFile": "${workspaceFolder}/stylelint.config.js",
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
"javascript.suggest.autoImports": true,
|
||||
"typescript.suggest.autoImports": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
# dui3
|
||||
|
||||
DUIv3 is a Speckle interface embedded inside the desktop connectors that allows users to interact with them - sync streams, manage servers etc. It's built in Vue 3 with Nuxt 3 and only supports client side rendering.
|
||||
|
||||
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
# yarn
|
||||
yarn install
|
||||
```
|
||||
|
||||
And create an `.env` file from `.env.example`.
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div id="speckle" class="bg-foundation-page text-foreground">
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
// Title suffix
|
||||
titleTemplate: (titleChunk) =>
|
||||
titleChunk ? `${titleChunk} - Speckle DUIv3` : 'Speckle DUIv3',
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
},
|
||||
bodyAttrs: {
|
||||
class: 'simple-scrollbar bg-foundation-page text-foreground'
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,25 @@
|
|||
/* stylelint-disable selector-id-pattern */
|
||||
@import '@speckle/ui-components/style.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/**
|
||||
* Don't pollute this - it's going to be bundled in all pages!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Making sure page is always stretched to the bottom of the screen even if there's nothing in it
|
||||
*/
|
||||
html,
|
||||
body,
|
||||
div#__nuxt,
|
||||
div#__nuxt > div {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
div#__nuxt {
|
||||
height: 100%;
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.3 KiB |
|
@ -0,0 +1,28 @@
|
|||
import type { CodegenConfig } from '@graphql-codegen/cli'
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: 'http://127.0.0.1:3000/graphql',
|
||||
documents: ['{lib,components,layouts,pages,middleware}/**/*.{vue,js,ts}'],
|
||||
ignoreNoDocuments: true, // for better experience with the watcher
|
||||
generates: {
|
||||
'./lib/common/generated/gql/': {
|
||||
preset: 'client',
|
||||
config: {
|
||||
useTypeImports: true,
|
||||
fragmentMasking: false,
|
||||
dedupeFragments: true,
|
||||
scalars: {
|
||||
JSONObject: '{}',
|
||||
DateTime: 'string'
|
||||
}
|
||||
},
|
||||
presetConfig: {
|
||||
fragmentMasking: false,
|
||||
dedupeFragments: true
|
||||
},
|
||||
plugins: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<NuxtLink class="flex items-center" to="/">
|
||||
<img
|
||||
class="block h-6 w-6"
|
||||
:class="{ 'mr-2': !minimal, grayscale: active }"
|
||||
src="~~/assets/images/speckle_logo_big.png"
|
||||
alt="Speckle"
|
||||
/>
|
||||
<div
|
||||
v-if="!minimal"
|
||||
class="text-primary h6 mt-0 hidden font-bold leading-7 md:flex"
|
||||
>
|
||||
Speckle
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
minimal: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<nav
|
||||
class="fixed top-0 h-14 bg-foundation max-w-full w-full shadow hover:shadow-md transition z-20"
|
||||
>
|
||||
<div class="px-4">
|
||||
<div class="flex items-center h-14 transition-all justify-between">
|
||||
<div class="flex items-center">
|
||||
<HeaderLogoBlock :active="false" class="mr-0" />
|
||||
<div class="flex flex-shrink-0 items-center -ml-2 md:ml-0">
|
||||
<HeaderNavLink
|
||||
to="/"
|
||||
name="Dashboard"
|
||||
:separator="true"
|
||||
class="hidden md:inline-block"
|
||||
/>
|
||||
<PortalTarget name="navigation"></PortalTarget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="transition text-foreground hover:text-primary-focus">
|
||||
<NuxtLink
|
||||
:to="to"
|
||||
class="flex items-center text-sm"
|
||||
active-class="text-primary font-bold"
|
||||
>
|
||||
<div v-if="separator">
|
||||
<ChevronRightIcon class="flex w-4 h-4 mt-[3px] mx-0 md:mx-1" />
|
||||
</div>
|
||||
<div class="max-w-[120px] md:max-w-[200px] lg:max-w-[300px] truncate">
|
||||
{{ name || to }}
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon } from '@heroicons/vue/20/solid'
|
||||
defineProps({
|
||||
separator: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: '/'
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<div class="min-h-full">
|
||||
<HeaderNavBar />
|
||||
<main class="my-4 layout-container pb-20 mt-20">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,42 @@
|
|||
/* eslint-disable */
|
||||
import * as types from './graphql';
|
||||
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
|
||||
/**
|
||||
* Map of all GraphQL operations in the project.
|
||||
*
|
||||
* This map has several performance disadvantages:
|
||||
* 1. It is not tree-shakeable, so it will include all operations in the project.
|
||||
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
|
||||
* 3. It does not support dead code elimination, so it will add unused operations.
|
||||
*
|
||||
* Therefore it is highly recommended to use the babel-plugin for production.
|
||||
*/
|
||||
const documents = {
|
||||
"\n query ServerInfoTest {\n serverInfo {\n version\n }\n }\n": types.ServerInfoTestDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const query = gql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
|
||||
* ```
|
||||
*
|
||||
* The query argument is unknown!
|
||||
* Please regenerate the types.
|
||||
**/
|
||||
export function graphql(source: string): unknown;
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ServerInfoTest {\n serverInfo {\n version\n }\n }\n"): (typeof documents)["\n query ServerInfoTest {\n serverInfo {\n version\n }\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
}
|
||||
|
||||
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1 @@
|
|||
export * from "./gql"
|
|
@ -0,0 +1,319 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import {
|
||||
ApolloLink,
|
||||
InMemoryCache,
|
||||
split,
|
||||
ApolloClientOptions
|
||||
} from '@apollo/client/core'
|
||||
import { setContext } from '@apollo/client/link/context'
|
||||
import { SubscriptionClient } from 'subscriptions-transport-ws'
|
||||
import { createUploadLink } from 'apollo-upload-client'
|
||||
import { WebSocketLink } from '@apollo/client/link/ws'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
import { OperationDefinitionNode, Kind } from 'graphql'
|
||||
import { Nullable } from '@speckle/shared'
|
||||
import {
|
||||
buildAbstractCollectionMergeFunction,
|
||||
buildArrayMergeFunction,
|
||||
incomingOverwritesExistingMergeFunction
|
||||
} from '~~/lib/core/helpers/apolloSetup'
|
||||
|
||||
const appVersion = (import.meta.env.SPECKLE_SERVER_VERSION as string) || 'unknown'
|
||||
const appName = 'dui-3'
|
||||
|
||||
function createCache(): InMemoryCache {
|
||||
return new InMemoryCache({
|
||||
/**
|
||||
* This is where you configure how various GQL fields should be read, written to or merged when new data comes in.
|
||||
* If you define a merge function here, you don't need to duplicate the merge logic inside an `update()` callback
|
||||
* of a fetchMore call, for example.
|
||||
*
|
||||
* Feel free to re-use utilities in the `apolloSetup` helper for defining merge functions or even use the ones that come from `@apollo/client/utilities`.
|
||||
*
|
||||
* Read more: https://www.apollographql.com/docs/react/caching/cache-field-behavior
|
||||
*/
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
otherUser: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'LimitedUser', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
},
|
||||
activeUser: {
|
||||
merge(existing, incoming, { mergeObjects }) {
|
||||
return mergeObjects(existing, incoming)
|
||||
},
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'User', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
},
|
||||
user: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'User', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
},
|
||||
stream: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'Stream', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
},
|
||||
streams: {
|
||||
keyArgs: ['query'],
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
},
|
||||
project: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'Project', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
},
|
||||
projects: {
|
||||
merge: buildArrayMergeFunction()
|
||||
}
|
||||
}
|
||||
},
|
||||
LimitedUser: {
|
||||
fields: {
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection')
|
||||
}
|
||||
}
|
||||
},
|
||||
User: {
|
||||
fields: {
|
||||
timeline: {
|
||||
keyArgs: ['after', 'before'],
|
||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
||||
},
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection')
|
||||
},
|
||||
favoriteStreams: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection')
|
||||
},
|
||||
projects: {
|
||||
keyArgs: ['filter', 'limit'],
|
||||
merge: buildAbstractCollectionMergeFunction('ProjectCollection')
|
||||
}
|
||||
}
|
||||
},
|
||||
Project: {
|
||||
fields: {
|
||||
models: {
|
||||
keyArgs: ['filter', 'limit'],
|
||||
merge: buildAbstractCollectionMergeFunction('ModelCollection')
|
||||
},
|
||||
versions: {
|
||||
keyArgs: ['filter', 'limit'],
|
||||
merge: buildAbstractCollectionMergeFunction('VersionCollection')
|
||||
},
|
||||
commentThreads: {
|
||||
keyArgs: ['filter', 'limit'],
|
||||
merge: buildAbstractCollectionMergeFunction('CommentCollection')
|
||||
},
|
||||
modelsTree: {
|
||||
keyArgs: ['filter', 'limit'],
|
||||
merge: buildAbstractCollectionMergeFunction('ModelsTreeItemCollection')
|
||||
},
|
||||
replyAuthors: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommentReplyAuthorCollection')
|
||||
},
|
||||
viewerResources: {
|
||||
merge: (_existing, incoming) => [...incoming]
|
||||
},
|
||||
model: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'Model', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
},
|
||||
team: {
|
||||
merge: (_existing, incoming) => incoming
|
||||
},
|
||||
invitedTeam: {
|
||||
merge: (_existing, incoming) => incoming
|
||||
},
|
||||
pendingImportedModels: {
|
||||
merge: (_existing, incoming) => incoming
|
||||
}
|
||||
}
|
||||
},
|
||||
Model: {
|
||||
fields: {
|
||||
versions: {
|
||||
keyArgs: ['filter', 'limit'],
|
||||
merge: buildAbstractCollectionMergeFunction('VersionCollection')
|
||||
},
|
||||
pendingImportedVersions: {
|
||||
merge: (_existing, incoming) => incoming
|
||||
}
|
||||
}
|
||||
},
|
||||
Comment: {
|
||||
fields: {
|
||||
replies: {
|
||||
keyArgs: ['limit']
|
||||
}
|
||||
}
|
||||
},
|
||||
Stream: {
|
||||
fields: {
|
||||
activity: {
|
||||
keyArgs: ['after', 'before', 'actionType'],
|
||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
||||
},
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
},
|
||||
pendingCollaborators: {
|
||||
merge: incomingOverwritesExistingMergeFunction
|
||||
},
|
||||
pendingAccessRequests: {
|
||||
merge: incomingOverwritesExistingMergeFunction
|
||||
}
|
||||
}
|
||||
},
|
||||
Branch: {
|
||||
fields: {
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
BranchCollection: {
|
||||
merge: true
|
||||
},
|
||||
ServerStats: {
|
||||
merge: true
|
||||
},
|
||||
WebhookEventCollection: {
|
||||
merge: true
|
||||
},
|
||||
ServerInfo: {
|
||||
merge: true
|
||||
},
|
||||
CommentThreadActivityMessage: {
|
||||
merge: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createWsClient(params: {
|
||||
wsEndpoint: string
|
||||
authToken: () => Nullable<string>
|
||||
}): SubscriptionClient {
|
||||
const { wsEndpoint, authToken } = params
|
||||
|
||||
return new SubscriptionClient(wsEndpoint, {
|
||||
reconnect: true,
|
||||
connectionParams: () => {
|
||||
const token = authToken()
|
||||
const Authorization = token?.length ? `Bearer ${token}` : null
|
||||
return Authorization ? { Authorization, headers: { Authorization } } : {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createLink(params: {
|
||||
httpEndpoint: string
|
||||
wsClient?: SubscriptionClient
|
||||
authToken: () => Nullable<string>
|
||||
}): ApolloLink {
|
||||
const { httpEndpoint, wsClient, authToken } = params
|
||||
// Prepare links
|
||||
const httpLink = createUploadLink({
|
||||
uri: httpEndpoint
|
||||
})
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const token = authToken()
|
||||
const authHeader = token?.length ? { Authorization: `Bearer ${token}` } : {}
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
...authHeader
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let link = authLink.concat(httpLink as unknown as ApolloLink)
|
||||
|
||||
if (wsClient) {
|
||||
const wsLink = new WebSocketLink(wsClient)
|
||||
link = split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query) as OperationDefinitionNode
|
||||
const { kind, operation } = definition
|
||||
|
||||
return kind === Kind.OPERATION_DEFINITION && operation === 'subscription'
|
||||
},
|
||||
wsLink,
|
||||
link
|
||||
)
|
||||
}
|
||||
|
||||
return link
|
||||
}
|
||||
|
||||
type ResolveClientConfigParams = {
|
||||
httpEndpoint: string
|
||||
authToken: () => Nullable<string>
|
||||
}
|
||||
|
||||
export const resolveClientConfig = (
|
||||
params: ResolveClientConfigParams
|
||||
): Pick<ApolloClientOptions<unknown>, 'cache' | 'link' | 'name' | 'version'> => {
|
||||
const { httpEndpoint, authToken } = params
|
||||
const wsEndpoint = httpEndpoint.replace('http', 'ws')
|
||||
|
||||
const wsClient = process.client
|
||||
? createWsClient({ wsEndpoint, authToken })
|
||||
: undefined
|
||||
const link = createLink({ httpEndpoint, wsClient, authToken })
|
||||
|
||||
return {
|
||||
// If we don't markRaw the cache, sometimes we get cryptic internal Apollo Client errors that essentially
|
||||
// result from parts of its internals being made reactive, even tho they shouldn't be
|
||||
cache: markRaw(createCache()),
|
||||
link,
|
||||
name: appName,
|
||||
version: appVersion
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import { Optional } from '@speckle/shared'
|
||||
import { FieldMergeFunction } from '@apollo/client/core'
|
||||
|
||||
interface AbstractCollection<T extends string> {
|
||||
__typename: T
|
||||
totalCount: number
|
||||
cursor: string | null
|
||||
items: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
interface MergeSettings {
|
||||
/**
|
||||
* Set to false if you want to merge incoming items without checking
|
||||
* for duplicates. Usually you don't want to do this as you can introduce duplicates this way.
|
||||
* Defaults to true
|
||||
*/
|
||||
checkIdentity: boolean
|
||||
/**
|
||||
* Optionally change the prop that should be used to compare
|
||||
* equality between items
|
||||
* Defaults to '__ref', which is the prop added by Apollo that contains the globally unique ID of the object
|
||||
*/
|
||||
identityProp: string
|
||||
}
|
||||
|
||||
const prepareMergeSettings = (
|
||||
settings: Optional<Partial<MergeSettings>>
|
||||
): MergeSettings => ({
|
||||
checkIdentity: true,
|
||||
identityProp: '__ref',
|
||||
...(settings || {})
|
||||
})
|
||||
|
||||
/**
|
||||
* Build an Apollo merge function for a field that returns an array of identifiable objects
|
||||
*/
|
||||
export function buildArrayMergeFunction(
|
||||
settings?: Partial<MergeSettings>
|
||||
): FieldMergeFunction<Record<string, unknown>[], Record<string, unknown>[]> {
|
||||
const { checkIdentity, identityProp } = prepareMergeSettings(settings)
|
||||
return (existing, incoming) => {
|
||||
let finalItems: Record<string, unknown>[]
|
||||
if (checkIdentity) {
|
||||
finalItems = [...(existing || [])]
|
||||
for (const newItem of incoming || []) {
|
||||
if (
|
||||
finalItems.findIndex(
|
||||
(item) => item[identityProp] === newItem[identityProp]
|
||||
) === -1
|
||||
) {
|
||||
finalItems.push(newItem)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finalItems = [...(existing || []), ...(incoming || [])]
|
||||
}
|
||||
|
||||
return finalItems
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an Apollo merge function for a field that returns a collection like AbstractCollection
|
||||
*/
|
||||
export function buildAbstractCollectionMergeFunction<T extends string>(
|
||||
typeName: T,
|
||||
settings?: Partial<MergeSettings>
|
||||
): FieldMergeFunction<Optional<AbstractCollection<T>>, AbstractCollection<T>> {
|
||||
const { checkIdentity, identityProp } = prepareMergeSettings(settings)
|
||||
return (
|
||||
existing: Optional<AbstractCollection<T>>,
|
||||
incoming: AbstractCollection<T>
|
||||
) => {
|
||||
const existingItems = existing?.items || []
|
||||
const incomingItems = incoming?.items || []
|
||||
|
||||
let finalItems: Record<string, unknown>[]
|
||||
if (checkIdentity) {
|
||||
finalItems = [...existingItems]
|
||||
for (const newItem of incomingItems) {
|
||||
if (
|
||||
finalItems.findIndex(
|
||||
(item) => item[identityProp] === newItem[identityProp]
|
||||
) === -1
|
||||
) {
|
||||
finalItems.push(newItem)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finalItems = [...existingItems, ...incomingItems]
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: incoming?.__typename || existing?.__typename || typeName,
|
||||
totalCount: incoming.totalCount || 0,
|
||||
cursor: incoming.cursor || null,
|
||||
items: finalItems
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge function that just takes incoming data and overrides all of old data with it
|
||||
* Useful for array fields w/o pagination, where a new array response is supposed to replace
|
||||
* the entire old one
|
||||
*/
|
||||
export const incomingOverwritesExistingMergeFunction: FieldMergeFunction = (
|
||||
_existing: unknown,
|
||||
incoming: unknown
|
||||
) => incoming
|
|
@ -0,0 +1,71 @@
|
|||
import legacy from '@vitejs/plugin-legacy'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
typescript: {
|
||||
shim: false,
|
||||
strict: true
|
||||
},
|
||||
modules: ['@nuxtjs/tailwindcss'],
|
||||
alias: {
|
||||
// Rewriting all lodash calls to lodash-es for proper tree-shaking & chunk splitting
|
||||
lodash: 'lodash-es'
|
||||
},
|
||||
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: [{ find: /^lodash$/, replacement: 'lodash-es' }]
|
||||
},
|
||||
|
||||
build: {
|
||||
// older chrome version for CEF 65 support. all identifiers except the chrome one are default ones.
|
||||
target: ['es2020', 'edge88', 'firefox78', 'chrome65', 'safari14'],
|
||||
// optionally disable minification for debugging
|
||||
minify: false
|
||||
},
|
||||
plugins: [
|
||||
// again - only for CEF 65
|
||||
legacy({
|
||||
renderLegacyChunks: false,
|
||||
// only adding the specific polyfills we need to reduce bundle size
|
||||
modernPolyfills: ['es.global-this', 'es/object', 'es/array']
|
||||
})
|
||||
]
|
||||
},
|
||||
ssr: false,
|
||||
build: {
|
||||
transpile: [
|
||||
/^@apollo\/client/,
|
||||
'ts-invariant/process',
|
||||
'@vue/apollo-composable',
|
||||
'@headlessui/vue',
|
||||
/^@heroicons\/vue/,
|
||||
'@vueuse/core',
|
||||
'@vueuse/shared',
|
||||
'@speckle/ui-components'
|
||||
]
|
||||
},
|
||||
hooks: {
|
||||
'build:manifest': (manifest) => {
|
||||
// kinda hacky, vite polyfills are incorrectly being loaded last so we have to move them to appear first in the object.
|
||||
// we can't replace `manifest` entirely, cause then we're only mutating a local variable, not the actual manifest
|
||||
// which is why we have to mutate the reference.
|
||||
// since ES2015 object string property order is more or less guaranteed - the order is chronological
|
||||
const polyfillKey = 'vite/legacy-polyfills'
|
||||
const polyfillEntry = manifest[polyfillKey]
|
||||
if (!polyfillEntry) return
|
||||
|
||||
const oldManifest = { ...manifest }
|
||||
delete oldManifest[polyfillKey]
|
||||
|
||||
for (const key in manifest) {
|
||||
delete manifest[key]
|
||||
}
|
||||
|
||||
manifest[polyfillKey] = polyfillEntry
|
||||
for (const key in oldManifest) {
|
||||
manifest[key] = oldManifest[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name": "@speckle/dui3",
|
||||
"description": "Speckle desktop UI embedded in connectors. Built w/ Vue 3 & Nuxt 3",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev:nuxt": "nuxt dev",
|
||||
"dev": "concurrently \"nuxt dev\" \"yarn gqlgen:watch\"",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "yarn ensure:tailwind-deps && nuxt prepare",
|
||||
"lint:js": "eslint --ext \".js,.ts,.vue\" .",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"lint:prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --check .",
|
||||
"lint:css": "stylelint \"**/*.{css,vue}\"",
|
||||
"lint": "yarn lint:js && yarn lint:tsc && yarn lint:prettier && yarn lint:css",
|
||||
"gqlgen": "graphql-codegen",
|
||||
"gqlgen:watch": "graphql-codegen --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.14",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@speckle/shared": "workspace:^",
|
||||
"@speckle/ui-components": "workspace:^",
|
||||
"@speckle/ui-components-nuxt": "workspace:^",
|
||||
"@vue/apollo-composable": "^4.0.0-beta.5",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"portal-vue": "^3.0.0",
|
||||
"subscriptions-transport-ws": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.13.6",
|
||||
"@graphql-codegen/client-preset": "^1.2.5",
|
||||
"@nuxtjs/tailwindcss": "^6.7.0",
|
||||
"@types/apollo-upload-client": "^17.0.1",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/node": "^18",
|
||||
"@vitejs/plugin-legacy": "^4.0.3",
|
||||
"concurrently": "^7.5.0",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-nuxt": "^4.0.0",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
||||
"nuxt": "^3.5.0",
|
||||
"postcss": "^8.4.18",
|
||||
"postcss-custom-properties": "^12.1.9",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-nesting": "^10.2.0",
|
||||
"prettier": "^2.7.1",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"type-fest": "^3.5.1",
|
||||
"typescript": "^4.8.3",
|
||||
"vue-tsc": "1.3.4"
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div>
|
||||
Hello world! Query results:
|
||||
<div>
|
||||
<div v-for="(res, clientId) in queries" :key="clientId">
|
||||
<strong>{{ clientId }}:</strong>
|
||||
{{ res.result.value?.serverInfo.version || '' }}
|
||||
</div>
|
||||
</div>
|
||||
<Portal to="navigation">
|
||||
<HeaderNavLink :to="'/'" :name="'Home'"></HeaderNavLink>
|
||||
</Portal>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { UseQueryReturn, useQuery } from '@vue/apollo-composable'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { ServerInfoTestQuery } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
const versionQuery = graphql(`
|
||||
query ServerInfoTest {
|
||||
serverInfo {
|
||||
version
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
/**
|
||||
* Imagine these come from window or something
|
||||
*/
|
||||
const clients = ['latest', 'xyz']
|
||||
|
||||
const queries: Record<
|
||||
string,
|
||||
UseQueryReturn<ServerInfoTestQuery, Record<string, never>>
|
||||
> = {}
|
||||
for (const clientId of clients) {
|
||||
queries[clientId] = useQuery(versionQuery, undefined, { clientId })
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,34 @@
|
|||
import { ApolloClient } from '@apollo/client/core'
|
||||
import { ApolloClients } from '@vue/apollo-composable'
|
||||
import { resolveClientConfig } from '~/lib/core/configs/apollo'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
/**
|
||||
* TODO: You can use `window` here to get credentials for all of the clients
|
||||
* we need from the parent connectors. The following is just an example
|
||||
*/
|
||||
|
||||
const apolloClients = {
|
||||
latest: new ApolloClient(
|
||||
// Imagine endpoint & token is resolved from window or something
|
||||
resolveClientConfig({
|
||||
httpEndpoint: 'https://latest.speckle.systems/graphql',
|
||||
authToken: () => null
|
||||
})
|
||||
),
|
||||
xyz: new ApolloClient(
|
||||
// Imagine endpoint & token is resolved from window or something
|
||||
resolveClientConfig({
|
||||
httpEndpoint: 'https://speckle.xyz/graphql',
|
||||
authToken: () => null
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
nuxtApp.vueApp.provide(ApolloClients, apolloClients)
|
||||
return {
|
||||
provide: {
|
||||
apolloClients
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,5 @@
|
|||
import PortalVue from 'portal-vue'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(PortalVue)
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'postcss-nesting': {}
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 4.2 KiB |
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
module.exports = {
|
||||
extends: [
|
||||
'stylelint-config-standard',
|
||||
'stylelint-config-recommended-vue',
|
||||
'stylelint-config-prettier'
|
||||
],
|
||||
// add your custom config here
|
||||
// https://stylelint.io/user-guide/configuration
|
||||
rules: {
|
||||
// Rules to make stylelint happy with tailwind syntax
|
||||
'at-rule-no-unknown': [
|
||||
true,
|
||||
{
|
||||
ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen']
|
||||
}
|
||||
],
|
||||
'declaration-block-trailing-semicolon': null,
|
||||
'no-descending-specificity': null
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: '**/*.vue',
|
||||
rules: {
|
||||
'value-keyword-case': null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import speckleTheme from '@speckle/tailwind-theme'
|
||||
import { tailwindContentEntry as themeEntry } from '@speckle/tailwind-theme/tailwind-configure'
|
||||
import { tailwindContentEntry as uiLibEntry } from '@speckle/ui-components/tailwind-configure'
|
||||
import formsPlugin from '@tailwindcss/forms'
|
||||
|
||||
import { createRequire } from 'module'
|
||||
const req = createRequire(import.meta.url)
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
`./components/**/*.{vue,js,ts}`,
|
||||
`./layouts/**/*.vue`,
|
||||
`./pages/**/*.vue`,
|
||||
`./composables/**/*.{js,ts}`,
|
||||
`./plugins/**/*.{js,ts}`,
|
||||
'./stories/**/*.{js,ts,vue,mdx}',
|
||||
'./app.vue',
|
||||
'./.storybook/**/*.{js,ts,vue}',
|
||||
'./lib/**/composables/*.{js,ts}',
|
||||
themeEntry(req),
|
||||
uiLibEntry(req)
|
||||
// `./lib/**/*.{js,ts,vue}`, // TODO: Wait for fix https://github.com/nuxt/framework/issues/2886#issuecomment-1108312903
|
||||
],
|
||||
plugins: [speckleTheme, formsPlugin]
|
||||
}
|
||||
|
||||
export default config
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
// https://v3.nuxtjs.org/concepts/typescript
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["./.nuxt/nuxt.d.ts", "**/*", ".*.js"]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
|
@ -4,6 +4,5 @@
|
|||
<main class="my-4 layout-container pb-20 mt-20">
|
||||
<slot />
|
||||
</main>
|
||||
<SingletonManagers />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
<div class="relative mt-4 mx-2">
|
||||
<slot />
|
||||
</div>
|
||||
<SingletonManagers />
|
||||
</main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"dev": "concurrently \"yarn dev:app\" \"yarn gqlgen:watch\" \"yarn storybook --no-open\" -n nuxt,gqlgen,storybook",
|
||||
"preview": "nuxt preview",
|
||||
"analyze": "nuxt analyze",
|
||||
"postinstall": "yarn build:tailwind-deps && nuxt prepare",
|
||||
"postinstall": "yarn ensure:tailwind-deps && nuxt prepare",
|
||||
"lint:js": "eslint --ext \".js,.ts,.vue\" .",
|
||||
"lint:tsc": "vue-tsc --noEmit",
|
||||
"lint:prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --check .",
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import mod from 'node:module'
|
||||
import { exec } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { lock, unlock, check } from 'lockfile'
|
||||
|
||||
const lockFileName = 'ensure-tailwind-deps.mjs.lock'
|
||||
|
||||
/**
|
||||
* Build tailwind dependencies only if they don't already exist
|
||||
*/
|
||||
|
||||
const require = mod.createRequire(import.meta.url)
|
||||
const __dirname = fileURLToPath(dirname(import.meta.url))
|
||||
|
||||
const lockFileOpts = { stale: 2 * 60 * 1000 }
|
||||
const lockFilePath = resolve(__dirname, lockFileName)
|
||||
|
||||
async function checkForPresence() {
|
||||
try {
|
||||
require('@speckle/tailwind-theme')
|
||||
require('@speckle/ui-components')
|
||||
require('@speckle/shared')
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function waitForUnlock() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('Tailwind deps already building...')
|
||||
const to = setInterval(() => {
|
||||
check(lockFilePath, lockFileOpts, (err, isLocked) => {
|
||||
if (err) {
|
||||
clearTimeout(to)
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
if (!isLocked) {
|
||||
clearTimeout(to)
|
||||
return resolve()
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
async function doWork() {
|
||||
return new Promise((resolve, reject) => {
|
||||
lock(lockFilePath, lockFileOpts, async (err) => {
|
||||
if (err) {
|
||||
await waitForUnlock()
|
||||
}
|
||||
|
||||
const depsExist = await checkForPresence()
|
||||
if (depsExist) {
|
||||
return resolve()
|
||||
}
|
||||
|
||||
// Trigger install
|
||||
const now = performance.now()
|
||||
console.log('Building tailwind deps...')
|
||||
const proc = exec(
|
||||
'yarn build:tailwind-deps',
|
||||
{ cwd: __dirname },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return reject()
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout)
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(stderr)
|
||||
}
|
||||
}
|
||||
)
|
||||
proc.on('exit', () => {
|
||||
console.log(`...done [${Math.round(performance.now() - now)}ms]`)
|
||||
return resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await doWork()
|
||||
unlock(lockFilePath, console.error)
|
||||
}
|
||||
|
||||
await main()
|
|
@ -12,6 +12,10 @@
|
|||
"path": "packages/frontend-2",
|
||||
"name": "🏬 frontend 2.0"
|
||||
},
|
||||
{
|
||||
"path": "packages/dui3",
|
||||
"name": "🥉 dui 3.0"
|
||||
},
|
||||
{
|
||||
"path": "packages/tailwind-theme",
|
||||
"name": "🎨 tailwind-theme"
|
||||
|
|
2138
yarn.lock
2138
yarn.lock
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче