This commit is contained in:
Kristaps Fabians Geikins 2023-05-19 16:57:28 +03:00 коммит произвёл GitHub
Родитель 1668cf6a65
Коммит 2eb5f51af3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
41 изменённых файлов: 6030 добавлений и 21 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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',

3
.gitignore поставляемый
Просмотреть файл

@ -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

119
packages/dui3/.eslintrc.js Normal file
Просмотреть файл

@ -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

10
packages/dui3/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,10 @@
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist
.DS_Store
.env

13
packages/dui3/.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -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"
}

40
packages/dui3/README.md Normal file
Просмотреть файл

@ -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.

20
packages/dui3/app.vue Normal file
Просмотреть файл

@ -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%;
}

Двоичные данные
packages/dui3/assets/images/speckle_logo_big.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

28
packages/dui3/codegen.ts Normal file
Просмотреть файл

@ -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': {}
}
}

Двоичные данные
packages/dui3/public/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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

Разница между файлами не показана из-за своего большого размера Загрузить разницу